summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/service-workers/service-worker/resources
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/service-workers/service-worker/resources')
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/404.py5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-frame.py31
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py49
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py32
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-worker.js95
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/basic-module-2.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/basic-module.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/blank.html2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py20
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/bytecheck-worker.py38
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html16
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html13
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html48
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html13
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/claim-worker.js19
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/classic-worker.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/client-id-worker.js27
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/client-navigate-frame.html12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/client-navigate-worker.js92
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/client-navigated-frame.html3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html26
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-frame-freeze.html15
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js11
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-frame.html17
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-worker.js11
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html50
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-get-frame.html12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-get-other-origin.html64
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js60
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-get-worker.js41
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html20
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js4
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js11
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-worker.js40
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/cors-approved.txt1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/cors-approved.txt.headers3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/cors-denied.txt2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/create-blob-url-worker.js22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/create-out-of-scope-worker.html19
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/echo-content.py16
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/echo-cookie-worker.py24
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/echo-message-to-source-worker.js3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js14
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html17
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html23
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/embedded-content-from-server.html6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html7
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/empty-but-slow-worker.js8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/empty-worker.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/empty.h2.js0
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/empty.html6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/empty.js0
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/enable-client-message-queue.html39
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/end-to-end-worker.js7
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/events-worker.js12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js210
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/extendable-event-waituntil.js87
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fail-on-fetch-worker.js5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-access-control-login.html16
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-access-control.py109
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js7
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html70
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js241
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html170
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-csp-iframe.html16
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-error-worker.js22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js66
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-handled-worker.js37
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html60
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js49
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html55
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js14
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js7
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js45
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js28
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js40
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js81
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html15
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js15
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-test-worker.js224
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js48
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html66
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html71
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html80
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html71
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html20
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js45
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html17
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html15
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js65
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html32
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js13
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html13
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js30
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js18
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html35
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html87
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-resources-worker.js26
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html208
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js19
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html13
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js41
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js7
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html53
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-response.html29
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-response.js35
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js4
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js166
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-variants-worker.js35
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js31
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/form-poster.html13
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/frame-for-getregistrations.html19
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/get-resultingClientId-worker.js107
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html25
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html14
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/iframe-with-image.html2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js19
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-mime-type-worker.py10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-relative.xsl5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404-after-update.js6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-echo.py6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-get.py6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js49
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js7
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js15
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js31
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-version.py17
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/imported-classic-script.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/imported-module-script.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/indexeddb-worker.js57
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/install-event-type-worker.js9
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/install-worker.html22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js59
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html28
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/invalid-blobtype-worker.js10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py9
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/invalid-chunked-encoding.py2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/invalid-header-iframe.https.html25
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/invalid-header-worker.js12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html23
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/iso-latin1-header-worker.js12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/load_worker.js29
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/loaded.html9
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html130
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/location-setter.html10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/malformed-http-response.asis1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/malformed-worker.py14
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/message-vs-microtask.html18
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/mime-sniffing-worker.js9
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/mime-type-worker.py4
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/mint-new-worker.py27
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/module-worker.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/multipart-image-iframe.html19
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/multipart-image-worker.js21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/multipart-image.py23
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/navigate-window-worker.js21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/navigation-headers-server.py19
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js11
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-body.py11
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html89
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-scope1.py22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-scope2.py22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html42
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/navigation-timing-worker.js15
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html16
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/nested-blob-url-workers.html38
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/nested-iframe-parent.html5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/nested-parent.html18
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html33
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/nested_load_worker.js23
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/no-dynamic-import.js18
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/notification_icon.py11
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html18
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html24
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js13
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js7
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js7
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js7
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js7
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html33
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html35
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-frame.html21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-large.js41
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-small.js3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-sw.js37
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/other.html3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/override_assert_object_equals.js58
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html114
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html108
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js53
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html35
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js53
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html59
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html44
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html30
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html40
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html27
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html36
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html41
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/partitioned-storage-sw.js81
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/partitioned-utils.js110
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/pass-through-worker.js3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/pass.txt1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/performance-timeline-worker.js62
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/postmessage-blob-url.js5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js24
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/postmessage-echo-worker.js3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/postmessage-fetched-text.js5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js19
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/postmessage-on-load-worker.js9
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/postmessage-to-client-worker.js10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/postmessage-transferables-worker.js24
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/postmessage-worker.js19
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js40
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js60
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/redirect-worker.js145
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/redirect.py27
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/referer-iframe.html39
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/referrer-policy-iframe.html32
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/register-closed-window-iframe.html19
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/register-iframe.html4
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/register-rewrite-worker.html32
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js96
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-scope.js120
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-script-url.js82
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-script.js121
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-security-error.js78
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/registration-worker.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/reject-install-worker.js3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/reply-to-message.html7
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/request-end-to-end-worker.js34
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/request-headers.py8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/resource-timing-worker.js12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/respond-then-throw-worker.js40
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html20
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js93
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/sample-worker-interceptor.js62
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/sample.html2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/sample.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/sample.txt1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html63
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py18
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js20
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html25
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/scope1/redirect.py6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/scope2/imported-module-script.js4
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/scope2/simple.txt1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/secure-context-service-worker.js21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/secure-context/sender.html1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/secure-context/window.html15
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/service-worker-csp-worker.py183
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/service-worker-header.py20
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js9
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/silence.ogabin0 -> 12983 bytes
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/simple-intercept-worker.js5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/simple.html3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/simple.txt1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js33
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/skip-waiting-worker.js21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/square.pngbin0 -> 18299 bytes
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/square.png.sub.headers2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/stalling-service-worker.js54
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/subdir/blank.html2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/subdir/simple.txt1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/success.py8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-001.html5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-frame.html2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/test-helpers.sub.js300
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/test-request-headers-worker.js10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/test-request-headers-worker.py21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/test-request-mode-worker.js10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/test-request-mode-worker.py22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/testharness-helpers.js136
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/trickle.py14
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/type-check-worker.js10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/unregister-controller-page.html16
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/unregister-immediately-helpers.js19
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/unregister-rewrite-worker.html18
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-claim-worker.py24
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-during-installation-worker.js61
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-during-installation-worker.py11
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-fetch-worker.py18
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py14
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-max-aged-worker.py30
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py9
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py15
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-nocookie-worker.py14
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-recovery-worker.py25
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-registration-with-type.py33
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-worker-from-file.py33
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update-worker.py62
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update/update-after-oneday.https.html8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/update_shell.py32
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/vtt-frame.html6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/websocket-worker.js35
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/websocket.js7
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/window-opener.html17
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/windowclient-navigate-worker.js75
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/worker-client-id-worker.js25
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js53
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js56
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/worker-load-interceptor.js16
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/worker-testharness.js49
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py20
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/xhr-content-length-worker.js22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/xhr-iframe.html23
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/xhr-response-url-worker.js32
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/xsl-base-url-worker.js12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/xslt-pass.xsl11
383 files changed, 10571 insertions, 0 deletions
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/404.py b/testing/web-platform/tests/service-workers/service-worker/resources/404.py
new file mode 100644
index 0000000000..1ee4af169e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html
new file mode 100644
index 0000000000..1e0c6209bf
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html
new file mode 100644
index 0000000000..16f7e7c60f
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-frame.py b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-frame.py
new file mode 100644
index 0000000000..a29ff9d413
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py
new file mode 100644
index 0000000000..30fbbbb535
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py
new file mode 100644
index 0000000000..04c12a6037
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html
new file mode 100644
index 0000000000..0122a00aa4
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html
new file mode 100644
index 0000000000..89509159a4
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/about-blank-replacement-worker.js
new file mode 100644
index 0000000000..f43598e41c
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/basic-module-2.js b/testing/web-platform/tests/service-workers/service-worker/resources/basic-module-2.js
new file mode 100644
index 0000000000..189b1c87fe
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/basic-module-2.js
@@ -0,0 +1 @@
+export default 'hello again!';
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/basic-module.js b/testing/web-platform/tests/service-workers/service-worker/resources/basic-module.js
new file mode 100644
index 0000000000..789a89bc63
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/basic-module.js
@@ -0,0 +1 @@
+export default 'hello!';
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/blank.html b/testing/web-platform/tests/service-workers/service-worker/resources/blank.html
new file mode 100644
index 0000000000..a3c3a4689a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/blank.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py b/testing/web-platform/tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py
new file mode 100644
index 0000000000..1931c77b67
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/bytecheck-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/bytecheck-worker.py
new file mode 100644
index 0000000000..10f3bceb4f
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html
new file mode 100644
index 0000000000..12ae1a8725
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html
new file mode 100644
index 0000000000..2fa15db61d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js
new file mode 100644
index 0000000000..f5ff7c234b
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html
new file mode 100644
index 0000000000..ad865b848f
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js
new file mode 100644
index 0000000000..ddc8bea7af
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html
new file mode 100644
index 0000000000..4150d7e685
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html
new file mode 100644
index 0000000000..92c5d15def
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js
new file mode 100644
index 0000000000..7080181c85
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/claim-worker.js
new file mode 100644
index 0000000000..1800407947
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/classic-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/classic-worker.js
new file mode 100644
index 0000000000..36a32b1a1f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/classic-worker.js
@@ -0,0 +1 @@
+importScripts('./imported-classic-script.js');
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/client-id-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/client-id-worker.js
new file mode 100644
index 0000000000..ec71b3458b
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/client-navigate-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/client-navigate-frame.html
new file mode 100644
index 0000000000..7e186f8ee7
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/client-navigate-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/client-navigate-worker.js
new file mode 100644
index 0000000000..6101d5d8f9
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/client-navigated-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/client-navigated-frame.html
new file mode 100644
index 0000000000..307f7f9ac6
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html
new file mode 100644
index 0000000000..00f6acede8
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js
new file mode 100644
index 0000000000..fd754f8250
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-frame-freeze.html b/testing/web-platform/tests/service-workers/service-worker/resources/clients-frame-freeze.html
new file mode 100644
index 0000000000..7468a660e9
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js
new file mode 100644
index 0000000000..0a1461b40e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-frame.html
new file mode 100644
index 0000000000..4324e6d405
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js
new file mode 100644
index 0000000000..fadef97037
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-client-types-worker.js
new file mode 100644
index 0000000000..0a1461b40e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html
new file mode 100644
index 0000000000..e16bb1116d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-frame.html
new file mode 100644
index 0000000000..27143d4b99
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-other-origin.html b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-other-origin.html
new file mode 100644
index 0000000000..6342fe04f4
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js
new file mode 100644
index 0000000000..5a46ff9cf4
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-worker.js
new file mode 100644
index 0000000000..8effa56c98
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html
new file mode 100644
index 0000000000..ee89a0d8b3
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js
new file mode 100644
index 0000000000..5a3f04d33a
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html
new file mode 100644
index 0000000000..7607b035de
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js
new file mode 100644
index 0000000000..1ae72fb894
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js
new file mode 100644
index 0000000000..f1559aca39
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/clients-matchall-worker.js
new file mode 100644
index 0000000000..13e111a2f9
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/cors-approved.txt b/testing/web-platform/tests/service-workers/service-worker/resources/cors-approved.txt
new file mode 100644
index 0000000000..1cd89bb14d
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/cors-approved.txt
@@ -0,0 +1 @@
+plaintext
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/cors-approved.txt.headers b/testing/web-platform/tests/service-workers/service-worker/resources/cors-approved.txt.headers
new file mode 100644
index 0000000000..f7985fd9bd
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/cors-denied.txt b/testing/web-platform/tests/service-workers/service-worker/resources/cors-denied.txt
new file mode 100644
index 0000000000..ff333bd97d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/create-blob-url-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/create-blob-url-worker.js
new file mode 100644
index 0000000000..57e4882c24
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/create-out-of-scope-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/create-out-of-scope-worker.html
new file mode 100644
index 0000000000..b51c451750
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/echo-content.py b/testing/web-platform/tests/service-workers/service-worker/resources/echo-content.py
new file mode 100644
index 0000000000..70ae4b6025
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/echo-cookie-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/echo-cookie-worker.py
new file mode 100644
index 0000000000..561f64a35a
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/echo-message-to-source-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/echo-message-to-source-worker.js
new file mode 100644
index 0000000000..bbbd35fb4f
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js
new file mode 100644
index 0000000000..ffcdb75128
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html
new file mode 100644
index 0000000000..7b8b257203
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html
new file mode 100644
index 0000000000..39149915cc
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html
new file mode 100644
index 0000000000..5e86f67735
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/embedded-content-from-server.html b/testing/web-platform/tests/service-workers/service-worker/resources/embedded-content-from-server.html
new file mode 100644
index 0000000000..ff50a9c752
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html
new file mode 100644
index 0000000000..2e2b923608
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/empty-but-slow-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/empty-but-slow-worker.js
new file mode 100644
index 0000000000..92abac7a38
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/empty-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/empty-worker.js
new file mode 100644
index 0000000000..49ceb2648a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/empty-worker.js
@@ -0,0 +1 @@
+// Do nothing.
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/empty.h2.js b/testing/web-platform/tests/service-workers/service-worker/resources/empty.h2.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/empty.h2.js
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/empty.html b/testing/web-platform/tests/service-workers/service-worker/resources/empty.html
new file mode 100644
index 0000000000..6feb11946b
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/empty.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<html>
+<body>
+hello world
+</body>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/empty.js b/testing/web-platform/tests/service-workers/service-worker/resources/empty.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/empty.js
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/enable-client-message-queue.html b/testing/web-platform/tests/service-workers/service-worker/resources/enable-client-message-queue.html
new file mode 100644
index 0000000000..512bd14bc6
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/end-to-end-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/end-to-end-worker.js
new file mode 100644
index 0000000000..d45a50556a
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/events-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/events-worker.js
new file mode 100644
index 0000000000..80a2188677
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js b/testing/web-platform/tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js
new file mode 100644
index 0000000000..8a975b0d2e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/extendable-event-waituntil.js b/testing/web-platform/tests/service-workers/service-worker/resources/extendable-event-waituntil.js
new file mode 100644
index 0000000000..20a9eb023f
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fail-on-fetch-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fail-on-fetch-worker.js
new file mode 100644
index 0000000000..517f289fbc
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-access-control-login.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-access-control-login.html
new file mode 100644
index 0000000000..ee296807ed
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-access-control.py b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-access-control.py
new file mode 100644
index 0000000000..446af87b24
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-access-control.py
@@ -0,0 +1,109 @@
+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:
+ 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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js
new file mode 100644
index 0000000000..17723dcdda
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html
new file mode 100644
index 0000000000..75d766c193
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js
new file mode 100644
index 0000000000..2aada3669e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js
new file mode 100644
index 0000000000..145952a22c
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html
new file mode 100644
index 0000000000..d88c5103d3
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-csp-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-csp-iframe.html
new file mode 100644
index 0000000000..33bf0416d5
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers
new file mode 100644
index 0000000000..5a1c7b941a
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-error-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-error-worker.js
new file mode 100644
index 0000000000..788252cf3b
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js
new file mode 100644
index 0000000000..a5a44a57c9
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html
new file mode 100644
index 0000000000..bf8a6d5ce5
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js
new file mode 100644
index 0000000000..dc3f1a1e98
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-handled-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-handled-worker.js
new file mode 100644
index 0000000000..53ee149374
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html
new file mode 100644
index 0000000000..f6c1919bbc
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js
new file mode 100644
index 0000000000..5bfe3a0bbd
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js
new file mode 100644
index 0000000000..376bdbed05
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js
@@ -0,0 +1,3 @@
+self.addEventListener('fetch', () => {
+ // Do nothing.
+});
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html
new file mode 100644
index 0000000000..0ebd1ca815
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js
new file mode 100644
index 0000000000..712c4b73c9
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js
new file mode 100644
index 0000000000..d3ba8a8df2
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js
new file mode 100644
index 0000000000..ff24aed128
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js
new file mode 100644
index 0000000000..b7307f29f5
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js
new file mode 100644
index 0000000000..f954e3a18a
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js
new file mode 100644
index 0000000000..056351322c
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html
new file mode 100644
index 0000000000..d15454daa5
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js
new file mode 100644
index 0000000000..0254e24f94
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js
new file mode 100644
index 0000000000..18da049d69
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-test-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-test-worker.js
new file mode 100644
index 0000000000..813f79d1b0
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js
new file mode 100644
index 0000000000..5903bab968
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html
new file mode 100644
index 0000000000..0d9ab6ff90
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html
new file mode 100644
index 0000000000..64a634e9db
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html
new file mode 100644
index 0000000000..be0b5c8f56
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html
new file mode 100644
index 0000000000..2831c381b5
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html
new file mode 100644
index 0000000000..504e104356
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css
new file mode 100644
index 0000000000..f14fcaae72
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js
new file mode 100644
index 0000000000..f3d6a73bdd
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css
new file mode 100644
index 0000000000..9a7545d070
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css
@@ -0,0 +1 @@
+#crossOriginCss { color: blue; }
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html
new file mode 100644
index 0000000000..3211f78084
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html
@@ -0,0 +1 @@
+#crossOriginHtml { color: red; }
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html
new file mode 100644
index 0000000000..9a4adedb84
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css
new file mode 100644
index 0000000000..55455bd5da
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css
@@ -0,0 +1 @@
+#sameOriginCss { color: blue; }
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html
new file mode 100644
index 0000000000..6fad4b9ff0
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html
@@ -0,0 +1 @@
+#sameOriginHtml { color: blue; }
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html
new file mode 100644
index 0000000000..c902366b02
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js
new file mode 100644
index 0000000000..a71e91216c
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html
new file mode 100644
index 0000000000..d117d0f55e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js
new file mode 100644
index 0000000000..3b028b24bd
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html
new file mode 100644
index 0000000000..07a084257a
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js
new file mode 100644
index 0000000000..110727bd52
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html
new file mode 100644
index 0000000000..e6e9380ba6
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py
new file mode 100644
index 0000000000..bf8df154a8
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js
new file mode 100644
index 0000000000..2bd59d7392
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html
new file mode 100644
index 0000000000..ffd76bfc49
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html
new file mode 100644
index 0000000000..86e9f4bb35
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-resources-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-resources-worker.js
new file mode 100644
index 0000000000..983cccb8db
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html
new file mode 100644
index 0000000000..b3ddec1a70
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js
new file mode 100644
index 0000000000..b8d3db99bc
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html
new file mode 100644
index 0000000000..900762ffc6
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js
new file mode 100644
index 0000000000..0d24ffc1f3
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js
new file mode 100644
index 0000000000..070e572f40
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js
new file mode 100644
index 0000000000..4e428374bc
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html
new file mode 100644
index 0000000000..5f09efe28d
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body></body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html
new file mode 100644
index 0000000000..c26eebee49
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js
new file mode 100644
index 0000000000..0301b12c18
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response.html b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response.html
new file mode 100644
index 0000000000..6d27cf19e5
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-response.js
new file mode 100644
index 0000000000..775efc0bbd
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js
new file mode 100644
index 0000000000..64c99c95d8
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers
new file mode 100644
index 0000000000..5ae4265418
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js
new file mode 100644
index 0000000000..20a8066527
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers
new file mode 100644
index 0000000000..123053b38c
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-variants-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-variants-worker.js
new file mode 100644
index 0000000000..b950b9a18a
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js
new file mode 100644
index 0000000000..92a96ff88f
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/form-poster.html b/testing/web-platform/tests/service-workers/service-worker/resources/form-poster.html
new file mode 100644
index 0000000000..cd11a30a5e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/frame-for-getregistrations.html b/testing/web-platform/tests/service-workers/service-worker/resources/frame-for-getregistrations.html
new file mode 100644
index 0000000000..7fc35f1891
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/get-resultingClientId-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/get-resultingClientId-worker.js
new file mode 100644
index 0000000000..f0e6c7beca
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html
new file mode 100644
index 0000000000..bcab35364d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html b/testing/web-platform/tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html
new file mode 100644
index 0000000000..3a61d7bb89
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/iframe-with-image.html b/testing/web-platform/tests/service-workers/service-worker/resources/iframe-with-image.html
new file mode 100644
index 0000000000..ce78840cb2
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/iframe-with-image.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<img src="square">
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js b/testing/web-platform/tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js
new file mode 100644
index 0000000000..d8a94ad46b
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py b/testing/web-platform/tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py
new file mode 100644
index 0000000000..8f0b68e5a3
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js
new file mode 100644
index 0000000000..f5eac9508c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js
@@ -0,0 +1 @@
+importScripts(`echo-cookie-worker.py${location.search}`);
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-mime-type-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/import-mime-type-worker.py
new file mode 100644
index 0000000000..b6e82f31d3
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-relative.xsl b/testing/web-platform/tests/service-workers/service-worker/resources/import-relative.xsl
new file mode 100644
index 0000000000..063a62d031
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js
new file mode 100644
index 0000000000..e9899d8e72
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404-after-update.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404-after-update.js
new file mode 100644
index 0000000000..b569346035
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404.js
new file mode 100644
index 0000000000..19c7a4b8e5
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-404.js
@@ -0,0 +1 @@
+importScripts('404.py');
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js
new file mode 100644
index 0000000000..b432854db8
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js
new file mode 100644
index 0000000000..fdabdafc63
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js
@@ -0,0 +1 @@
+importScripts('data:text/javascript,');
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js
new file mode 100644
index 0000000000..0fdcb0fcf8
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-echo.py b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-echo.py
new file mode 100644
index 0000000000..d38d660e65
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-get.py b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-get.py
new file mode 100644
index 0000000000..ab7b84e3e3
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js
new file mode 100644
index 0000000000..d4f1f3e26d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js
new file mode 100644
index 0000000000..56c04f0946
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js
@@ -0,0 +1 @@
+// empty script
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js
new file mode 100644
index 0000000000..f612ab8e6a
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js
new file mode 100644
index 0000000000..d02a45349c
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js
new file mode 100644
index 0000000000..b3b9bc46a0
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js
new file mode 100644
index 0000000000..e01664662e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-version.py b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-version.py
new file mode 100644
index 0000000000..cde28544e6
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/imported-classic-script.js b/testing/web-platform/tests/service-workers/service-worker/resources/imported-classic-script.js
new file mode 100644
index 0000000000..5fc5204051
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/imported-classic-script.js
@@ -0,0 +1 @@
+const imported = 'A classic script.';
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/imported-module-script.js b/testing/web-platform/tests/service-workers/service-worker/resources/imported-module-script.js
new file mode 100644
index 0000000000..56d196df04
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/imported-module-script.js
@@ -0,0 +1 @@
+export const imported = 'A module script.';
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/indexeddb-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/indexeddb-worker.js
new file mode 100644
index 0000000000..9add476838
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/install-event-type-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/install-event-type-worker.js
new file mode 100644
index 0000000000..1c94ae21ea
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/install-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/install-worker.html
new file mode 100644
index 0000000000..ed20cd4dca
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js b/testing/web-platform/tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js
new file mode 100644
index 0000000000..a3f239b654
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html
new file mode 100644
index 0000000000..04a9cb515e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/invalid-blobtype-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-blobtype-worker.js
new file mode 100644
index 0000000000..865dc30d42
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py
new file mode 100644
index 0000000000..05977c6ab0
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/invalid-chunked-encoding.py b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-chunked-encoding.py
new file mode 100644
index 0000000000..a8edd06b8d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/invalid-header-iframe.https.html b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-header-iframe.https.html
new file mode 100644
index 0000000000..8f0e6baca1
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/invalid-header-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/invalid-header-worker.js
new file mode 100644
index 0000000000..850874b811
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html
new file mode 100644
index 0000000000..cf2fa8d14f
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/iso-latin1-header-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/iso-latin1-header-worker.js
new file mode 100644
index 0000000000..d9ecca277b
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/load_worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/load_worker.js
new file mode 100644
index 0000000000..18c673bebc
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/loaded.html b/testing/web-platform/tests/service-workers/service-worker/resources/loaded.html
new file mode 100644
index 0000000000..0cabce69f8
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html
new file mode 100644
index 0000000000..b1e554d220
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js
new file mode 100644
index 0000000000..4b7aad0f58
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/location-setter.html b/testing/web-platform/tests/service-workers/service-worker/resources/location-setter.html
new file mode 100644
index 0000000000..f0ced06ec2
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/malformed-http-response.asis b/testing/web-platform/tests/service-workers/service-worker/resources/malformed-http-response.asis
new file mode 100644
index 0000000000..bc3c68d46d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/malformed-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/malformed-worker.py
new file mode 100644
index 0000000000..319b6e277b
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/message-vs-microtask.html b/testing/web-platform/tests/service-workers/service-worker/resources/message-vs-microtask.html
new file mode 100644
index 0000000000..2c45c59a47
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/mime-sniffing-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/mime-sniffing-worker.js
new file mode 100644
index 0000000000..5c34a7a49e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/mime-type-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/mime-type-worker.py
new file mode 100644
index 0000000000..92a602e634
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/mint-new-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/mint-new-worker.py
new file mode 100644
index 0000000000..ebee4ff8e8
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/module-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/module-worker.js
new file mode 100644
index 0000000000..385fe71015
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/module-worker.js
@@ -0,0 +1 @@
+import * as module from './imported-module-script.js';
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image-iframe.html
new file mode 100644
index 0000000000..c59b95594f
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image-worker.js
new file mode 100644
index 0000000000..a38fe54d34
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image.py b/testing/web-platform/tests/service-workers/service-worker/resources/multipart-image.py
new file mode 100644
index 0000000000..9a3c035f49
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/navigate-window-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/navigate-window-worker.js
new file mode 100644
index 0000000000..f9617439fc
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/navigation-headers-server.py b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-headers-server.py
new file mode 100644
index 0000000000..5b2e044f8b
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js
new file mode 100644
index 0000000000..39f11baf8c
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-body.py b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-body.py
new file mode 100644
index 0000000000..d10329e783
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html
new file mode 100644
index 0000000000..d82571d1a3
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py
new file mode 100644
index 0000000000..9b90b14695
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-scope1.py b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-scope1.py
new file mode 100644
index 0000000000..9b90b14695
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-scope2.py b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-scope2.py
new file mode 100644
index 0000000000..9b90b14695
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html
new file mode 100644
index 0000000000..40e27c630d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js
new file mode 100644
index 0000000000..6f2a8ae1d7
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js
new file mode 100644
index 0000000000..79c54088ff
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/navigation-timing-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/navigation-timing-worker.js
new file mode 100644
index 0000000000..8539b40066
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html
new file mode 100644
index 0000000000..fc048e288e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/nested-blob-url-workers.html b/testing/web-platform/tests/service-workers/service-worker/resources/nested-blob-url-workers.html
new file mode 100644
index 0000000000..f0eafcd3e0
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/nested-iframe-parent.html b/testing/web-platform/tests/service-workers/service-worker/resources/nested-iframe-parent.html
new file mode 100644
index 0000000000..115ab26e12
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/nested-parent.html b/testing/web-platform/tests/service-workers/service-worker/resources/nested-parent.html
new file mode 100644
index 0000000000..b4832d461d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html
new file mode 100644
index 0000000000..3fad2c9228
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/nested_load_worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/nested_load_worker.js
new file mode 100644
index 0000000000..ef0ed8fc70
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/no-dynamic-import.js b/testing/web-platform/tests/service-workers/service-worker/resources/no-dynamic-import.js
new file mode 100644
index 0000000000..ecedd6c5d7
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/notification_icon.py b/testing/web-platform/tests/service-workers/service-worker/resources/notification_icon.py
new file mode 100644
index 0000000000..71f5a9d488
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html
new file mode 100644
index 0000000000..5a20a58ab1
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html
new file mode 100644
index 0000000000..0aeb81951e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html
new file mode 100644
index 0000000000..5c8ab79a50
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js
new file mode 100644
index 0000000000..7c97014fd0
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js
new file mode 100644
index 0000000000..0bd9d318b2
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js
new file mode 100644
index 0000000000..d56c951139
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js
new file mode 100644
index 0000000000..eb12ae862c
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js
new file mode 100644
index 0000000000..1e88ac5c4e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js b/testing/web-platform/tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js
new file mode 100644
index 0000000000..65b02b12b3
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js b/testing/web-platform/tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js
new file mode 100644
index 0000000000..b905d55598
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js
new file mode 100644
index 0000000000..6729ab61a3
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js
new file mode 100644
index 0000000000..c2c499ab1a
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js
new file mode 100644
index 0000000000..7667c2781d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js
new file mode 100644
index 0000000000..8f56d1bf14
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js
new file mode 100644
index 0000000000..cc2f6d7e5e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js
new file mode 100644
index 0000000000..964483f2f4
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js
new file mode 100644
index 0000000000..6cb8f6ede6
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js
new file mode 100644
index 0000000000..6f439aee94
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html
new file mode 100644
index 0000000000..9c6d8bd504
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js
new file mode 100644
index 0000000000..4fbe35df27
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html
new file mode 100644
index 0000000000..f31ac9b5c4
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-frame.html
new file mode 100644
index 0000000000..a57aacec7c
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-large.js b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-large.js
new file mode 100644
index 0000000000..7e1c598efc
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-small.js b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-small.js
new file mode 100644
index 0000000000..8b89098575
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-small.js
@@ -0,0 +1,3 @@
+function runScript() {
+ throw new Error("Intentional error.");
+}
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-sw.js b/testing/web-platform/tests/service-workers/service-worker/resources/opaque-script-sw.js
new file mode 100644
index 0000000000..4d882c617d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/other.html b/testing/web-platform/tests/service-workers/service-worker/resources/other.html
new file mode 100644
index 0000000000..b9f3504387
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/override_assert_object_equals.js b/testing/web-platform/tests/service-workers/service-worker/resources/override_assert_object_equals.js
new file mode 100644
index 0000000000..835046d472
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html
new file mode 100644
index 0000000000..25ddf60145
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html
new file mode 100644
index 0000000000..00b3412c41
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js
new file mode 100644
index 0000000000..767dbf4432
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html
new file mode 100644
index 0000000000..8e90609da2
--- /dev/null
+++ b/testing/web-platform/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);
+ 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);
+ fetch_tests_from_window(credentialless_frame.contentWindow);
+});
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js
new file mode 100644
index 0000000000..767dbf4432
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html
new file mode 100644
index 0000000000..12b048ee04
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html
new file mode 100644
index 0000000000..d05fef48bf
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html
new file mode 100644
index 0000000000..f748e2f78d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html
new file mode 100644
index 0000000000..747c058946
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html
new file mode 100644
index 0000000000..7a2c36693e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html
new file mode 100644
index 0000000000..1b7f671b37
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html
new file mode 100644
index 0000000000..86384ce280
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-storage-sw.js b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-storage-sw.js
new file mode 100644
index 0000000000..00f7979810
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-utils.js b/testing/web-platform/tests/service-workers/service-worker/resources/partitioned-utils.js
new file mode 100644
index 0000000000..22e90beaec
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/pass-through-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/pass-through-worker.js
new file mode 100644
index 0000000000..5eaf48d588
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/pass.txt b/testing/web-platform/tests/service-workers/service-worker/resources/pass.txt
new file mode 100644
index 0000000000..7ef22e9a43
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/pass.txt
@@ -0,0 +1 @@
+PASS
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/performance-timeline-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/performance-timeline-worker.js
new file mode 100644
index 0000000000..6c6dfcbd28
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-blob-url.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-blob-url.js
new file mode 100644
index 0000000000..9095194a4c
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js
new file mode 100644
index 0000000000..87a4500d75
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-echo-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-echo-worker.js
new file mode 100644
index 0000000000..f088ad1278
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-fetched-text.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-fetched-text.js
new file mode 100644
index 0000000000..9fc67171d0
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js
new file mode 100644
index 0000000000..7af935f4f8
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-on-load-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-on-load-worker.js
new file mode 100644
index 0000000000..c2b0bcb8bf
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-to-client-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-to-client-worker.js
new file mode 100644
index 0000000000..1791306358
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-transferables-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-transferables-worker.js
new file mode 100644
index 0000000000..d35c1c952b
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/postmessage-worker.js
new file mode 100644
index 0000000000..858cf04267
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js
new file mode 100644
index 0000000000..cab6058339
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js
new file mode 100644
index 0000000000..7580b0b68a
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/redirect-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/redirect-worker.js
new file mode 100644
index 0000000000..82e21fc26f
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/redirect.py b/testing/web-platform/tests/service-workers/service-worker/resources/redirect.py
new file mode 100644
index 0000000000..bd559d5d1e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/referer-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/referer-iframe.html
new file mode 100644
index 0000000000..295ff45671
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/referrer-policy-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/referrer-policy-iframe.html
new file mode 100644
index 0000000000..9ef3cd19a9
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/register-closed-window-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/register-closed-window-iframe.html
new file mode 100644
index 0000000000..117f25477b
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/register-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/register-iframe.html
new file mode 100644
index 0000000000..f5a040e41d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/register-rewrite-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/register-rewrite-worker.html
new file mode 100644
index 0000000000..bf06317ad9
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js
new file mode 100644
index 0000000000..037e6c0fde
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-scope.js b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-scope.js
new file mode 100644
index 0000000000..30c424b2b4
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-script-url.js b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-script-url.js
new file mode 100644
index 0000000000..55cbe6fa95
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-script.js b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-script.js
new file mode 100644
index 0000000000..e5bdaf4291
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-security-error.js b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-security-error.js
new file mode 100644
index 0000000000..c45fbd4578
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/registration-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/registration-worker.js
new file mode 100644
index 0000000000..44d1d2774a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/registration-worker.js
@@ -0,0 +1 @@
+// empty for now
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/reject-install-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/reject-install-worker.js
new file mode 100644
index 0000000000..41f07fd5db
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/reply-to-message.html b/testing/web-platform/tests/service-workers/service-worker/resources/reply-to-message.html
new file mode 100644
index 0000000000..8a70e2ad93
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/request-end-to-end-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/request-end-to-end-worker.js
new file mode 100644
index 0000000000..6bd2b72137
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/request-headers.py b/testing/web-platform/tests/service-workers/service-worker/resources/request-headers.py
new file mode 100644
index 0000000000..6ab148e22e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html b/testing/web-platform/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html
new file mode 100644
index 0000000000..384c29b536
--- /dev/null
+++ b/testing/web-platform/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.jpg">
+<img src='missing.jpg?SWRespondsWithFetch'>
+<script src='empty-worker.js'></script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/resource-timing-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/resource-timing-worker.js
new file mode 100644
index 0000000000..b74e8cd6a2
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/respond-then-throw-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/respond-then-throw-worker.js
new file mode 100644
index 0000000000..adb48de69e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html
new file mode 100644
index 0000000000..7be3148794
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js
new file mode 100644
index 0000000000..c602109bc6
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp b/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp
new file mode 100644
index 0000000000..b9c28f51f9
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp
@@ -0,0 +1 @@
+callback('OK');
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/sample-worker-interceptor.js b/testing/web-platform/tests/service-workers/service-worker/resources/sample-worker-interceptor.js
new file mode 100644
index 0000000000..c06f8dd77b
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/sample.html b/testing/web-platform/tests/service-workers/service-worker/resources/sample.html
new file mode 100644
index 0000000000..12a179980d
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/sample.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body>Hello world
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/sample.js b/testing/web-platform/tests/service-workers/service-worker/resources/sample.js
new file mode 100644
index 0000000000..b8889db05d
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/sample.js
@@ -0,0 +1 @@
+var hello = "world";
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/sample.txt b/testing/web-platform/tests/service-workers/service-worker/resources/sample.txt
new file mode 100644
index 0000000000..802992c422
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/sample.txt
@@ -0,0 +1 @@
+Hello world
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html
new file mode 100644
index 0000000000..239fa73303
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py b/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py
new file mode 100644
index 0000000000..0281b6c275
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js
new file mode 100644
index 0000000000..4035a8b19b
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html
new file mode 100644
index 0000000000..1d682e47ef
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js b/testing/web-platform/tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js
new file mode 100644
index 0000000000..ae681ba30e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js b/testing/web-platform/tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js
new file mode 100644
index 0000000000..e28505249c
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/scope1/redirect.py b/testing/web-platform/tests/service-workers/service-worker/resources/scope1/redirect.py
new file mode 100644
index 0000000000..bb4c874aac
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py b/testing/web-platform/tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py
new file mode 100644
index 0000000000..5f785b5cc2
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/scope2/imported-module-script.js b/testing/web-platform/tests/service-workers/service-worker/resources/scope2/imported-module-script.js
new file mode 100644
index 0000000000..a18e704a3c
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/scope2/simple.txt b/testing/web-platform/tests/service-workers/service-worker/resources/scope2/simple.txt
new file mode 100644
index 0000000000..cd876676e8
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/scope2/simple.txt
@@ -0,0 +1 @@
+a simple text file (scope2/)
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py b/testing/web-platform/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py
new file mode 100644
index 0000000000..bb4c874aac
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/secure-context-service-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/secure-context-service-worker.js
new file mode 100644
index 0000000000..5ba99f0753
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/secure-context/sender.html b/testing/web-platform/tests/service-workers/service-worker/resources/secure-context/sender.html
new file mode 100644
index 0000000000..05e58822a8
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/secure-context/sender.html
@@ -0,0 +1 @@
+<script>window.parent.postMessage('network', '*');</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/secure-context/window.html b/testing/web-platform/tests/service-workers/service-worker/resources/secure-context/window.html
new file mode 100644
index 0000000000..071a507cb3
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-csp-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-csp-worker.py
new file mode 100644
index 0000000000..35a46964a7
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-header.py b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-header.py
new file mode 100644
index 0000000000..d64a9d2494
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js
new file mode 100644
index 0000000000..680e07ff58
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js
new file mode 100644
index 0000000000..5ff3900101
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js
@@ -0,0 +1 @@
+postMessage('LOADED_FROM_NETWORK');
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js
new file mode 100644
index 0000000000..6b43a37696
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js
new file mode 100644
index 0000000000..e570958701
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/silence.oga b/testing/web-platform/tests/service-workers/service-worker/resources/silence.oga
new file mode 100644
index 0000000000..af59188043
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/silence.oga
Binary files differ
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/simple-intercept-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/simple-intercept-worker.js
new file mode 100644
index 0000000000..f8b5f8c5cb
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers b/testing/web-platform/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers
new file mode 100644
index 0000000000..a17a9a3a12
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers
@@ -0,0 +1 @@
+Content-Type: application/javascript
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/simple.html b/testing/web-platform/tests/service-workers/service-worker/resources/simple.html
new file mode 100644
index 0000000000..0c3e3e7870
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/simple.txt b/testing/web-platform/tests/service-workers/service-worker/resources/simple.txt
new file mode 100644
index 0000000000..9e3cb91fb9
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/simple.txt
@@ -0,0 +1 @@
+a simple text file
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js
new file mode 100644
index 0000000000..6f7008bddc
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/skip-waiting-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/skip-waiting-worker.js
new file mode 100644
index 0000000000..3fc1d1e237
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/square.png b/testing/web-platform/tests/service-workers/service-worker/resources/square.png
new file mode 100644
index 0000000000..01c9666a8d
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/square.png
Binary files differ
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/square.png.sub.headers b/testing/web-platform/tests/service-workers/service-worker/resources/square.png.sub.headers
new file mode 100644
index 0000000000..7341132745
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/stalling-service-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/stalling-service-worker.js
new file mode 100644
index 0000000000..fdf1e6cac0
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/subdir/blank.html b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/blank.html
new file mode 100644
index 0000000000..a3c3a4689a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/blank.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py
new file mode 100644
index 0000000000..f745d7ae46
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/subdir/simple.txt b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/simple.txt
new file mode 100644
index 0000000000..86bcdd7dc5
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/simple.txt
@@ -0,0 +1 @@
+a simple text file (subdir/)
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py b/testing/web-platform/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py
new file mode 100644
index 0000000000..bb4c874aac
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/success.py b/testing/web-platform/tests/service-workers/service-worker/resources/success.py
new file mode 100644
index 0000000000..a0269918ee
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html
new file mode 100644
index 0000000000..59fb524049
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-001.html b/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-001.html
new file mode 100644
index 0000000000..9a93d3b370
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/svg-target-reftest-frame.html
new file mode 100644
index 0000000000..d6fc820f78
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/test-helpers.sub.js b/testing/web-platform/tests/service-workers/service-worker/resources/test-helpers.sub.js
new file mode 100644
index 0000000000..74301523e7
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/test-request-headers-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/test-request-headers-worker.js
new file mode 100644
index 0000000000..566e2e9984
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/test-request-headers-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/test-request-headers-worker.py
new file mode 100644
index 0000000000..78a93356b7
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/test-request-mode-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/test-request-mode-worker.js
new file mode 100644
index 0000000000..566e2e9984
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/test-request-mode-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/test-request-mode-worker.py
new file mode 100644
index 0000000000..8449841a99
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/testharness-helpers.js b/testing/web-platform/tests/service-workers/service-worker/resources/testharness-helpers.js
new file mode 100644
index 0000000000..b1a5b960e0
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/trickle.py b/testing/web-platform/tests/service-workers/service-worker/resources/trickle.py
new file mode 100644
index 0000000000..6423f7f36f
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/type-check-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/type-check-worker.js
new file mode 100644
index 0000000000..1779e2323d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/unregister-controller-page.html b/testing/web-platform/tests/service-workers/service-worker/resources/unregister-controller-page.html
new file mode 100644
index 0000000000..18a95ee892
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/unregister-immediately-helpers.js b/testing/web-platform/tests/service-workers/service-worker/resources/unregister-immediately-helpers.js
new file mode 100644
index 0000000000..91a30de5b7
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/unregister-rewrite-worker.html b/testing/web-platform/tests/service-workers/service-worker/resources/unregister-rewrite-worker.html
new file mode 100644
index 0000000000..f5d0367877
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update-claim-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-claim-worker.py
new file mode 100644
index 0000000000..64914a9dfe
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update-during-installation-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/update-during-installation-worker.js
new file mode 100644
index 0000000000..f1997bd824
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update-during-installation-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-during-installation-worker.py
new file mode 100644
index 0000000000..3e15926185
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update-fetch-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-fetch-worker.py
new file mode 100644
index 0000000000..02cbb42dc6
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py
new file mode 100644
index 0000000000..7cc5a6561e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update-max-aged-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-max-aged-worker.py
new file mode 100644
index 0000000000..4f879069ef
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py
new file mode 100644
index 0000000000..1547cb5235
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py
new file mode 100644
index 0000000000..1c447e118e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update-nocookie-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-nocookie-worker.py
new file mode 100644
index 0000000000..34eff0263c
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update-recovery-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-recovery-worker.py
new file mode 100644
index 0000000000..9ac7ce7c75
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update-registration-with-type.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-registration-with-type.py
new file mode 100644
index 0000000000..3cabc0fb46
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js
new file mode 100644
index 0000000000..d43f6b2f5c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js
@@ -0,0 +1 @@
+// Hello world!
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js
new file mode 100644
index 0000000000..30c8783a70
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update-worker-from-file.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-worker-from-file.py
new file mode 100644
index 0000000000..ac0850f476
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update-worker.py b/testing/web-platform/tests/service-workers/service-worker/resources/update-worker.py
new file mode 100644
index 0000000000..5638a8849c
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update/update-after-oneday.https.html b/testing/web-platform/tests/service-workers/service-worker/resources/update/update-after-oneday.https.html
new file mode 100644
index 0000000000..9d4c982721
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/update_shell.py b/testing/web-platform/tests/service-workers/service-worker/resources/update_shell.py
new file mode 100644
index 0000000000..2070509437
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/vtt-frame.html b/testing/web-platform/tests/service-workers/service-worker/resources/vtt-frame.html
new file mode 100644
index 0000000000..c3ac8034e1
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js
new file mode 100644
index 0000000000..af85a73ad3
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/websocket-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/websocket-worker.js
new file mode 100644
index 0000000000..bb2dc81e55
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/websocket.js b/testing/web-platform/tests/service-workers/service-worker/resources/websocket.js
new file mode 100644
index 0000000000..fc6abd283a
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/window-opener.html b/testing/web-platform/tests/service-workers/service-worker/resources/window-opener.html
new file mode 100644
index 0000000000..32d0744646
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/windowclient-navigate-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/windowclient-navigate-worker.js
new file mode 100644
index 0000000000..383f66631d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/worker-client-id-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/worker-client-id-worker.js
new file mode 100644
index 0000000000..f592629d07
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js b/testing/web-platform/tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js
new file mode 100644
index 0000000000..a81bb3dd6e
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js b/testing/web-platform/tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js
new file mode 100644
index 0000000000..d36b0b6da6
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js b/testing/web-platform/tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js
new file mode 100644
index 0000000000..b7e6d81b09
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/worker-load-interceptor.js b/testing/web-platform/tests/service-workers/service-worker/resources/worker-load-interceptor.js
new file mode 100644
index 0000000000..ebc0db67aa
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/worker-testharness.js b/testing/web-platform/tests/service-workers/service-worker/resources/worker-testharness.js
new file mode 100644
index 0000000000..73e97be1ea
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py b/testing/web-platform/tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py
new file mode 100644
index 0000000000..4ed5beea74
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/xhr-content-length-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/xhr-content-length-worker.js
new file mode 100644
index 0000000000..604deece2d
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/xhr-iframe.html b/testing/web-platform/tests/service-workers/service-worker/resources/xhr-iframe.html
new file mode 100644
index 0000000000..4c57bbbc65
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/xhr-response-url-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/xhr-response-url-worker.js
new file mode 100644
index 0000000000..906ad5005b
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml b/testing/web-platform/tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml
new file mode 100644
index 0000000000..065a07acb2
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/xsl-base-url-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/xsl-base-url-worker.js
new file mode 100644
index 0000000000..50e2b1842f
--- /dev/null
+++ b/testing/web-platform/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/testing/web-platform/tests/service-workers/service-worker/resources/xslt-pass.xsl b/testing/web-platform/tests/service-workers/service-worker/resources/xslt-pass.xsl
new file mode 100644
index 0000000000..2cd7f2f8f8
--- /dev/null
+++ b/testing/web-platform/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>