summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/service-workers
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /testing/web-platform/tests/service-workers
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/service-workers')
-rw-r--r--testing/web-platform/tests/service-workers/META.yml6
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/META.yml3
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-abort.https.any.js81
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-add.https.any.js368
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-delete.https.any.js164
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html75
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-keys.https.any.js212
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-match.https.any.js437
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-matchAll.https.any.js244
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-put.https.any.js411
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js64
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-storage-keys.https.any.js35
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-storage-match.https.any.js245
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-storage.https.any.js239
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/common.https.window.js44
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html17
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/credentials.https.html46
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cross-partition.https.tentative.html269
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/blank.html2
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js22
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/common-worker.js15
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/credentials-iframe.html38
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/credentials-worker.js59
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/fetch-status.py2
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/iframe.html18
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/simple.txt1
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/test-helpers.js272
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/vary.py25
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/sandboxed-iframes.https.html66
-rw-r--r--testing/web-platform/tests/service-workers/idlharness.https.any.js53
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html88
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html11
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html226
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js14
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html32
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html83
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html107
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js197
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js36
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js23
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js18
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js78
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js15
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js15
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js4
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js33
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js139
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html0
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js25
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js22
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py16
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html31
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html139
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html48
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/about-blank-replacement.https.html181
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html33
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/activation-after-registration.https.html28
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/activation.https.html168
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/active.https.html50
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/claim-affect-other-registration.https.html136
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/claim-fetch.https.html90
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/claim-not-using-registration.https.html131
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html71
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/claim-using-registration.https.html103
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/claim-with-redirect.https.html59
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/claim-worker-fetch.https.html83
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/client-id.https.html60
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/client-navigate.https.html107
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html29
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/clients-get-client-types.https.html108
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/clients-get-cross-origin.https.html69
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/clients-get-resultingClientId.https.html177
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/clients-get.https.html154
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html85
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/clients-matchall-client-types.https.html92
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html67
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/clients-matchall-frozen.https.html64
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html117
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html24
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/clients-matchall-order.https.html427
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/clients-matchall.https.html50
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/controller-on-disconnect.https.html40
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/controller-on-load.https.html46
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/controller-on-reload.https.html58
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html56
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/credentials.https.html100
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/data-iframe.html25
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/data-transfer-files.https.html41
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html40
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/detached-context.https.html124
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html104
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html120
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/extendable-event-waituntil.https.html140
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-audio-tainting.https.html47
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html57
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html16
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html16
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html17
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html92
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html17
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html30
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-cors-xhr.https.html49
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-csp.https.html138
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-error.https.html33
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-add-async.https.html11
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html71
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html73
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-handled.https.html86
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html31
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-network-error.https.html44
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-redirect.https.html1038
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html274
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html44
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html24
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html82
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html62
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html23
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html88
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html46
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html37
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html37
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html122
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event-within-sw.https.html53
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event.https.h2.html112
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-event.https.html1000
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-frame-resource.https.html236
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-header-visibility.https.html54
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-request-css-base-url.https.html87
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html81
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-request-css-images.https.html214
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-request-fallback.https.html282
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html55
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-request-redirect.https.html385
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-request-resources.https.html302
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js19
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html41
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html53
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr.https.html75
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-response-taint.https.html223
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-response-xhr.https.html50
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/fetch-waits-for-activate.https.html128
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/getregistration.https.html108
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/getregistrations.https.html134
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/global-serviceworker.https.any.js53
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/historical.https.any.js5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html49
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html23
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/import-scripts-cross-origin.https.html18
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/import-scripts-data-url.https.html18
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/import-scripts-mime-types.https.html30
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/import-scripts-redirect.https.html55
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/import-scripts-resource-map.https.html34
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/import-scripts-updated-flag.https.html83
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/indexeddb.https.html78
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/install-event-type.https.html30
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/installing.https.html48
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/interface-requirements-sw.https.html16
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/invalid-blobtype.https.html40
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/invalid-header.https.html39
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/iso-latin1-header.https.html40
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/local-url-inherit-controller.https.html115
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/mime-sniffing.https.html24
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/multi-globals/current/current.https.html2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/multi-globals/current/test-sw.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html20
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/multi-globals/test-sw.js1
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/multi-globals/url-parsing.https.html73
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/multipart-image.https.html68
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/multiple-register.https.html117
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/multiple-update.https.html94
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigate-window.https.html151
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-headers.https.html819
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html42
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html25
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html25
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/get-state.https.html217
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html20
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/redirect.https.html93
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/request-headers.https.html41
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html92
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis6
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js11
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py19
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/cookie.py20
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html0
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js15
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/helpers.js5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py38
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js35
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py14
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py19
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js37
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html34
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js8
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js40
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html61
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html67
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-redirect-body.https.html53
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-redirect-resolution.https.html58
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-redirect-to-http.https.html25
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-redirect.https.html846
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-sets-cookie.https.html133
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-timing-extended.https.html55
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-timing.https.html77
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/nested-blob-url-workers.https.html42
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/next-hop-protocol.https.html49
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js7
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/no-dynamic-import.any.js3
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/onactivate-script-error.https.html74
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/oninstall-script-error.https.html72
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/opaque-response-preloaded.https.html50
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/opaque-script.https.html71
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/partitioned-claim.tentative.https.html74
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html119
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html99
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html65
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/partitioned.tentative.https.html188
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/performance-timeline.https.html49
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/postmessage-blob-url.https.html33
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html50
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html43
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html212
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/postmessage-to-client.https.html42
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/postmessage.https.html202
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/ready.https.window.js223
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/redirected-response.https.html471
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/referer.https.html40
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/referrer-policy-header.https.html67
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html64
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/register-closed-window.https.html35
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/register-default-scope.https.html69
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html233
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html57
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/registration-basic.https.html39
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/registration-end-to-end.https.html88
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/registration-events.https.html42
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/registration-iframe.https.html116
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/registration-mime-types.https.html10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/registration-schedule-job.https.html107
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/registration-scope-module-static-import.https.html41
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/registration-scope.https.html9
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/registration-script-module.https.html13
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/registration-script-url.https.html9
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/registration-script.https.html12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/registration-security-error.https.html9
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/registration-service-worker-attributes.https.html72
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/registration-updateviacache.https.html204
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/rejections.https.html21
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/request-end-to-end.https.html40
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resource-timing-bodySize.https.html55
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resource-timing-cross-origin.https.html46
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html121
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resource-timing.sub.https.html150
-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
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/respond-with-body-accessed-response.https.html54
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/same-site-cookies.https.html496
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html536
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html120
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/secure-context.https.html57
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/service-worker-csp-connect.https.html10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/service-worker-csp-default.https.html10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/service-worker-csp-script.https.html10
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/service-worker-header.https.html23
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/serviceworker-message-event-historical.https.html45
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html26
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/skip-waiting-installed.https.html70
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/skip-waiting-using-registration.https.html66
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/skip-waiting-without-client.https.html12
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html44
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/skip-waiting.https.html58
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/state.https.html74
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/svg-target-reftest.https.html28
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/synced-state.https.html93
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/uncontrolled-page.https.html39
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/unregister-controller.https.html108
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/unregister-immediately-before-installed.https.html57
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html50
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/unregister-immediately.https.html134
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/unregister-then-register-new-script.https.html136
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/unregister-then-register.https.html107
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/unregister.https.html40
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html91
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update-after-navigation-redirect.https.html74
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update-after-oneday.https.html51
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update-bytecheck-cors-import.https.html92
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update-bytecheck.https.html92
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update-import-scripts.https.html135
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update-missing-import-scripts.https.html33
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update-module-request-mode.https.html45
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update-no-cache-request-headers.https.html48
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update-not-allowed.https.html140
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update-on-navigation.https.html20
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update-recovery.https.html73
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update-registration-with-type.https.html208
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update-result.https.html23
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/update.https.html164
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/waiting.https.html47
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/websocket-in-service-worker.https.html27
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/websocket.https.html45
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/webvtt-cross-origin.https.html175
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/windowclient-navigate.https.html190
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/worker-client-id.https.html58
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html132
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/worker-interception-redirect.https.html212
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/worker-interception.https.html244
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/xhr-content-length.https.window.js55
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/xhr-response-url.https.html103
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/xsl-base-url.https.html32
709 files changed, 39214 insertions, 0 deletions
diff --git a/testing/web-platform/tests/service-workers/META.yml b/testing/web-platform/tests/service-workers/META.yml
new file mode 100644
index 0000000000..03a0dd0fe1
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/META.yml
@@ -0,0 +1,6 @@
+spec: https://w3c.github.io/ServiceWorker/
+suggested_reviewers:
+ - asutherland
+ - mkruisselbrink
+ - mattto
+ - wanderview
diff --git a/testing/web-platform/tests/service-workers/cache-storage/META.yml b/testing/web-platform/tests/service-workers/cache-storage/META.yml
new file mode 100644
index 0000000000..bf34474f74
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/META.yml
@@ -0,0 +1,3 @@
+suggested_reviewers:
+ - inexorabletash
+ - wanderview
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-abort.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-abort.https.any.js
new file mode 100644
index 0000000000..960d1bb1bf
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-abort.https.any.js
@@ -0,0 +1,81 @@
+// META: title=Cache Storage: Abort
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: script=/common/utils.js
+// META: timeout=long
+
+// We perform the same tests on put, add, addAll. Parameterise the tests to
+// reduce repetition.
+const methodsToTest = {
+ put: async (cache, request) => {
+ const response = await fetch(request);
+ return cache.put(request, response);
+ },
+ add: async (cache, request) => cache.add(request),
+ addAll: async (cache, request) => cache.addAll([request]),
+};
+
+for (const method in methodsToTest) {
+ const perform = methodsToTest[method];
+
+ cache_test(async (cache, test) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+ const request = new Request('../resources/simple.txt', { signal });
+ return promise_rejects_dom(test, 'AbortError', perform(cache, request),
+ `${method} should reject`);
+ }, `${method}() on an already-aborted request should reject with AbortError`);
+
+ cache_test(async (cache, test) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request('../resources/simple.txt', { signal });
+ const promise = perform(cache, request);
+ controller.abort();
+ return promise_rejects_dom(test, 'AbortError', promise,
+ `${method} should reject`);
+ }, `${method}() synchronously followed by abort should reject with ` +
+ `AbortError`);
+
+ cache_test(async (cache, test) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ const request = new Request(
+ `../../../fetch/api/resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`,
+ { signal });
+
+ const promise = perform(cache, request);
+
+ // Wait for the server to start sending the response body.
+ let opened = false;
+ do {
+ // Normally only one fetch to 'stash-take' is needed, but the fetches
+ // will be served in reverse order sometimes
+ // (i.e., 'stash-take' gets served before 'infinite-slow-response').
+
+ const response =
+ await fetch(`../../../fetch/api/resources/stash-take.py?key=${stateKey}`);
+ const body = await response.json();
+ if (body === 'open') opened = true;
+ } while (!opened);
+
+ // Sadly the above loop cannot guarantee that the browser has started
+ // processing the response body. This delay is needed to make the test
+ // failures non-flaky in Chrome version 66. My deepest apologies.
+ await new Promise(resolve => setTimeout(resolve, 250));
+
+ controller.abort();
+
+ await promise_rejects_dom(test, 'AbortError', promise,
+ `${method} should reject`);
+
+ // infinite-slow-response.py doesn't know when to stop.
+ return fetch(`../../../fetch/api/resources/stash-put.py?key=${abortKey}`);
+ }, `${method}() followed by abort after headers received should reject ` +
+ `with AbortError`);
+}
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-add.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-add.https.any.js
new file mode 100644
index 0000000000..eca516abd5
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-add.https.any.js
@@ -0,0 +1,368 @@
+// META: title=Cache.add and Cache.addAll
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+const { REMOTE_HOST } = get_host_info();
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add(),
+ 'Cache.add should throw a TypeError when no arguments are given.');
+ }, 'Cache.add called with no arguments');
+
+cache_test(function(cache) {
+ return cache.add('./resources/simple.txt')
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ return cache.match('./resources/simple.txt');
+ })
+ .then(function(response) {
+ assert_class_string(response, 'Response',
+ 'Cache.add should put a resource in the cache.');
+ return response.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'a simple text file\n',
+ 'Cache.add should retrieve the correct body.');
+ });
+ }, 'Cache.add called with relative URL specified as a string');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('javascript://this-is-not-http-mmkay'),
+ 'Cache.add should throw a TypeError for non-HTTP/HTTPS URLs.');
+ }, 'Cache.add called with non-HTTP/HTTPS URL');
+
+cache_test(function(cache) {
+ var request = new Request('./resources/simple.txt');
+ return cache.add(request)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ });
+ }, 'Cache.add called with Request object');
+
+cache_test(function(cache, test) {
+ var request = new Request('./resources/simple.txt',
+ {method: 'POST', body: 'This is a body.'});
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add(request),
+ 'Cache.add should throw a TypeError for non-GET requests.');
+ }, 'Cache.add called with POST request');
+
+cache_test(function(cache) {
+ var request = new Request('./resources/simple.txt');
+ return cache.add(request)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ })
+ .then(function() {
+ return cache.add(request);
+ })
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ });
+ }, 'Cache.add called twice with the same Request object');
+
+cache_test(function(cache) {
+ var request = new Request('./resources/simple.txt');
+ return request.text()
+ .then(function() {
+ assert_false(request.bodyUsed);
+ })
+ .then(function() {
+ return cache.add(request);
+ });
+ }, 'Cache.add with request with null body (not consumed)');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('./resources/fetch-status.py?status=206'),
+ 'Cache.add should reject on partial response');
+ }, 'Cache.add with 206 response');
+
+cache_test(function(cache, test) {
+ var urls = ['./resources/fetch-status.py?status=206',
+ './resources/fetch-status.py?status=200'];
+ var requests = urls.map(function(url) {
+ return new Request(url);
+ });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(requests),
+ 'Cache.addAll should reject with TypeError if any request fails');
+ }, 'Cache.addAll with 206 response');
+
+cache_test(function(cache, test) {
+ var urls = ['./resources/fetch-status.py?status=206',
+ './resources/fetch-status.py?status=200'];
+ var requests = urls.map(function(url) {
+ var cross_origin_url = new URL(url, location.href);
+ cross_origin_url.hostname = REMOTE_HOST;
+ return new Request(cross_origin_url.href, { mode: 'no-cors' });
+ });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(requests),
+ 'Cache.addAll should reject with TypeError if any request fails');
+ }, 'Cache.addAll with opaque-filtered 206 response');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('this-does-not-exist-please-dont-create-it'),
+ 'Cache.add should reject if response is !ok');
+ }, 'Cache.add with request that results in a status of 404');
+
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('./resources/fetch-status.py?status=500'),
+ 'Cache.add should reject if response is !ok');
+ }, 'Cache.add with request that results in a status of 500');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(),
+ 'Cache.addAll with no arguments should throw TypeError.');
+ }, 'Cache.addAll with no arguments');
+
+cache_test(function(cache, test) {
+ // Assumes the existence of ../resources/simple.txt and ../resources/blank.html
+ var urls = ['./resources/simple.txt', undefined, './resources/blank.html'];
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(urls),
+ 'Cache.addAll should throw TypeError for an undefined argument.');
+ }, 'Cache.addAll with a mix of valid and undefined arguments');
+
+cache_test(function(cache) {
+ return cache.addAll([])
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.addAll should resolve with undefined on ' +
+ 'success.');
+ return cache.keys();
+ })
+ .then(function(result) {
+ assert_equals(result.length, 0,
+ 'There should be no entry in the cache.');
+ });
+ }, 'Cache.addAll with an empty array');
+
+cache_test(function(cache) {
+ // Assumes the existence of ../resources/simple.txt and
+ // ../resources/blank.html
+ var urls = ['./resources/simple.txt',
+ self.location.href,
+ './resources/blank.html'];
+ return cache.addAll(urls)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.addAll should resolve with undefined on ' +
+ 'success.');
+ return Promise.all(
+ urls.map(function(url) { return cache.match(url); }));
+ })
+ .then(function(responses) {
+ assert_class_string(
+ responses[0], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[1], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[2], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ return Promise.all(
+ responses.map(function(response) { return response.text(); }));
+ })
+ .then(function(bodies) {
+ assert_equals(
+ bodies[0], 'a simple text file\n',
+ 'Cache.add should retrieve the correct body.');
+ assert_equals(
+ bodies[2], '<!DOCTYPE html>\n<title>Empty doc</title>\n',
+ 'Cache.add should retrieve the correct body.');
+ });
+ }, 'Cache.addAll with string URL arguments');
+
+cache_test(function(cache) {
+ // Assumes the existence of ../resources/simple.txt and
+ // ../resources/blank.html
+ var urls = ['./resources/simple.txt',
+ self.location.href,
+ './resources/blank.html'];
+ var requests = urls.map(function(url) {
+ return new Request(url);
+ });
+ return cache.addAll(requests)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.addAll should resolve with undefined on ' +
+ 'success.');
+ return Promise.all(
+ urls.map(function(url) { return cache.match(url); }));
+ })
+ .then(function(responses) {
+ assert_class_string(
+ responses[0], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[1], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[2], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ return Promise.all(
+ responses.map(function(response) { return response.text(); }));
+ })
+ .then(function(bodies) {
+ assert_equals(
+ bodies[0], 'a simple text file\n',
+ 'Cache.add should retrieve the correct body.');
+ assert_equals(
+ bodies[2], '<!DOCTYPE html>\n<title>Empty doc</title>\n',
+ 'Cache.add should retrieve the correct body.');
+ });
+ }, 'Cache.addAll with Request arguments');
+
+cache_test(function(cache, test) {
+ // Assumes that ../resources/simple.txt and ../resources/blank.html exist.
+ // The second resource does not.
+ var urls = ['./resources/simple.txt',
+ 'this-resource-should-not-exist',
+ './resources/blank.html'];
+ var requests = urls.map(function(url) {
+ return new Request(url);
+ });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(requests),
+ 'Cache.addAll should reject with TypeError if any request fails')
+ .then(function() {
+ return Promise.all(urls.map(function(url) {
+ return cache.match(url);
+ }));
+ })
+ .then(function(matches) {
+ assert_array_equals(
+ matches,
+ [undefined, undefined, undefined],
+ 'If any response fails, no response should be added to cache');
+ });
+ }, 'Cache.addAll with a mix of succeeding and failing requests');
+
+cache_test(function(cache, test) {
+ var request = new Request('../resources/simple.txt');
+ return promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll([request, request]),
+ 'Cache.addAll should throw InvalidStateError if the same request is added ' +
+ 'twice.');
+ }, 'Cache.addAll called with the same Request object specified twice');
+
+cache_test(async function(cache, test) {
+ const url = './resources/vary.py?vary=x-shape';
+ let requests = [
+ new Request(url, { headers: { 'x-shape': 'circle' }}),
+ new Request(url, { headers: { 'x-shape': 'square' }}),
+ ];
+ let result = await cache.addAll(requests);
+ assert_equals(result, undefined, 'Cache.addAll() should succeed');
+ }, 'Cache.addAll should succeed when entries differ by vary header');
+
+cache_test(async function(cache, test) {
+ const url = './resources/vary.py?vary=x-shape';
+ let requests = [
+ new Request(url, { headers: { 'x-shape': 'circle' }}),
+ new Request(url, { headers: { 'x-shape': 'circle' }}),
+ ];
+ await promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll(requests),
+ 'Cache.addAll() should reject when entries are duplicate by vary header');
+ }, 'Cache.addAll should reject when entries are duplicate by vary header');
+
+// VARY header matching is asymmetric. Determining if two entries are duplicate
+// depends on which entry's response is used in the comparison. The target
+// response's VARY header determines what request headers are examined. This
+// test verifies that Cache.addAll() duplicate checking handles this asymmetric
+// behavior correctly.
+cache_test(async function(cache, test) {
+ const base_url = './resources/vary.py';
+
+ // Define a request URL that sets a VARY header in the
+ // query string to be echoed back by the server.
+ const url = base_url + '?vary=x-size';
+
+ // Set a cookie to override the VARY header of the response
+ // when the request is made with credentials. This will
+ // take precedence over the query string vary param. This
+ // is a bit confusing, but it's necessary to construct a test
+ // where the URL is the same, but the VARY headers differ.
+ //
+ // Note, the test could also pass this information in additional
+ // request headers. If the cookie approach becomes too unwieldy
+ // this test could be rewritten to use that technique.
+ await fetch(base_url + '?set-vary-value-override-cookie=x-shape');
+ test.add_cleanup(_ => fetch(base_url + '?clear-vary-value-override-cookie'));
+
+ let requests = [
+ // This request will result in a Response with a "Vary: x-shape"
+ // header. This *will not* result in a duplicate match with the
+ // other entry.
+ new Request(url, { headers: { 'x-shape': 'circle',
+ 'x-size': 'big' },
+ credentials: 'same-origin' }),
+
+ // This request will result in a Response with a "Vary: x-size"
+ // header. This *will* result in a duplicate match with the other
+ // entry.
+ new Request(url, { headers: { 'x-shape': 'square',
+ 'x-size': 'big' },
+ credentials: 'omit' }),
+ ];
+ await promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll(requests),
+ 'Cache.addAll() should reject when one entry has a vary header ' +
+ 'matching an earlier entry.');
+
+ // Test the reverse order now.
+ await promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll(requests.reverse()),
+ 'Cache.addAll() should reject when one entry has a vary header ' +
+ 'matching a later entry.');
+
+ }, 'Cache.addAll should reject when one entry has a vary header ' +
+ 'matching another entry');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-delete.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-delete.https.any.js
new file mode 100644
index 0000000000..3eae2b6a08
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-delete.https.any.js
@@ -0,0 +1,164 @@
+// META: title=Cache.delete
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+
+// Construct a generic Request object. The URL is |test_url|. All other fields
+// are defaults.
+function new_test_request() {
+ return new Request(test_url);
+}
+
+// Construct a generic Response object.
+function new_test_response() {
+ return new Response('Hello world!', { status: 200 });
+}
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.delete(),
+ 'Cache.delete should reject with a TypeError when called with no ' +
+ 'arguments.');
+ }, 'Cache.delete with no arguments');
+
+cache_test(function(cache) {
+ return cache.put(new_test_request(), new_test_response())
+ .then(function() {
+ return cache.delete(test_url);
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should resolve with "true" if an entry ' +
+ 'was successfully deleted.');
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.delete should remove matching entries from cache.');
+ });
+ }, 'Cache.delete called with a string URL');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ return cache.put(request, new_test_response())
+ .then(function() {
+ return cache.delete(request);
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should resolve with "true" if an entry ' +
+ 'was successfully deleted.');
+ });
+ }, 'Cache.delete called with a Request object');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new_test_response();
+ return cache.put(request, response)
+ .then(function() {
+ return cache.delete(new Request(test_url, {method: 'HEAD'}));
+ })
+ .then(function(result) {
+ assert_false(result,
+ 'Cache.delete should not match a non-GET request ' +
+ 'unless ignoreMethod option is set.');
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.delete should leave non-matching response in the cache.');
+ return cache.delete(new Request(test_url, {method: 'HEAD'}),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should match a non-GET request ' +
+ ' if ignoreMethod is true.');
+ });
+ }, 'Cache.delete called with a HEAD request');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.delete(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_false(result,
+ 'Cache.delete should not delete if vary does not ' +
+ 'match unless ignoreVary is true');
+ return cache.delete(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should ignore vary if ignoreVary is true');
+ });
+ }, 'Cache.delete supports ignoreVary');
+
+cache_test(function(cache) {
+ return cache.delete(test_url)
+ .then(function(result) {
+ assert_false(result,
+ 'Cache.delete should resolve with "false" if there ' +
+ 'are no matching entries.');
+ });
+ }, 'Cache.delete with a non-existent entry');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ]);
+ return cache.delete(entries.a_with_query.request,
+ { ignoreSearch: true });
+ })
+ .then(function(result) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true });
+ })
+ .then(function(result) {
+ assert_response_array_equals(result, []);
+ });
+ },
+ 'Cache.delete with ignoreSearch option (request with search parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ]);
+ // cache.delete()'s behavior should be the same if ignoreSearch is
+ // not provided or if ignoreSearch is false.
+ return cache.delete(entries.a_with_query.request,
+ { ignoreSearch: false });
+ })
+ .then(function(result) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true });
+ })
+ .then(function(result) {
+ assert_response_array_equals(result, [ entries.a.response ]);
+ });
+ },
+ 'Cache.delete with ignoreSearch option (when it is specified as false)');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html b/testing/web-platform/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html
new file mode 100644
index 0000000000..3c96348e0e
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<title>Cache.keys (checking request attributes that can be set only on service workers)</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-keys">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./../service-worker/resources/test-helpers.sub.js"></script>
+<script>
+const worker = './resources/cache-keys-attributes-for-service-worker.js';
+
+function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+}
+
+promise_test(async (t) => {
+ const scope = './resources/blank.html?name=isReloadNavigation';
+ let frame;
+ let reg;
+
+ try {
+ reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: false, stored: false');
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: true, stored: true');
+ } finally {
+ if (frame) {
+ frame.remove();
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+ }
+}, 'Request.IsReloadNavigation should persist.');
+
+promise_test(async (t) => {
+ const scope = './resources/blank.html?name=isHistoryNavigation';
+ let frame;
+ let reg;
+
+ try {
+ reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: false, stored: false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.src = '../resources/blank.html?ignore';
+ });
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: true, stored: true');
+ } finally {
+ if (frame) {
+ frame.remove();
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+ }
+}, 'Request.IsHistoryNavigation should persist.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-keys.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-keys.https.any.js
new file mode 100644
index 0000000000..232fb760d4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-keys.https.any.js
@@ -0,0 +1,212 @@
+// META: title=Cache.keys
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+cache_test(cache => {
+ return cache.keys()
+ .then(requests => {
+ assert_equals(
+ requests.length, 0,
+ 'Cache.keys should resolve to an empty array for an empty cache');
+ });
+ }, 'Cache.keys() called on an empty cache');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys('not-present-in-the-cache')
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should resolve with an empty array on failure.');
+ });
+ }, 'Cache.keys with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a.request.url)
+ .then(function(result) {
+ assert_request_array_equals(result, [entries.a.request],
+ 'Cache.keys should match by URL.');
+ });
+ }, 'Cache.keys with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a.request)
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [entries.a.request],
+ 'Cache.keys should match by Request.');
+ });
+ }, 'Cache.keys with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(new Request(entries.a.request.url))
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [entries.a.request],
+ 'Cache.keys should match by Request.');
+ });
+ }, 'Cache.keys with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a.request, {ignoreSearch: true})
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.a.request,
+ entries.a_with_query.request
+ ],
+ 'Cache.keys with ignoreSearch should ignore the ' +
+ 'search parameters of cached request.');
+ });
+ },
+ 'Cache.keys with ignoreSearch option (request with no search ' +
+ 'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a_with_query.request, {ignoreSearch: true})
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.a.request,
+ entries.a_with_query.request
+ ],
+ 'Cache.keys with ignoreSearch should ignore the ' +
+ 'search parameters of request.');
+ });
+ },
+ 'Cache.keys with ignoreSearch option (request with search parameters)');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return cache.keys(head_request.clone());
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should resolve with an empty array with a ' +
+ 'mismatched method.');
+ return cache.keys(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ request,
+ ],
+ 'Cache.keys with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.keys supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.keys(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should resolve with an empty array with a ' +
+ 'mismatched vary.');
+ return cache.keys(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ vary_request,
+ ],
+ 'Cache.keys with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'Cache.keys supports ignoreVary');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.cat.request.url + '#mouse')
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.cat.request,
+ ],
+ 'Cache.keys should ignore URL fragment.');
+ });
+ }, 'Cache.keys with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys('http')
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should treat query as a URL and not ' +
+ 'just a string fragment.');
+ });
+ }, 'Cache.keys with string fragment "http" as query');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys()
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ simple_entries.map(entry => entry.request),
+ 'Cache.keys without parameters should match all entries.');
+ });
+ }, 'Cache.keys without parameters');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(undefined)
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ simple_entries.map(entry => entry.request),
+ 'Cache.keys with undefined request should match all entries.');
+ });
+ }, 'Cache.keys with explicitly undefined request');
+
+cache_test(cache => {
+ return cache.keys(undefined, {})
+ .then(requests => {
+ assert_equals(
+ requests.length, 0,
+ 'Cache.keys should resolve to an empty array for an empty cache');
+ });
+ }, 'Cache.keys with explicitly undefined request and empty options');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.keys()
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.vary_cookie_is_cookie.request,
+ entries.vary_cookie_is_good.request,
+ entries.vary_cookie_absent.request,
+ ],
+ 'Cache.keys without parameters should match all entries.');
+ });
+ }, 'Cache.keys without parameters and VARY entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(new Request(entries.cat.request.url, {method: 'HEAD'}))
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should not match HEAD request unless ignoreMethod ' +
+ 'option is set.');
+ });
+ }, 'Cache.keys with a HEAD Request');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-match.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-match.https.any.js
new file mode 100644
index 0000000000..9ca45903cb
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-match.https.any.js
@@ -0,0 +1,437 @@
+// META: title=Cache.match
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: script=/common/get-host-info.sub.js
+// META: timeout=long
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match('not-present-in-the-cache')
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.match failures should resolve with undefined.');
+ });
+ }, 'Cache.match with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a.request.url)
+ .then(function(result) {
+ assert_response_equals(result, entries.a.response,
+ 'Cache.match should match by URL.');
+ });
+ }, 'Cache.match with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a.request)
+ .then(function(result) {
+ assert_response_equals(result, entries.a.response,
+ 'Cache.match should match by Request.');
+ });
+ }, 'Cache.match with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var alt_response = new Response('', {status: 201});
+
+ return self.caches.open('second_matching_cache')
+ .then(function(cache) {
+ return cache.put(entries.a.request, alt_response.clone());
+ })
+ .then(function() {
+ return cache.match(entries.a.request);
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, entries.a.response,
+ 'Cache.match should match the first cache.');
+ });
+ }, 'Cache.match with multiple cache hits');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(new Request(entries.a.request.url))
+ .then(function(result) {
+ assert_response_equals(result, entries.a.response,
+ 'Cache.match should match by Request.');
+ });
+ }, 'Cache.match with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(new Request(entries.a.request.url, {method: 'HEAD'}))
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.match should not match HEAD Request.');
+ });
+ }, 'Cache.match with HEAD');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_in_array(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.match with ignoreSearch should ignore the ' +
+ 'search parameters of cached request.');
+ });
+ },
+ 'Cache.match with ignoreSearch option (request with no search ' +
+ 'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a_with_query.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_in_array(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.match with ignoreSearch should ignore the ' +
+ 'search parameters of request.');
+ });
+ },
+ 'Cache.match with ignoreSearch option (request with search parameter)');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return cache.match(head_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should resolve as undefined with a ' +
+ 'mismatched method.');
+ return cache.match(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'Cache.match with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.match supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.match(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should resolve as undefined with a ' +
+ 'mismatched vary.');
+ return cache.match(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, vary_response,
+ 'Cache.match with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'Cache.match supports ignoreVary');
+
+cache_test(function(cache) {
+ let has_cache_name = false;
+ const opts = {
+ get cacheName() {
+ has_cache_name = true;
+ return undefined;
+ }
+ };
+ return self.caches.open('foo')
+ .then(function() {
+ return cache.match('bar', opts);
+ })
+ .then(function() {
+ assert_false(has_cache_name,
+ 'Cache.match does not support cacheName option ' +
+ 'which was removed in CacheQueryOptions.');
+ });
+ }, 'Cache.match does not support cacheName option');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.cat.request.url + '#mouse')
+ .then(function(result) {
+ assert_response_equals(result, entries.cat.response,
+ 'Cache.match should ignore URL fragment.');
+ });
+ }, 'Cache.match with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match('http')
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should treat query as a URL and not ' +
+ 'just a string fragment.');
+ });
+ }, 'Cache.match with string fragment "http" as query');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.match('http://example.com/c')
+ .then(function(result) {
+ assert_response_in_array(
+ result,
+ [
+ entries.vary_cookie_absent.response
+ ],
+ 'Cache.match should honor "Vary" header.');
+ });
+ }, 'Cache.match with responses containing "Vary" header');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com');
+ var response;
+ var request_url = new URL('./resources/simple.txt', location.href).href;
+ return fetch(request_url)
+ .then(function(fetch_result) {
+ response = fetch_result;
+ assert_equals(
+ response.url, request_url,
+ '[https://fetch.spec.whatwg.org/#dom-response-url] ' +
+ 'Reponse.url should return the URL of the response.');
+ return cache.put(request, response.clone());
+ })
+ .then(function() {
+ return cache.match(request.url);
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'Cache.match should return a Response object that has the same ' +
+ 'properties as the stored response.');
+ return cache.match(response.url);
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should not match cache entry based on response URL.');
+ });
+ }, 'Cache.match with Request and Response objects with different URLs');
+
+cache_test(function(cache) {
+ var request_url = new URL('./resources/simple.txt', location.href).href;
+ return fetch(request_url)
+ .then(function(fetch_result) {
+ return cache.put(new Request(request_url), fetch_result);
+ })
+ .then(function() {
+ return cache.match(request_url);
+ })
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body_text) {
+ assert_equals(body_text, 'a simple text file\n',
+ 'Cache.match should return a Response object with a ' +
+ 'valid body.');
+ })
+ .then(function() {
+ return cache.match(request_url);
+ })
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body_text) {
+ assert_equals(body_text, 'a simple text file\n',
+ 'Cache.match should return a Response object with a ' +
+ 'valid body each time it is called.');
+ });
+ }, 'Cache.match invoked multiple times for the same Request/Response');
+
+cache_test(function(cache) {
+ var request_url = new URL('./resources/simple.txt', location.href).href;
+ return fetch(request_url)
+ .then(function(fetch_result) {
+ return cache.put(new Request(request_url), fetch_result);
+ })
+ .then(function() {
+ return cache.match(request_url);
+ })
+ .then(function(result) {
+ return result.blob();
+ })
+ .then(function(blob) {
+ var sliced = blob.slice(2,8);
+
+ return new Promise(function (resolve, reject) {
+ var reader = new FileReader();
+ reader.onloadend = function(event) {
+ resolve(event.target.result);
+ };
+ reader.readAsText(sliced);
+ });
+ })
+ .then(function(text) {
+ assert_equals(text, 'simple',
+ 'A Response blob returned by Cache.match should be ' +
+ 'sliceable.' );
+ });
+ }, 'Cache.match blob should be sliceable');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var request = new Request(entries.a.request.clone(), {method: 'POST'});
+ return cache.match(request)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.match should not find a match');
+ });
+ }, 'Cache.match with POST Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var response = entries.non_2xx_response.response;
+ return cache.match(entries.non_2xx_response.request.url)
+ .then(function(result) {
+ assert_response_equals(
+ result, entries.non_2xx_response.response,
+ 'Cache.match should return a Response object that has the ' +
+ 'same properties as a stored non-2xx response.');
+ });
+ }, 'Cache.match with a non-2xx Response');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var response = entries.error_response.response;
+ return cache.match(entries.error_response.request.url)
+ .then(function(result) {
+ assert_response_equals(
+ result, entries.error_response.response,
+ 'Cache.match should return a Response object that has the ' +
+ 'same properties as a stored network error response.');
+ });
+ }, 'Cache.match with a network error Response');
+
+cache_test(function(cache) {
+ // This test validates that we can get a Response from the Cache API,
+ // clone it, and read just one side of the clone. This was previously
+ // bugged in FF for Responses with large bodies.
+ var data = [];
+ data.length = 80 * 1024;
+ data.fill('F');
+ var response;
+ return cache.put('/', new Response(data.toString()))
+ .then(function(result) {
+ return cache.match('/');
+ })
+ .then(function(r) {
+ // Make sure the original response is not GC'd.
+ response = r;
+ // Return only the clone. We purposefully test that the other
+ // half of the clone does not need to be read here.
+ return response.clone().text();
+ })
+ .then(function(text) {
+ assert_equals(text, data.toString(), 'cloned body text can be read correctly');
+ });
+ }, 'Cache produces large Responses that can be cloned and read correctly.');
+
+cache_test(async (cache) => {
+ const url = get_host_info().HTTPS_REMOTE_ORIGIN +
+ '/service-workers/cache-storage/resources/simple.txt?pipe=' +
+ 'header(access-control-allow-origin,*)|' +
+ 'header(access-control-expose-headers,*)|' +
+ 'header(foo,bar)|' +
+ 'header(set-cookie,X)';
+
+ const response = await fetch(url);
+ await cache.put(new Request(url), response);
+ const cached_response = await cache.match(url);
+
+ const headers = cached_response.headers;
+ assert_equals(headers.get('access-control-expose-headers'), '*');
+ assert_equals(headers.get('foo'), 'bar');
+ assert_equals(headers.get('set-cookie'), null);
+ }, 'cors-exposed header should be stored correctly.');
+
+cache_test(async (cache) => {
+ // A URL that should load a resource with a known mime type.
+ const url = '/service-workers/cache-storage/resources/blank.html';
+ const expected_mime_type = 'text/html';
+
+ // Verify we get the expected mime type from the network. Note,
+ // we cannot use an exact match here since some browsers append
+ // character encoding information to the blob.type value.
+ const net_response = await fetch(url);
+ const net_mime_type = (await net_response.blob()).type;
+ assert_true(net_mime_type.includes(expected_mime_type),
+ 'network response should include the expected mime type');
+
+ // Verify we get the exact same mime type when reading the same
+ // URL resource back out of the cache.
+ await cache.add(url);
+ const cache_response = await cache.match(url);
+ const cache_mime_type = (await cache_response.blob()).type;
+ assert_equals(cache_mime_type, net_mime_type,
+ 'network and cache response mime types should match');
+ }, 'MIME type should be set from content-header correctly.');
+
+cache_test(async (cache) => {
+ const url = '/dummy';
+ const original_type = 'text/html';
+ const override_type = 'text/plain';
+ const init_with_headers = {
+ headers: {
+ 'content-type': original_type
+ }
+ }
+
+ // Verify constructing a synthetic response with a content-type header
+ // gets the correct mime type.
+ const response = new Response('hello world', init_with_headers);
+ const original_response_type = (await response.blob()).type;
+ assert_true(original_response_type.includes(original_type),
+ 'original response should include the expected mime type');
+
+ // Verify overwriting the content-type header changes the mime type.
+ const overwritten_response = new Response('hello world', init_with_headers);
+ overwritten_response.headers.set('content-type', override_type);
+ const overwritten_response_type = (await overwritten_response.blob()).type;
+ assert_equals(overwritten_response_type, override_type,
+ 'mime type can be overridden');
+
+ // Verify the Response read from Cache uses the original mime type
+ // computed when it was first constructed.
+ const tmp = new Response('hello world', init_with_headers);
+ tmp.headers.set('content-type', override_type);
+ await cache.put(url, tmp);
+ const cache_response = await cache.match(url);
+ const cache_mime_type = (await cache_response.blob()).type;
+ assert_equals(cache_mime_type, override_type,
+ 'overwritten and cached response mime types should match');
+ }, 'MIME type should reflect Content-Type headers of response.');
+
+cache_test(async (cache) => {
+ const url = new URL('./resources/vary.py?vary=foo',
+ get_host_info().HTTPS_REMOTE_ORIGIN + self.location.pathname);
+ const original_request = new Request(url, { mode: 'no-cors',
+ headers: { 'foo': 'bar' } });
+ const fetch_response = await fetch(original_request);
+ assert_equals(fetch_response.type, 'opaque');
+
+ await cache.put(original_request, fetch_response);
+
+ const match_response_1 = await cache.match(original_request);
+ assert_not_equals(match_response_1, undefined);
+
+ // Verify that cache.match() finds the entry even if queried with a varied
+ // header that does not match the cache key. Vary headers should be ignored
+ // for opaque responses.
+ const different_request = new Request(url, { headers: { 'foo': 'CHANGED' } });
+ const match_response_2 = await cache.match(different_request);
+ assert_not_equals(match_response_2, undefined);
+}, 'Cache.match ignores vary headers on opaque response.');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-matchAll.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-matchAll.https.any.js
new file mode 100644
index 0000000000..93c5517891
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-matchAll.https.any.js
@@ -0,0 +1,244 @@
+// META: title=Cache.matchAll
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll('not-present-in-the-cache')
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should resolve with an empty array on failure.');
+ });
+ }, 'Cache.matchAll with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a.request.url)
+ .then(function(result) {
+ assert_response_array_equals(result, [entries.a.response],
+ 'Cache.matchAll should match by URL.');
+ });
+ }, 'Cache.matchAll with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a.request)
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [entries.a.response],
+ 'Cache.matchAll should match by Request.');
+ });
+ }, 'Cache.matchAll with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(new Request(entries.a.request.url))
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [entries.a.response],
+ 'Cache.matchAll should match by Request.');
+ });
+ }, 'Cache.matchAll with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(new Request(entries.a.request.url, {method: 'HEAD'}),
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should not match HEAD Request.');
+ });
+ }, 'Cache.matchAll with HEAD');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.matchAll with ignoreSearch should ignore the ' +
+ 'search parameters of cached request.');
+ });
+ },
+ 'Cache.matchAll with ignoreSearch option (request with no search ' +
+ 'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a_with_query.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.matchAll with ignoreSearch should ignore the ' +
+ 'search parameters of request.');
+ });
+ },
+ 'Cache.matchAll with ignoreSearch option (request with search parameters)');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return cache.matchAll(head_request.clone());
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should resolve with empty array for a ' +
+ 'mismatched method.');
+ return cache.matchAll(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [response],
+ 'Cache.matchAll with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.matchAll supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.matchAll(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should resolve as undefined with a ' +
+ 'mismatched vary.');
+ return cache.matchAll(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [vary_response],
+ 'Cache.matchAll with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'Cache.matchAll supports ignoreVary');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.cat.request.url + '#mouse')
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.cat.response,
+ ],
+ 'Cache.matchAll should ignore URL fragment.');
+ });
+ }, 'Cache.matchAll with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll('http')
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should treat query as a URL and not ' +
+ 'just a string fragment.');
+ });
+ }, 'Cache.matchAll with string fragment "http" as query');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll()
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ simple_entries.map(entry => entry.response),
+ 'Cache.matchAll without parameters should match all entries.');
+ });
+ }, 'Cache.matchAll without parameters');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(undefined)
+ .then(result => {
+ assert_response_array_equals(
+ result,
+ simple_entries.map(entry => entry.response),
+ 'Cache.matchAll with undefined request should match all entries.');
+ });
+ }, 'Cache.matchAll with explicitly undefined request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(undefined, {})
+ .then(result => {
+ assert_response_array_equals(
+ result,
+ simple_entries.map(entry => entry.response),
+ 'Cache.matchAll with undefined request should match all entries.');
+ });
+ }, 'Cache.matchAll with explicitly undefined request and empty options');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.matchAll('http://example.com/c')
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.vary_cookie_absent.response
+ ],
+ 'Cache.matchAll should exclude matches if a vary header is ' +
+ 'missing in the query request, but is present in the cached ' +
+ 'request.');
+ })
+
+ .then(function() {
+ return cache.matchAll(
+ new Request('http://example.com/c',
+ {headers: {'Cookies': 'none-of-the-above'}}));
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ ],
+ 'Cache.matchAll should exclude matches if a vary header is ' +
+ 'missing in the cached request, but is present in the query ' +
+ 'request.');
+ })
+
+ .then(function() {
+ return cache.matchAll(
+ new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}}));
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [entries.vary_cookie_is_cookie.response],
+ 'Cache.matchAll should match the entire header if a vary header ' +
+ 'is present in both the query and cached requests.');
+ });
+ }, 'Cache.matchAll with responses containing "Vary" header');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.matchAll('http://example.com/c',
+ {ignoreVary: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.vary_cookie_is_cookie.response,
+ entries.vary_cookie_is_good.response,
+ entries.vary_cookie_absent.response
+ ],
+ 'Cache.matchAll should support multiple vary request/response ' +
+ 'pairs.');
+ });
+ }, 'Cache.matchAll with multiple vary pairs');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-put.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-put.https.any.js
new file mode 100644
index 0000000000..dbf2650a75
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-put.https.any.js
@@ -0,0 +1,411 @@
+// META: title=Cache.put
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+var test_body = 'Hello world!';
+const { REMOTE_HOST } = get_host_info();
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response(test_body);
+ return cache.put(request, response)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.put should resolve with undefined on success.');
+ });
+ }, 'Cache.put called with simple Request and Response');
+
+cache_test(function(cache) {
+ var test_url = new URL('./resources/simple.txt', location.href).href;
+ var request = new Request(test_url);
+ var response;
+ return fetch(test_url)
+ .then(function(fetch_result) {
+ response = fetch_result.clone();
+ return cache.put(request, fetch_result);
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should update the cache with ' +
+ 'new request and response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'a simple text file\n',
+ 'Cache.put should store response body.');
+ });
+ }, 'Cache.put called with Request and Response from fetch()');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response(test_body);
+ assert_false(request.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+ 'Request.bodyUsed should be initially false.');
+ return cache.put(request, response)
+ .then(function() {
+ assert_false(request.bodyUsed,
+ 'Cache.put should not mark empty request\'s body used');
+ });
+ }, 'Cache.put with Request without a body');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response();
+ assert_false(response.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+ 'Response.bodyUsed should be initially false.');
+ return cache.put(request, response)
+ .then(function() {
+ assert_false(response.bodyUsed,
+ 'Cache.put should not mark empty response\'s body used');
+ });
+ }, 'Cache.put with Response without a body');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response(test_body);
+ return cache.put(request, response.clone())
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should update the cache with ' +
+ 'new Request and Response.');
+ });
+ }, 'Cache.put with a Response containing an empty URL');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response('', {
+ status: 200,
+ headers: [['Content-Type', 'text/plain']]
+ });
+ return cache.put(request, response)
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_equals(result.status, 200, 'Cache.put should store status.');
+ assert_equals(result.headers.get('Content-Type'), 'text/plain',
+ 'Cache.put should store headers.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, '',
+ 'Cache.put should store response body.');
+ });
+ }, 'Cache.put with an empty response body');
+
+cache_test(function(cache, test) {
+ var request = new Request(test_url);
+ var response = new Response('', {
+ status: 206,
+ headers: [['Content-Type', 'text/plain']]
+ });
+
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(request, response),
+ 'Cache.put should reject 206 Responses with a TypeError.');
+ }, 'Cache.put with synthetic 206 response');
+
+cache_test(function(cache, test) {
+ var test_url = new URL('./resources/fetch-status.py?status=206', location.href).href;
+ var request = new Request(test_url);
+ var response;
+ return fetch(test_url)
+ .then(function(fetch_result) {
+ assert_equals(fetch_result.status, 206,
+ 'Test framework error: The status code should be 206.');
+ response = fetch_result.clone();
+ return promise_rejects_js(test, TypeError, cache.put(request, fetch_result));
+ });
+ }, 'Cache.put with HTTP 206 response');
+
+cache_test(function(cache, test) {
+ // We need to jump through some hoops to allow the test to perform opaque
+ // response filtering, but bypass the ORB safelist check. This is
+ // done, by forcing the MIME type retrieval to fail and the
+ // validation of partial first response to succeed.
+ var pipe = "status(206)|header(Content-Type,)|header(Content-Range, bytes 0-1/41)|slice(null, 1)";
+ var test_url = new URL(`./resources/blank.html?pipe=${pipe}`, location.href);
+ test_url.hostname = REMOTE_HOST;
+ var request = new Request(test_url.href, { mode: 'no-cors' });
+ var response;
+ return fetch(request)
+ .then(function(fetch_result) {
+ assert_equals(fetch_result.type, 'opaque',
+ 'Test framework error: The response type should be opaque.');
+ assert_equals(fetch_result.status, 0,
+ 'Test framework error: The status code should be 0 for an ' +
+ ' opaque-filtered response. This is actually HTTP 206.');
+ response = fetch_result.clone();
+ return cache.put(request, fetch_result);
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_not_equals(result, undefined,
+ 'Cache.put should store an entry for the opaque response');
+ });
+ }, 'Cache.put with opaque-filtered HTTP 206 response');
+
+cache_test(function(cache) {
+ var test_url = new URL('./resources/fetch-status.py?status=500', location.href).href;
+ var request = new Request(test_url);
+ var response;
+ return fetch(test_url)
+ .then(function(fetch_result) {
+ assert_equals(fetch_result.status, 500,
+ 'Test framework error: The status code should be 500.');
+ response = fetch_result.clone();
+ return cache.put(request, fetch_result);
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should update the cache with ' +
+ 'new request and response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, '',
+ 'Cache.put should store response body.');
+ });
+ }, 'Cache.put with HTTP 500 response');
+
+cache_test(function(cache) {
+ var alternate_response_body = 'New body';
+ var alternate_response = new Response(alternate_response_body,
+ { statusText: 'New status' });
+ return cache.put(new Request(test_url),
+ new Response('Old body', { statusText: 'Old status' }))
+ .then(function() {
+ return cache.put(new Request(test_url), alternate_response.clone());
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, alternate_response,
+ 'Cache.put should replace existing ' +
+ 'response with new response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, alternate_response_body,
+ 'Cache put should store new response body.');
+ });
+ }, 'Cache.put called twice with matching Requests and different Responses');
+
+cache_test(function(cache) {
+ var first_url = test_url;
+ var second_url = first_url + '#(O_o)';
+ var third_url = first_url + '#fragment';
+ var alternate_response_body = 'New body';
+ var alternate_response = new Response(alternate_response_body,
+ { statusText: 'New status' });
+ return cache.put(new Request(first_url),
+ new Response('Old body', { statusText: 'Old status' }))
+ .then(function() {
+ return cache.put(new Request(second_url), alternate_response.clone());
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, alternate_response,
+ 'Cache.put should replace existing ' +
+ 'response with new response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, alternate_response_body,
+ 'Cache put should store new response body.');
+ })
+ .then(function() {
+ return cache.put(new Request(third_url), alternate_response.clone());
+ })
+ .then(function() {
+ return cache.keys();
+ })
+ .then(function(results) {
+ // Should match urls (without fragments or with different ones) to the
+ // same cache key. However, result.url should be the latest url used.
+ assert_equals(results[0].url, third_url);
+ return;
+ });
+}, 'Cache.put called multiple times with request URLs that differ only by a fragment');
+
+cache_test(function(cache) {
+ var url = 'http://example.com/foo';
+ return cache.put(url, new Response('some body'))
+ .then(function() { return cache.match(url); })
+ .then(function(response) { return response.text(); })
+ .then(function(body) {
+ assert_equals(body, 'some body',
+ 'Cache.put should accept a string as request.');
+ });
+ }, 'Cache.put with a string request');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url), 'Hello world!'),
+ 'Cache.put should only accept a Response object as the response.');
+ }, 'Cache.put with an invalid response');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request('file:///etc/passwd'),
+ new Response(test_body)),
+ 'Cache.put should reject non-HTTP/HTTPS requests with a TypeError.');
+ }, 'Cache.put with a non-HTTP/HTTPS request');
+
+cache_test(function(cache) {
+ var response = new Response(test_body);
+ return cache.put(new Request('relative-url'), response.clone())
+ .then(function() {
+ return cache.match(new URL('relative-url', location.href).href);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should accept a relative URL ' +
+ 'as the request.');
+ });
+ }, 'Cache.put with a relative URL');
+
+cache_test(function(cache, test) {
+ var request = new Request('http://example.com/foo', { method: 'HEAD' });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(request, new Response(test_body)),
+ 'Cache.put should throw a TypeError for non-GET requests.');
+ }, 'Cache.put with a non-GET request');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url), null),
+ 'Cache.put should throw a TypeError for a null response.');
+ }, 'Cache.put with a null response');
+
+cache_test(function(cache, test) {
+ var request = new Request(test_url, {method: 'POST', body: test_body});
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(request, new Response(test_body)),
+ 'Cache.put should throw a TypeError for a POST request.');
+ }, 'Cache.put with a POST request');
+
+cache_test(function(cache) {
+ var response = new Response(test_body);
+ assert_false(response.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+ 'Response.bodyUsed should be initially false.');
+ return response.text().then(function() {
+ assert_true(
+ response.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#concept-body-consume-body] ' +
+ 'The text() method should make the body disturbed.');
+ var request = new Request(test_url);
+ return cache.put(request, response).then(() => {
+ assert_unreached('cache.put should be rejected');
+ }, () => {});
+ });
+ }, 'Cache.put with a used response body');
+
+cache_test(function(cache) {
+ var response = new Response(test_body);
+ return cache.put(new Request(test_url), response)
+ .then(function() {
+ assert_throws_js(TypeError, () => response.body.getReader());
+ });
+ }, 'getReader() after Cache.put');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url),
+ new Response(test_body, { headers: { VARY: '*' }})),
+ 'Cache.put should reject VARY:* Responses with a TypeError.');
+ }, 'Cache.put with a VARY:* Response');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url),
+ new Response(test_body,
+ { headers: { VARY: 'Accept-Language,*' }})),
+ 'Cache.put should reject Responses with an embedded VARY:* with a ' +
+ 'TypeError.');
+ }, 'Cache.put with an embedded VARY:* Response');
+
+cache_test(async function(cache, test) {
+ const url = new URL('./resources/vary.py?vary=*',
+ get_host_info().HTTPS_REMOTE_ORIGIN + self.location.pathname);
+ const request = new Request(url, { mode: 'no-cors' });
+ const response = await fetch(request);
+ assert_equals(response.type, 'opaque');
+ await cache.put(request, response);
+ }, 'Cache.put with a VARY:* opaque response should not reject');
+
+cache_test(function(cache) {
+ var url = 'foo.html';
+ var redirectURL = 'http://example.com/foo-bar.html';
+ var redirectResponse = Response.redirect(redirectURL);
+ assert_equals(redirectResponse.headers.get('Location'), redirectURL,
+ 'Response.redirect() should set Location header.');
+ return cache.put(url, redirectResponse.clone())
+ .then(function() {
+ return cache.match(url);
+ })
+ .then(function(response) {
+ assert_response_equals(response, redirectResponse,
+ 'Redirect response is reproduced by the Cache API');
+ assert_equals(response.headers.get('Location'), redirectURL,
+ 'Location header is preserved by Cache API.');
+ });
+ }, 'Cache.put should store Response.redirect() correctly');
+
+cache_test(async (cache) => {
+ var request = new Request(test_url);
+ var response = new Response(new Blob([test_body]));
+ await cache.put(request, response);
+ var cachedResponse = await cache.match(request);
+ assert_equals(await cachedResponse.text(), test_body);
+ }, 'Cache.put called with simple Request and blob Response');
+
+cache_test(async (cache) => {
+ var formData = new FormData();
+ formData.append("name", "value");
+
+ var request = new Request(test_url);
+ var response = new Response(formData);
+ await cache.put(request, response);
+ var cachedResponse = await cache.match(request);
+ var cachedResponseText = await cachedResponse.text();
+ assert_true(cachedResponseText.indexOf("name=\"name\"\r\n\r\nvalue") !== -1);
+}, 'Cache.put called with simple Request and form data Response');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js
new file mode 100644
index 0000000000..fd59ba464d
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js
@@ -0,0 +1,64 @@
+// META: title=Cache.put
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/test-helpers.js
+// META: script=/storage/buckets/resources/util.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+var test_body = 'Hello world!';
+const { REMOTE_HOST } = get_host_info();
+
+promise_test(async function(test) {
+ await prepareForBucketTest(test);
+ var inboxBucket = await navigator.storageBuckets.open('inbox');
+ var draftsBucket = await navigator.storageBuckets.open('drafts');
+
+ const cacheName = 'attachments';
+ const cacheKey = 'receipt1.txt';
+
+ var inboxCache = await inboxBucket.caches.open(cacheName);
+ var draftsCache = await draftsBucket.caches.open(cacheName);
+
+ await inboxCache.put(cacheKey, new Response('bread x 2'))
+ await draftsCache.put(cacheKey, new Response('eggs x 1'));
+
+ return inboxCache.match(cacheKey)
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'bread x 2', 'Wrong cache contents');
+ return draftsCache.match(cacheKey);
+ })
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'eggs x 1', 'Wrong cache contents');
+ });
+}, 'caches from different buckets have different contents');
+
+promise_test(async function(test) {
+ await prepareForBucketTest(test);
+ var inboxBucket = await navigator.storageBuckets.open('inbox');
+ var draftBucket = await navigator.storageBuckets.open('drafts');
+
+ var caches = inboxBucket.caches;
+ var attachments = await caches.open('attachments');
+ await attachments.put('receipt1.txt', new Response('bread x 2'));
+ var result = await attachments.match('receipt1.txt');
+ assert_equals(await result.text(), 'bread x 2');
+
+ await navigator.storageBuckets.delete('inbox');
+
+ await promise_rejects_dom(
+ test, 'UnknownError', caches.open('attachments'));
+
+ // Also test when `caches` is first accessed after the deletion.
+ await navigator.storageBuckets.delete('drafts');
+ return promise_rejects_dom(
+ test, 'UnknownError', draftBucket.caches.open('attachments'));
+}, 'cache.open promise is rejected when bucket is gone');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-storage-keys.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-keys.https.any.js
new file mode 100644
index 0000000000..f19522be1b
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-keys.https.any.js
@@ -0,0 +1,35 @@
+// META: title=CacheStorage.keys
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_cache_list =
+ ['', 'example', 'Another cache name', 'A', 'a', 'ex ample'];
+
+promise_test(function(test) {
+ return self.caches.keys()
+ .then(function(keys) {
+ assert_true(Array.isArray(keys),
+ 'CacheStorage.keys should return an Array.');
+ return Promise.all(keys.map(function(key) {
+ return self.caches.delete(key);
+ }));
+ })
+ .then(function() {
+ return Promise.all(test_cache_list.map(function(key) {
+ return self.caches.open(key);
+ }));
+ })
+
+ .then(function() { return self.caches.keys(); })
+ .then(function(keys) {
+ assert_true(Array.isArray(keys),
+ 'CacheStorage.keys should return an Array.');
+ assert_array_equals(keys,
+ test_cache_list,
+ 'CacheStorage.keys should only return ' +
+ 'existing caches.');
+ });
+ }, 'CacheStorage keys');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-storage-match.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-match.https.any.js
new file mode 100644
index 0000000000..0c31b72629
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-match.https.any.js
@@ -0,0 +1,245 @@
+// META: title=CacheStorage.match
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+(function() {
+ var next_index = 1;
+
+ // Returns a transaction (request, response, and url) for a unique URL.
+ function create_unique_transaction(test) {
+ var uniquifier = String(next_index++);
+ var url = 'http://example.com/' + uniquifier;
+
+ return {
+ request: new Request(url),
+ response: new Response('hello'),
+ url: url
+ };
+ }
+
+ self.create_unique_transaction = create_unique_transaction;
+})();
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+
+ return cache.put(transaction.request.clone(), transaction.response.clone())
+ .then(function() {
+ return self.caches.match(transaction.request);
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ });
+}, 'CacheStorageMatch with no cache name provided');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+
+ var test_cache_list = ['a', 'b', 'c'];
+ return cache.put(transaction.request.clone(), transaction.response.clone())
+ .then(function() {
+ return Promise.all(test_cache_list.map(function(key) {
+ return self.caches.open(key);
+ }));
+ })
+ .then(function() {
+ return self.caches.match(transaction.request);
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ });
+}, 'CacheStorageMatch from one of many caches');
+
+promise_test(function(test) {
+ var transaction = create_unique_transaction();
+
+ var test_cache_list = ['x', 'y', 'z'];
+ return Promise.all(test_cache_list.map(function(key) {
+ return self.caches.open(key);
+ }))
+ .then(function() { return self.caches.open('x'); })
+ .then(function(cache) {
+ return cache.put(transaction.request.clone(),
+ transaction.response.clone());
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: 'x'});
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: 'y'});
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'Cache y should not have a response for the request.');
+ });
+}, 'CacheStorageMatch from one of many caches by name');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+ return cache.put(transaction.url, transaction.response.clone())
+ .then(function() {
+ return self.caches.match(transaction.request);
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ });
+}, 'CacheStorageMatch a string request');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+ return cache.put(transaction.request.clone(), transaction.response.clone())
+ .then(function() {
+ return self.caches.match(new Request(transaction.request.url,
+ {method: 'HEAD'}));
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'A HEAD request should not be matched');
+ });
+}, 'CacheStorageMatch a HEAD request');
+
+promise_test(function(test) {
+ var transaction = create_unique_transaction();
+ return self.caches.match(transaction.request)
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'The response should not be found.');
+ });
+}, 'CacheStorageMatch with no cached entry');
+
+promise_test(function(test) {
+ var transaction = create_unique_transaction();
+ return self.caches.delete('foo')
+ .then(function() {
+ return self.caches.has('foo');
+ })
+ .then(function(has_foo) {
+ assert_false(has_foo, "The cache should not exist.");
+ return self.caches.match(transaction.request, {cacheName: 'foo'});
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'The match with bad cache name should resolve to ' +
+ 'undefined.');
+ return self.caches.has('foo');
+ })
+ .then(function(has_foo) {
+ assert_false(has_foo, "The cache should still not exist.");
+ });
+}, 'CacheStorageMatch with no caches available but name provided');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+
+ return self.caches.delete('')
+ .then(function() {
+ return self.caches.has('');
+ })
+ .then(function(has_cache) {
+ assert_false(has_cache, "The cache should not exist.");
+ return cache.put(transaction.request, transaction.response.clone());
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: ''});
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'The response should not be found.');
+ return self.caches.open('');
+ })
+ .then(function(cache) {
+ return cache.put(transaction.request, transaction.response);
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: ''});
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should be matched.');
+ return self.caches.delete('');
+ });
+}, 'CacheStorageMatch with empty cache name provided');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/?foo');
+ var no_query_request = new Request('http://example.com/');
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return self.caches.match(no_query_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'CacheStorageMatch should resolve as undefined with a ' +
+ 'mismatched query.');
+ return self.caches.match(no_query_request.clone(),
+ {ignoreSearch: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'CacheStorageMatch with ignoreSearch should ignore the ' +
+ 'query of the request.');
+ });
+ }, 'CacheStorageMatch supports ignoreSearch');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return self.caches.match(head_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'CacheStorageMatch should resolve as undefined with a ' +
+ 'mismatched method.');
+ return self.caches.match(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'CacheStorageMatch with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.match supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return self.caches.match(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'CacheStorageMatch should resolve as undefined with a ' +
+ ' mismatched vary.');
+ return self.caches.match(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, vary_response,
+ 'CacheStorageMatch with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'CacheStorageMatch supports ignoreVary');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-storage.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-storage.https.any.js
new file mode 100644
index 0000000000..b7d5af7b53
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-storage.https.any.js
@@ -0,0 +1,239 @@
+// META: title=CacheStorage
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/foo';
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ assert_true(cache instanceof Cache,
+ 'CacheStorage.open should return a Cache.');
+ });
+ }, 'CacheStorage.open');
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/bar';
+ var first_cache = null;
+ var second_cache = null;
+ return self.caches.open(cache_name)
+ .then(function(cache) {
+ first_cache = cache;
+ return self.caches.delete(cache_name);
+ })
+ .then(function() {
+ return first_cache.add('./resources/simple.txt');
+ })
+ .then(function() {
+ return self.caches.keys();
+ })
+ .then(function(cache_names) {
+ assert_equals(cache_names.indexOf(cache_name), -1);
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ second_cache = cache;
+ return second_cache.keys();
+ })
+ .then(function(keys) {
+ assert_equals(keys.length, 0);
+ return first_cache.keys();
+ })
+ .then(function(keys) {
+ assert_equals(keys.length, 1);
+ // Clean up
+ return self.caches.delete(cache_name);
+ });
+ }, 'CacheStorage.delete dooms, but does not delete immediately');
+
+promise_test(function(t) {
+ // Note that this test may collide with other tests running in the same
+ // origin that also uses an empty cache name.
+ var cache_name = '';
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ assert_true(cache instanceof Cache,
+ 'CacheStorage.open should accept an empty name.');
+ });
+ }, 'CacheStorage.open with an empty name');
+
+promise_test(function(t) {
+ return promise_rejects_js(
+ t,
+ TypeError,
+ self.caches.open(),
+ 'CacheStorage.open should throw TypeError if called with no arguments.');
+ }, 'CacheStorage.open with no arguments');
+
+promise_test(function(t) {
+ var test_cases = [
+ {
+ name: 'cache-storage/lowercase',
+ should_not_match:
+ [
+ 'cache-storage/Lowercase',
+ ' cache-storage/lowercase',
+ 'cache-storage/lowercase '
+ ]
+ },
+ {
+ name: 'cache-storage/has a space',
+ should_not_match:
+ [
+ 'cache-storage/has'
+ ]
+ },
+ {
+ name: 'cache-storage/has\000_in_the_name',
+ should_not_match:
+ [
+ 'cache-storage/has',
+ 'cache-storage/has_in_the_name'
+ ]
+ }
+ ];
+ return Promise.all(test_cases.map(function(testcase) {
+ var cache_name = testcase.name;
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function() {
+ return self.caches.has(cache_name);
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'CacheStorage.has should return true for existing ' +
+ 'cache.');
+ })
+ .then(function() {
+ return Promise.all(
+ testcase.should_not_match.map(function(cache_name) {
+ return self.caches.has(cache_name)
+ .then(function(result) {
+ assert_false(result,
+ 'CacheStorage.has should only perform ' +
+ 'exact matches on cache names.');
+ });
+ }));
+ })
+ .then(function() {
+ return self.caches.delete(cache_name);
+ });
+ }));
+ }, 'CacheStorage.has with existing cache');
+
+promise_test(function(t) {
+ return self.caches.has('cheezburger')
+ .then(function(result) {
+ assert_false(result,
+ 'CacheStorage.has should return false for ' +
+ 'nonexistent cache.');
+ });
+ }, 'CacheStorage.has with nonexistent cache');
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/open';
+ var cache;
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(result) {
+ cache = result;
+ })
+ .then(function() {
+ return cache.add('./resources/simple.txt');
+ })
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(result) {
+ assert_true(result instanceof Cache,
+ 'CacheStorage.open should return a Cache object');
+ assert_not_equals(result, cache,
+ 'CacheStorage.open should return a new Cache ' +
+ 'object each time its called.');
+ return Promise.all([cache.keys(), result.keys()]);
+ })
+ .then(function(results) {
+ var expected_urls = results[0].map(function(r) { return r.url });
+ var actual_urls = results[1].map(function(r) { return r.url });
+ assert_array_equals(actual_urls, expected_urls,
+ 'CacheStorage.open should return a new Cache ' +
+ 'object for the same backing store.');
+ });
+ }, 'CacheStorage.open with existing cache');
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/delete';
+
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function() { return self.caches.delete(cache_name); })
+ .then(function(result) {
+ assert_true(result,
+ 'CacheStorage.delete should return true after ' +
+ 'deleting an existing cache.');
+ })
+
+ .then(function() { return self.caches.has(cache_name); })
+ .then(function(cache_exists) {
+ assert_false(cache_exists,
+ 'CacheStorage.has should return false after ' +
+ 'fulfillment of CacheStorage.delete promise.');
+ });
+ }, 'CacheStorage.delete with existing cache');
+
+promise_test(function(t) {
+ return self.caches.delete('cheezburger')
+ .then(function(result) {
+ assert_false(result,
+ 'CacheStorage.delete should return false for a ' +
+ 'nonexistent cache.');
+ });
+ }, 'CacheStorage.delete with nonexistent cache');
+
+promise_test(function(t) {
+ var unpaired_name = 'unpaired\uD800';
+ var converted_name = 'unpaired\uFFFD';
+
+ // The test assumes that a cache with converted_name does not
+ // exist, but if the implementation fails the test then such
+ // a cache will be created. Start off in a fresh state by
+ // deleting all caches.
+ return delete_all_caches()
+ .then(function() {
+ return self.caches.has(converted_name);
+ })
+ .then(function(cache_exists) {
+ assert_false(cache_exists,
+ 'Test setup failure: cache should not exist');
+ })
+ .then(function() { return self.caches.open(unpaired_name); })
+ .then(function() { return self.caches.keys(); })
+ .then(function(keys) {
+ assert_true(keys.indexOf(unpaired_name) !== -1,
+ 'keys should include cache with bad name');
+ })
+ .then(function() { return self.caches.has(unpaired_name); })
+ .then(function(cache_exists) {
+ assert_true(cache_exists,
+ 'CacheStorage names should be not be converted.');
+ })
+ .then(function() { return self.caches.has(converted_name); })
+ .then(function(cache_exists) {
+ assert_false(cache_exists,
+ 'CacheStorage names should be not be converted.');
+ });
+ }, 'CacheStorage names are DOMStrings not USVStrings');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/common.https.window.js b/testing/web-platform/tests/service-workers/cache-storage/common.https.window.js
new file mode 100644
index 0000000000..eba312c273
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/common.https.window.js
@@ -0,0 +1,44 @@
+// META: title=Cache Storage: Verify that Window and Workers see same storage
+// META: timeout=long
+
+function wait_for_message(worker) {
+ return new Promise(function(resolve) {
+ worker.addEventListener('message', function listener(e) {
+ resolve(e.data);
+ worker.removeEventListener('message', listener);
+ });
+ });
+}
+
+promise_test(function(t) {
+ var cache_name = 'common-test';
+ return self.caches.delete(cache_name)
+ .then(function() {
+ var worker = new Worker('resources/common-worker.js');
+ worker.postMessage({name: cache_name});
+ return wait_for_message(worker);
+ })
+ .then(function(message) {
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ return Promise.all([
+ cache.match('https://example.com/a'),
+ cache.match('https://example.com/b'),
+ cache.match('https://example.com/c')
+ ]);
+ })
+ .then(function(responses) {
+ return Promise.all(responses.map(
+ function(response) { return response.text(); }
+ ));
+ })
+ .then(function(bodies) {
+ assert_equals(bodies[0], 'a',
+ 'Body should match response put by worker');
+ assert_equals(bodies[1], 'b',
+ 'Body should match response put by worker');
+ assert_equals(bodies[2], 'c',
+ 'Body should match response put by worker');
+ });
+}, 'Window sees cache puts by Worker');
diff --git a/testing/web-platform/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html b/testing/web-platform/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html
new file mode 100644
index 0000000000..ec930a87d9
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="test-wait">
+<meta charset="utf-8">
+<script type="module">
+ const cache = await window.caches.open('cache_name_0')
+ await cache.add("")
+ const resp1 = await cache.match("")
+ const readStream = resp1.body
+ // Cloning will open the stream via NS_AsyncCopy in Gecko
+ resp1.clone()
+ // Give a little bit of time
+ await new Promise(setTimeout)
+ // At this point the previous open operation is about to finish but not yet.
+ // It will finish after the second open operation is made, potentially causing incorrect state.
+ await readStream.getReader().read();
+ document.documentElement.classList.remove('test-wait')
+</script>
diff --git a/testing/web-platform/tests/service-workers/cache-storage/credentials.https.html b/testing/web-platform/tests/service-workers/cache-storage/credentials.https.html
new file mode 100644
index 0000000000..0fe4a0a0ac
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/credentials.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Cache Storage: Verify credentials are respected by Cache operations</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-storage">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./../service-worker/resources/test-helpers.sub.js"></script>
+<style>iframe { display: none; }</style>
+<script>
+
+var worker = "./resources/credentials-worker.js";
+var scope = "./resources/credentials-iframe.html";
+promise_test(function(t) {
+ return self.caches.delete('credentials')
+ .then(function() {
+ return service_worker_unregister_and_register(t, worker, scope)
+ })
+ .then(function(reg) {
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ frame.contentWindow.postMessage([
+ {name: 'file.txt', username: 'aa', password: 'bb'},
+ {name: 'file.txt', username: 'cc', password: 'dd'},
+ {name: 'file.txt'}
+ ], '*');
+ return new Promise(function(resolve, reject) {
+ window.onmessage = t.step_func(function(e) {
+ resolve(e.data);
+ });
+ });
+ })
+ .then(function(data) {
+ assert_equals(data.length, 3, 'three entries should be present');
+ assert_equals(data.filter(function(url) { return /@/.test(url); }).length, 2,
+ 'two entries should contain credentials');
+ assert_true(data.some(function(url) { return /aa:bb@/.test(url); }),
+ 'entry with credentials aa:bb should be present');
+ assert_true(data.some(function(url) { return /cc:dd@/.test(url); }),
+ 'entry with credentials cc:dd should be present');
+ });
+}, "Cache API matching includes credentials");
+</script>
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cross-partition.https.tentative.html b/testing/web-platform/tests/service-workers/cache-storage/cross-partition.https.tentative.html
new file mode 100644
index 0000000000..1cfc256908
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cross-partition.https.tentative.html
@@ -0,0 +1,269 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<!-- Pull in executor_path needed by newPopup / newIframe -->
+<script src="/html/cross-origin-embedder-policy/credentialless/resources/common.js"></script>
+<!-- Pull in importScript / newPopup / newIframe -->
+<script src="/html/anonymous-iframe/resources/common.js"></script>
+<body>
+<script>
+
+const cache_exists_js = (cache_name, response_queue_name) => `
+ try {
+ const exists = await self.caches.has("${cache_name}");
+ if (exists) {
+ await send("${response_queue_name}", "true");
+ } else {
+ await send("${response_queue_name}", "false");
+ }
+ } catch {
+ await send("${response_queue_name}", "exception");
+ }
+`;
+
+const add_iframe_js = (iframe_origin, response_queue_uuid) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ await send("${response_queue_uuid}", newIframe("${iframe_origin}"));
+`;
+
+const same_site_origin = get_host_info().HTTPS_ORIGIN;
+const cross_site_origin = get_host_info().HTTPS_NOTSAMESITE_ORIGIN;
+
+async function create_test_iframes(t, response_queue_uuid) {
+
+ // Create a same-origin iframe in a cross-site popup.
+ const not_same_site_popup_uuid = newPopup(t, cross_site_origin);
+ await send(not_same_site_popup_uuid,
+ add_iframe_js(same_site_origin, response_queue_uuid));
+ const iframe_1_uuid = await receive(response_queue_uuid);
+
+ // Create a same-origin iframe in a same-site popup.
+ const same_origin_popup_uuid = newPopup(t, same_site_origin);
+ await send(same_origin_popup_uuid,
+ add_iframe_js(same_site_origin, response_queue_uuid));
+ const iframe_2_uuid = await receive(response_queue_uuid);
+
+ return [iframe_1_uuid, iframe_2_uuid];
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(iframe_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site iframe");
+ }
+
+ await send(iframe_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site iframe");
+ }
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition iframe");
+
+const newWorker = (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`;
+ const worker = new Worker(worker_url);
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newWorker = ${newWorker};
+ await send("${response_queue_uuid}", newWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a dedicated worker in the cross-top-level-site iframe.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ const worker_1_uuid = await receive(response_queue_uuid);
+
+ // Create a dedicated worker in the same-top-level-site iframe.
+ await send(iframe_2_uuid, create_worker_js(same_site_origin));
+ const worker_2_uuid = await receive(response_queue_uuid);
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(worker_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site worker");
+ }
+
+ await send(worker_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site worker");
+ }
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition dedicated worker");
+
+const newSharedWorker = (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`;
+ const worker = new SharedWorker(worker_url, worker_token);
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newSharedWorker = ${newSharedWorker};
+ await send("${response_queue_uuid}", newSharedWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a shared worker in the cross-top-level-site iframe.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ const worker_1_uuid = await receive(response_queue_uuid);
+
+ // Create a shared worker in the same-top-level-site iframe.
+ await send(iframe_2_uuid, create_worker_js(same_site_origin));
+ const worker_2_uuid = await receive(response_queue_uuid);
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(worker_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site worker");
+ }
+
+ await send(worker_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site worker");
+ }
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition shared worker");
+
+const newServiceWorker = async (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_service_worker_path +
+ `&uuid=${worker_token}`;
+ const worker_url_path = executor_service_worker_path.substring(0,
+ executor_service_worker_path.lastIndexOf('/'));
+ const scope = worker_url_path + "/not-used/";
+ const reg = await navigator.serviceWorker.register(worker_url,
+ {'scope': scope});
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newServiceWorker = ${newServiceWorker};
+ await send("${response_queue_uuid}", await newServiceWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a service worker in the same-top-level-site iframe.
+ await send(iframe_2_uuid, create_worker_js(same_site_origin));
+ const worker_2_uuid = await receive(response_queue_uuid);
+
+ t.add_cleanup(() =>
+ send(worker_2_uuid, "self.registration.unregister();"));
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(worker_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site worker");
+ }
+
+ // Create a service worker in the cross-top-level-site iframe. Note that
+ // if service workers are unpartitioned then this new service worker would
+ // replace the one created above. This is why we wait to create the second
+ // service worker until after we are done with the first one.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ const worker_1_uuid = await receive(response_queue_uuid);
+
+ t.add_cleanup(() =>
+ send(worker_1_uuid, "self.registration.unregister();"));
+
+ await send(worker_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site worker");
+ }
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition service worker");
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/blank.html b/testing/web-platform/tests/service-workers/cache-storage/resources/blank.html
new file mode 100644
index 0000000000..a3c3a4689a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/blank.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js b/testing/web-platform/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js
new file mode 100644
index 0000000000..ee574d2cb7
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js
@@ -0,0 +1,22 @@
+self.addEventListener('fetch', (event) => {
+ const params = new URL(event.request.url).searchParams;
+ if (params.has('ignore')) {
+ return;
+ }
+ if (!params.has('name')) {
+ event.respondWith(Promise.reject(TypeError('No name is provided.')));
+ return;
+ }
+
+ event.respondWith(Promise.resolve().then(async () => {
+ const name = params.get('name');
+ await caches.delete('foo');
+ const cache = await caches.open('foo');
+ await cache.put(event.request, new Response('hello'));
+ const keys = await cache.keys();
+
+ const original = event.request[name];
+ const stored = keys[0][name];
+ return new Response(`original: ${original}, stored: ${stored}`);
+ }));
+ });
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/common-worker.js b/testing/web-platform/tests/service-workers/cache-storage/resources/common-worker.js
new file mode 100644
index 0000000000..d0e8544b56
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/common-worker.js
@@ -0,0 +1,15 @@
+self.onmessage = function(e) {
+ var cache_name = e.data.name;
+
+ self.caches.open(cache_name)
+ .then(function(cache) {
+ return Promise.all([
+ cache.put('https://example.com/a', new Response('a')),
+ cache.put('https://example.com/b', new Response('b')),
+ cache.put('https://example.com/c', new Response('c'))
+ ]);
+ })
+ .then(function() {
+ self.postMessage('ok');
+ });
+};
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-iframe.html b/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-iframe.html
new file mode 100644
index 0000000000..00702df9e5
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-iframe.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Controlled frame for Cache API test with credentials</title>
+<script>
+
+function xhr(url, username, password) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest(), async = true;
+ xhr.open('GET', url, async, username, password);
+ xhr.send();
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState !== XMLHttpRequest.DONE)
+ return;
+ if (xhr.status === 200) {
+ resolve(xhr.responseText);
+ } else {
+ reject(new Error(xhr.statusText));
+ }
+ };
+ });
+}
+
+window.onmessage = function(e) {
+ Promise.all(e.data.map(function(item) {
+ return xhr(item.name, item.username, item.password);
+ }))
+ .then(function() {
+ navigator.serviceWorker.controller.postMessage('keys');
+ navigator.serviceWorker.onmessage = function(e) {
+ window.parent.postMessage(e.data, '*');
+ };
+ });
+};
+
+</script>
+<body>
+Hello? Yes, this is iframe.
+</body>
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-worker.js b/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-worker.js
new file mode 100644
index 0000000000..43965b5fe4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-worker.js
@@ -0,0 +1,59 @@
+var cache_name = 'credentials';
+
+function assert_equals(actual, expected, message) {
+ if (!Object.is(actual, expected))
+ throw Error(message + ': expected: ' + expected + ', actual: ' + actual);
+}
+
+self.onfetch = function(e) {
+ if (!/\.txt$/.test(e.request.url)) return;
+ var content = e.request.url;
+ var cache;
+ e.respondWith(
+ self.caches.open(cache_name)
+ .then(function(result) {
+ cache = result;
+ return cache.put(e.request, new Response(content));
+ })
+
+ .then(function() { return cache.match(e.request); })
+ .then(function(result) { return result.text(); })
+ .then(function(text) {
+ assert_equals(text, content, 'Cache.match() body should match');
+ })
+
+ .then(function() { return cache.matchAll(e.request); })
+ .then(function(results) {
+ assert_equals(results.length, 1, 'Should have one response');
+ return results[0].text();
+ })
+ .then(function(text) {
+ assert_equals(text, content, 'Cache.matchAll() body should match');
+ })
+
+ .then(function() { return self.caches.match(e.request); })
+ .then(function(result) { return result.text(); })
+ .then(function(text) {
+ assert_equals(text, content, 'CacheStorage.match() body should match');
+ })
+
+ .then(function() {
+ return new Response('dummy');
+ })
+ );
+};
+
+self.onmessage = function(e) {
+ if (e.data === 'keys') {
+ self.caches.open(cache_name)
+ .then(function(cache) { return cache.keys(); })
+ .then(function(requests) {
+ var urls = requests.map(function(request) { return request.url; });
+ self.clients.matchAll().then(function(clients) {
+ clients.forEach(function(client) {
+ client.postMessage(urls);
+ });
+ });
+ });
+ }
+};
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/fetch-status.py b/testing/web-platform/tests/service-workers/cache-storage/resources/fetch-status.py
new file mode 100644
index 0000000000..b7109f4787
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/fetch-status.py
@@ -0,0 +1,2 @@
+def main(request, response):
+ return int(request.GET[b"status"]), [], b""
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/iframe.html b/testing/web-platform/tests/service-workers/cache-storage/resources/iframe.html
new file mode 100644
index 0000000000..a2f1e502bb
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/iframe.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>ok</title>
+<script>
+window.onmessage = function(e) {
+ var id = e.data.id;
+ try {
+ var name = 'checkallowed';
+ self.caches.open(name).then(function (cache) {
+ self.caches.delete(name);
+ window.parent.postMessage({id: id, result: 'allowed'}, '*');
+ }).catch(function(e) {
+ window.parent.postMessage({id: id, result: 'denied', name: e.name, message: e.message}, '*');
+ });
+ } catch (e) {
+ window.parent.postMessage({id: id, result: 'unexpecteddenied', name: e.name, message: e.message}, '*');
+ }
+};
+</script>
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/simple.txt b/testing/web-platform/tests/service-workers/cache-storage/resources/simple.txt
new file mode 100644
index 0000000000..9e3cb91fb9
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/simple.txt
@@ -0,0 +1 @@
+a simple text file
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/test-helpers.js b/testing/web-platform/tests/service-workers/cache-storage/resources/test-helpers.js
new file mode 100644
index 0000000000..050ac0b542
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/test-helpers.js
@@ -0,0 +1,272 @@
+(function() {
+ var next_cache_index = 1;
+
+ // Returns a promise that resolves to a newly created Cache object. The
+ // returned Cache will be destroyed when |test| completes.
+ function create_temporary_cache(test) {
+ var uniquifier = String(++next_cache_index);
+ var cache_name = self.location.pathname + '/' + uniquifier;
+
+ test.add_cleanup(function() {
+ self.caches.delete(cache_name);
+ });
+
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ });
+ }
+
+ self.create_temporary_cache = create_temporary_cache;
+})();
+
+// Runs |test_function| with a temporary unique Cache passed in as the only
+// argument. The function is run as a part of Promise chain owned by
+// promise_test(). As such, it is expected to behave in a manner identical (with
+// the exception of the argument) to a function passed into promise_test().
+//
+// E.g.:
+// cache_test(function(cache) {
+// // Do something with |cache|, which is a Cache object.
+// }, "Some Cache test");
+function cache_test(test_function, description) {
+ promise_test(function(test) {
+ return create_temporary_cache(test)
+ .then(function(cache) { return test_function(cache, test); });
+ }, description);
+}
+
+// A set of Request/Response pairs to be used with prepopulated_cache_test().
+var simple_entries = [
+ {
+ name: 'a',
+ request: new Request('http://example.com/a'),
+ response: new Response('')
+ },
+
+ {
+ name: 'b',
+ request: new Request('http://example.com/b'),
+ response: new Response('')
+ },
+
+ {
+ name: 'a_with_query',
+ request: new Request('http://example.com/a?q=r'),
+ response: new Response('')
+ },
+
+ {
+ name: 'A',
+ request: new Request('http://example.com/A'),
+ response: new Response('')
+ },
+
+ {
+ name: 'a_https',
+ request: new Request('https://example.com/a'),
+ response: new Response('')
+ },
+
+ {
+ name: 'a_org',
+ request: new Request('http://example.org/a'),
+ response: new Response('')
+ },
+
+ {
+ name: 'cat',
+ request: new Request('http://example.com/cat'),
+ response: new Response('')
+ },
+
+ {
+ name: 'catmandu',
+ request: new Request('http://example.com/catmandu'),
+ response: new Response('')
+ },
+
+ {
+ name: 'cat_num_lives',
+ request: new Request('http://example.com/cat?lives=9'),
+ response: new Response('')
+ },
+
+ {
+ name: 'cat_in_the_hat',
+ request: new Request('http://example.com/cat/in/the/hat'),
+ response: new Response('')
+ },
+
+ {
+ name: 'non_2xx_response',
+ request: new Request('http://example.com/non2xx'),
+ response: new Response('', {status: 404, statusText: 'nope'})
+ },
+
+ {
+ name: 'error_response',
+ request: new Request('http://example.com/error'),
+ response: Response.error()
+ },
+];
+
+// A set of Request/Response pairs to be used with prepopulated_cache_test().
+// These contain a mix of test cases that use Vary headers.
+var vary_entries = [
+ {
+ name: 'vary_cookie_is_cookie',
+ request: new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}}),
+ response: new Response('',
+ {headers: {'Vary': 'Cookies'}})
+ },
+
+ {
+ name: 'vary_cookie_is_good',
+ request: new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-good-enough-for-me'}}),
+ response: new Response('',
+ {headers: {'Vary': 'Cookies'}})
+ },
+
+ {
+ name: 'vary_cookie_absent',
+ request: new Request('http://example.com/c'),
+ response: new Response('',
+ {headers: {'Vary': 'Cookies'}})
+ }
+];
+
+// Run |test_function| with a Cache object and a map of entries. Prior to the
+// call, the Cache is populated by cache entries from |entries|. The latter is
+// expected to be an Object mapping arbitrary keys to objects of the form
+// {request: <Request object>, response: <Response object>}. Entries are
+// serially added to the cache in the order specified.
+//
+// |test_function| should return a Promise that can be used with promise_test.
+function prepopulated_cache_test(entries, test_function, description) {
+ cache_test(function(cache) {
+ var p = Promise.resolve();
+ var hash = {};
+ entries.forEach(function(entry) {
+ hash[entry.name] = entry;
+ p = p.then(function() {
+ return cache.put(entry.request.clone(), entry.response.clone())
+ .catch(function(e) {
+ assert_unreached(
+ 'Test setup failed for entry ' + entry.name + ': ' + e
+ );
+ });
+ });
+ });
+ return p
+ .then(function() {
+ assert_equals(Object.keys(hash).length, entries.length);
+ })
+ .then(function() {
+ return test_function(cache, hash);
+ });
+ }, description);
+}
+
+// Helper for testing with Headers objects. Compares Headers instances
+// by serializing |expected| and |actual| to arrays and comparing.
+function assert_header_equals(actual, expected, description) {
+ assert_class_string(actual, "Headers", description);
+ var header;
+ var actual_headers = [];
+ var expected_headers = [];
+ for (header of actual)
+ actual_headers.push(header[0] + ": " + header[1]);
+ for (header of expected)
+ expected_headers.push(header[0] + ": " + header[1]);
+ assert_array_equals(actual_headers, expected_headers,
+ description + " Headers differ.");
+}
+
+// Helper for testing with Response objects. Compares simple
+// attributes defined on the interfaces, as well as the headers. It
+// does not compare the response bodies.
+function assert_response_equals(actual, expected, description) {
+ assert_class_string(actual, "Response", description);
+ ["type", "url", "status", "ok", "statusText"].forEach(function(attribute) {
+ assert_equals(actual[attribute], expected[attribute],
+ description + " Attributes differ: " + attribute + ".");
+ });
+ assert_header_equals(actual.headers, expected.headers, description);
+}
+
+// Assert that the two arrays |actual| and |expected| contain the same
+// set of Responses as determined by assert_response_equals. The order
+// is not significant.
+//
+// |expected| is assumed to not contain any duplicates.
+function assert_response_array_equivalent(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ expected.forEach(function(expected_element) {
+ // assert_response_in_array treats the first argument as being
+ // 'actual', and the second as being 'expected array'. We are
+ // switching them around because we want to be resilient
+ // against the |actual| array containing duplicates.
+ assert_response_in_array(expected_element, actual, description);
+ });
+}
+
+// Asserts that two arrays |actual| and |expected| contain the same
+// set of Responses as determined by assert_response_equals(). The
+// corresponding elements must occupy corresponding indices in their
+// respective arrays.
+function assert_response_array_equals(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ actual.forEach(function(value, index) {
+ assert_response_equals(value, expected[index],
+ description + " : object[" + index + "]");
+ });
+}
+
+// Equivalent to assert_in_array, but uses assert_response_equals.
+function assert_response_in_array(actual, expected_array, description) {
+ assert_true(expected_array.some(function(element) {
+ try {
+ assert_response_equals(actual, element);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }), description);
+}
+
+// Helper for testing with Request objects. Compares simple
+// attributes defined on the interfaces, as well as the headers.
+function assert_request_equals(actual, expected, description) {
+ assert_class_string(actual, "Request", description);
+ ["url"].forEach(function(attribute) {
+ assert_equals(actual[attribute], expected[attribute],
+ description + " Attributes differ: " + attribute + ".");
+ });
+ assert_header_equals(actual.headers, expected.headers, description);
+}
+
+// Asserts that two arrays |actual| and |expected| contain the same
+// set of Requests as determined by assert_request_equals(). The
+// corresponding elements must occupy corresponding indices in their
+// respective arrays.
+function assert_request_array_equals(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ actual.forEach(function(value, index) {
+ assert_request_equals(value, expected[index],
+ description + " : object[" + index + "]");
+ });
+}
+
+// Deletes all caches, returning a promise indicating success.
+function delete_all_caches() {
+ return self.caches.keys()
+ .then(function(keys) {
+ return Promise.all(keys.map(self.caches.delete.bind(self.caches)));
+ });
+}
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/vary.py b/testing/web-platform/tests/service-workers/cache-storage/resources/vary.py
new file mode 100644
index 0000000000..7fde1b1094
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/vary.py
@@ -0,0 +1,25 @@
+def main(request, response):
+ if b"clear-vary-value-override-cookie" in request.GET:
+ response.unset_cookie(b"vary-value-override")
+ return b"vary cookie cleared"
+
+ set_cookie_vary = request.GET.first(b"set-vary-value-override-cookie",
+ default=b"")
+ if set_cookie_vary:
+ response.set_cookie(b"vary-value-override", set_cookie_vary)
+ return b"vary cookie set"
+
+ # If there is a vary-value-override cookie set, then use its value
+ # for the VARY header no matter what the query string is set to. This
+ # override is necessary to test the case when two URLs are identical
+ # (including query), but differ by VARY header.
+ cookie_vary = request.cookies.get(b"vary-value-override")
+ if cookie_vary:
+ response.headers.set(b"vary", str(cookie_vary))
+ else:
+ # If there is no cookie, then use the query string value, if present.
+ query_vary = request.GET.first(b"vary", default=b"")
+ if query_vary:
+ response.headers.set(b"vary", query_vary)
+
+ return b"vary response"
diff --git a/testing/web-platform/tests/service-workers/cache-storage/sandboxed-iframes.https.html b/testing/web-platform/tests/service-workers/cache-storage/sandboxed-iframes.https.html
new file mode 100644
index 0000000000..098fa89daf
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/sandboxed-iframes.https.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>Cache Storage: Verify access in sandboxed iframes</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-storage">
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+function load_iframe(src, sandbox) {
+ return new Promise(function(resolve, reject) {
+ var iframe = document.createElement('iframe');
+ iframe.onload = function() { resolve(iframe); };
+
+ iframe.sandbox = sandbox;
+ iframe.src = src;
+
+ document.documentElement.appendChild(iframe);
+ });
+}
+
+function wait_for_message(id) {
+ return new Promise(function(resolve) {
+ self.addEventListener('message', function listener(e) {
+ if (e.data.id === id) {
+ resolve(e.data);
+ self.removeEventListener('message', listener);
+ }
+ });
+ });
+}
+
+var counter = 0;
+
+promise_test(function(t) {
+ return load_iframe('./resources/iframe.html',
+ 'allow-scripts allow-same-origin')
+ .then(function(iframe) {
+ var id = ++counter;
+ iframe.contentWindow.postMessage({id: id}, '*');
+ return wait_for_message(id);
+ })
+ .then(function(message) {
+ assert_equals(
+ message.result, 'allowed',
+ 'Access should be allowed if sandbox has allow-same-origin');
+ });
+}, 'Sandboxed iframe with allow-same-origin is allowed access');
+
+promise_test(function(t) {
+ return load_iframe('./resources/iframe.html',
+ 'allow-scripts')
+ .then(function(iframe) {
+ var id = ++counter;
+ iframe.contentWindow.postMessage({id: id}, '*');
+ return wait_for_message(id);
+ })
+ .then(function(message) {
+ assert_equals(
+ message.result, 'denied',
+ 'Access should be denied if sandbox lacks allow-same-origin');
+ assert_equals(message.name, 'SecurityError',
+ 'Failure should be a SecurityError');
+ });
+}, 'Sandboxed iframe without allow-same-origin is denied access');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/idlharness.https.any.js b/testing/web-platform/tests/service-workers/idlharness.https.any.js
new file mode 100644
index 0000000000..8db5d4d10f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/idlharness.https.any.js
@@ -0,0 +1,53 @@
+// META: global=window,worker
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: script=cache-storage/resources/test-helpers.js
+// META: script=service-worker/resources/test-helpers.sub.js
+// META: timeout=long
+
+// https://w3c.github.io/ServiceWorker
+
+idl_test(
+ ['service-workers'],
+ ['dom', 'html'],
+ async (idl_array, t) => {
+ self.cacheInstance = await create_temporary_cache(t);
+
+ idl_array.add_objects({
+ CacheStorage: ['caches'],
+ Cache: ['self.cacheInstance'],
+ ServiceWorkerContainer: ['navigator.serviceWorker']
+ });
+
+ // TODO: Add ServiceWorker and ServiceWorkerRegistration instances for the
+ // other worker scopes.
+ if (self.GLOBAL.isWindow()) {
+ idl_array.add_objects({
+ ServiceWorkerRegistration: ['registrationInstance'],
+ ServiceWorker: ['registrationInstance.installing']
+ });
+
+ const scope = 'service-worker/resources/scope/idlharness';
+ const registration = await service_worker_unregister_and_register(
+ t, 'service-worker/resources/empty-worker.js', scope);
+ t.add_cleanup(() => registration.unregister());
+
+ self.registrationInstance = registration;
+ } else if (self.ServiceWorkerGlobalScope) {
+ // self.ServiceWorkerGlobalScope should only be defined for the
+ // ServiceWorker scope, which allows us to detect and test the interfaces
+ // exposed only for ServiceWorker.
+ idl_array.add_objects({
+ Clients: ['clients'],
+ ExtendableEvent: ['new ExtendableEvent("type")'],
+ FetchEvent: ['new FetchEvent("type", { request: new Request("") })'],
+ ServiceWorkerGlobalScope: ['self'],
+ ServiceWorkerRegistration: ['registration'],
+ ServiceWorker: ['serviceWorker'],
+ // TODO: Test instances of Client and WindowClient, e.g.
+ // Client: ['self.clientInstance'],
+ // WindowClient: ['self.windowClientInstance']
+ });
+ }
+ }
+);
diff --git a/testing/web-platform/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html b/testing/web-platform/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html
new file mode 100644
index 0000000000..6f44bb17e7
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<title>Service Worker: Service-Worker-Allowed header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+const host_info = get_host_info();
+
+// Returns a URL for a service worker script whose Service-Worker-Allowed
+// header value is set to |allowed_path|. If |origin| is specified, that origin
+// is used.
+function build_script_url(allowed_path, origin) {
+ const script = 'resources/empty-worker.js';
+ const url = origin ? `${origin}${base_path()}${script}` : script;
+ return `${url}?pipe=header(Service-Worker-Allowed,${allowed_path})`;
+}
+
+// register_test is a promise_test that registers a service worker.
+function register_test(script, scope, description) {
+ promise_test(async t => {
+ t.add_cleanup(() => {
+ return service_worker_unregister(t, scope);
+ });
+
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ assert_true(registration instanceof ServiceWorkerRegistration, 'registered');
+ assert_equals(registration.scope, normalizeURL(scope));
+ }, description);
+}
+
+// register_fail_test is like register_test but expects a SecurityError.
+function register_fail_test(script, scope, description) {
+ promise_test(async t => {
+ t.add_cleanup(() => {
+ return service_worker_unregister(t, scope);
+ });
+
+ await service_worker_unregister(t, scope);
+ await promise_rejects_dom(t,
+ 'SecurityError',
+ navigator.serviceWorker.register(script, {scope}));
+ }, description);
+}
+
+register_test(
+ build_script_url('/allowed-path'),
+ '/allowed-path',
+ 'Registering within Service-Worker-Allowed path');
+
+register_test(
+ build_script_url(new URL('/allowed-path', document.location)),
+ '/allowed-path',
+ 'Registering within Service-Worker-Allowed path (absolute URL)');
+
+register_test(
+ build_script_url('../allowed-path-with-parent'),
+ 'allowed-path-with-parent',
+ 'Registering within Service-Worker-Allowed path with parent reference');
+
+register_fail_test(
+ build_script_url('../allowed-path'),
+ '/disallowed-path',
+ 'Registering outside Service-Worker-Allowed path'),
+
+register_fail_test(
+ build_script_url('../allowed-path-with-parent'),
+ '/allowed-path-with-parent',
+ 'Registering outside Service-Worker-Allowed path with parent reference');
+
+register_fail_test(
+ build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/'),
+ 'resources/this-scope-is-normally-allowed',
+ 'Service-Worker-Allowed is cross-origin to script, registering on a normally allowed scope');
+
+register_fail_test(
+ build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/'),
+ '/this-scope-is-normally-disallowed',
+ 'Service-Worker-Allowed is cross-origin to script, registering on a normally disallowed scope');
+
+register_fail_test(
+ build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/cross-origin/',
+ host_info.HTTPS_REMOTE_ORIGIN),
+ '/cross-origin/',
+ 'Service-Worker-Allowed is cross-origin to page, same-origin to script');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html
new file mode 100644
index 0000000000..3e3cc8b2b0
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: close operation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+
+service_worker_test(
+ 'resources/close-worker.js', 'ServiceWorkerGlobalScope: close operation');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html
new file mode 100644
index 0000000000..525245fe9e
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: ExtendableMessageEvent Constructor</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+service_worker_test(
+ 'resources/extendable-message-event-constructor-worker.js', document.title
+ );
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html
new file mode 100644
index 0000000000..89efd7a4a6
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html
@@ -0,0 +1,226 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: ExtendableMessageEvent</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script src='./resources/extendable-message-event-utils.js'></script>
+<script>
+promise_test(function(t) {
+ var script = 'resources/extendable-message-event-worker.js';
+ var scope = 'resources/scope/extendable-message-event-from-toplevel';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage =
+ function(event) { resolve(event.data); }
+ });
+ var channel = new MessageChannel;
+ registration.active.postMessage('', [channel.port1]);
+ return saw_message;
+ })
+ .then(function(results) {
+ var expected = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'WindowClient' },
+ frameType: 'top-level',
+ url: location.href,
+ visibilityState: 'visible',
+ focused: true
+ },
+ ports: [ { constructor: { name: 'MessagePort' } } ]
+ };
+ ExtendableMessageEventUtils.assert_equals(results, expected);
+ });
+ }, 'Post an extendable message from a top-level client');
+
+promise_test(function(t) {
+ var script = 'resources/extendable-message-event-worker.js';
+ var scope = 'resources/scope/extendable-message-event-from-nested';
+ var frame;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ add_completion_callback(function() { frame.remove(); });
+ var saw_message = new Promise(function(resolve) {
+ frame.contentWindow.navigator.serviceWorker.onmessage =
+ function(event) { resolve(event.data); }
+ });
+ f.contentWindow.navigator.serviceWorker.controller.postMessage('');
+ return saw_message;
+ })
+ .then(function(results) {
+ var expected = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'WindowClient' },
+ url: frame.contentWindow.location.href,
+ frameType: 'nested',
+ visibilityState: 'visible',
+ focused: false
+ },
+ ports: []
+ };
+ ExtendableMessageEventUtils.assert_equals(results, expected);
+ });
+ }, 'Post an extendable message from a nested client');
+
+promise_test(function(t) {
+ var script = 'resources/extendable-message-event-loopback-worker.js';
+ var scope = 'resources/scope/extendable-message-event-loopback';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var results = [];
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = function(event) {
+ switch (event.data.type) {
+ case 'record':
+ results.push(event.data.results);
+ break;
+ case 'finish':
+ resolve(results);
+ break;
+ }
+ };
+ });
+ registration.active.postMessage({type: 'start'});
+ return saw_message;
+ })
+ .then(function(results) {
+ assert_equals(results.length, 2);
+
+ var expected_trial_1 = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'ServiceWorker' },
+ scriptURL: normalizeURL(script),
+ state: 'activated'
+ },
+ ports: []
+ };
+ assert_equals(results[0].trial, 1);
+ ExtendableMessageEventUtils.assert_equals(
+ results[0].event, expected_trial_1
+ );
+
+ var expected_trial_2 = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'ServiceWorker' },
+ scriptURL: normalizeURL(script),
+ state: 'activated'
+ },
+ ports: [],
+ };
+ assert_equals(results[1].trial, 2);
+ ExtendableMessageEventUtils.assert_equals(
+ results[1].event, expected_trial_2
+ );
+ });
+ }, 'Post loopback extendable messages');
+
+promise_test(function(t) {
+ var script1 = 'resources/extendable-message-event-ping-worker.js';
+ var script2 = 'resources/extendable-message-event-pong-worker.js';
+ var scope = 'resources/scope/extendable-message-event-pingpong';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ // A controlled frame is necessary for keeping a waiting worker.
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ add_completion_callback(function() { frame.remove(); });
+ return navigator.serviceWorker.register(script2, {scope: scope});
+ })
+ .then(function(r) {
+ return wait_for_state(t, r.installing, 'installed');
+ })
+ .then(function() {
+ var results = [];
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = function(event) {
+ switch (event.data.type) {
+ case 'record':
+ results.push(event.data.results);
+ break;
+ case 'finish':
+ resolve(results);
+ break;
+ }
+ };
+ });
+ registration.active.postMessage({type: 'start'});
+ return saw_message;
+ })
+ .then(function(results) {
+ assert_equals(results.length, 2);
+
+ var expected_ping = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'ServiceWorker' },
+ scriptURL: normalizeURL(script1),
+ state: 'activated'
+ },
+ ports: []
+ };
+ assert_equals(results[0].pingOrPong, 'ping');
+ ExtendableMessageEventUtils.assert_equals(
+ results[0].event, expected_ping
+ );
+
+ var expected_pong = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'ServiceWorker' },
+ scriptURL: normalizeURL(script2),
+ state: 'installed'
+ },
+ ports: []
+ };
+ assert_equals(results[1].pingOrPong, 'pong');
+ ExtendableMessageEventUtils.assert_equals(
+ results[1].event, expected_pong
+ );
+ });
+ }, 'Post extendable messages among service workers');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js
new file mode 100644
index 0000000000..5ca5f65680
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js
@@ -0,0 +1,14 @@
+// META: title=fetch method on the right interface
+// META: global=serviceworker
+
+test(function() {
+ assert_false(self.hasOwnProperty('fetch'), 'ServiceWorkerGlobalScope ' +
+ 'instance should not have "fetch" method as its property.');
+ assert_inherits(self, 'fetch', 'ServiceWorkerGlobalScope should ' +
+ 'inherit "fetch" method.');
+ assert_own_property(Object.getPrototypeOf(Object.getPrototypeOf(self)), 'fetch',
+ 'WorkerGlobalScope should have "fetch" propery in its prototype.');
+ assert_equals(self.fetch, Object.getPrototypeOf(Object.getPrototypeOf(self)).fetch,
+ 'ServiceWorkerGlobalScope.fetch should be the same as ' +
+ 'WorkerGlobalScope.fetch.');
+}, 'Fetch method on the right interface');
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html
new file mode 100644
index 0000000000..399820dd2c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Service Worker: isSecureContext</title>
+</head>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(async (t) => {
+ var url = 'isSecureContext.serviceworker.js';
+ var scope = 'empty.html';
+ var frame_sw, sw_registration;
+
+ await service_worker_unregister(t, scope);
+ var f = await with_iframe(scope);
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame_sw = f.contentWindow.navigator.serviceWorker;
+ var registration = await navigator.serviceWorker.register(url, {scope: scope});
+ sw_registration = registration;
+ await wait_for_state(t, registration.installing, 'activated');
+ fetch_tests_from_worker(sw_registration.active);
+}, 'Setting up tests');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js
new file mode 100644
index 0000000000..5033594e34
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js
@@ -0,0 +1,5 @@
+importScripts("/resources/testharness.js");
+
+test(() => {
+ assert_true(self.isSecureContext, true);
+}, "isSecureContext");
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html
new file mode 100644
index 0000000000..99dedebf2e
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: postMessage</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/postmessage-loopback-worker.js';
+ var scope = 'resources/scope/postmessage-loopback';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(event) {
+ resolve(event.data);
+ };
+ });
+ registration.active.postMessage({port: channel.port2},
+ [channel.port2]);
+ return saw_message;
+ })
+ .then(function(result) {
+ assert_equals(result, 'OK');
+ });
+ }, 'Post loopback messages');
+
+promise_test(function(t) {
+ var script1 = 'resources/postmessage-ping-worker.js';
+ var script2 = 'resources/postmessage-pong-worker.js';
+ var scope = 'resources/scope/postmessage-pingpong';
+ var registration;
+ var frame;
+
+ return service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ // A controlled frame is necessary for keeping a waiting worker.
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return navigator.serviceWorker.register(script2, {scope: scope});
+ })
+ .then(function(r) {
+ return wait_for_state(t, r.installing, 'installed');
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(event) {
+ resolve(event.data);
+ };
+ });
+ registration.active.postMessage({port: channel.port2},
+ [channel.port2]);
+ return saw_message;
+ })
+ .then(function(result) {
+ assert_equals(result, 'OK');
+ frame.remove();
+ });
+ }, 'Post messages among service workers');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html
new file mode 100644
index 0000000000..aa3c74a13b
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: registration</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/registration-attribute-worker.js';
+ var scope = 'resources/scope/registration-attribute';
+ var registration;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(reg) {
+ registration = reg;
+ add_result_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(frame) {
+ var expected_events_seen = [
+ 'updatefound',
+ 'install',
+ 'statechange(installed)',
+ 'statechange(activating)',
+ 'activate',
+ 'statechange(activated)',
+ 'fetch',
+ ];
+
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ expected_events_seen.toString(),
+ 'Service Worker should respond to fetch');
+ frame.remove();
+ return registration.unregister();
+ });
+ }, 'Verify registration attributes on ServiceWorkerGlobalScope');
+
+promise_test(function(t) {
+ var script = 'resources/registration-attribute-worker.js';
+ var newer_script = 'resources/registration-attribute-newer-worker.js';
+ var scope = 'resources/scope/registration-attribute';
+ var newer_worker;
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(reg) {
+ registration = reg;
+ add_result_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(newer_script, {scope: scope});
+ })
+ .then(function(reg) {
+ assert_equals(reg, registration);
+ newer_worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var channel = new MessageChannel;
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) { resolve(e.data); };
+ });
+ newer_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+ .then(function(results) {
+ var script_url = normalizeURL(script);
+ var newer_script_url = normalizeURL(newer_script);
+ var expectations = [
+ 'evaluate',
+ ' installing: empty',
+ ' waiting: empty',
+ ' active: ' + script_url,
+ 'updatefound',
+ ' installing: ' + newer_script_url,
+ ' waiting: empty',
+ ' active: ' + script_url,
+ 'install',
+ ' installing: ' + newer_script_url,
+ ' waiting: empty',
+ ' active: ' + script_url,
+ 'statechange(installed)',
+ ' installing: empty',
+ ' waiting: ' + newer_script_url,
+ ' active: ' + script_url,
+ 'statechange(activating)',
+ ' installing: empty',
+ ' waiting: empty',
+ ' active: ' + newer_script_url,
+ 'activate',
+ ' installing: empty',
+ ' waiting: empty',
+ ' active: ' + newer_script_url,
+ 'statechange(activated)',
+ ' installing: empty',
+ ' waiting: empty',
+ ' active: ' + newer_script_url,
+ ];
+ assert_array_equals(results, expectations);
+ return registration.unregister();
+ });
+ }, 'Verify registration attributes on ServiceWorkerGlobalScope of the ' +
+ 'newer worker');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js
new file mode 100644
index 0000000000..41a8bc069a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js
@@ -0,0 +1,5 @@
+importScripts('../../resources/worker-testharness.js');
+
+test(function() {
+ assert_false('close' in self);
+}, 'ServiceWorkerGlobalScope close operation');
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js
new file mode 100644
index 0000000000..f6838ffb39
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js
@@ -0,0 +1,12 @@
+var source;
+
+self.addEventListener('message', function(e) {
+ source = e.source;
+ throw 'testError';
+});
+
+self.addEventListener('error', function(e) {
+ source.postMessage({
+ error: e.error, filename: e.filename, message: e.message, lineno: e.lineno,
+ colno: e.colno});
+});
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js
new file mode 100644
index 0000000000..42da5825c5
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js
@@ -0,0 +1,197 @@
+importScripts('/resources/testharness.js');
+
+const TEST_OBJECT = { wanwan: 123 };
+const CHANNEL1 = new MessageChannel();
+const CHANNEL2 = new MessageChannel();
+const PORTS = [CHANNEL1.port1, CHANNEL1.port2, CHANNEL2.port1];
+function createEvent(initializer) {
+ if (initializer === undefined)
+ return new ExtendableMessageEvent('type');
+ return new ExtendableMessageEvent('type', initializer);
+}
+
+// These test cases are mostly copied from the following file in the Chromium
+// project (as of commit 848ad70823991e0f12b437d789943a4ab24d65bb):
+// third_party/WebKit/LayoutTests/fast/events/constructors/message-event-constructor.html
+
+test(function() {
+ assert_false(createEvent().bubbles);
+ assert_false(createEvent().cancelable);
+ assert_equals(createEvent().data, null);
+ assert_equals(createEvent().origin, '');
+ assert_equals(createEvent().lastEventId, '');
+ assert_equals(createEvent().source, null);
+ assert_array_equals(createEvent().ports, []);
+}, 'no initializer specified');
+
+test(function() {
+ assert_false(createEvent({ bubbles: false }).bubbles);
+ assert_true(createEvent({ bubbles: true }).bubbles);
+}, '`bubbles` is specified');
+
+test(function() {
+ assert_false(createEvent({ cancelable: false }).cancelable);
+ assert_true(createEvent({ cancelable: true }).cancelable);
+}, '`cancelable` is specified');
+
+test(function() {
+ assert_equals(createEvent({ data: TEST_OBJECT }).data, TEST_OBJECT);
+ assert_equals(createEvent({ data: undefined }).data, null);
+ assert_equals(createEvent({ data: null }).data, null);
+ assert_equals(createEvent({ data: false }).data, false);
+ assert_equals(createEvent({ data: true }).data, true);
+ assert_equals(createEvent({ data: '' }).data, '');
+ assert_equals(createEvent({ data: 'chocolate' }).data, 'chocolate');
+ assert_equals(createEvent({ data: 12345 }).data, 12345);
+ assert_equals(createEvent({ data: 18446744073709551615 }).data,
+ 18446744073709552000);
+ assert_equals(createEvent({ data: NaN }).data, NaN);
+ // Note that valueOf() is not called, when the left hand side is
+ // evaluated.
+ assert_false(
+ createEvent({ data: {
+ valueOf: function() { return TEST_OBJECT; } } }).data ==
+ TEST_OBJECT);
+ assert_equals(createEvent({ get data(){ return 123; } }).data, 123);
+ let thrown = { name: 'Error' };
+ assert_throws_exactly(thrown, function() {
+ createEvent({ get data() { throw thrown; } }); });
+}, '`data` is specified');
+
+test(function() {
+ assert_equals(createEvent({ origin: 'melancholy' }).origin, 'melancholy');
+ assert_equals(createEvent({ origin: '' }).origin, '');
+ assert_equals(createEvent({ origin: null }).origin, 'null');
+ assert_equals(createEvent({ origin: false }).origin, 'false');
+ assert_equals(createEvent({ origin: true }).origin, 'true');
+ assert_equals(createEvent({ origin: 12345 }).origin, '12345');
+ assert_equals(
+ createEvent({ origin: 18446744073709551615 }).origin,
+ '18446744073709552000');
+ assert_equals(createEvent({ origin: NaN }).origin, 'NaN');
+ assert_equals(createEvent({ origin: [] }).origin, '');
+ assert_equals(createEvent({ origin: [1, 2, 3] }).origin, '1,2,3');
+ assert_equals(
+ createEvent({ origin: { melancholy: 12345 } }).origin,
+ '[object Object]');
+ // Note that valueOf() is not called, when the left hand side is
+ // evaluated.
+ assert_equals(
+ createEvent({ origin: {
+ valueOf: function() { return 'melancholy'; } } }).origin,
+ '[object Object]');
+ assert_equals(
+ createEvent({ get origin() { return 123; } }).origin, '123');
+ let thrown = { name: 'Error' };
+ assert_throws_exactly(thrown, function() {
+ createEvent({ get origin() { throw thrown; } }); });
+}, '`origin` is specified');
+
+test(function() {
+ assert_equals(
+ createEvent({ lastEventId: 'melancholy' }).lastEventId, 'melancholy');
+ assert_equals(createEvent({ lastEventId: '' }).lastEventId, '');
+ assert_equals(createEvent({ lastEventId: null }).lastEventId, 'null');
+ assert_equals(createEvent({ lastEventId: false }).lastEventId, 'false');
+ assert_equals(createEvent({ lastEventId: true }).lastEventId, 'true');
+ assert_equals(createEvent({ lastEventId: 12345 }).lastEventId, '12345');
+ assert_equals(
+ createEvent({ lastEventId: 18446744073709551615 }).lastEventId,
+ '18446744073709552000');
+ assert_equals(createEvent({ lastEventId: NaN }).lastEventId, 'NaN');
+ assert_equals(createEvent({ lastEventId: [] }).lastEventId, '');
+ assert_equals(
+ createEvent({ lastEventId: [1, 2, 3] }).lastEventId, '1,2,3');
+ assert_equals(
+ createEvent({ lastEventId: { melancholy: 12345 } }).lastEventId,
+ '[object Object]');
+ // Note that valueOf() is not called, when the left hand side is
+ // evaluated.
+ assert_equals(
+ createEvent({ lastEventId: {
+ valueOf: function() { return 'melancholy'; } } }).lastEventId,
+ '[object Object]');
+ assert_equals(
+ createEvent({ get lastEventId() { return 123; } }).lastEventId,
+ '123');
+ let thrown = { name: 'Error' };
+ assert_throws_exactly(thrown, function() {
+ createEvent({ get lastEventId() { throw thrown; } }); });
+}, '`lastEventId` is specified');
+
+test(function() {
+ assert_equals(createEvent({ source: CHANNEL1.port1 }).source, CHANNEL1.port1);
+ assert_equals(
+ createEvent({ source: self.registration.active }).source,
+ self.registration.active);
+ assert_equals(
+ createEvent({ source: CHANNEL1.port1 }).source, CHANNEL1.port1);
+ assert_throws_js(
+ TypeError, function() { createEvent({ source: this }); },
+ 'source should be Client or ServiceWorker or MessagePort');
+}, '`source` is specified');
+
+test(function() {
+ // Valid message ports.
+ var passed_ports = createEvent({ ports: PORTS}).ports;
+ assert_equals(passed_ports[0], CHANNEL1.port1);
+ assert_equals(passed_ports[1], CHANNEL1.port2);
+ assert_equals(passed_ports[2], CHANNEL2.port1);
+ assert_array_equals(createEvent({ ports: [] }).ports, []);
+ assert_array_equals(createEvent({ ports: undefined }).ports, []);
+
+ // Invalid message ports.
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: [1, 2, 3] }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: TEST_OBJECT }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: null }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: this }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: false }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: true }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: '' }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: 'chocolate' }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: 12345 }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: 18446744073709551615 }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: NaN }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ get ports() { return 123; } }); });
+ let thrown = { name: 'Error' };
+ assert_throws_exactly(thrown, function() {
+ createEvent({ get ports() { throw thrown; } }); });
+ // Note that valueOf() is not called, when the left hand side is
+ // evaluated.
+ var valueOf = function() { return PORTS; };
+ assert_throws_js(TypeError, function() {
+ createEvent({ ports: { valueOf: valueOf } }); });
+}, '`ports` is specified');
+
+test(function() {
+ var initializers = {
+ bubbles: true,
+ cancelable: true,
+ data: TEST_OBJECT,
+ origin: 'wonderful',
+ lastEventId: 'excellent',
+ source: CHANNEL1.port1,
+ ports: PORTS
+ };
+ assert_equals(createEvent(initializers).bubbles, true);
+ assert_equals(createEvent(initializers).cancelable, true);
+ assert_equals(createEvent(initializers).data, TEST_OBJECT);
+ assert_equals(createEvent(initializers).origin, 'wonderful');
+ assert_equals(createEvent(initializers).lastEventId, 'excellent');
+ assert_equals(createEvent(initializers).source, CHANNEL1.port1);
+ assert_equals(createEvent(initializers).ports[0], PORTS[0]);
+ assert_equals(createEvent(initializers).ports[1], PORTS[1]);
+ assert_equals(createEvent(initializers).ports[2], PORTS[2]);
+}, 'all initial values are specified');
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js
new file mode 100644
index 0000000000..13cae88d80
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js
@@ -0,0 +1,36 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+ switch (event.data.type) {
+ case 'start':
+ self.registration.active.postMessage(
+ {type: '1st', client_id: event.source.id});
+ break;
+ case '1st':
+ // 1st loopback message via ServiceWorkerRegistration.active.
+ var results = {
+ trial: 1,
+ event: ExtendableMessageEventUtils.serialize(event)
+ };
+ var client_id = event.data.client_id;
+ event.source.postMessage({type: '2nd', client_id: client_id});
+ event.waitUntil(clients.get(client_id)
+ .then(function(client) {
+ client.postMessage({type: 'record', results: results});
+ }));
+ break;
+ case '2nd':
+ // 2nd loopback message via ExtendableMessageEvent.source.
+ var results = {
+ trial: 2,
+ event: ExtendableMessageEventUtils.serialize(event)
+ };
+ var client_id = event.data.client_id;
+ event.waitUntil(clients.get(client_id)
+ .then(function(client) {
+ client.postMessage({type: 'record', results: results});
+ client.postMessage({type: 'finish'});
+ }));
+ break;
+ }
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js
new file mode 100644
index 0000000000..d07b22959c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js
@@ -0,0 +1,23 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+ switch (event.data.type) {
+ case 'start':
+ // Send a ping message to another service worker.
+ self.registration.waiting.postMessage(
+ {type: 'ping', client_id: event.source.id});
+ break;
+ case 'pong':
+ var results = {
+ pingOrPong: 'pong',
+ event: ExtendableMessageEventUtils.serialize(event)
+ };
+ var client_id = event.data.client_id;
+ event.waitUntil(clients.get(client_id)
+ .then(function(client) {
+ client.postMessage({type: 'record', results: results});
+ client.postMessage({type: 'finish'});
+ }));
+ break;
+ }
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js
new file mode 100644
index 0000000000..5e9669e83c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js
@@ -0,0 +1,18 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+ switch (event.data.type) {
+ case 'ping':
+ var results = {
+ pingOrPong: 'ping',
+ event: ExtendableMessageEventUtils.serialize(event)
+ };
+ var client_id = event.data.client_id;
+ event.waitUntil(clients.get(client_id)
+ .then(function(client) {
+ client.postMessage({type: 'record', results: results});
+ event.source.postMessage({type: 'pong', client_id: client_id});
+ }));
+ break;
+ }
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js
new file mode 100644
index 0000000000..d6a3b483f5
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js
@@ -0,0 +1,78 @@
+var ExtendableMessageEventUtils = {};
+
+// Create a representation of a given ExtendableMessageEvent that is suitable
+// for transmission via the `postMessage` API.
+ExtendableMessageEventUtils.serialize = function(event) {
+ var ports = event.ports.map(function(port) {
+ return { constructor: { name: port.constructor.name } };
+ });
+ return {
+ constructor: {
+ name: event.constructor.name
+ },
+ origin: event.origin,
+ lastEventId: event.lastEventId,
+ source: {
+ constructor: {
+ name: event.source.constructor.name
+ },
+ url: event.source.url,
+ frameType: event.source.frameType,
+ visibilityState: event.source.visibilityState,
+ focused: event.source.focused
+ },
+ ports: ports
+ };
+};
+
+// Compare the actual and expected values of an ExtendableMessageEvent that has
+// been transformed using the `serialize` function defined in this file.
+ExtendableMessageEventUtils.assert_equals = function(actual, expected) {
+ assert_equals(
+ actual.constructor.name, expected.constructor.name, 'event constructor'
+ );
+ assert_equals(actual.origin, expected.origin, 'event `origin` property');
+ assert_equals(
+ actual.lastEventId,
+ expected.lastEventId,
+ 'event `lastEventId` property'
+ );
+
+ assert_equals(
+ actual.source.constructor.name,
+ expected.source.constructor.name,
+ 'event `source` property constructor'
+ );
+ assert_equals(
+ actual.source.url, expected.source.url, 'event `source` property `url`'
+ );
+ assert_equals(
+ actual.source.frameType,
+ expected.source.frameType,
+ 'event `source` property `frameType`'
+ );
+ assert_equals(
+ actual.source.visibilityState,
+ expected.source.visibilityState,
+ 'event `source` property `visibilityState`'
+ );
+ assert_equals(
+ actual.source.focused,
+ expected.source.focused,
+ 'event `source` property `focused`'
+ );
+
+ assert_equals(
+ actual.ports.length,
+ expected.ports.length,
+ 'event `ports` property length'
+ );
+
+ for (var idx = 0; idx < expected.ports.length; ++idx) {
+ assert_equals(
+ actual.ports[idx].constructor.name,
+ expected.ports[idx].constructor.name,
+ 'MessagePort #' + idx + ' constructor'
+ );
+ }
+};
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js
new file mode 100644
index 0000000000..f5e7647e3e
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js
@@ -0,0 +1,5 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+ event.source.postMessage(ExtendableMessageEventUtils.serialize(event));
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js
new file mode 100644
index 0000000000..083e9aa2a8
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('message', function(event) {
+ if ('port' in event.data) {
+ var port = event.data.port;
+
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(event) {
+ if ('pong' in event.data)
+ port.postMessage(event.data.pong);
+ };
+ self.registration.active.postMessage({ping: channel.port2},
+ [channel.port2]);
+ } else if ('ping' in event.data) {
+ event.data.ping.postMessage({pong: 'OK'});
+ }
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js
new file mode 100644
index 0000000000..ebb1eccce2
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('message', function(event) {
+ if ('port' in event.data) {
+ var port = event.data.port;
+
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(event) {
+ if ('pong' in event.data)
+ port.postMessage(event.data.pong);
+ };
+
+ // Send a ping message to another service worker.
+ self.registration.waiting.postMessage({ping: channel.port2},
+ [channel.port2]);
+ }
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js
new file mode 100644
index 0000000000..4a0d90b618
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js
@@ -0,0 +1,4 @@
+self.addEventListener('message', function(event) {
+ if ('ping' in event.data)
+ event.data.ping.postMessage({pong: 'OK'});
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js
new file mode 100644
index 0000000000..44f3e2e8e9
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js
@@ -0,0 +1,33 @@
+// TODO(nhiroki): stop using global states because service workers can be killed
+// at any point. Instead, we could post a message to the page on each event via
+// Client object (http://crbug.com/558244).
+var results = [];
+
+function stringify(worker) {
+ return worker ? worker.scriptURL : 'empty';
+}
+
+function record(event_name) {
+ results.push(event_name);
+ results.push(' installing: ' + stringify(self.registration.installing));
+ results.push(' waiting: ' + stringify(self.registration.waiting));
+ results.push(' active: ' + stringify(self.registration.active));
+}
+
+record('evaluate');
+
+self.registration.addEventListener('updatefound', function() {
+ record('updatefound');
+ var worker = self.registration.installing;
+ self.registration.installing.addEventListener('statechange', function() {
+ record('statechange(' + worker.state + ')');
+ });
+ });
+
+self.addEventListener('install', function(e) { record('install'); });
+
+self.addEventListener('activate', function(e) { record('activate'); });
+
+self.addEventListener('message', function(e) {
+ e.data.port.postMessage(results);
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js
new file mode 100644
index 0000000000..315f437593
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js
@@ -0,0 +1,139 @@
+importScripts('../../resources/test-helpers.sub.js');
+importScripts('../../resources/worker-testharness.js');
+
+// TODO(nhiroki): stop using global states because service workers can be killed
+// at any point. Instead, we could post a message to the page on each event via
+// Client object (http://crbug.com/558244).
+var events_seen = [];
+
+// TODO(nhiroki): Move these assertions to registration-attribute.html because
+// an assertion failure on the worker is not shown on the result page and
+// handled as timeout. See registration-attribute-newer-worker.js for example.
+
+assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On worker script evaluation, registration attribute should be set');
+assert_equals(
+ self.registration.installing,
+ null,
+ 'On worker script evaluation, installing worker should be null');
+assert_equals(
+ self.registration.waiting,
+ null,
+ 'On worker script evaluation, waiting worker should be null');
+assert_equals(
+ self.registration.active,
+ null,
+ 'On worker script evaluation, active worker should be null');
+
+self.registration.addEventListener('updatefound', function() {
+ events_seen.push('updatefound');
+
+ assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On updatefound event, registration attribute should be set');
+ assert_equals(
+ self.registration.installing.scriptURL,
+ normalizeURL('registration-attribute-worker.js'),
+ 'On updatefound event, installing worker should be set');
+ assert_equals(
+ self.registration.waiting,
+ null,
+ 'On updatefound event, waiting worker should be null');
+ assert_equals(
+ self.registration.active,
+ null,
+ 'On updatefound event, active worker should be null');
+
+ assert_equals(
+ self.registration.installing.state,
+ 'installing',
+ 'On updatefound event, worker should be in the installing state');
+
+ var worker = self.registration.installing;
+ self.registration.installing.addEventListener('statechange', function() {
+ events_seen.push('statechange(' + worker.state + ')');
+ });
+ });
+
+self.addEventListener('install', function(e) {
+ events_seen.push('install');
+
+ assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On install event, registration attribute should be set');
+ assert_equals(
+ self.registration.installing.scriptURL,
+ normalizeURL('registration-attribute-worker.js'),
+ 'On install event, installing worker should be set');
+ assert_equals(
+ self.registration.waiting,
+ null,
+ 'On install event, waiting worker should be null');
+ assert_equals(
+ self.registration.active,
+ null,
+ 'On install event, active worker should be null');
+
+ assert_equals(
+ self.registration.installing.state,
+ 'installing',
+ 'On install event, worker should be in the installing state');
+ });
+
+self.addEventListener('activate', function(e) {
+ events_seen.push('activate');
+
+ assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On activate event, registration attribute should be set');
+ assert_equals(
+ self.registration.installing,
+ null,
+ 'On activate event, installing worker should be null');
+ assert_equals(
+ self.registration.waiting,
+ null,
+ 'On activate event, waiting worker should be null');
+ assert_equals(
+ self.registration.active.scriptURL,
+ normalizeURL('registration-attribute-worker.js'),
+ 'On activate event, active worker should be set');
+
+ assert_equals(
+ self.registration.active.state,
+ 'activating',
+ 'On activate event, worker should be in the activating state');
+ });
+
+self.addEventListener('fetch', function(e) {
+ events_seen.push('fetch');
+
+ assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On fetch event, registration attribute should be set');
+ assert_equals(
+ self.registration.installing,
+ null,
+ 'On fetch event, installing worker should be null');
+ assert_equals(
+ self.registration.waiting,
+ null,
+ 'On fetch event, waiting worker should be null');
+ assert_equals(
+ self.registration.active.scriptURL,
+ normalizeURL('registration-attribute-worker.js'),
+ 'On fetch event, active worker should be set');
+
+ assert_equals(
+ self.registration.active.state,
+ 'activated',
+ 'On fetch event, worker should be in the activated state');
+
+ e.respondWith(new Response(events_seen));
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js
new file mode 100644
index 0000000000..6da397dd15
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js
@@ -0,0 +1,25 @@
+function matchQuery(query) {
+ return self.location.href.indexOf(query) != -1;
+}
+
+if (matchQuery('?evaluation'))
+ self.registration.unregister();
+
+self.addEventListener('install', function(e) {
+ if (matchQuery('?install')) {
+ // Don't do waitUntil(unregister()) as that would deadlock as specified.
+ self.registration.unregister();
+ }
+ });
+
+self.addEventListener('activate', function(e) {
+ if (matchQuery('?activate'))
+ e.waitUntil(self.registration.unregister());
+ });
+
+self.addEventListener('message', function(e) {
+ e.waitUntil(self.registration.unregister()
+ .then(function(result) {
+ e.data.port.postMessage({result: result});
+ }));
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js
new file mode 100644
index 0000000000..8be8a1ffeb
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js
@@ -0,0 +1,22 @@
+var events_seen = [];
+
+self.registration.addEventListener('updatefound', function() {
+ events_seen.push('updatefound');
+ });
+
+self.addEventListener('activate', function(e) {
+ events_seen.push('activate');
+ });
+
+self.addEventListener('fetch', function(e) {
+ events_seen.push('fetch');
+ e.respondWith(new Response(events_seen));
+ });
+
+self.addEventListener('message', function(e) {
+ events_seen.push('message');
+ self.registration.update();
+ });
+
+// update() during the script evaluation should be ignored.
+self.registration.update();
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py
new file mode 100644
index 0000000000..8a87e1baa4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py
@@ -0,0 +1,16 @@
+import os
+import time
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ # update() does not bypass cache so set the max-age to 0 such that update()
+ # can find a new version in the network.
+ headers = [(b'Cache-Control', b'max-age: 0'),
+ (b'Content-Type', b'application/javascript')]
+ with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+ u'update-worker.js'), u'r') as file:
+ script = file.read()
+ # Return a different script for each access.
+ return headers, u'// %s\n%s' % (time.time(), script)
+
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html
new file mode 100644
index 0000000000..988f5466b9
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: Error event error message</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+promise_test(t => {
+ var script = 'resources/error-worker.js';
+ var scope = 'resources/scope/service-worker-error-event';
+ var error_name = 'testError'
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ var worker = registration.installing;
+ add_completion_callback(() => { registration.unregister(); });
+ return new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = resolve;
+ worker.postMessage('');
+ });
+ })
+ .then(e => {
+ assert_equals(e.data.error, error_name, 'error type');
+ assert_greater_than(
+ e.data.message.indexOf(error_name), -1, 'error message');
+ assert_greater_than(
+ e.data.filename.indexOf(script), -1, 'filename');
+ assert_equals(e.data.lineno, 5, 'error line number');
+ assert_equals(e.data.colno, 3, 'error column number');
+ });
+ }, 'Error handlers inside serviceworker should see the attributes of ' +
+ 'ErrorEvent');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html
new file mode 100644
index 0000000000..1a124d7276
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: unregister</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/unregister-worker.js?evaluation';
+ var scope = 'resources/scope/unregister-on-script-evaluation';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'redundant');
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ undefined,
+ 'After unregister(), the registration should not found');
+ });
+ }, 'Unregister on script evaluation');
+
+promise_test(function(t) {
+ var script = 'resources/unregister-worker.js?install';
+ var scope = 'resources/scope/unregister-on-install-event';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'redundant');
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ undefined,
+ 'After unregister(), the registration should not found');
+ });
+ }, 'Unregister on installing event');
+
+promise_test(function(t) {
+ var script = 'resources/unregister-worker.js?activate';
+ var scope = 'resources/scope/unregister-on-activate-event';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'redundant');
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ undefined,
+ 'After unregister(), the registration should not found');
+ });
+ }, 'Unregister on activate event');
+
+promise_test(function(t) {
+ var script = 'resources/unregister-worker.js';
+ var scope = 'resources/unregister-controlling-worker.html';
+
+ var controller;
+ var frame;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ controller = frame.contentWindow.navigator.serviceWorker.controller;
+
+ assert_equals(
+ controller.scriptURL,
+ normalizeURL(script),
+ 'Service worker should control a new document')
+
+ // Wait for the completion of unregister() on the worker.
+ var channel = new MessageChannel();
+ var promise = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_true(e.data.result,
+ 'unregister() should successfully finish');
+ resolve();
+ });
+ });
+ controller.postMessage({port: channel.port2}, [channel.port2]);
+ return promise;
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ undefined,
+ 'After unregister(), the registration should not found');
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller,
+ controller,
+ 'After unregister(), the worker should still control the document');
+ return with_iframe(scope);
+ })
+ .then(function(new_frame) {
+ assert_equals(
+ new_frame.contentWindow.navigator.serviceWorker.controller,
+ null,
+ 'After unregister(), the worker should not control a new document');
+
+ frame.remove();
+ new_frame.remove();
+ })
+ }, 'Unregister controlling service worker');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html
new file mode 100644
index 0000000000..a7dde22339
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: update</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/update-worker.py';
+ var scope = 'resources/scope/update';
+ var registration;
+ var frame1;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame1 = f;
+ registration.active.postMessage('update');
+ return wait_for_update(t, registration);
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(frame2) {
+ var expected_events_seen = [
+ 'updatefound', // by register().
+ 'activate',
+ 'fetch',
+ 'message',
+ 'updatefound', // by update() in the message handler.
+ 'fetch',
+ ];
+ assert_equals(
+ frame2.contentDocument.body.textContent,
+ expected_events_seen.toString(),
+ 'events seen by the worker');
+ frame1.remove();
+ frame2.remove();
+ });
+ }, 'Update a registration on ServiceWorkerGlobalScope');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/about-blank-replacement.https.html b/testing/web-platform/tests/service-workers/service-worker/about-blank-replacement.https.html
new file mode 100644
index 0000000000..b6efe3ec56
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/about-blank-replacement.https.html
@@ -0,0 +1,181 @@
+<!DOCTYPE html>
+<title>Service Worker: about:blank replacement handling</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This test attempts to verify various initial about:blank document
+// creation is accurately reflected via the Clients API. The goal is
+// for Clients API to reflect what the browser actually does and not
+// to make special cases for the API.
+//
+// If your browser does not create an about:blank document in certain
+// cases then please just mark the test expected fail for now. The
+// reuse of globals from about:blank documents to the final load document
+// has particularly bad interop at the moment. Hopefully we can evolve
+// tests like this to eventually align browsers.
+
+const worker = 'resources/about-blank-replacement-worker.js';
+
+// Helper routine that creates an iframe that internally has some kind
+// of nested window. The nested window could be another iframe or
+// it could be a popup window.
+function createFrameWithNestedWindow(url) {
+ return new Promise((resolve, reject) => {
+ let frame = document.createElement('iframe');
+ frame.src = url;
+ document.body.appendChild(frame);
+
+ window.addEventListener('message', function onMsg(evt) {
+ if (evt.data.type !== 'NESTED_LOADED') {
+ return;
+ }
+ window.removeEventListener('message', onMsg);
+ if (evt.data.result && evt.data.result.startsWith('failure:')) {
+ reject(evt.data.result);
+ return;
+ }
+ resolve(frame);
+ });
+ });
+}
+
+// Helper routine to request the given worker find the client with
+// the specified URL using the clients.matchAll() API.
+function getClientIdByURL(worker, url) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.addEventListener('message', function onMsg(evt) {
+ if (evt.data.type !== 'GET_CLIENT_ID') {
+ return;
+ }
+ navigator.serviceWorker.removeEventListener('message', onMsg);
+ resolve(evt.data.result);
+ });
+ worker.postMessage({ type: 'GET_CLIENT_ID', url: url.toString() });
+ });
+}
+
+async function doAsyncTest(t, scope) {
+ let reg = await service_worker_unregister_and_register(t, worker, scope);
+
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Load the scope as a frame. We expect this in turn to have a nested
+ // iframe. The service worker will intercept the load of the nested
+ // iframe and populate its body with the client ID of the initial
+ // about:blank document it sees via clients.matchAll().
+ let frame = await createFrameWithNestedWindow(scope);
+ let initialResult = frame.contentWindow.nested().document.body.textContent;
+ assert_false(initialResult.startsWith('failure:'), `result: ${initialResult}`);
+
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ frame.contentWindow.nested().navigator.serviceWorker.controller.scriptURL,
+ 'nested about:blank should have same controlling service worker');
+
+ // Next, ask the service worker to find the final client ID for the fully
+ // loaded nested frame.
+ let nestedURL = new URL(frame.contentWindow.nested().location);
+ let finalResult = await getClientIdByURL(reg.active, nestedURL);
+ assert_false(finalResult.startsWith('failure:'), `result: ${finalResult}`);
+
+ // If the nested frame doesn't have a URL to load, then there is no fetch
+ // event and the body should be empty. We can't verify the final client ID
+ // against anything.
+ if (nestedURL.href === 'about:blank' ||
+ nestedURL.href === 'about:srcdoc') {
+ assert_equals('', initialResult, 'about:blank text content should be blank');
+ }
+
+ // If the nested URL is not about:blank, though, then the fetch event handler
+ // should have populated the body with the client id of the initial about:blank.
+ // Verify the final client id matches.
+ else {
+ assert_equals(initialResult, finalResult, 'client ID values should match');
+ }
+
+ frame.remove();
+}
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is simply loaded normally.
+ await doAsyncTest(t, 'resources/about-blank-replacement-frame.py');
+}, 'Initial about:blank is controlled, exposed to clients.matchAll(), and ' +
+ 'matches final Client.');
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is modified immediately by
+ // its parent. In this case we add a message listener so the service
+ // worker can ping the client to verify its existence. This ping-pong
+ // check is performed during the initial load and when verifying the
+ // final loaded client.
+ await doAsyncTest(t, 'resources/about-blank-replacement-ping-frame.py');
+}, 'Initial about:blank modified by parent is controlled, exposed to ' +
+ 'clients.matchAll(), and matches final Client.');
+
+promise_test(async function(t) {
+ // Execute a test where the nested window is a popup window instead of
+ // an iframe. This should behave the same as the simple iframe case.
+ await doAsyncTest(t, 'resources/about-blank-replacement-popup-frame.py');
+}, 'Popup initial about:blank is controlled, exposed to clients.matchAll(), and ' +
+ 'matches final Client.');
+
+promise_test(async function(t) {
+ const scope = 'resources/about-blank-replacement-uncontrolled-nested-frame.html';
+
+ let reg = await service_worker_unregister_and_register(t, worker, scope);
+
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Load the scope as a frame. We expect this in turn to have a nested
+ // iframe. Unlike the other tests in this file the nested iframe URL
+ // is not covered by a service worker scope. It should end up as
+ // uncontrolled even though its initial about:blank is controlled.
+ let frame = await createFrameWithNestedWindow(scope);
+ let nested = frame.contentWindow.nested();
+ let initialResult = nested.document.body.textContent;
+
+ // The nested iframe should not have been intercepted by the service
+ // worker. The empty.html nested frame has "hello world" for its body.
+ assert_equals(initialResult.trim(), 'hello world', `result: ${initialResult}`);
+
+ assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null,
+ 'outer frame should be controlled');
+
+ assert_equals(nested.navigator.serviceWorker.controller, null,
+ 'nested frame should not be controlled');
+
+ frame.remove();
+}, 'Initial about:blank is controlled, exposed to clients.matchAll(), and ' +
+ 'final Client is not controlled by a service worker.');
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is an iframe without a src
+ // attribute. This simple nested about:blank should still inherit the
+ // controller and be visible to clients.matchAll().
+ await doAsyncTest(t, 'resources/about-blank-replacement-blank-nested-frame.html');
+}, 'Simple about:blank is controlled and is exposed to clients.matchAll().');
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is an iframe using a non-empty
+ // srcdoc containing only a tag pair so its textContent is still empty.
+ // This nested iframe should still inherit the controller and be visible
+ // to clients.matchAll().
+ await doAsyncTest(t, 'resources/about-blank-replacement-srcdoc-nested-frame.html');
+}, 'Nested about:srcdoc is controlled and is exposed to clients.matchAll().');
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is dynamically added without a src
+ // attribute. This simple nested about:blank should still inherit the
+ // controller and be visible to clients.matchAll().
+ await doAsyncTest(t, 'resources/about-blank-replacement-blank-dynamic-nested-frame.html');
+}, 'Dynamic about:blank is controlled and is exposed to clients.matchAll().');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html b/testing/web-platform/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html
new file mode 100644
index 0000000000..016a52c13c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Service Worker: registration events</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/blank.html';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ var sw = registration.installing;
+
+ return new Promise(t.step_func(function(resolve) {
+ sw.onstatechange = t.step_func(function() {
+ if (sw.state === 'installed') {
+ assert_equals(registration.active, null,
+ 'installed event should be fired before activating service worker');
+ resolve();
+ }
+ });
+ }));
+ })
+ .catch(unreached_rejection(t));
+ }, 'installed event should be fired before activating service worker');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/activation-after-registration.https.html b/testing/web-platform/tests/service-workers/service-worker/activation-after-registration.https.html
new file mode 100644
index 0000000000..29f97e3e3f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/activation-after-registration.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<title>Service Worker: Activation occurs after registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+ var scope = 'resources/blank.html';
+ var registration;
+
+ return service_worker_unregister_and_register(
+ t, 'resources/empty-worker.js', scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ assert_equals(
+ r.installing.state,
+ 'installing',
+ 'worker should be in the "installing" state upon registration');
+ return wait_for_state(t, r.installing, 'activated');
+ });
+}, 'activation occurs after registration');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/activation.https.html b/testing/web-platform/tests/service-workers/service-worker/activation.https.html
new file mode 100644
index 0000000000..278454d338
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/activation.https.html
@@ -0,0 +1,168 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>service worker: activation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// Returns {registration, iframe}, where |registration| has an active and
+// waiting worker. The active worker controls |iframe| and has an inflight
+// message event that can be finished by calling
+// |registration.active.postMessage('go')|.
+function setup_activation_test(t, scope, worker_url) {
+ var registration;
+ var iframe;
+ return navigator.serviceWorker.getRegistration(scope)
+ .then(r => {
+ if (r)
+ return r.unregister();
+ })
+ .then(() => {
+ // Create an in-scope iframe. Do this prior to registration to avoid
+ // racing between an update triggered by navigation and the update()
+ // call below.
+ return with_iframe(scope);
+ })
+ .then(f => {
+ iframe = f;
+ // Create an active worker.
+ return navigator.serviceWorker.register(worker_url, { scope: scope });
+ })
+ .then(r => {
+ registration = r;
+ add_result_callback(() => registration.unregister());
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => {
+ // Check that the frame was claimed.
+ assert_not_equals(
+ iframe.contentWindow.navigator.serviceWorker.controller, null);
+ // Create an in-flight request.
+ registration.active.postMessage('wait');
+ // Now there is both a controllee and an in-flight request.
+ // Initiate an update.
+ return registration.update();
+ })
+ .then(() => {
+ // Wait for a waiting worker.
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(() => {
+ return wait_for_activation_on_sample_scope(t, self);
+ })
+ .then(() => {
+ assert_not_equals(registration.waiting, null);
+ assert_not_equals(registration.active, null);
+ return Promise.resolve({registration: registration, iframe: iframe});
+ });
+}
+promise_test(t => {
+ var scope = 'resources/no-controllee';
+ var worker_url = 'resources/mint-new-worker.py';
+ var registration;
+ var iframe;
+ var new_worker;
+ return setup_activation_test(t, scope, worker_url)
+ .then(result => {
+ registration = result.registration;
+ iframe = result.iframe;
+ // Finish the in-flight request.
+ registration.active.postMessage('go');
+ return wait_for_activation_on_sample_scope(t, self);
+ })
+ .then(() => {
+ // The new worker is still waiting. Remove the frame and it should
+ // activate.
+ new_worker = registration.waiting;
+ assert_equals(new_worker.state, 'installed');
+ var reached_active = wait_for_state(t, new_worker, 'activating');
+ iframe.remove();
+ return reached_active;
+ })
+ .then(() => {
+ assert_equals(new_worker, registration.active);
+ });
+ }, 'loss of controllees triggers activation');
+promise_test(t => {
+ var scope = 'resources/no-request';
+ var worker_url = 'resources/mint-new-worker.py';
+ var registration;
+ var iframe;
+ var new_worker;
+ return setup_activation_test(t, scope, worker_url)
+ .then(result => {
+ registration = result.registration;
+ iframe = result.iframe;
+ // Remove the iframe.
+ iframe.remove();
+ return new Promise(resolve => setTimeout(resolve, 0));
+ })
+ .then(() => {
+ // Finish the request.
+ new_worker = registration.waiting;
+ var reached_active = wait_for_state(t, new_worker, 'activating');
+ registration.active.postMessage('go');
+ return reached_active;
+ })
+ .then(() => {
+ assert_equals(registration.active, new_worker);
+ });
+ }, 'finishing a request triggers activation');
+promise_test(t => {
+ var scope = 'resources/skip-waiting';
+ var worker_url = 'resources/mint-new-worker.py?skip-waiting';
+ var registration;
+ var iframe;
+ var new_worker;
+ return setup_activation_test(t, scope, worker_url)
+ .then(result => {
+ registration = result.registration;
+ iframe = result.iframe;
+ // Finish the request. The iframe does not need to be removed because
+ // skipWaiting() was called.
+ new_worker = registration.waiting;
+ var reached_active = wait_for_state(t, new_worker, 'activating');
+ registration.active.postMessage('go');
+ return reached_active;
+ })
+ .then(() => {
+ assert_equals(registration.active, new_worker);
+ // Remove the iframe.
+ iframe.remove();
+ });
+ }, 'skipWaiting bypasses no controllee requirement');
+
+// This test is not really about activation, but otherwise is very
+// similar to the other tests here.
+promise_test(t => {
+ var scope = 'resources/unregister';
+ var worker_url = 'resources/mint-new-worker.py';
+ var registration;
+ var iframe;
+ var new_worker;
+ return setup_activation_test(t, scope, worker_url)
+ .then(result => {
+ registration = result.registration;
+ iframe = result.iframe;
+ // Remove the iframe.
+ iframe.remove();
+ return registration.unregister();
+ })
+ .then(() => {
+ // The unregister operation should wait for the active worker to
+ // finish processing its events before clearing the registration.
+ new_worker = registration.waiting;
+ var reached_redundant = wait_for_state(t, new_worker, 'redundant');
+ registration.active.postMessage('go');
+ return reached_redundant;
+ })
+ .then(() => {
+ assert_equals(registration.installing, null);
+ assert_equals(registration.waiting, null);
+ assert_equals(registration.active, null);
+ });
+ }, 'finishing a request triggers unregister');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/active.https.html b/testing/web-platform/tests/service-workers/service-worker/active.https.html
new file mode 100644
index 0000000000..350a34b802
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/active.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.active</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/empty-worker.js';
+const SCOPE = 'resources/blank.html';
+
+// "active" is set
+promise_test(async t => {
+
+ t.add_cleanup(async() => {
+ if (frame)
+ frame.remove();
+ if (registration)
+ await registration.unregister();
+ });
+
+ await service_worker_unregister(t, SCOPE);
+ const frame = await with_iframe(SCOPE);
+ const registration =
+ await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ await wait_for_state(t, registration.installing, 'activating');
+ const container = frame.contentWindow.navigator.serviceWorker;
+ assert_equals(container.controller, null, 'controller');
+ assert_equals(registration.active.state, 'activating',
+ 'registration.active');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.installing, null, 'registration.installing');
+
+ // FIXME: Add a test for a frame created after installation.
+ // Should the existing frame ("frame") block activation?
+}, 'active is set');
+
+// Tests that the ServiceWorker objects returned from active attribute getter
+// that represent the same service worker are the same objects.
+promise_test(async t => {
+ const registration1 =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ const registration2 = await navigator.serviceWorker.getRegistration(SCOPE);
+ assert_equals(registration1.active, registration2.active,
+ 'ServiceWorkerRegistration.active should return the same ' +
+ 'object');
+ await registration1.unregister();
+}, 'The ServiceWorker objects returned from active attribute getter that ' +
+ 'represent the same service worker are the same objects');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/claim-affect-other-registration.https.html b/testing/web-platform/tests/service-workers/service-worker/claim-affect-other-registration.https.html
new file mode 100644
index 0000000000..52555ac271
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/claim-affect-other-registration.https.html
@@ -0,0 +1,136 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Service Worker: claim() should affect other registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var frame;
+
+ var init_scope = 'resources/blank.html?affect-other';
+ var init_worker_url = 'resources/empty.js';
+ var init_registration;
+ var init_workers = [undefined, undefined];
+
+ var claim_scope = 'resources/blank.html?affect-other-registration';
+ var claim_worker_url = 'resources/claim-worker.js';
+ var claim_registration;
+ var claim_worker;
+
+ return Promise.resolve()
+ // Register the first service worker to init_scope.
+ .then(() => navigator.serviceWorker.register(init_worker_url + '?v1',
+ {scope: init_scope}))
+ .then(r => {
+ init_registration = r;
+ init_workers[0] = r.installing;
+ return Promise.resolve()
+ .then(() => wait_for_state(t, init_workers[0], 'activated'))
+ .then(() => assert_array_equals([init_registration.active,
+ init_registration.waiting,
+ init_registration.installing],
+ [init_workers[0],
+ null,
+ null],
+ 'Wrong workers.'));
+ })
+
+ // Create an iframe as the client of the first service worker of init_scope.
+ .then(() => with_iframe(claim_scope))
+ .then(f => frame = f)
+
+ // Check the controller.
+ .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration(
+ normalizeURL(init_scope)))
+ .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ r.active,
+ '.controller should belong to init_scope.'))
+
+ // Register the second service worker to init_scope.
+ .then(() => navigator.serviceWorker.register(init_worker_url + '?v2',
+ {scope: init_scope}))
+ .then(r => {
+ assert_equals(r, init_registration, 'Should be the same registration');
+ init_workers[1] = r.installing;
+ return Promise.resolve()
+ .then(() => wait_for_state(t, init_workers[1], 'installed'))
+ .then(() => assert_array_equals([init_registration.active,
+ init_registration.waiting,
+ init_registration.installing],
+ [init_workers[0],
+ init_workers[1],
+ null],
+ 'Wrong workers.'));
+ })
+
+ // Register a service worker to claim_scope.
+ .then(() => navigator.serviceWorker.register(claim_worker_url,
+ {scope: claim_scope}))
+ .then(r => {
+ claim_registration = r;
+ claim_worker = r.installing;
+ return wait_for_state(t, claim_worker, 'activated')
+ })
+
+ // Let claim_worker claim the created iframe.
+ .then(function() {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+
+ claim_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+
+ // Check the controller.
+ .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration(
+ normalizeURL(claim_scope)))
+ .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ r.active,
+ '.controller should belong to claim_scope.'))
+
+ // Check the status of created registrations and service workers.
+ .then(() => wait_for_state(t, init_workers[1], 'activated'))
+ .then(() => {
+ assert_array_equals([claim_registration.active,
+ claim_registration.waiting,
+ claim_registration.installing],
+ [claim_worker,
+ null,
+ null],
+ 'claim_worker should be the only worker.')
+
+ assert_array_equals([init_registration.active,
+ init_registration.waiting,
+ init_registration.installing],
+ [init_workers[1],
+ null,
+ null],
+ 'The waiting sw should become the active worker.')
+
+ assert_array_equals([init_workers[0].state,
+ init_workers[1].state,
+ claim_worker.state],
+ ['redundant',
+ 'activated',
+ 'activated'],
+ 'Wrong worker states.');
+ })
+
+ // Cleanup and finish testing.
+ .then(() => frame.remove())
+ .then(() => Promise.all([
+ init_registration.unregister(),
+ claim_registration.unregister()
+ ]))
+ .then(() => t.done());
+}, 'claim() should affect the originally controlling registration.');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/claim-fetch.https.html b/testing/web-platform/tests/service-workers/service-worker/claim-fetch.https.html
new file mode 100644
index 0000000000..ae0082df06
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/claim-fetch.https.html
@@ -0,0 +1,90 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+async function tryFetch(fetchFunc, path) {
+ let response;
+ try {
+ response = await fetchFunc(path);
+ } catch (err) {
+ throw (`fetch() threw: ${err}`);
+ }
+
+ let responseText;
+ try {
+ responseText = await response.text();
+ } catch (err) {
+ throw (`text() threw: ${err}`);
+ }
+
+ return responseText;
+}
+
+promise_test(async function(t) {
+ const scope = 'resources/';
+ const script = 'resources/claim-worker.js';
+ const resource = 'simple.txt';
+
+ // Create the test frame.
+ const frame = await with_iframe('resources/blank.html');
+ t.add_cleanup(() => frame.remove());
+
+ // Check the controller and test with fetch.
+ assert_equals(frame.contentWindow.navigator.controller, undefined,
+ 'Should have no controller.');
+ let response;
+ try {
+ response = await tryFetch(frame.contentWindow.fetch, resource);
+ } catch (err) {
+ assert_unreached(`uncontrolled fetch failed: ${err}`);
+ }
+ assert_equals(response, 'a simple text file\n',
+ 'fetch() should not be intercepted.');
+
+ // Register a service worker.
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+
+ // Register a controllerchange event to wait until the controller is updated
+ // and check if the frame is controlled by a service worker.
+ const controllerChanged = new Promise((resolve) => {
+ frame.contentWindow.navigator.serviceWorker.oncontrollerchange = () => {
+ resolve(frame.contentWindow.navigator.serviceWorker.controller);
+ };
+ });
+
+ // Tell the service worker to claim the iframe.
+ const sawMessage = new Promise((resolve) => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func((event) => {
+ resolve(event.data);
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+ const data = await sawMessage;
+ assert_equals(data, 'PASS', 'Worker call to claim() should fulfill.');
+
+ // Check if the controller is updated after claim() and test with fetch.
+ const controller = await controllerChanged;
+ assert_true(controller instanceof frame.contentWindow.ServiceWorker,
+ 'iframe should be controlled.');
+ try {
+ response = await tryFetch(frame.contentWindow.fetch, resource);
+ } catch (err) {
+ assert_unreached(`controlled fetch failed: ${err}`);
+ }
+ assert_equals(response, 'Intercepted!',
+ 'fetch() should be intercepted.');
+}, 'fetch() should be intercepted after the client is claimed.');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/claim-not-using-registration.https.html b/testing/web-platform/tests/service-workers/service-worker/claim-not-using-registration.https.html
new file mode 100644
index 0000000000..fd61d05ba4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/claim-not-using-registration.https.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<title>Service Worker: claim client not using registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(function(t) {
+ var init_scope = 'resources/blank.html?not-using-init';
+ var claim_scope = 'resources/blank.html?not-using';
+ var init_worker_url = 'resources/empty.js';
+ var claim_worker_url = 'resources/claim-worker.js';
+ var claim_worker, claim_registration, frame1, frame2;
+ return service_worker_unregister_and_register(
+ t, init_worker_url, init_scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, init_scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return Promise.all(
+ [with_iframe(init_scope), with_iframe(claim_scope)]);
+ })
+ .then(function(frames) {
+ frame1 = frames[0];
+ frame2 = frames[1];
+ assert_equals(
+ frame1.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(init_worker_url),
+ 'Frame1 controller should not be null');
+ assert_equals(
+ frame2.contentWindow.navigator.serviceWorker.controller, null,
+ 'Frame2 controller should be null');
+ return navigator.serviceWorker.register(claim_worker_url,
+ {scope: claim_scope});
+ })
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, claim_scope);
+ });
+
+ claim_worker = registration.installing;
+ claim_registration = registration;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var saw_controllerchanged = new Promise(function(resolve) {
+ frame2.contentWindow.navigator.serviceWorker.oncontrollerchange =
+ function() { resolve(); }
+ });
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ claim_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return Promise.all([saw_controllerchanged, saw_message]);
+ })
+ .then(function() {
+ assert_equals(
+ frame1.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(init_worker_url),
+ 'Frame1 should not be influenced');
+ assert_equals(
+ frame2.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(claim_worker_url),
+ 'Frame2 should be controlled by the new registration');
+ frame1.remove();
+ frame2.remove();
+ return claim_registration.unregister();
+ });
+ }, 'Test claim client which is not using registration');
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?longer-matched';
+ var claim_scope = 'resources/blank.html?longer';
+ var claim_worker_url = 'resources/claim-worker.js';
+ var installing_worker_url = 'resources/empty-worker.js';
+ var frame, claim_worker;
+ return with_iframe(scope)
+ .then(function(f) {
+ frame = f;
+ return navigator.serviceWorker.register(
+ claim_worker_url, {scope: claim_scope});
+ })
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, claim_scope);
+ });
+
+ claim_worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(
+ installing_worker_url, {scope: scope});
+ })
+ .then(function() {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ claim_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+ .then(function() {
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller, null,
+ 'Frame should not be claimed when a longer-matched ' +
+ 'registration exists');
+ frame.remove();
+ });
+ }, 'Test claim client when there\'s a longer-matched registration not ' +
+ 'already used by the page');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html b/testing/web-platform/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html
new file mode 100644
index 0000000000..f5f44886ba
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(function(t) {
+ var frame;
+ var resource = 'simple.txt';
+
+ var worker;
+ var scope = 'resources/';
+ var script = 'resources/claim-worker.js';
+
+ return Promise.resolve()
+ // Create the test iframe with a shared worker.
+ .then(() => with_iframe('resources/claim-shared-worker-fetch-iframe.html'))
+ .then(f => frame = f)
+
+ // Check the controller and test with fetch in the shared worker.
+ .then(() => assert_equals(frame.contentWindow.navigator.controller,
+ undefined,
+ 'Should have no controller.'))
+ .then(() => frame.contentWindow.fetch_in_shared_worker(resource))
+ .then(response_text => assert_equals(response_text,
+ 'a simple text file\n',
+ 'fetch() should not be intercepted.'))
+ // Register a service worker.
+ .then(() => service_worker_unregister_and_register(t, script, scope))
+ .then(r => {
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ worker = r.installing;
+
+ return wait_for_state(t, worker, 'activated')
+ })
+ // Let the service worker claim the iframe and the shared worker.
+ .then(() => {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+
+ // Check the controller and test with fetch in the shared worker.
+ .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration(scope))
+ .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ r.active,
+ 'Test iframe should be claimed.'))
+ // TODO(horo): Check the SharedWorker's navigator.seviceWorker.controller.
+ .then(() => frame.contentWindow.fetch_in_shared_worker(resource))
+ .then(response_text =>
+ assert_equals(response_text,
+ 'Intercepted!',
+ 'fetch() in the shared worker should be intercepted.'))
+
+ // Cleanup this testcase.
+ .then(() => frame.remove());
+}, 'fetch() in SharedWorker should be intercepted after the client is claimed.')
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/claim-using-registration.https.html b/testing/web-platform/tests/service-workers/service-worker/claim-using-registration.https.html
new file mode 100644
index 0000000000..8a2a6ff25c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/claim-using-registration.https.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<title>Service Worker: claim client using registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var scope = 'resources/';
+ var frame_url = 'resources/blank.html?using-different-registration';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/claim-worker.js';
+ var worker, sw_registration, frame;
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(frame_url);
+ })
+ .then(function(f) {
+ frame = f;
+ return navigator.serviceWorker.register(url2, {scope: frame_url});
+ })
+ .then(function(registration) {
+ worker = registration.installing;
+ sw_registration = registration;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var saw_controllerchanged = new Promise(function(resolve) {
+ frame.contentWindow.navigator.serviceWorker.oncontrollerchange =
+ function() { resolve(); }
+ });
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return Promise.all([saw_controllerchanged, saw_message]);
+ })
+ .then(function() {
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(url2),
+ 'Frame1 controller scriptURL should be changed to url2');
+ frame.remove();
+ return sw_registration.unregister();
+ });
+ }, 'Test worker claims client which is using another registration');
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?using-same-registration';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/claim-worker.js';
+ var frame, worker;
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return navigator.serviceWorker.register(url2, {scope: scope});
+ })
+ .then(function(registration) {
+ worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'FAIL: exception: InvalidStateError',
+ 'Worker call to claim() should reject with ' +
+ 'InvalidStateError');
+ resolve();
+ });
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Test for the waiting worker claims a client which is using the the ' +
+ 'same registration');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/claim-with-redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/claim-with-redirect.https.html
new file mode 100644
index 0000000000..fd89cb9b00
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/claim-with-redirect.https.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<title>Service Worker: Claim() when update happens after redirect</title>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+var BASE_URL = host_info['HTTPS_ORIGIN'] + base_path();
+var OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + base_path();
+
+var WORKER_URL = OTHER_BASE_URL + 'resources/update-claim-worker.py'
+var SCOPE_URL = OTHER_BASE_URL + 'resources/redirect.py'
+var OTHER_IFRAME_URL = OTHER_BASE_URL +
+ 'resources/claim-with-redirect-iframe.html';
+
+// Different origin from the registration
+var REDIRECT_TO_URL = BASE_URL +
+ 'resources/claim-with-redirect-iframe.html?redirected';
+
+var REGISTER_IFRAME_URL = OTHER_IFRAME_URL + '?register=' +
+ encodeURIComponent(WORKER_URL) + '&' +
+ 'scope=' + encodeURIComponent(SCOPE_URL);
+var REDIRECT_IFRAME_URL = SCOPE_URL + '?Redirect=' +
+ encodeURIComponent(REDIRECT_TO_URL);
+var UPDATE_IFRAME_URL = OTHER_IFRAME_URL + '?update=' +
+ encodeURIComponent(SCOPE_URL);
+var UNREGISTER_IFRAME_URL = OTHER_IFRAME_URL + '?unregister=' +
+ encodeURIComponent(SCOPE_URL);
+
+var waiting_resolver = undefined;
+
+addEventListener('message', e => {
+ if (waiting_resolver !== undefined) {
+ waiting_resolver(e.data);
+ }
+ });
+
+function assert_with_iframe(url, expected_message) {
+ return new Promise(resolve => {
+ waiting_resolver = resolve;
+ with_iframe(url);
+ })
+ .then(data => assert_equals(data.message, expected_message));
+}
+
+// This test checks behavior when browser got a redirect header from in-scope
+// page and navigated to out-of-scope page which has a different origin from any
+// registrations.
+promise_test(t => {
+ return assert_with_iframe(REGISTER_IFRAME_URL, 'registered')
+ .then(() => assert_with_iframe(REDIRECT_IFRAME_URL, 'redirected'))
+ .then(() => assert_with_iframe(UPDATE_IFRAME_URL, 'updated'))
+ .then(() => assert_with_iframe(UNREGISTER_IFRAME_URL, 'unregistered'));
+ }, 'Claim works after redirection to another origin');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/claim-worker-fetch.https.html b/testing/web-platform/tests/service-workers/service-worker/claim-worker-fetch.https.html
new file mode 100644
index 0000000000..7cb26c742b
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/claim-worker-fetch.https.html
@@ -0,0 +1,83 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test((t) => {
+ return runTest(t, 'resources/claim-worker-fetch-iframe.html');
+}, 'fetch() in Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/claim-nested-worker-fetch-iframe.html');
+}, 'fetch() in nested Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/claim-blob-url-worker-fetch-iframe.html');
+}, 'fetch() in blob URL Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-blob-url-workers.html');
+}, 'fetch() in nested blob URL Worker created from a blob URL Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-worker-created-from-blob-url-worker.html');
+}, 'fetch() in nested Worker created from a blob URL Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-blob-url-worker-created-from-worker.html');
+}, 'fetch() in nested blob URL Worker created from a Worker should be intercepted after the client is claimed.');
+
+async function runTest(t, iframe_url) {
+ const resource = 'simple.txt';
+ const scope = 'resources/';
+ const script = 'resources/claim-worker.js';
+
+ // Create the test iframe with a dedicated worker.
+ const frame = await with_iframe(iframe_url);
+ t.add_cleanup(_ => frame.remove());
+
+ // Check the controller and test with fetch in the worker.
+ assert_equals(frame.contentWindow.navigator.controller,
+ undefined, 'Should have no controller.');
+ {
+ const response_text = await frame.contentWindow.fetch_in_worker(resource);
+ assert_equals(response_text, 'a simple text file\n',
+ 'fetch() should not be intercepted.');
+ }
+
+ // Register a service worker.
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Let the service worker claim the iframe and the worker.
+ const channel = new MessageChannel();
+ const saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS', 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ reg.active.postMessage({port: channel.port2}, [channel.port2]);
+ await saw_message;
+
+ // Check the controller and test with fetch in the worker.
+ const reg2 =
+ await frame.contentWindow.navigator.serviceWorker.getRegistration(scope);
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ reg2.active, 'Test iframe should be claimed.');
+
+ {
+ // TODO(horo): Check the worker's navigator.seviceWorker.controller.
+ const response_text = await frame.contentWindow.fetch_in_worker(resource);
+ assert_equals(response_text, 'Intercepted!',
+ 'fetch() in the worker should be intercepted.');
+ }
+}
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/client-id.https.html b/testing/web-platform/tests/service-workers/service-worker/client-id.https.html
new file mode 100644
index 0000000000..b93b341899
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/client-id.https.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<title>Service Worker: Client.id</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/blank.html?client-id';
+var frame1, frame2;
+
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/client-id-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope + '#1'); })
+ .then(function(f) {
+ frame1 = f;
+ // To be sure Clients.matchAll() iterates in the same order.
+ f.focus();
+ return with_iframe(scope + '#2');
+ })
+ .then(function(f) {
+ frame2 = f;
+ var channel = new MessageChannel();
+
+ return new Promise(function(resolve, reject) {
+ channel.port1.onmessage = resolve;
+ channel.port1.onmessageerror = reject;
+ f.contentWindow.navigator.serviceWorker.controller.postMessage(
+ {port:channel.port2}, [channel.port2]);
+ });
+ })
+ .then(on_message);
+ }, 'Client.id returns the client\'s ID.');
+
+function on_message(e) {
+ // The result of two sequential clients.matchAll() calls in the SW.
+ // 1st matchAll() results in e.data[0], e.data[1].
+ // 2nd matchAll() results in e.data[2], e.data[3].
+ assert_equals(e.data.length, 4);
+ // All should be string values.
+ assert_equals(typeof e.data[0], 'string');
+ assert_equals(typeof e.data[1], 'string');
+ assert_equals(typeof e.data[2], 'string');
+ assert_equals(typeof e.data[3], 'string');
+ // Different clients should have different ids.
+ assert_not_equals(e.data[0], e.data[1]);
+ assert_not_equals(e.data[2], e.data[3]);
+ // Same clients should have an identical id.
+ assert_equals(e.data[0], e.data[2]);
+ assert_equals(e.data[1], e.data[3]);
+ frame1.remove();
+ frame2.remove();
+}
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/client-navigate.https.html b/testing/web-platform/tests/service-workers/service-worker/client-navigate.https.html
new file mode 100644
index 0000000000..f40a08635c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/client-navigate.https.html
@@ -0,0 +1,107 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>Service Worker: WindowClient.navigate</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+ function wait_for_message(msg) {
+ return new Promise(function(resolve, reject) {
+ var get_message_data = function get_message_data(e) {
+ window.removeEventListener("message", get_message_data);
+ resolve(e.data);
+ }
+ window.addEventListener("message", get_message_data, false);
+ });
+ }
+
+ function run_test(controller, clientId, test) {
+ return new Promise(function(resolve, reject) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(e) {
+ resolve(e.data);
+ };
+ var message = {
+ port: channel.port2,
+ test: test,
+ clientId: clientId,
+ };
+ controller.postMessage(
+ message, [channel.port2]);
+ });
+ }
+
+ async function with_controlled_iframe_and_url(t, name, f) {
+ const SCRIPT = "resources/client-navigate-worker.js";
+ const SCOPE = "resources/client-navigate-frame.html";
+
+ // Register service worker and wait for it to become activated
+ const registration = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+
+ // Create child iframe and make sure we register a listener for the message
+ // it sends before it's created
+ const client_id_promise = wait_for_message();
+ const iframe = await with_iframe(SCOPE);
+ t.add_cleanup(() => iframe.remove());
+ const { id } = await client_id_promise;
+
+ // Run the test in the service worker and fetch it
+ const { result, url } = await run_test(worker, id, name);
+ fetch_tests_from_worker(worker);
+ assert_equals(result, name);
+
+ // Hand over the iframe and URL from the service worker to the callback
+ await f(iframe, url);
+ }
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_success', async (iframe, url) => {
+ assert_equals(
+ url, new URL("resources/client-navigated-frame.html",
+ location).toString());
+ assert_equals(
+ iframe.contentWindow.location.href,
+ new URL("resources/client-navigated-frame.html",
+ location).toString());
+ });
+ }, "Frame location should update on successful navigation");
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_redirect', async (iframe, url) => {
+ assert_equals(url, "");
+ assert_throws_dom("SecurityError", function() { return iframe.contentWindow.location.href });
+ });
+ }, "Frame location should not be accessible after redirect");
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_cross_origin', async (iframe, url) => {
+ assert_equals(url, "");
+ assert_throws_dom("SecurityError", function() { return iframe.contentWindow.location.href });
+ });
+ }, "Frame location should not be accessible after cross-origin navigation");
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_about_blank', async (iframe, url) => {
+ assert_equals(
+ iframe.contentWindow.location.href,
+ new URL("resources/client-navigate-frame.html",
+ location).toString());
+ iframe.contentWindow.document.body.style = "background-color: green"
+ });
+ }, "Frame location should not update on failed about:blank navigation");
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_mixed_content', async (iframe, url) => {
+ assert_equals(
+ iframe.contentWindow.location.href,
+ new URL("resources/client-navigate-frame.html",
+ location).toString());
+ iframe.contentWindow.document.body.style = "background-color: green"
+ });
+ }, "Frame location should not update on failed mixed-content navigation");
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html b/testing/web-platform/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html
new file mode 100644
index 0000000000..97a2fcf98f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<title>Service Worker: client.url of a worker created from a blob URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+const SCRIPT = 'resources/client-url-of-blob-url-worker.js';
+const SCOPE = 'resources/client-url-of-blob-url-worker.html';
+
+promise_test(async (t) => {
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ const frame = await with_iframe(SCOPE);
+ t.add_cleanup(_ => frame.remove());
+ assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ null, 'frame should be controlled');
+
+ const response = await frame.contentWindow.createAndFetchFromBlobWorker();
+
+ assert_not_equals(response.result, 'one worker client should exist',
+ 'worker client should exist');
+ assert_equals(response.result, response.expected,
+ 'client.url and worker location href should be the same');
+
+}, 'Client.url of a blob URL worker should be a blob URL.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-get-client-types.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-get-client-types.https.html
new file mode 100644
index 0000000000..63e3e51b32
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-get-client-types.https.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.get with window and worker clients</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/clients-get-client-types';
+var frame_url = scope + '-frame.html';
+var shared_worker_url = scope + '-shared-worker.js';
+var worker_url = scope + '-worker.js';
+var client_ids = [];
+var registration;
+var frame;
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/clients-get-worker.js', scope)
+ .then(function(r) {
+ registration = r;
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(frame_url);
+ })
+ .then(function(f) {
+ frame = f;
+ add_completion_callback(function() { frame.remove(); });
+ frame.focus();
+ return wait_for_clientId();
+ })
+ .then(function(client_id) {
+ client_ids.push(client_id);
+ return new Promise(function(resolve) {
+ var w = new SharedWorker(shared_worker_url);
+ w.port.onmessage = function(e) {
+ resolve(e.data.clientId);
+ };
+ });
+ })
+ .then(function(client_id) {
+ client_ids.push(client_id);
+ var channel = new MessageChannel();
+ var w = new Worker(worker_url);
+ w.postMessage({cmd:'GetClientId', port:channel.port2},
+ [channel.port2]);
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) {
+ resolve(e.data.clientId);
+ };
+ });
+ })
+ .then(function(client_id) {
+ client_ids.push(client_id);
+ var channel = new MessageChannel();
+ frame.contentWindow.postMessage('StartWorker', '*', [channel.port2]);
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) {
+ resolve(e.data.clientId);
+ };
+ });
+ })
+ .then(function(client_id) {
+ client_ids.push(client_id);
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = resolve;
+ });
+ registration.active.postMessage({clientIds: client_ids});
+ return saw_message;
+ })
+ .then(function(e) {
+ assert_equals(e.data.length, expected.length);
+ // We use these assert_not_equals because assert_array_equals doesn't
+ // print the error description when passed an undefined value.
+ assert_not_equals(e.data[0], undefined,
+ 'Window client should not be undefined');
+ assert_array_equals(e.data[0], expected[0], 'Window client');
+ assert_not_equals(e.data[1], undefined,
+ 'Shared worker client should not be undefined');
+ assert_array_equals(e.data[1], expected[1], 'Shared worker client');
+ assert_not_equals(e.data[2], undefined,
+ 'Worker(Started by main frame) client should not be undefined');
+ assert_array_equals(e.data[2], expected[2],
+ 'Worker(Started by main frame) client');
+ assert_not_equals(e.data[3], undefined,
+ 'Worker(Started by sub frame) client should not be undefined');
+ assert_array_equals(e.data[3], expected[3],
+ 'Worker(Started by sub frame) client');
+ });
+ }, 'Test Clients.get() with window and worker clients');
+
+function wait_for_clientId() {
+ return new Promise(function(resolve) {
+ function get_client_id(e) {
+ window.removeEventListener('message', get_client_id);
+ resolve(e.data.clientId);
+ }
+ window.addEventListener('message', get_client_id, false);
+ });
+}
+
+var expected = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, normalizeURL(scope) + '-frame.html', 'window', 'nested'],
+ [undefined, undefined, normalizeURL(scope) + '-shared-worker.js', 'sharedworker', 'none'],
+ [undefined, undefined, normalizeURL(scope) + '-worker.js', 'worker', 'none'],
+ [undefined, undefined, normalizeURL(scope) + '-frame-worker.js', 'worker', 'none']
+];
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-get-cross-origin.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-get-cross-origin.https.html
new file mode 100644
index 0000000000..1e4acfb286
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-get-cross-origin.https.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.get across origins</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var host_info = get_host_info();
+
+var scope = 'resources/clients-get-frame.html';
+var other_origin_iframe = host_info['HTTPS_REMOTE_ORIGIN'] + base_path() +
+ 'resources/clients-get-cross-origin-frame.html';
+// The ID of a client from the same origin as us.
+var my_origin_client_id;
+// This test asserts the behavior of the Client API in cases where the client
+// belongs to a foreign origin. It does this by creating an iframe with a
+// foreign origin which connects to a server worker in the current origin.
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/clients-get-worker.js', scope)
+ .then(function(registration) {
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ // Create a same-origin client and use it to populate |my_origin_client_id|.
+ .then(function(frame1) {
+ add_completion_callback(function() { frame1.remove(); });
+ return new Promise(function(resolve, reject) {
+ function get_client_id(e) {
+ window.removeEventListener('message', get_client_id);
+ resolve(e.data.clientId);
+ }
+ window.addEventListener('message', get_client_id, false);
+ });
+ })
+ // Create a cross-origin client. We'll communicate with this client to
+ // test the cross-origin service worker's behavior.
+ .then(function(client_id) {
+ my_origin_client_id = client_id;
+ return with_iframe(other_origin_iframe);
+ })
+ // Post the 'getClientId' message to the cross-origin client. The client
+ // will then ask its service worker to look up |my_origin_client_id| via
+ // Clients#get. Since this client ID is for a different origin, we expect
+ // the client to not be found.
+ .then(function(frame2) {
+ add_completion_callback(function() { frame2.remove(); });
+
+ frame2.contentWindow.postMessage(
+ {clientId: my_origin_client_id, type: 'getClientId'},
+ host_info['HTTPS_REMOTE_ORIGIN']
+ );
+
+ return new Promise(function(resolve) {
+ window.addEventListener('message', function(e) {
+ if (e.data && e.data.type === 'clientId') {
+ resolve(e.data.value);
+ }
+ });
+ });
+ })
+ .then(function(client_id) {
+ assert_equals(client_id, undefined, 'iframe client ID');
+ });
+ }, 'Test Clients.get() cross origin');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-get-resultingClientId.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-get-resultingClientId.https.html
new file mode 100644
index 0000000000..3419cf14b5
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-get-resultingClientId.https.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test clients.get(resultingClientId)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = "resources/";
+let worker;
+
+// Setup. Keep this as the first promise_test.
+promise_test(async (t) => {
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/get-resultingClientId-worker.js',
+ scope);
+ worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+}, 'global setup');
+
+// Sends |command| to the worker and returns a promise that resolves to its
+// response. There should only be one inflight command at a time.
+async function sendCommand(command) {
+ const saw_message = new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ });
+ worker.postMessage(command);
+ return saw_message;
+}
+
+// Wrapper for 'startTest' command. Tells the worker a test is starting,
+// so it resets state and keeps itself alive until 'finishTest'.
+async function startTest(t) {
+ const result = await sendCommand({command: 'startTest'});
+ assert_equals(result, 'ok', 'startTest');
+
+ t.add_cleanup(async () => {
+ return finishTest();
+ });
+}
+
+// Wrapper for 'finishTest' command.
+async function finishTest() {
+ const result = await sendCommand({command: 'finishTest'});
+ assert_equals(result, 'ok', 'finishTest');
+}
+
+// Wrapper for 'getResultingClient' command. Tells the worker to return
+// clients.get(event.resultingClientId) for the navigation that occurs
+// during this test.
+//
+// The return value describes how clients.get() settled. It also includes
+// |queriedId| which is the id passed to clients.get() (the resultingClientId
+// in this case).
+//
+// Example value:
+// {
+// queriedId: 'abc',
+// promiseState: fulfilled,
+// promiseValue: client,
+// client: {
+// id: 'abc',
+// url: '//example.com/client'
+// }
+// }
+async function getResultingClient() {
+ return sendCommand({command: 'getResultingClient'});
+}
+
+// Wrapper for 'getClient' command. Tells the worker to return
+// clients.get(|id|). The return value is as in the getResultingClient()
+// documentation.
+async function getClient(id) {
+ return sendCommand({command: 'getClient', id: id});
+}
+
+// Navigates to |url|. Returns the result of clients.get() on the
+// resultingClientId.
+async function navigateAndGetResultingClient(t, url) {
+ const resultPromise = getResultingClient();
+ const frame = await with_iframe(url);
+ t.add_cleanup(() => {
+ frame.remove();
+ });
+ const result = await resultPromise;
+ const resultingClientId = result.queriedId;
+
+ // First test clients.get(event.resultingClientId) inside the fetch event. The
+ // behavior of this is subtle due to the use of iframes and about:blank
+ // replacement. The spec probably requires that it resolve to the original
+ // about:blank client, and that later that client should be discarded after
+ // load if the load was to another origin. Implementations might differ. For
+ // now, this test just asserts that the promise resolves. See
+ // https://github.com/w3c/ServiceWorker/issues/1385.
+ assert_equals(result.promiseState, 'fulfilled',
+ 'get(event.resultingClientId) in the fetch event should fulfill');
+
+ // Test clients.get() on the previous resultingClientId again. By this
+ // time the load finished, so it's more straightforward how this promise
+ // should settle. Return the result of this promise.
+ return await getClient(resultingClientId);
+}
+
+// Test get(resultingClientId) in the basic same-origin case.
+promise_test(async (t) => {
+ await startTest(t);
+
+ const url = new URL('resources/empty.html', window.location);
+ const result = await navigateAndGetResultingClient(t, url);
+ assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+ assert_equals(result.promiseValue, 'client', 'promiseValue');
+ assert_equals(result.client.url, url.href, 'client.url',);
+ assert_equals(result.client.id, result.queriedId, 'client.id');
+}, 'get(resultingClientId) for same-origin document');
+
+// Test get(resultingClientId) when the response redirects to another origin.
+promise_test(async (t) => {
+ await startTest(t);
+
+ // Navigate to a URL that redirects to another origin.
+ const base_url = new URL('.', window.location);
+ const host_info = get_host_info();
+ const other_origin_url = new URL(base_url.pathname + 'resources/empty.html',
+ host_info['HTTPS_REMOTE_ORIGIN']);
+ const url = new URL('resources/empty.html', window.location);
+ const pipe = `status(302)|header(Location, ${other_origin_url})`;
+ url.searchParams.set('pipe', pipe);
+
+ // The original reserved client should have been discarded on cross-origin
+ // redirect.
+ const result = await navigateAndGetResultingClient(t, url);
+ assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+ assert_equals(result.promiseValue, 'undefinedValue', 'promiseValue');
+}, 'get(resultingClientId) on cross-origin redirect');
+
+// Test get(resultingClientId) when the document is sandboxed to a unique
+// origin using a CSP HTTP response header.
+promise_test(async (t) => {
+ await startTest(t);
+
+ // Navigate to a URL that has CSP sandboxing set in the HTTP response header.
+ const url = new URL('resources/empty.html', window.location);
+ const pipe = 'header(Content-Security-Policy, sandbox)';
+ url.searchParams.set('pipe', pipe);
+
+ // The original reserved client should have been discarded upon loading
+ // the sandboxed document.
+ const result = await navigateAndGetResultingClient(t, url);
+ assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+ assert_equals(result.promiseValue, 'undefinedValue', 'promiseValue');
+}, 'get(resultingClientId) for document sandboxed by CSP header');
+
+// Test get(resultingClientId) when the document is sandboxed with
+// allow-same-origin.
+promise_test(async (t) => {
+ await startTest(t);
+
+ // Navigate to a URL that has CSP sandboxing set in the HTTP response header.
+ const url = new URL('resources/empty.html', window.location);
+ const pipe = 'header(Content-Security-Policy, sandbox allow-same-origin)';
+ url.searchParams.set('pipe', pipe);
+
+ // The client should be the original reserved client, as it's same-origin.
+ const result = await navigateAndGetResultingClient(t, url);
+ assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+ assert_equals(result.promiseValue, 'client', 'promiseValue');
+ assert_equals(result.client.url, url.href, 'client.url',);
+ assert_equals(result.client.id, result.queriedId, 'client.id');
+}, 'get(resultingClientId) for document sandboxed by CSP header with allow-same-origin');
+
+// Cleanup. Keep this as the last promise_test.
+promise_test(async (t) => {
+ return service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-get.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-get.https.html
new file mode 100644
index 0000000000..4cfbf595ca
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-get.https.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.get</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_clientId() {
+ return new Promise(function(resolve, reject) {
+ window.onmessage = e => {
+ resolve(e.data.clientId);
+ };
+ });
+}
+
+promise_test(async t => {
+ // Register service worker.
+ const scope = 'resources/clients-get-frame.html';
+ const client_ids = [];
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-get-worker.js', scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Prepare for test cases.
+ // Case 1: frame1 which is focused.
+ const frame1 = await with_iframe(scope + '#1');
+ t.add_cleanup(() => frame1.remove());
+ frame1.focus();
+ client_ids.push(await wait_for_clientId());
+ // Case 2: frame2 which is not focused.
+ const frame2 = await with_iframe(scope + '#2');
+ t.add_cleanup(() => frame2.remove());
+ client_ids.push(await wait_for_clientId());
+ // Case 3: invalid id.
+ client_ids.push('invalid-id');
+
+ // Call clients.get() for each id on the service worker.
+ const message_event = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = resolve;
+ registration.active.postMessage({clientIds: client_ids});
+ });
+
+ const expected = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, normalizeURL(scope) + '#1', 'window', 'nested'],
+ ['visible', false, normalizeURL(scope) + '#2', 'window', 'nested'],
+ undefined
+ ];
+ assert_equals(message_event.data.length, 3);
+ assert_array_equals(message_event.data[0], expected[0]);
+ assert_array_equals(message_event.data[1], expected[1]);
+ assert_equals(message_event.data[2], expected[2]);
+}, 'Test Clients.get()');
+
+promise_test(async t => {
+ // Register service worker.
+ const scope = 'resources/simple.html';
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-get-resultingClientId-worker.js', scope)
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const worker = registration.active;
+
+ // Load frame within the scope.
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+ frame.focus();
+
+ // Get resulting client id.
+ const resultingClientId = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ if (e.data.msg == 'getResultingClientId') {
+ resolve(e.data.resultingClientId);
+ }
+ };
+ worker.postMessage({msg: 'getResultingClientId'});
+ });
+
+ // Query service worker for clients.get(resultingClientId).
+ const isResultingClientUndefined = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ if (e.data.msg == 'getIsResultingClientUndefined') {
+ resolve(e.data.isResultingClientUndefined);
+ }
+ };
+ worker.postMessage({msg: 'getIsResultingClientUndefined',
+ resultingClientId});
+ });
+
+ assert_false(
+ isResultingClientUndefined,
+ 'Clients.get(FetchEvent.resultingClientId) resolved with a Client');
+}, 'Test successful Clients.get(FetchEvent.resultingClientId)');
+
+promise_test(async t => {
+ // Register service worker.
+ const scope = 'resources/simple.html?fail';
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-get-resultingClientId-worker.js', scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Load frame, and destroy it while loading.
+ const worker = registration.active;
+ let frame = document.createElement('iframe');
+ frame.src = scope;
+ t.add_cleanup(() => {
+ if (frame) {
+ frame.remove();
+ }
+ });
+
+ await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ // The service worker posts a message to remove the iframe during fetch
+ // event.
+ if (e.data.msg == 'destroyResultingClient') {
+ frame.remove();
+ frame = null;
+ worker.postMessage({msg: 'resultingClientDestroyed'});
+ resolve();
+ }
+ };
+ document.body.appendChild(frame);
+ });
+
+ resultingDestroyedClientId = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ // The worker sends a message back when it receives the message
+ // 'resultingClientDestroyed' with the resultingClientId.
+ if (e.data.msg == 'resultingClientDestroyedAck') {
+ assert_equals(frame, null, 'Frame should be destroyed at this point.');
+ resolve(e.data.resultingDestroyedClientId);
+ }
+ };
+ });
+
+ // Query service worker for clients.get(resultingDestroyedClientId).
+ const isResultingClientUndefined = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ if (e.data.msg == 'getIsResultingClientUndefined') {
+ resolve(e.data.isResultingClientUndefined);
+ }
+ };
+ worker.postMessage({msg: 'getIsResultingClientUndefined',
+ resultingClientId: resultingDestroyedClientId });
+ });
+
+ assert_true(
+ isResultingClientUndefined,
+ 'Clients.get(FetchEvent.resultingClientId) resolved with `undefined`');
+}, 'Test unsuccessful Clients.get(FetchEvent.resultingClientId)');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html
new file mode 100644
index 0000000000..c29bac8b89
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with a blob URL worker client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const SCRIPT = 'resources/clients-matchall-worker.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/clients-matchall-blob-url-worker.html';
+
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, scope);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ const frame = await with_iframe(scope);
+ t.add_cleanup(_ => frame.remove());
+
+ {
+ const message = await frame.contentWindow.waitForWorker();
+ assert_equals(message.data, 'Worker is ready.',
+ 'Worker should reply to the message.');
+ }
+
+ const channel = new MessageChannel();
+ const message = await new Promise(resolve => {
+ channel.port1.onmessage = resolve;
+ frame.contentWindow.navigator.serviceWorker.controller.postMessage(
+ {port: channel.port2, options: {type: 'worker'}}, [channel.port2]);
+ });
+
+ checkMessageEvent(message);
+
+}, 'Test Clients.matchAll() with a blob URL worker client.');
+
+promise_test(async (t) => {
+ const scope = 'resources/blank.html';
+
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, scope);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ const workerScript = `
+ self.onmessage = (e) => {
+ self.postMessage("Worker is ready.");
+ };
+ `;
+ const blob = new Blob([workerScript], { type: 'text/javascript' });
+ const blobUrl = URL.createObjectURL(blob);
+ const worker = new Worker(blobUrl);
+
+ {
+ const message = await new Promise(resolve => {
+ worker.onmessage = resolve;
+ worker.postMessage("Ping to worker.");
+ });
+ assert_equals(message.data, 'Worker is ready.',
+ 'Worker should reply to the message.');
+ }
+
+ const channel = new MessageChannel();
+ const message = await new Promise(resolve => {
+ channel.port1.onmessage = resolve;
+ reg.active.postMessage(
+ {port: channel.port2,
+ options: {includeUncontrolled: true, type: 'worker'}},
+ [channel.port2]
+ );
+ });
+
+ checkMessageEvent(message);
+
+}, 'Test Clients.matchAll() with an uncontrolled blob URL worker client.');
+
+function checkMessageEvent(e) {
+ assert_equals(e.data.length, 1);
+
+ const workerClient = e.data[0];
+ assert_equals(workerClient[0], undefined); // visibilityState
+ assert_equals(workerClient[1], undefined); // focused
+ assert_true(workerClient[2].includes('blob:')); // url
+ assert_equals(workerClient[3], 'worker'); // type
+ assert_equals(workerClient[4], 'none'); // frameType
+}
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall-client-types.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-client-types.https.html
new file mode 100644
index 0000000000..54f182b620
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-client-types.https.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with various clientTypes</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = 'resources/clients-matchall-client-types';
+const iframe_url = scope + '-iframe.html';
+const shared_worker_url = scope + '-shared-worker.js';
+const dedicated_worker_url = scope + '-dedicated-worker.js';
+
+/* visibilityState, focused, url, type, frameType */
+const expected_only_window = [
+ ['visible', true, new URL(iframe_url, location).href, 'window', 'nested']
+];
+const expected_only_shared_worker = [
+ [undefined, undefined, new URL(shared_worker_url, location).href, 'sharedworker', 'none']
+];
+const expected_only_dedicated_worker = [
+ [undefined, undefined, new URL(dedicated_worker_url, location).href, 'worker', 'none']
+];
+
+// These are explicitly sorted by URL in the service worker script.
+const expected_all_clients = [
+ expected_only_dedicated_worker[0],
+ expected_only_window[0],
+ expected_only_shared_worker[0],
+];
+
+async function test_matchall(frame, expected, query_options) {
+ // Make sure the frame gets focus.
+ frame.focus();
+ const data = await new Promise(resolve => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = e => resolve(e.data);
+ frame.contentWindow.navigator.serviceWorker.controller.postMessage(
+ {port:channel.port2, options:query_options},
+ [channel.port2]);
+ });
+
+ if (typeof data === 'string') {
+ throw new Error(data);
+ }
+
+ assert_equals(data.length, expected.length, 'result count');
+
+ for (let i = 0; i < data.length; ++i) {
+ assert_array_equals(data[i], expected[i]);
+ }
+}
+
+promise_test(async t => {
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope);
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(iframe_url);
+ t.add_cleanup(_ => frame.remove());
+ await test_matchall(frame, expected_only_window, {});
+ await test_matchall(frame, expected_only_window, {type:'window'});
+}, 'Verify matchAll() with window client type');
+
+promise_test(async t => {
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope);
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(iframe_url);
+ t.add_cleanup(_ => frame.remove());
+
+ // Set up worker clients.
+ const shared_worker = await new Promise((resolve, reject) => {
+ const w = new SharedWorker(shared_worker_url);
+ w.onerror = e => reject(e.message);
+ w.port.onmessage = _ => resolve(w);
+ });
+ const dedicated_worker = await new Promise((resolve, reject) => {
+ const w = new Worker(dedicated_worker_url);
+ w.onerror = e => reject(e.message);
+ w.onmessage = _ => resolve(w);
+ w.postMessage('Start');
+ });
+
+ await test_matchall(frame, expected_only_window, {});
+ await test_matchall(frame, expected_only_window, {type:'window'});
+ await test_matchall(frame, expected_only_shared_worker,
+ {type:'sharedworker'});
+ await test_matchall(frame, expected_only_dedicated_worker, {type:'worker'});
+ await test_matchall(frame, expected_all_clients, {type:'all'});
+}, 'Verify matchAll() with {window, sharedworker, worker} client types');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html
new file mode 100644
index 0000000000..a61c8af701
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with exact controller</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = 'resources/blank.html?clients-matchAll';
+let frames = [];
+
+function checkWorkerClients(worker, expected) {
+ return new Promise((resolve, reject) => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = evt => {
+ try {
+ assert_equals(evt.data.length, expected.length);
+ for (let i = 0; i < expected.length; ++i) {
+ assert_array_equals(evt.data[i], expected[i]);
+ }
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ };
+
+ worker.postMessage({port:channel.port2}, [channel.port2]);
+ });
+}
+
+let expected = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+ ['visible', false, new URL(scope + '#2', location).toString(), 'window', 'nested']
+];
+
+promise_test(t => {
+ let script = 'resources/clients-matchall-worker.js';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(_ => with_iframe(scope + '#1') )
+ .then(frame1 => {
+ frames.push(frame1);
+ frame1.focus();
+ return with_iframe(scope + '#2');
+ })
+ .then(frame2 => {
+ frames.push(frame2);
+ return navigator.serviceWorker.register(script + '?updated', { scope: scope });
+ })
+ .then(registration => {
+ return wait_for_state(t, registration.installing, 'installed')
+ .then(_ => registration);
+ })
+ .then(registration => {
+ return Promise.all([
+ checkWorkerClients(registration.waiting, []),
+ checkWorkerClients(registration.active, expected),
+ ]);
+ })
+ .then(_ => {
+ frames.forEach(f => f.remove() );
+ });
+}, 'Test Clients.matchAll() with exact controller');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall-frozen.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-frozen.https.html
new file mode 100644
index 0000000000..479c28a60f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-frozen.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/clients-frame-freeze.html';
+var windows = [];
+var expected_window_1 =
+ {visibilityState: 'visible', focused: false, lifecycleState: "frozen", url: new URL(scope + '#1', location).toString(), type: 'window', frameType: 'top-level'};
+var expected_window_2 =
+ {visibilityState: 'visible', focused: false, lifecycleState: "active", url: new URL(scope + '#2', location).toString(), type: 'window', frameType: 'top-level'};
+function with_window(url, name) {
+ return new Promise(function(resolve) {
+ var child = window.open(url, name);
+ window.onmessage = () => {resolve(child)};
+ });
+}
+
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_window(scope + '#1', 'Child 1'); })
+ .then(function(window1) {
+ windows.push(window1);
+ return with_window(scope + '#2', 'Child 2');
+ })
+ .then(function(window2) {
+ windows.push(window2);
+ return new Promise(function(resolve) {
+ window.onmessage = resolve;
+ windows[0].postMessage('freeze');
+ });
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ windows[1].navigator.serviceWorker.controller.postMessage(
+ {port:channel.port2, includeLifecycleState: true}, [channel.port2]);
+ });
+ })
+ .then(function(e) {
+ assert_equals(e.data.length, 2);
+ // No specific order is required, so support inversion.
+ if (e.data[0][3] == new URL(scope + '#2', location)) {
+ assert_object_equals(e.data[0], expected_window_2);
+ assert_object_equals(e.data[1], expected_window_1);
+ } else {
+ assert_object_equals(e.data[0], expected_window_1);
+ assert_object_equals(e.data[1], expected_window_2);
+ }
+ });
+}, 'Test Clients.matchAll()');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html
new file mode 100644
index 0000000000..9f34e5709e
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with includeUncontrolled</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function test_matchall(service_worker, expected, query_options) {
+ expected.sort((a, b) => a[2] > b[2] ? 1 : -1);
+ return new Promise((resolve, reject) => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = e => {
+ const data = e.data.filter(info => {
+ return info[2].indexOf('clients-matchall') > -1;
+ });
+ data.sort((a, b) => a[2] > b[2] ? 1 : -1);
+ assert_equals(data.length, expected.length);
+ for (let i = 0; i < data.length; i++)
+ assert_array_equals(data[i], expected[i]);
+ resolve();
+ };
+ service_worker.postMessage({port:channel.port2, options:query_options},
+ [channel.port2]);
+ });
+}
+
+// Run clients.matchAll without and with includeUncontrolled=true.
+// (We want to run the two tests sequentially in the same promise_test
+// so that we can use the same set of iframes without intefering each other.
+promise_test(async t => {
+ // |base_url| is out-of-scope.
+ const base_url = 'resources/blank.html?clients-matchall';
+ const scope = base_url + '-includeUncontrolled';
+
+ const registration =
+ await service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ const service_worker = registration.installing;
+ await wait_for_state(t, service_worker, 'activated');
+
+ // Creates 3 iframes, 2 for in-scope and 1 for out-of-scope.
+ let frames = [];
+ frames.push(await with_iframe(base_url));
+ frames.push(await with_iframe(scope + '#1'));
+ frames.push(await with_iframe(scope + '#2'));
+
+ // Make sure we have focus for '#2' frame and its parent window.
+ frames[2].focus();
+ frames[2].contentWindow.focus();
+
+ const expected_without_include_uncontrolled = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', false, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+ ['visible', true, new URL(scope + '#2', location).toString(), 'window', 'nested']
+ ];
+ const expected_with_include_uncontrolled = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, location.href, 'window', 'top-level'],
+ ['visible', false, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+ ['visible', true, new URL(scope + '#2', location).toString(), 'window', 'nested'],
+ ['visible', false, new URL(base_url, location).toString(), 'window', 'nested']
+ ];
+
+ await test_matchall(service_worker, expected_without_include_uncontrolled);
+ await test_matchall(service_worker, expected_with_include_uncontrolled,
+ { includeUncontrolled: true });
+}, 'Verify matchAll() with windows respect includeUncontrolled');
+
+// TODO: Add tests for clients.matchAll for dedicated workers.
+
+async function create_shared_worker(script_url) {
+ const shared_worker = new SharedWorker(script_url);
+ const msgEvent = await new Promise(r => shared_worker.port.onmessage = r);
+ assert_equals(msgEvent.data, 'started');
+ return shared_worker;
+}
+
+// Run clients.matchAll for shared workers without and with
+// includeUncontrolled=true.
+promise_test(async t => {
+ const script_url = 'resources/clients-matchall-client-types-shared-worker.js';
+ const uncontrolled_script_url =
+ new URL(script_url + '?uncontrolled', location).toString();
+ const controlled_script_url =
+ new URL(script_url + '?controlled', location).toString();
+
+ // Start a shared worker that is not controlled by a service worker.
+ const uncontrolled_shared_worker =
+ await create_shared_worker(uncontrolled_script_url);
+
+ // Register a service worker.
+ const registration =
+ await service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', script_url);
+ t.add_cleanup(() => service_worker_unregister(t, script_url));
+ const service_worker = registration.installing;
+ await wait_for_state(t, service_worker, 'activated');
+
+ // Start another shared worker controlled by the service worker.
+ await create_shared_worker(controlled_script_url);
+
+ const expected_without_include_uncontrolled = [
+ // visibilityState, focused, url, type, frameType
+ [undefined, undefined, controlled_script_url, 'sharedworker', 'none'],
+ ];
+ const expected_with_include_uncontrolled = [
+ // visibilityState, focused, url, type, frameType
+ [undefined, undefined, controlled_script_url, 'sharedworker', 'none'],
+ [undefined, undefined, uncontrolled_script_url, 'sharedworker', 'none'],
+ ];
+
+ await test_matchall(service_worker, expected_without_include_uncontrolled,
+ { type: 'sharedworker' });
+ await test_matchall(service_worker, expected_with_include_uncontrolled,
+ { includeUncontrolled: true, type: 'sharedworker' });
+}, 'Verify matchAll() with shared workers respect includeUncontrolled');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html
new file mode 100644
index 0000000000..8705f85b56
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll on script evaluation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var script = 'resources/clients-matchall-on-evaluation-worker.js';
+ var scope = 'resources/blank.html?clients-matchAll-on-evaluation';
+
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = function(e) {
+ assert_equals(e.data, 'matched');
+ resolve();
+ };
+ });
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ add_completion_callback(function() { registration.unregister(); });
+ return saw_message;
+ });
+ }, 'Test Clients.matchAll() on script evaluation');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall-order.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-order.https.html
new file mode 100644
index 0000000000..ec650f2264
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall-order.https.html
@@ -0,0 +1,427 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll ordering</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+// Utility function for URLs this test will open.
+function makeURL(name, num, type) {
+ let u = new URL('resources/empty.html', location);
+ u.searchParams.set('name', name);
+ if (num !== undefined) {
+ u.searchParams.set('q', num);
+ }
+ if (type === 'nested') {
+ u.searchParams.set('nested', true);
+ }
+ return u.href;
+}
+
+// Non-test URLs that will be open during each test. The harness URLs
+// are from the WPT harness. The "extra" URL is a final window opened
+// by the test.
+const EXTRA_URL = makeURL('extra');
+const TEST_HARNESS_URL = location.href;
+const TOP_HARNESS_URL = new URL('/testharness_runner.html', location).href;
+
+// Utility function to open an iframe in the target parent window. We
+// can't just use with_iframe() because it does not support a configurable
+// parent window.
+function openFrame(parentWindow, url) {
+ return new Promise(resolve => {
+ let frame = parentWindow.document.createElement('iframe');
+ frame.src = url;
+ parentWindow.document.body.appendChild(frame);
+
+ frame.contentWindow.addEventListener('load', evt => {
+ resolve(frame);
+ }, { once: true });
+ });
+}
+
+// Utility function to open a window and wait for it to load. The
+// window may optionally have a nested iframe as well. Returns
+// a result like `{ top: <frame ref> nested: <nested frame ref if present> }`.
+function openFrameConfig(opts) {
+ let url = new URL(opts.url, location.href);
+ return openFrame(window, url.href).then(top => {
+ if (!opts.withNested) {
+ return { top: top };
+ }
+
+ url.searchParams.set('nested', true);
+ return openFrame(top.contentWindow, url.href).then(nested => {
+ return { top: top, nested: nested };
+ });
+ });
+}
+
+// Utility function that takes a list of configurations and opens the
+// corresponding windows in sequence. An array of results is returned.
+function openFrameConfigList(optList) {
+ let resultList = [];
+ function openNextWindow(optList, nextWindow) {
+ if (nextWindow >= optList.length) {
+ return resultList;
+ }
+ return openFrameConfig(optList[nextWindow]).then(result => {
+ resultList.push(result);
+ return openNextWindow(optList, nextWindow + 1);
+ });
+ }
+ return openNextWindow(optList, 0);
+}
+
+// Utility function that focuses the given entry in window result list.
+function executeFocus(frameResultList, opts) {
+ return new Promise(resolve => {
+ let w = frameResultList[opts.index][opts.type];
+ let target = w.contentWindow ? w.contentWindow : w;
+ target.addEventListener('focus', evt => {
+ resolve();
+ }, { once: true });
+ target.focus();
+ });
+}
+
+// Utility function that performs a list of focus commands in sequence
+// based on the window result list.
+function executeFocusList(frameResultList, optList) {
+ function executeNextCommand(frameResultList, optList, nextCommand) {
+ if (nextCommand >= optList.length) {
+ return;
+ }
+ return executeFocus(frameResultList, optList[nextCommand]).then(_ => {
+ return executeNextCommand(frameResultList, optList, nextCommand + 1);
+ });
+ }
+ return executeNextCommand(frameResultList, optList, 0);
+}
+
+// Perform a `clients.matchAll()` in the service worker with the given
+// options dictionary.
+function doMatchAll(worker, options) {
+ return new Promise(resolve => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = evt => {
+ resolve(evt.data);
+ };
+ worker.postMessage({ port: channel.port2, options: options, disableSort: true },
+ [channel.port2]);
+ });
+}
+
+// Function that performs a single test case. It takes a configuration object
+// describing the windows to open, how to focus them, the matchAll options,
+// and the resulting expectations. See the test cases for examples of how to
+// use this.
+function matchAllOrderTest(t, opts) {
+ let script = 'resources/clients-matchall-worker.js';
+ let worker;
+ let frameResultList;
+ let extraWindowResult;
+ return service_worker_unregister_and_register(t, script, opts.scope).then(swr => {
+ t.add_cleanup(() => service_worker_unregister(t, opts.scope));
+
+ worker = swr.installing;
+ return wait_for_state(t, worker, 'activated');
+ }).then(_ => {
+ return openFrameConfigList(opts.frameConfigList);
+ }).then(results => {
+ frameResultList = results;
+ return openFrameConfig({ url: EXTRA_URL });
+ }).then(result => {
+ extraWindowResult = result;
+ return executeFocusList(frameResultList, opts.focusConfigList);
+ }).then(_ => {
+ return doMatchAll(worker, opts.matchAllOptions);
+ }).then(data => {
+ assert_equals(data.length, opts.expected.length);
+ for (let i = 0; i < data.length; ++i) {
+ assert_equals(data[i][2], opts.expected[i], 'expected URL index ' + i);
+ }
+ }).then(_ => {
+ frameResultList.forEach(result => result.top.remove());
+ extraWindowResult.top.remove();
+ }).catch(e => {
+ if (frameResultList) {
+ frameResultList.forEach(result => result.top.remove());
+ }
+ if (extraWindowResult) {
+ extraWindowResult.top.remove();
+ }
+ throw(e);
+ });
+}
+
+// ----------
+// Test cases
+// ----------
+
+promise_test(t => {
+ let name = 'no-focus-controlled-windows';
+ let opts = {
+ scope: makeURL(name),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ // no focus commands
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: false
+ },
+
+ expected: [
+ makeURL(name, 0),
+ makeURL(name, 1),
+ makeURL(name, 2),
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns non-focused controlled windows in creation order.');
+
+promise_test(t => {
+ let name = 'focus-controlled-windows-1';
+ let opts = {
+ scope: makeURL(name),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ { index: 0, type: 'top' },
+ { index: 1, type: 'top' },
+ { index: 2, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: false
+ },
+
+ expected: [
+ makeURL(name, 2),
+ makeURL(name, 1),
+ makeURL(name, 0),
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns controlled windows in focus order. Case 1.');
+
+promise_test(t => {
+ let name = 'focus-controlled-windows-2';
+ let opts = {
+ scope: makeURL(name),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ { index: 2, type: 'top' },
+ { index: 1, type: 'top' },
+ { index: 0, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: false
+ },
+
+ expected: [
+ makeURL(name, 0),
+ makeURL(name, 1),
+ makeURL(name, 2),
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns controlled windows in focus order. Case 2.');
+
+promise_test(t => {
+ let name = 'no-focus-uncontrolled-windows';
+ let opts = {
+ scope: makeURL(name + '-outofscope'),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ // no focus commands
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: true
+ },
+
+ expected: [
+ // The harness windows have been focused, so appear first
+ TEST_HARNESS_URL,
+ TOP_HARNESS_URL,
+
+ // Test frames have not been focused, so appear in creation order
+ makeURL(name, 0),
+ makeURL(name, 1),
+ makeURL(name, 2),
+ EXTRA_URL,
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns non-focused uncontrolled windows in creation order.');
+
+promise_test(t => {
+ let name = 'focus-uncontrolled-windows-1';
+ let opts = {
+ scope: makeURL(name + '-outofscope'),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ { index: 0, type: 'top' },
+ { index: 1, type: 'top' },
+ { index: 2, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: true
+ },
+
+ expected: [
+ // The test harness window is a parent of all test frames. It will
+ // always have the same focus time or later as its frames. So it
+ // appears first.
+ TEST_HARNESS_URL,
+
+ makeURL(name, 2),
+ makeURL(name, 1),
+ makeURL(name, 0),
+
+ // The overall harness has been focused
+ TOP_HARNESS_URL,
+
+ // The extra frame was never focused
+ EXTRA_URL,
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns uncontrolled windows in focus order. Case 1.');
+
+promise_test(t => {
+ let name = 'focus-uncontrolled-windows-2';
+ let opts = {
+ scope: makeURL(name + '-outofscope'),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ { index: 2, type: 'top' },
+ { index: 1, type: 'top' },
+ { index: 0, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: true
+ },
+
+ expected: [
+ // The test harness window is a parent of all test frames. It will
+ // always have the same focus time or later as its frames. So it
+ // appears first.
+ TEST_HARNESS_URL,
+
+ makeURL(name, 0),
+ makeURL(name, 1),
+ makeURL(name, 2),
+
+ // The overall harness has been focused
+ TOP_HARNESS_URL,
+
+ // The extra frame was never focused
+ EXTRA_URL,
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns uncontrolled windows in focus order. Case 2.');
+
+promise_test(t => {
+ let name = 'focus-controlled-nested-windows';
+ let opts = {
+ scope: makeURL(name),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: true },
+ { url: makeURL(name, 1), withNested: true },
+ { url: makeURL(name, 2), withNested: true },
+ ],
+
+ focusConfigList: [
+ { index: 0, type: 'top' },
+
+ // Note, some browsers don't let programmatic focus of a frame unless
+ // an ancestor window is already focused. So focus the window and
+ // then the frame.
+ { index: 1, type: 'top' },
+ { index: 1, type: 'nested' },
+
+ { index: 2, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: false
+ },
+
+ expected: [
+ // Focus order for window 2, but not its frame. We only focused
+ // the window.
+ makeURL(name, 2),
+
+ // Window 1 is next via focus order, but the window is always
+ // shown first here. The window gets its last focus time updated
+ // when the frame is focused. Since the times match between the
+ // two it falls back to creation order. The window was created
+ // before the frame. This behavior is being discussed in:
+ // https://github.com/w3c/ServiceWorker/issues/1080
+ makeURL(name, 1),
+ makeURL(name, 1, 'nested'),
+
+ // Focus order for window 0, but not its frame. We only focused
+ // the window.
+ makeURL(name, 0),
+
+ // Creation order of the frames since they are not focused by
+ // default when they are created.
+ makeURL(name, 0, 'nested'),
+ makeURL(name, 2, 'nested'),
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns controlled windows and frames in focus order.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/clients-matchall.https.html b/testing/web-platform/tests/service-workers/service-worker/clients-matchall.https.html
new file mode 100644
index 0000000000..ce44f1924d
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-matchall.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/blank.html?clients-matchAll';
+var frames = [];
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope + '#1'); })
+ .then(function(frame1) {
+ frames.push(frame1);
+ frame1.focus();
+ return with_iframe(scope + '#2');
+ })
+ .then(function(frame2) {
+ frames.push(frame2);
+ var channel = new MessageChannel();
+
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ frame2.contentWindow.navigator.serviceWorker.controller.postMessage(
+ {port:channel.port2}, [channel.port2]);
+ });
+ })
+ .then(onMessage);
+}, 'Test Clients.matchAll()');
+
+var expected = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+ ['visible', false, new URL(scope + '#2', location).toString(), 'window', 'nested']
+];
+
+function onMessage(e) {
+ assert_equals(e.data.length, 2);
+ assert_array_equals(e.data[0], expected[0]);
+ assert_array_equals(e.data[1], expected[1]);
+ frames.forEach(function(f) { f.remove(); });
+}
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/controller-on-disconnect.https.html b/testing/web-platform/tests/service-workers/service-worker/controller-on-disconnect.https.html
new file mode 100644
index 0000000000..f23dfe71ba
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/controller-on-disconnect.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: Controller on load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+ var url = 'resources/empty-worker.js';
+ var scope = 'resources/blank.html';
+ var registration;
+ var controller;
+ var frame;
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(swr) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = swr;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope)
+ })
+ .then(function(f) {
+ frame = f;
+ var w = frame.contentWindow;
+ var swc = w.navigator.serviceWorker;
+ assert_true(swc.controller instanceof w.ServiceWorker,
+ 'controller should be a ServiceWorker object');
+
+ frame.remove();
+
+ assert_equals(swc.controller, null,
+ 'disconnected frame should not be controlled');
+ });
+}, 'controller is cleared on disconnected window');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/controller-on-load.https.html b/testing/web-platform/tests/service-workers/service-worker/controller-on-load.https.html
new file mode 100644
index 0000000000..e4c5e5f81f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/controller-on-load.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<title>Service Worker: Controller on load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+ var url = 'resources/empty-worker.js';
+ var scope = 'resources/blank.html';
+ var registration;
+ var controller;
+ var frame;
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(swr) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = swr;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ var w = frame.contentWindow;
+ controller = w.navigator.serviceWorker.controller;
+ assert_true(controller instanceof w.ServiceWorker,
+ 'controller should be a ServiceWorker object');
+ assert_equals(controller.scriptURL, normalizeURL(url));
+
+ // objects from different windows should not be equal
+ assert_not_equals(controller, registration.active);
+
+ return w.navigator.serviceWorker.getRegistration();
+ })
+ .then(function(frameRegistration) {
+ // SW objects from same window should be equal
+ assert_equals(frameRegistration.active, controller);
+ frame.remove();
+ });
+}, 'controller is set for a controlled document');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/controller-on-reload.https.html b/testing/web-platform/tests/service-workers/service-worker/controller-on-reload.https.html
new file mode 100644
index 0000000000..2e966d4257
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/controller-on-reload.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Controller on reload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+ const iframe_scope = 'blank.html';
+ const scope = 'resources/' + iframe_scope;
+ var frame;
+ var registration;
+ var controller;
+ return service_worker_unregister(t, scope)
+ .then(function() {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ 'empty-worker.js', {scope: iframe_scope});
+ })
+ .then(function(swr) {
+ registration = swr;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var w = frame.contentWindow;
+ assert_equals(w.navigator.serviceWorker.controller, null,
+ 'controller should be null until the document is ' +
+ 'reloaded');
+ return new Promise(function(resolve) {
+ frame.onload = function() { resolve(); }
+ w.location.reload();
+ });
+ })
+ .then(function() {
+ var w = frame.contentWindow;
+ controller = w.navigator.serviceWorker.controller;
+ assert_true(controller instanceof w.ServiceWorker,
+ 'controller should be a ServiceWorker object upon reload');
+
+ // objects from separate windows should not be equal
+ assert_not_equals(controller, registration.active);
+
+ return w.navigator.serviceWorker.getRegistration(iframe_scope);
+ })
+ .then(function(frameRegistration) {
+ assert_equals(frameRegistration.active, controller);
+ frame.remove();
+ });
+ }, 'controller is set upon reload after registration');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html b/testing/web-platform/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html
new file mode 100644
index 0000000000..d947139c9e
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: controller without a fetch event handler</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+let registration;
+let frame;
+const host_info = get_host_info();
+const remote_base_url =
+ new URL(`${host_info.HTTPS_REMOTE_ORIGIN}${base_path()}resources/`);
+
+promise_test(async t => {
+ const script = 'resources/empty.js'
+ const scope = 'resources/';
+
+ promise_test(async t => {
+ if (frame)
+ frame.remove();
+
+ if (registration)
+ await registration.unregister();
+ }, 'cleanup global state');
+
+ registration = await
+ service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ frame = await with_iframe(scope + 'blank.html');
+}, 'global setup');
+
+promise_test(async t => {
+ const url = new URL('cors-approved.txt', remote_base_url);
+ const response = await frame.contentWindow.fetch(url, {mode:'no-cors'});
+ const text = await response.text();
+ assert_equals(text, '');
+}, 'cross-origin request, no-cors mode');
+
+
+promise_test(async t => {
+ const url = new URL('cors-denied.txt', remote_base_url);
+ const response = frame.contentWindow.fetch(url);
+ await promise_rejects_js(t, frame.contentWindow.TypeError, response);
+}, 'cross-origin request, cors denied');
+
+promise_test(async t => {
+ const url = new URL('cors-approved.txt', remote_base_url);
+ response = await frame.contentWindow.fetch(url);
+ let text = await response.text();
+ text = text.trim();
+ assert_equals(text, 'plaintext');
+}, 'cross-origin request, cors approved');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/credentials.https.html b/testing/web-platform/tests/service-workers/service-worker/credentials.https.html
new file mode 100644
index 0000000000..0a90dc2897
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/credentials.https.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Credentials for service worker scripts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/cookies/resources/cookie-helper.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// Check if the service worker's script has appropriate credentials for a new
+// worker and byte-for-byte checking.
+
+const SCOPE = 'resources/in-scope';
+const COOKIE_NAME = `service-worker-credentials-${Math.random()}`;
+
+promise_test(async t => {
+ // Set-Cookies for path=/.
+ await fetch(
+ `/cookies/resources/set-cookie.py?name=${COOKIE_NAME}&path=%2F`);
+}, 'Set cookies as initialization');
+
+async function get_cookies(worker) {
+ worker.postMessage('get cookie');
+ const message = await new Promise(resolve =>
+ navigator.serviceWorker.addEventListener('message', resolve));
+ return message.data;
+}
+
+promise_test(async t => {
+ const key = token();
+ const registration = await service_worker_unregister_and_register(
+ t, `resources/echo-cookie-worker.py?key=${key}`, SCOPE);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+
+ const cookies = await get_cookies(worker);
+ assert_equals(cookies[COOKIE_NAME], '1', 'new worker has credentials');
+
+ await registration.update();
+ const updated_worker = registration.installing;
+ const updated_cookies = await get_cookies(updated_worker);
+ assert_equals(updated_cookies[COOKIE_NAME], '1',
+ 'updated worker has credentials');
+}, 'Main script should have credentials');
+
+promise_test(async t => {
+ const key = token();
+ const registration = await service_worker_unregister_and_register(
+ t, `resources/import-echo-cookie-worker.js?key=${key}`, SCOPE);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+
+ const cookies = await get_cookies(worker);
+ assert_equals(cookies[COOKIE_NAME], '1', 'new worker has credentials');
+
+ await registration.update();
+ const updated_worker = registration.installing;
+ const updated_cookies = await get_cookies(updated_worker);
+ assert_equals(updated_cookies[COOKIE_NAME], '1',
+ 'updated worker has credentials');
+}, 'Imported script should have credentials');
+
+promise_test(async t => {
+ const key = token();
+ const registration = await service_worker_unregister_and_register(
+ t, `resources/import-echo-cookie-worker-module.py?key=${key}`, SCOPE, {type: 'module'});
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+
+ const cookies = await get_cookies(worker);
+ assert_equals(cookies[COOKIE_NAME], undefined, 'new module worker should not have credentials');
+
+ await registration.update();
+ const updated_worker = registration.installing;
+ const updated_cookies = await get_cookies(updated_worker);
+ assert_equals(updated_cookies[COOKIE_NAME], undefined,
+ 'updated worker should not have credentials');
+}, 'Module with an imported statement should not have credentials');
+
+promise_test(async t => {
+ const key = token();
+ const registration = await service_worker_unregister_and_register(
+t, `resources/echo-cookie-worker.py?key=${key}`, SCOPE, {type: 'module'});
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+
+ const cookies = await get_cookies(worker);
+ assert_equals(cookies[COOKIE_NAME], undefined, 'new module worker should not have credentials');
+
+ await registration.update();
+ const updated_worker = registration.installing;
+ const updated_cookies = await get_cookies(updated_worker);
+ assert_equals(updated_cookies[COOKIE_NAME], undefined,
+ 'updated worker should not have credentials');
+}, 'Script with service worker served as modules should not have credentials');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/data-iframe.html b/testing/web-platform/tests/service-workers/service-worker/data-iframe.html
new file mode 100644
index 0000000000..d767d57434
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/data-iframe.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Service Workers in data iframes</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body></body>
+<script>
+'use strict';
+
+promise_test(t => {
+ const url = encodeURI(`data:text/html,<!DOCTYPE html>
+ <script>
+ parent.postMessage({ isDefined: 'serviceWorker' in navigator }, '*');
+ </` + `script>`);
+ var p = new Promise((resolve, reject) => {
+ window.addEventListener('message', event => {
+ resolve(event.data.isDefined);
+ });
+ });
+ with_iframe(url);
+ return p.then(isDefined => {
+ assert_false(isDefined, 'navigator.serviceWorker should not be defined in iframe');
+ });
+}, 'navigator.serviceWorker is not available in a data: iframe');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/data-transfer-files.https.html b/testing/web-platform/tests/service-workers/service-worker/data-transfer-files.https.html
new file mode 100644
index 0000000000..c503a28f96
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/data-transfer-files.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Post a file in a navigation controlled by a service worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<iframe id=testframe name=testframe></iframe>
+<form id=testform method=post action="/html/semantics/forms/form-submission-0/resources/file-submission.py" target=testframe enctype="multipart/form-data">
+<input name=testinput id=testinput type=file>
+</form>
+<script>
+// Test that DataTransfer with a File entry works when posted to a
+// service worker that falls back to network. Regression test for
+// https://crbug.com/944145.
+promise_test(async (t) => {
+ const scope = '/html/semantics/forms/form-submission-0/resources/';
+ const header = `pipe=header(Service-Worker-Allowed,${scope})`;
+ const script = `resources/fetch-event-network-fallback-worker.js?${header}`;
+
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const dataTransfer = new DataTransfer();
+ dataTransfer.items.add(new File(['foobar'], 'name'));
+ assert_equals(1, dataTransfer.files.length);
+
+ testinput.files = dataTransfer.files;
+ testform.submit();
+
+ const data = await new Promise(resolve => {
+ onmessage = e => {
+ if (e.source !== testframe) return;
+ resolve(e.data);
+ };
+ });
+ assert_equals(data, "foobar");
+}, 'Posting a File in a navigation handled by a service worker');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html b/testing/web-platform/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html
new file mode 100644
index 0000000000..2144f48271
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>DedicatedWorker: ServiceWorker interception</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+// Note that Chrome cannot pass these tests because of https://crbug.com/731599.
+
+function service_worker_interception_test(url, description) {
+ promise_test(async t => {
+ // Register a service worker whose scope includes |url|.
+ const kServiceWorkerScriptURL =
+ 'resources/service-worker-interception-service-worker.js';
+ const registration = await service_worker_unregister_and_register(
+ t, kServiceWorkerScriptURL, url);
+ add_result_callback(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Start a dedicated worker for |url|. The top-level script request and any
+ // module imports should be intercepted by the service worker.
+ const worker = new Worker(url, { type: 'module' });
+ const msg_event = await new Promise(resolve => worker.onmessage = resolve);
+ assert_equals(msg_event.data, 'LOADED_FROM_SERVICE_WORKER');
+ }, description);
+}
+
+service_worker_interception_test(
+ 'resources/service-worker-interception-network-worker.js',
+ 'Top-level module loading should be intercepted by a service worker.');
+
+service_worker_interception_test(
+ 'resources/service-worker-interception-static-import-worker.js',
+ 'Static import should be intercepted by a service worker.');
+
+service_worker_interception_test(
+ 'resources/service-worker-interception-dynamic-import-worker.js',
+ 'Dynamic import should be intercepted by a service worker.');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/detached-context.https.html b/testing/web-platform/tests/service-workers/service-worker/detached-context.https.html
new file mode 100644
index 0000000000..ce8e4cc840
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/detached-context.https.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service WorkerRegistration from a removed iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+</body>
+<script>
+// NOTE: This file tests corner case behavior that might not be defined in the
+// spec. See https://github.com/w3c/ServiceWorker/issues/1221
+
+promise_test(t => {
+ const url = 'resources/blank.html';
+ const scope_for_iframe = 'removed-registration'
+ const scope_for_main = 'resources/' + scope_for_iframe;
+ const script = 'resources/empty-worker.js';
+ let frame;
+ let resolvedCount = 0;
+
+ return service_worker_unregister(t, scope_for_main)
+ .then(() => {
+ return with_iframe(url);
+ })
+ .then(f => {
+ frame = f;
+ return navigator.serviceWorker.register(script,
+ {scope: scope_for_main});
+ })
+ .then(r => {
+ add_completion_callback(() => { r.unregister(); });
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => {
+ return frame.contentWindow.navigator.serviceWorker.getRegistration(
+ scope_for_iframe);
+ })
+ .then(r => {
+ frame.remove();
+ assert_equals(r.installing, null);
+ assert_equals(r.waiting, null);
+ assert_equals(r.active.state, 'activated');
+ assert_equals(r.scope, normalizeURL(scope_for_main));
+ r.onupdatefound = () => { /* empty */ };
+
+ // We want to verify that unregister() and update() do not
+ // resolve on a detached registration. We can't check for
+ // an explicit rejection, though, because not all browsers
+ // fire rejection callbacks on detached promises. Instead
+ // we wait for a sample scope to install, activate, and
+ // unregister before declaring that the promises did not
+ // resolve.
+ r.unregister().then(() => resolvedCount += 1,
+ () => {});
+ r.update().then(() => resolvedCount += 1,
+ () => {});
+ return wait_for_activation_on_sample_scope(t, window);
+ })
+ .then(() => {
+ assert_equals(resolvedCount, 0,
+ 'methods called on a detached registration should not resolve');
+ frame.remove();
+ })
+ }, 'accessing a ServiceWorkerRegistration from a removed iframe');
+
+promise_test(t => {
+ const script = 'resources/empty-worker.js';
+ const scope = 'resources/scope/serviceworker-from-detached';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(() => { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => { return with_iframe(scope); })
+ .then(frame => {
+ const worker = frame.contentWindow.navigator.serviceWorker.controller;
+ const ctor = frame.contentWindow.DOMException;
+ frame.remove();
+ assert_equals(worker.scriptURL, normalizeURL(script));
+ assert_equals(worker.state, 'activated');
+ worker.onstatechange = () => { /* empty */ };
+ assert_throws_dom(
+ 'InvalidStateError',
+ ctor,
+ () => { worker.postMessage(''); },
+ 'postMessage on a detached client should throw an exception.');
+ });
+ }, 'accessing a ServiceWorker object from a removed iframe');
+
+promise_test(t => {
+ const iframe = document.createElement('iframe');
+ iframe.src = 'resources/blank.html';
+ document.body.appendChild(iframe);
+ const f = iframe.contentWindow.Function;
+ function get_navigator() {
+ return f('return navigator')();
+ }
+ return new Promise(resolve => {
+ assert_equals(iframe.contentWindow.navigator, get_navigator());
+ iframe.src = 'resources/blank.html?navigate-to-new-url';
+ iframe.onload = resolve;
+ }).then(function() {
+ assert_not_equals(get_navigator().serviceWorker, null);
+ assert_equals(
+ get_navigator().serviceWorker,
+ iframe.contentWindow.navigator.serviceWorker);
+ iframe.remove();
+ });
+ }, 'accessing navigator.serviceWorker on a detached iframe');
+
+test(t => {
+ const iframe = document.createElement('iframe');
+ iframe.src = 'resources/blank.html';
+ document.body.appendChild(iframe);
+ const f = iframe.contentWindow.Function;
+ function get_navigator() {
+ return f('return navigator')();
+ }
+ assert_not_equals(get_navigator().serviceWorker, null);
+ iframe.remove();
+ assert_not_equals(get_navigator().serviceWorker, null);
+ }, 'accessing navigator on a removed frame');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html b/testing/web-platform/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html
new file mode 100644
index 0000000000..581dbeca97
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>embed and object are not intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+let registration;
+
+const kScript = 'resources/embed-and-object-are-not-intercepted-worker.js';
+const kScope = 'resources/';
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ promise_test(() => {
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ }, 'initialize global state');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/embed-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request for embedded content was not intercepted');
+ });
+ }, 'requests for EMBED elements of embedded HTML content should not be intercepted by service workers');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/object-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request for embedded content was not intercepted');
+ });
+ }, 'requests for OBJECT elements of embedded HTML content should not be intercepted by service workers');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/embed-image-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request was not intercepted');
+ });
+ }, 'requests for EMBED elements of an image should not be intercepted by service workers');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/object-image-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request was not intercepted');
+ });
+ }, 'requests for OBJECT elements of an image should not be intercepted by service workers');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/object-navigation-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request for embedded content was not intercepted');
+ });
+ }, 'post-load navigation of OBJECT elements should not be intercepted by service workers');
+
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/embed-navigation-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request for embedded content was not intercepted');
+ });
+ }, 'post-load navigation of EMBED elements should not be intercepted by service workers');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html b/testing/web-platform/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html
new file mode 100644
index 0000000000..04e98266b4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+function sync_message(worker, message, transfer) {
+ let wait = new Promise((res, rej) => {
+ navigator.serviceWorker.addEventListener('message', function(e) {
+ if (e.data === 'ACK') {
+ res();
+ } else {
+ rej();
+ }
+ });
+ });
+ worker.postMessage(message, transfer);
+ return wait;
+}
+
+function runTest(test, step, testBody) {
+ var scope = './resources/' + step;
+ var script = 'resources/extendable-event-async-waituntil.js?' + scope;
+ return service_worker_unregister_and_register(test, script, scope)
+ .then(function(registration) {
+ test.add_cleanup(function() {
+ return service_worker_unregister(test, scope);
+ });
+
+ let worker = registration.installing;
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) { resolve(e.data); }
+ });
+
+ return wait_for_state(test, worker, 'activated')
+ .then(function() {
+ return sync_message(worker, { step: 'init', port: channel.port2 },
+ [channel.port2]);
+ })
+ .then(function() { return testBody(worker); })
+ .then(function() { return saw_message; })
+ .then(function(output) {
+ assert_equals(output.result, output.expected);
+ })
+ .then(function() { return sync_message(worker, { step: 'done' }); });
+ });
+}
+
+function msg_event_test(scope, test) {
+ var testBody = function(worker) {
+ return sync_message(worker, { step: scope });
+ };
+ return runTest(test, scope, testBody);
+}
+
+promise_test(msg_event_test.bind(this, 'no-current-extension-different-task'),
+ 'Test calling waitUntil in a task at the end of the event handler without an existing extension throws');
+
+promise_test(msg_event_test.bind(this, 'no-current-extension-different-microtask'),
+ 'Test calling waitUntil in a microtask at the end of the event handler without an existing extension suceeds');
+
+promise_test(msg_event_test.bind(this, 'current-extension-different-task'),
+ 'Test calling waitUntil in a different task an existing extension succeeds');
+
+promise_test(msg_event_test.bind(this, 'during-event-dispatch-current-extension-expired-same-microtask-turn'),
+ 'Test calling waitUntil at the end of an existing extension promise handler succeeds (event is still being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'during-event-dispatch-current-extension-expired-same-microtask-turn-extra'),
+ 'Test calling waitUntil in a microtask at the end of an existing extension promise handler succeeds (event is still being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'after-event-dispatch-current-extension-expired-same-microtask-turn'),
+ 'Test calling waitUntil in an existing extension promise handler succeeds (event is not being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'after-event-dispatch-current-extension-expired-same-microtask-turn-extra'),
+ 'Test calling waitUntil in a microtask at the end of an existing extension promise handler throws (event is not being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'current-extension-expired-different-task'),
+ 'Test calling waitUntil after the current extension expired in a different task fails');
+
+promise_test(msg_event_test.bind(this, 'script-extendable-event'),
+ 'Test calling waitUntil on a script constructed ExtendableEvent throws exception');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/pending-respondwith-async-waituntil');
+ }
+ return runTest(t, 'pending-respondwith-async-waituntil', testBody);
+ }, 'Test calling waitUntil asynchronously with pending respondWith promise.');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/during-event-dispatch-respondwith-microtask-sync-waituntil');
+ }
+ return runTest(t, 'during-event-dispatch-respondwith-microtask-sync-waituntil', testBody);
+ }, 'Test calling waitUntil synchronously inside microtask of respondWith promise (event is being dispatched).');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/during-event-dispatch-respondwith-microtask-async-waituntil');
+ }
+ return runTest(t, 'during-event-dispatch-respondwith-microtask-async-waituntil', testBody);
+ }, 'Test calling waitUntil asynchronously inside microtask of respondWith promise (event is being dispatched).');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/after-event-dispatch-respondwith-microtask-sync-waituntil');
+ }
+ return runTest(t, 'after-event-dispatch-respondwith-microtask-sync-waituntil', testBody);
+ }, 'Test calling waitUntil synchronously inside microtask of respondWith promise (event is not being dispatched).');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/after-event-dispatch-respondwith-microtask-async-waituntil');
+ }
+ return runTest(t, 'after-event-dispatch-respondwith-microtask-async-waituntil', testBody);
+ }, 'Test calling waitUntil asynchronously inside microtask of respondWith promise (event is not being dispatched).');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/extendable-event-waituntil.https.html b/testing/web-platform/tests/service-workers/service-worker/extendable-event-waituntil.https.html
new file mode 100644
index 0000000000..33b4eac5c1
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/extendable-event-waituntil.https.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<title>ExtendableEvent: waitUntil</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function runTest(test, scope, onRegister) {
+ var script = 'resources/extendable-event-waituntil.js?' + scope;
+ return service_worker_unregister_and_register(test, script, scope)
+ .then(function(registration) {
+ test.add_cleanup(function() {
+ return service_worker_unregister(test, scope);
+ });
+
+ return onRegister(registration.installing);
+ });
+}
+
+// Sends a SYN to the worker and asynchronously listens for an ACK; sets
+// |obj.synced| to true once ack'd.
+function syncWorker(worker, obj) {
+ var channel = new MessageChannel();
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ }).then(function(e) {
+ var message = e.data;
+ assert_equals(message, 'SYNC',
+ 'Should receive sync message from worker.');
+ obj.synced = true;
+ channel.port1.postMessage('ACK');
+ });
+}
+
+promise_test(function(t) {
+ // Passing scope as the test switch for worker script.
+ var scope = 'resources/install-fulfilled';
+ var onRegister = function(worker) {
+ var obj = {};
+
+ return Promise.all([
+ syncWorker(worker, obj),
+ wait_for_state(t, worker, 'installed')
+ ]).then(function() {
+ assert_true(
+ obj.synced,
+ 'state should be "installed" after the waitUntil promise ' +
+ 'for "oninstall" is fulfilled.');
+ service_worker_unregister_and_done(t, scope);
+ });
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test install event waitUntil fulfilled');
+
+promise_test(function(t) {
+ var scope = 'resources/install-multiple-fulfilled';
+ var onRegister = function(worker) {
+ var obj1 = {};
+ var obj2 = {};
+
+ return Promise.all([
+ syncWorker(worker, obj1),
+ syncWorker(worker, obj2),
+ wait_for_state(t, worker, 'installed')
+ ]).then(function() {
+ assert_true(
+ obj1.synced && obj2.synced,
+ 'state should be "installed" after all waitUntil promises ' +
+ 'for "oninstall" are fulfilled.');
+ });
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test ExtendableEvent multiple waitUntil fulfilled.');
+
+promise_test(function(t) {
+ var scope = 'resources/install-reject-precedence';
+ var onRegister = function(worker) {
+ var obj1 = {};
+ var obj2 = {};
+
+ return Promise.all([
+ syncWorker(worker, obj1)
+ .then(function() {
+ syncWorker(worker, obj2);
+ }),
+ wait_for_state(t, worker, 'redundant')
+ ]).then(function() {
+ assert_true(
+ obj1.synced,
+ 'The "redundant" state was entered after the first "extend ' +
+ 'lifetime promise" resolved.'
+ );
+ assert_true(
+ obj2.synced,
+ 'The "redundant" state was entered after the third "extend ' +
+ 'lifetime promise" resolved.'
+ );
+ });
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test ExtendableEvent waitUntil reject precedence.');
+
+promise_test(function(t) {
+ var scope = 'resources/activate-fulfilled';
+ var onRegister = function(worker) {
+ var obj = {};
+ return wait_for_state(t, worker, 'activating')
+ .then(function() {
+ return Promise.all([
+ syncWorker(worker, obj),
+ wait_for_state(t, worker, 'activated')
+ ]);
+ })
+ .then(function() {
+ assert_true(
+ obj.synced,
+ 'state should be "activated" after the waitUntil promise ' +
+ 'for "onactivate" is fulfilled.');
+ });
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test activate event waitUntil fulfilled');
+
+promise_test(function(t) {
+ var scope = 'resources/install-rejected';
+ var onRegister = function(worker) {
+ return wait_for_state(t, worker, 'redundant');
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test install event waitUntil rejected');
+
+promise_test(function(t) {
+ var scope = 'resources/activate-rejected';
+ var onRegister = function(worker) {
+ return wait_for_state(t, worker, 'activated');
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test activate event waitUntil rejected.');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-audio-tainting.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-audio-tainting.https.html
new file mode 100644
index 0000000000..9821759bc7
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-audio-tainting.https.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+promise_test(async (t) => {
+ const SCOPE = 'resources/empty.html';
+ const SCRIPT = 'resources/fetch-rewrite-worker.js';
+ const host_info = get_host_info();
+ const REMOTE_ORIGIN = host_info.HTTPS_REMOTE_ORIGIN;
+
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ await wait_for_state(t, reg.installing, 'activated');
+ const frame = await with_iframe(SCOPE);
+
+ const doc = frame.contentDocument;
+ const win = frame.contentWindow;
+
+ const context = new win.AudioContext();
+ try {
+ context.suspend();
+ const audio = doc.createElement('audio');
+ audio.autoplay = true;
+ const source = context.createMediaElementSource(audio);
+ const spn = context.createScriptProcessor(16384, 1, 1);
+ source.connect(spn).connect(context.destination);
+ const url = `${REMOTE_ORIGIN}/webaudio/resources/sin_440Hz_-6dBFS_1s.wav`;
+ audio.src = '/test?url=' + encodeURIComponent(url);
+ doc.body.appendChild(audio);
+
+ await new Promise((resolve) => {
+ audio.addEventListener('playing', resolve);
+ });
+ await context.resume();
+ const event = await new Promise((resolve) => {
+ spn.addEventListener('audioprocess', resolve);
+ });
+ const data = event.inputBuffer.getChannelData(0);
+ for (const e of data) {
+ assert_equals(e, 0);
+ }
+ } finally {
+ context.close();
+ }
+ }, 'Verify CORS XHR of fetch() in a Service Worker');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html
new file mode 100644
index 0000000000..dab2153baa
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<meta charset="utf-8">
+<title>canvas tainting when written twice</title>
+<script>
+function loadImage(doc, url) {
+ return new Promise((resolve, reject) => {
+ const image = doc.createElement('img');
+ image.onload = () => { resolve(image); }
+ image.onerror = () => { reject('failed to load: ' + url); };
+ image.src = url;
+ });
+}
+
+// Tests that a canvas is tainted after it's written to with both a clear image
+// and opaque image from the same URL. A bad implementation might cache the
+// info of the clear image and assume the opaque image is also clear because
+// it's from the same URL. See https://crbug.com/907047 for details.
+promise_test(async (t) => {
+ // Set up a service worker and a controlled iframe.
+ const script = 'resources/fetch-canvas-tainting-double-write-worker.js';
+ const scope = 'resources/fetch-canvas-tainting-double-write-iframe.html';
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ // Load the same cross-origin image URL through the controlled iframe and
+ // this uncontrolled frame. The service worker responds with a same-origin
+ // image for the controlled iframe, so it is cleartext.
+ const imagePath = base_path() + 'resources/fetch-access-control.py?PNGIMAGE';
+ const imageUrl = get_host_info()['HTTPS_REMOTE_ORIGIN'] + imagePath;
+ const clearImage = await loadImage(iframe.contentDocument, imageUrl);
+ const opaqueImage = await loadImage(document, imageUrl);
+
+ // Set up a canvas for testing tainting.
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+ canvas.width = clearImage.width;
+ canvas.height = clearImage.height;
+
+ // The clear image and the opaque image have the same src URL. But...
+
+ // ... the clear image doesn't taint the canvas.
+ context.drawImage(clearImage, 0, 0);
+ assert_true(canvas.toDataURL().length > 0);
+
+ // ... the opaque image taints the canvas.
+ context.drawImage(opaqueImage, 0, 0);
+ assert_throws_dom('SecurityError', () => { canvas.toDataURL(); });
+}, 'canvas is tainted after writing both a non-opaque image and an opaque image from the same URL');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html
new file mode 100644
index 0000000000..2132381122
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched image using cached responses</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+ resource_path: base_path() + 'resources/fetch-access-control.py?PNGIMAGE',
+ cache: true
+});
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html
new file mode 100644
index 0000000000..57dc7d98ca
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched image</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+ resource_path: base_path() + 'resources/fetch-access-control.py?PNGIMAGE',
+ cache: false
+});
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html
new file mode 100644
index 0000000000..c37e8e5624
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched video using cache responses</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+ resource_path: base_path() + 'resources/fetch-access-control.py?VIDEO',
+ cache: true
+});
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html
new file mode 100644
index 0000000000..28c3071804
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Canvas tainting due to video whose responses are fetched via a service worker including range requests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+// These tests try to test canvas tainting due to a <video> element. The video
+// src URL is same-origin as the page, but the response is fetched via a service
+// worker that does tricky things like returning opaque responses from another
+// origin. Furthermore, this tests range requests so there are multiple
+// responses.
+//
+// We test range requests by having the server return 206 Partial Content to the
+// first request (which doesn't necessarily have a "Range" header or one with a
+// byte range). Then the <video> element automatically makes ranged requests
+// (the "Range" HTTP request header specifies a byte range). The server responds
+// to these with 206 Partial Content for the given range.
+function range_request_test(script, expected, description) {
+ promise_test(t => {
+ let frame;
+ let registration;
+ add_result_callback(() => {
+ if (frame) frame.remove();
+ if (registration) registration.unregister();
+ });
+
+ const scope = 'resources/fetch-canvas-tainting-iframe.html';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(scope);
+ })
+ .then(f => {
+ frame = f;
+ // Add "?VIDEO&PartialContent" to get a video resource from the
+ // server using range requests.
+ const video_url = 'fetch-access-control.py?VIDEO&PartialContent';
+ return frame.contentWindow.create_test_case_promise(video_url);
+ })
+ .then(result => {
+ assert_equals(result, expected);
+ });
+ }, description);
+}
+
+// We want to consider a number of scenarios:
+// (1) Range responses come from a single origin, the same-origin as the page.
+// The canvas should not be tainted.
+range_request_test(
+ 'resources/fetch-event-network-fallback-worker.js',
+ 'NOT_TAINTED',
+ 'range responses from single origin (same-origin)');
+
+// (2) Range responses come from a single origin, cross-origin from the page
+// (and without CORS sharing). This is not possible to test, since service
+// worker can't make a request with a "Range" HTTP header in no-cors mode.
+
+// (3) Range responses come from multiple origins. The first response comes from
+// cross-origin (and without CORS sharing, so is opaque). Subsequent
+// responses come from same-origin. This should result in a load error, as regardless of canvas
+// loading range requests from multiple opaque origins can reveal information across those origins.
+range_request_test(
+ 'resources/range-request-to-different-origins-worker.js',
+ 'LOAD_ERROR',
+ 'range responses from multiple origins (cross-origin first)');
+
+// (4) Range responses come from multiple origins. The first response comes from
+// same-origin. Subsequent responses come from cross-origin (and without
+// CORS sharing). Like (2) this is not possible since the service worker
+// cannot make range requests cross-origin.
+
+// (5) Range responses come from a single origin, with a mix of opaque and
+// non-opaque responses. The first request uses 'no-cors' mode to
+// receive an opaque response, and subsequent range requests use 'cors'
+// to receive non-opaque responses. The canvas should be tainted.
+range_request_test(
+ 'resources/range-request-with-different-cors-modes-worker.js',
+ 'TAINTED',
+ 'range responses from single origin with both opaque and non-opaque responses');
+
+// (6) Range responses come from a single origin, with a mix of opaque and
+// non-opaque responses. The first request uses 'cors' mode to
+// receive an non-opaque response, and subsequent range requests use
+// 'no-cors' to receive non-opaque responses. Like (2) this is not possible.
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html
new file mode 100644
index 0000000000..e8c23a2edd
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched video</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+ resource_path: base_path() + 'resources/fetch-access-control.py?VIDEO',
+ cache: false
+});
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html
new file mode 100644
index 0000000000..317b02175f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>Service Worker: CORS-exposed header names should be transferred correctly</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(async function(t) {
+ const SCOPE = 'resources/simple.html';
+ const SCRIPT = 'resources/fetch-cors-exposed-header-names-worker.js';
+ const host_info = get_host_info();
+
+ const URL = get_host_info().HTTPS_REMOTE_ORIGIN +
+ '/service-workers/service-worker/resources/simple.txt?pipe=' +
+ 'header(access-control-allow-origin,*)|' +
+ 'header(access-control-expose-headers,*)|' +
+ 'header(foo,bar)|' +
+ 'header(set-cookie,X)';
+
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ await wait_for_state(t, reg.installing, 'activated');
+ const frame = await with_iframe(SCOPE);
+
+ const response = await frame.contentWindow.fetch(URL);
+ const headers = response.headers;
+ assert_equals(headers.get('foo'), 'bar');
+ assert_equals(headers.get('set-cookie'), null);
+ assert_equals(headers.get('access-control-expose-headers'), '*');
+ }, 'CORS-exposed header names for a response from sw');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-cors-xhr.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-cors-xhr.https.html
new file mode 100644
index 0000000000..f8ff445673
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-cors-xhr.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>Service Worker: CORS XHR of fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-cors-xhr-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var host_info = get_host_info();
+
+ return login_https(t)
+ .then(function() {
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ })
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ return new Promise(function(resolve, reject) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = (event) => {
+ if (event.data === 'done') {
+ resolve();
+ return;
+ }
+ test(() => {
+ assert_true(event.data.result);
+ }, event.data.testName);
+ };
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ });
+ });
+ }, 'Verify CORS XHR of fetch() in a Service Worker');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-csp.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-csp.https.html
new file mode 100644
index 0000000000..9e7b242b69
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-csp.https.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP control of fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+
+function assert_resolves(promise, description) {
+ return promise.catch(function(reason) {
+ throw new Error(description + ' - ' + reason.message);
+ });
+}
+
+function assert_rejects(promise, description) {
+ return promise.then(
+ function() { throw new Error(description); },
+ function() {});
+}
+
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-csp-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var host_info = get_host_info();
+ var IMAGE_PATH =
+ base_path() + 'resources/fetch-access-control.py?PNGIMAGE';
+ var IMAGE_URL = host_info['HTTPS_ORIGIN'] + IMAGE_PATH;
+ var REMOTE_IMAGE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_PATH;
+ var REDIRECT_URL =
+ host_info['HTTPS_ORIGIN'] + base_path() + 'resources/redirect.py';
+ var frame;
+
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(
+ SCOPE + '?' +
+ encodeURIComponent('img-src ' + host_info['HTTPS_ORIGIN'] +
+ '; script-src \'unsafe-inline\''));
+ })
+ .then(function(f) {
+ frame = f;
+ return assert_resolves(
+ frame.contentWindow.load_image(IMAGE_URL),
+ 'Allowed scope image resource should be loaded.');
+ })
+ .then(function() {
+ return assert_rejects(
+ frame.contentWindow.load_image(REMOTE_IMAGE_URL),
+ 'Disallowed scope image resource should not be loaded.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.load_image(
+ // The request for IMAGE_URL will be fetched in SW.
+ './sample?url=' + encodeURIComponent(IMAGE_URL)),
+ 'Allowed scope image resource which was fetched via SW should ' +
+ 'be loaded.');
+ })
+ .then(function() {
+ return assert_rejects(
+ frame.contentWindow.load_image(
+ // The request for REMOTE_IMAGE_URL will be fetched in SW.
+ './sample?mode=no-cors&url=' +
+ encodeURIComponent(REMOTE_IMAGE_URL)),
+ 'Disallowed scope image resource which was fetched via SW ' +
+ 'should not be loaded.');
+ })
+ .then(function() {
+ frame.remove();
+ return with_iframe(
+ SCOPE + '?' +
+ encodeURIComponent(
+ 'img-src ' + REDIRECT_URL +
+ '; script-src \'unsafe-inline\''));
+ })
+ .then(function(f) {
+ frame = f;
+ return assert_resolves(
+ frame.contentWindow.load_image(
+ // Set 'ignore' not to call respondWith() in the SW.
+ REDIRECT_URL + '?ignore&Redirect=' +
+ encodeURIComponent(IMAGE_URL)),
+ 'When the request was redirected, CSP match algorithm should ' +
+ 'ignore the path component of the URL.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.load_image(
+ // This request will be fetched via SW and redirected by
+ // redirect.php.
+ REDIRECT_URL + '?Redirect=' + encodeURIComponent(IMAGE_URL)),
+ 'When the request was redirected via SW, CSP match algorithm ' +
+ 'should ignore the path component of the URL.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.load_image(
+ // The request for IMAGE_URL will be fetched in SW.
+ REDIRECT_URL + '?url=' + encodeURIComponent(IMAGE_URL)),
+ 'When the request was fetched via SW, CSP match algorithm ' +
+ 'should ignore the path component of the URL.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.fetch(IMAGE_URL + "&fetch1", { mode: 'no-cors'}),
+ 'Allowed scope fetch resource should be loaded.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.fetch(
+ // The request for IMAGE_URL will be fetched in SW.
+ './sample?url=' + encodeURIComponent(IMAGE_URL + '&fetch2'), { mode: 'no-cors'}),
+ 'Allowed scope fetch resource which was fetched via SW should be loaded.');
+ })
+ .then(function() {
+ return assert_rejects(
+ frame.contentWindow.fetch(REMOTE_IMAGE_URL + "&fetch3", { mode: 'no-cors'}),
+ 'Disallowed scope fetch resource should not be loaded.');
+ })
+ .then(function() {
+ return assert_rejects(
+ frame.contentWindow.fetch(
+ // The request for REMOTE_IMAGE_URL will be fetched in SW.
+ './sample?url=' + encodeURIComponent(REMOTE_IMAGE_URL + '&fetch4'), { mode: 'no-cors'}),
+ 'Disallowed scope fetch resource which was fetched via SW should not be loaded.');
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Verify CSP control of fetch() in a Service Worker');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-error.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-error.https.html
new file mode 100644
index 0000000000..ca2f884a9b
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-error.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+</head>
+<body>
+<script>
+const scope = "./resources/in-scope";
+
+promise_test(async (test) => {
+ const registration = await service_worker_unregister_and_register(
+ test, "./resources/fetch-error-worker.js", scope);
+ promise_test(async () => registration.unregister(),
+ "Unregister service worker");
+ await wait_for_state(test, registration.installing, 'activated');
+}, "Setup service worker");
+
+promise_test(async (test) => {
+ const iframe = await with_iframe(scope);
+ test.add_cleanup(() => iframe.remove());
+ const response = await iframe.contentWindow.fetch("fetch-error-test");
+ try {
+ await response.text();
+ assert_unreached();
+ } catch (error) {
+ assert_true(error.message.includes("Sorry"));
+ }
+}, "Make sure a load that makes progress does not time out");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-add-async.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-add-async.https.html
new file mode 100644
index 0000000000..ac13e4f416
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-add-async.https.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch event added asynchronously doesn't throw</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+service_worker_test(
+ 'resources/fetch-event-add-async-worker.js');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html
new file mode 100644
index 0000000000..4812d8a915
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.waiting</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(function(t) {
+ var scope =
+ 'resources/fetch-event-after-navigation-within-page-iframe.html' +
+ '?hashchange';
+ var worker = 'resources/simple-intercept-worker.js';
+ var frame;
+
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker');
+ frame.contentWindow.location.hash = 'foo';
+ return frame.contentWindow.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker');
+ frame.remove();
+ })
+ }, 'Service Worker should respond to fetch event after the hash changes');
+
+promise_test(function(t) {
+ var scope =
+ 'resources/fetch-event-after-navigation-within-page-iframe.html' +
+ '?pushState';
+ var worker = 'resources/simple-intercept-worker.js';
+ var frame;
+
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker');
+ frame.contentWindow.history.pushState('', '', 'bar');
+ return frame.contentWindow.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker');
+ frame.remove();
+ })
+ }, 'Service Worker should respond to fetch event after the pushState');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html
new file mode 100644
index 0000000000..d9147f8549
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+<title>respondWith cannot be called asynchronously</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// This file has tests that call respondWith() asynchronously.
+
+let frame;
+let worker;
+const script = 'resources/fetch-event-async-respond-with-worker.js';
+const scope = 'resources/simple.html';
+
+// Global setup: this must be the first promise_test.
+promise_test(async (t) => {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+ frame = await with_iframe(scope);
+}, 'global setup');
+
+// Waits for a single message from the service worker and then removes the
+// message handler. Not safe for concurrent use.
+function wait_for_message() {
+ return new Promise((resolve) => {
+ const handler = (event) => {
+ navigator.serviceWorker.removeEventListener('message', handler);
+ resolve(event.data);
+ };
+ navigator.serviceWorker.addEventListener('message', handler);
+ });
+}
+
+// Does one test case. It fetches |url|. The service worker gets a fetch event
+// for |url| and attempts to call respondWith() asynchronously. It reports back
+// to the test whether an exception was thrown.
+async function do_test(url) {
+ // Send a message to tell the worker a new test case is starting.
+ const message = wait_for_message();
+ worker.postMessage('initializeMessageHandler');
+ const response = await message;
+ assert_equals(response, 'messageHandlerInitialized');
+
+ // Start a fetch.
+ const fetchPromise = frame.contentWindow.fetch(url);
+
+ // Receive the test result from the service worker.
+ const result = wait_for_message();
+ await fetchPromise.then(()=> {}, () => {});
+ return result;
+};
+
+promise_test(async (t) => {
+ const result = await do_test('respondWith-in-task');
+ assert_true(result.didThrow, 'should throw');
+ assert_equals(result.error, 'InvalidStateError');
+}, 'respondWith in a task throws InvalidStateError');
+
+promise_test(async (t) => {
+ const result = await do_test('respondWith-in-microtask');
+ assert_equals(result.didThrow, false, 'should not throw');
+}, 'respondWith in a microtask does not throw');
+
+// Global cleanup: the final promise_test.
+promise_test(async (t) => {
+ if (frame)
+ frame.remove();
+ await service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-handled.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-handled.https.html
new file mode 100644
index 0000000000..08b88ce377
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-handled.https.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+<title>Service Worker: FetchEvent.handled</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+let frame = null;
+let worker = null;
+const script = 'resources/fetch-event-handled-worker.js';
+const scope = 'resources/simple.html';
+const channel = new MessageChannel();
+
+// Wait for a message from the service worker and removes the message handler.
+function wait_for_message_from_worker() {
+ return new Promise((resolve) => channel.port2.onmessage = (event) => resolve(event.data));
+}
+
+// Global setup: this must be the first promise_test.
+promise_test(async (t) => {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ worker = registration.installing;
+ if (!worker)
+ worker = registration.active;
+ worker.postMessage({port:channel.port1}, [channel.port1]);
+ await wait_for_state(t, worker, 'activated');
+}, 'global setup');
+
+promise_test(async (t) => {
+ const promise = with_iframe(scope);
+ const message = await wait_for_message_from_worker();
+ frame = await promise;
+ assert_equals(message, 'RESOLVED');
+}, 'FetchEvent.handled should resolve when respondWith() is not called for a' +
+ ' navigation request');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch('sample.txt?respondWith-not-called');
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'RESOLVED');
+}, 'FetchEvent.handled should resolve when respondWith() is not called for a' +
+ ' sub-resource request');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch(
+ 'sample.txt?respondWith-not-called-and-event-canceled').catch((e) => {});
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when respondWith() is not called and the' +
+ ' event is canceled');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch(
+ 'sample.txt?respondWith-called-and-promise-resolved');
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'RESOLVED');
+}, 'FetchEvent.handled should resolve when the promise provided' +
+ ' to respondWith() is resolved');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch(
+ 'sample.txt?respondWith-called-and-promise-resolved-to-invalid-response')
+ .catch((e) => {});
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when the promise provided' +
+ ' to respondWith() is resolved to an invalid response');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch(
+ 'sample.txt?respondWith-called-and-promise-rejected').catch((e) => {});
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when the promise provided to' +
+ ' respondWith() is rejected');
+
+// Global cleanup: the final promise_test.
+promise_test(async (t) => {
+ if (frame)
+ frame.remove();
+ await service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html
new file mode 100644
index 0000000000..3cf5922f39
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<body>
+<p>Click <a href="resources/install-worker.html?isHistoryNavigation&amp;script=fetch-event-test-worker.js">this link</a>.
+ Once you see &quot;method = GET,...&quot; in the page, go to another page, and then go back to the page using the Backward button.
+ You should see &quot;method = GET, isHistoryNavigation = true&quot;.
+</p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html
new file mode 100644
index 0000000000..401939b3cb
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<body>
+<p>Click <a href="resources/install-worker.html?isHistoryNavigation&amp;script=fetch-event-test-worker.js">this link</a>.
+ Once you see &quot;method = GET,...&quot; in the page, go back to this page using the Backward button, and then go to the second page using the Forward button.
+ You should see &quot;method = GET, isHistoryNavigation = true&quot;.
+</p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html
new file mode 100644
index 0000000000..cf1feccf6e
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/fetch-event-test-worker.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/simple.html?isReloadNavigation';
+
+ const reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ const frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentDocument.body.innerText =
+ 'Reload this frame manually!';
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = true');
+ frame.remove();
+ await reg.unregister();
+}, 'FetchEvent#request.isReloadNavigation is true for manual reload.');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html
new file mode 100644
index 0000000000..a349f07c36
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<body>
+<p>Click <a href="resources/install-worker.html?isReloadNavigation&script=fetch-event-test-worker.js">this link</a>.
+ Once you see &quot;method = GET,...&quot; in the page, reload the page.
+ You will see &quot;method = GET, isReloadNavigation = true&quot;.
+</p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-network-error.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-network-error.https.html
new file mode 100644
index 0000000000..fea2ad1e3c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-network-error.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch event network error</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var resolve_test_done;
+
+var test_done_promise = new Promise(function(resolve) {
+ resolve_test_done = resolve;
+ });
+
+// Called by the child frame.
+function notify_test_done(result) {
+ resolve_test_done(result);
+}
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-event-network-error-controllee-iframe.html';
+ var script = 'resources/fetch-event-network-error-worker.js';
+ var frame;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return test_done_promise;
+ })
+ .then(function(result) {
+ frame.remove();
+ assert_equals(result, 'PASS');
+ });
+ }, 'Rejecting the fetch event or using preventDefault() causes a network ' +
+ 'error');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-redirect.https.html
new file mode 100644
index 0000000000..5229284757
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-redirect.https.html
@@ -0,0 +1,1038 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch Event Redirect Handling</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// ------------------------
+// Utilities for testing non-navigation requests that are intercepted with
+// a redirect.
+
+const host_info = get_host_info();
+const kScript = 'resources/fetch-rewrite-worker.js';
+const kScope = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/blank.html?fetch-event-redirect';
+let frame;
+
+function redirect_fetch_test(t, test) {
+ const hostKeySuffix = test['url_credentials'] ? '_WITH_CREDS' : '';
+ const successPath = base_path() + 'resources/success.py';
+
+ let acaOrigin = '';
+ let host = host_info['HTTPS_ORIGIN' + hostKeySuffix];
+ if (test['redirect_dest'] === 'no-cors') {
+ host = host_info['HTTPS_REMOTE_ORIGIN' + hostKeySuffix]
+ } else if (test['redirect_dest'] === 'cors') {
+ acaOrigin = '?ACAOrigin=' + encodeURIComponent(host_info['HTTPS_ORIGIN']);
+ host = host_info['HTTPS_REMOTE_ORIGIN' + hostKeySuffix]
+ }
+
+ const dest = '?Redirect=' + encodeURIComponent(host + successPath + acaOrigin);
+ const expectedTypeParam =
+ test['expected_type']
+ ? '&expected_type=' + test['expected_type']
+ : '';
+ const expectedRedirectedParam =
+ test['expected_redirected']
+ ? '&expected_redirected=' + test['expected_redirected']
+ : '';
+ const url = '/' + test.name +
+ '?url=' + encodeURIComponent('redirect.py' + dest) +
+ expectedTypeParam + expectedRedirectedParam
+ const request = new Request(url, test.request_init);
+
+ if (test.should_reject) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(request),
+ 'Must fail to fetch: url=' + url);
+ }
+ return frame.contentWindow.fetch(request).then((response) => {
+ assert_equals(response.type, test.expected_type,
+ 'response.type');
+ assert_equals(response.redirected, test.expected_redirected,
+ 'response.redirected');
+ if (response.type === 'opaque' || response.type === 'opaqueredirect') {
+ return;
+ }
+ return response.json().then((json) => {
+ assert_equals(json.result, 'success', 'JSON result must be "success".');
+ });
+ });
+}
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ promise_test(() => {
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => { f.remove(); });
+ });
+ }, 'initialize global state');
+
+// ------------------------
+// Test every combination of:
+// - RequestMode (same-origin, cors, no-cors)
+// - RequestRedirect (manual, follow, error)
+// - redirect destination origin (same-origin, cors, no-cors)
+// - redirect destination credentials (no user/pass, user/pass)
+//
+// TODO: add navigation requests
+// TODO: add redirects to data URI and verify same-origin data-URL flag behavior
+// TODO: add test where original redirect URI is cross-origin
+// TODO: verify final method is correct for 301, 302, and 303
+// TODO: verify CORS redirect results in all further redirects being
+// considered cross origin
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'same-origin without credentials should succeed opaqueredirect ' +
+ 'interception and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'no-cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'same-origin without credentials should succeed opaqueredirect ' +
+ 'interception and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'no-cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'same-origin without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ // This should succeed because its redirecting from same-origin to
+ // cross-origin. Since the same-origin URL provides the location
+ // header the manual redirect mode should result in an opaqueredirect
+ // response.
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'no-cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ // This should succeed because its redirecting from same-origin to
+ // cross-origin. Since the same-origin URL provides the location
+ // header the manual redirect mode should result in an opaqueredirect
+ // response.
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'same-origin with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'no-cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'same-origin with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'no-cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'same-origin with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ // This should succeed because its redirecting from same-origin to
+ // cross-origin. Since the same-origin URL provides the location
+ // header the manual redirect mode should result in an opaqueredirect
+ // response.
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'no-cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ // This should succeed because its redirecting from same-origin to
+ // cross-origin. Since the same-origin URL provides the location
+ // header the manual redirect mode should result in an opaqueredirect
+ // response.
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'same-origin without credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ // should reject because CORS requests require CORS headers on cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'cors',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'cors without credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'same-origin without credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ // should reject because same-origin requests cannot load cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ // should reject because same-origin requests cannot load cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'same-origin without credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ expected_type: 'opaque',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'no-cors without credentials should succeed interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'opaque',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'cors without credentials should succeed interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'same-origin with credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ // should reject because CORS requests require CORS headers on cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ // should reject because CORS requests do not allow user/pass entries in
+ // cross-origin URLs
+ // NOTE: https://github.com/whatwg/fetch/issues/112
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'cors with credentials should fail interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'same-origin with credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ // should reject because same-origin requests cannot load cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ // should reject because same-origin requests cannot load cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'same-origin with credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ expected_type: 'opaque',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'no-cors with credentials should succeed interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ expected_type: 'opaque',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'cors with credentials should succeed interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'same-origin without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'same-origin without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'same-origin without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'same-origin with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'same-origin with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'same-origin with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'cors with credentials should fail interception and response should not ' +
+ 'be redirected');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html
new file mode 100644
index 0000000000..af4b20a9a4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html
@@ -0,0 +1,274 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker = 'resources/fetch-event-test-worker.js';
+
+function do_test(referrer, value, expected, name)
+{
+ test(() => {
+ assert_equals(value, expected);
+ }, name + (referrer ? " - Custom Referrer" : " - Default Referrer"));
+}
+
+function run_referrer_policy_tests(frame, referrer, href, origin) {
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {method: "GET", referrer: referrer})
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer URL when a member of RequestInit is present');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {method: "GET", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with no referrer when a member of RequestInit is present with an HTTP request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer with ""');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with no referrer with ""');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: origin',
+ 'Service Worker should respond to fetch with the referrer origin with "origin" and a same origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: origin',
+ 'Service Worker should respond to fetch with the referrer origin with "origin" and a cross origin request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer URL with "origin-when-cross-origin" and a same origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer origin with "origin-when-cross-origin" and a cross origin request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "no-referrer-when-downgrade", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: no-referrer-when-downgrade',
+ 'Service Worker should respond to fetch with no referrer with "no-referrer-when-downgrade" and a same origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "no-referrer-when-downgrade", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: no-referrer-when-downgrade',
+ 'Service Worker should respond to fetch with no referrer with "no-referrer-when-downgrade" and an HTTP request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url, {referrerPolicy: "unsafe-url", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: unsafe-url',
+ 'Service Worker should respond to fetch with no referrer with "unsafe-url"');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "no-referrer", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: no-referrer',
+ 'Service Worker should respond to fetch with no referrer URL with "no-referrer"');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "same-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: same-origin',
+ 'Service Worker should respond to fetch with referrer URL with "same-origin" and a same origin request');
+ var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "same-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: same-origin',
+ 'Service Worker should respond to fetch with no referrer with "same-origin" and cross origin request');
+ var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "strict-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: strict-origin',
+ 'Service Worker should respond to fetch with the referrer origin with "strict-origin" and a HTTPS cross origin request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "strict-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: strict-origin',
+ 'Service Worker should respond to fetch with the referrer origin with "strict-origin" and a same origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "strict-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: strict-origin',
+ 'Service Worker should respond to fetch with no referrer with "strict-origin" and a HTTP request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer URL with "strict-origin-when-cross-origin" and a same origin request');
+ var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer origin with "strict-origin-when-cross-origin" and a HTTPS cross origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with no referrer with "strict-origin-when-cross-origin" and a HTTP request');
+ });
+}
+
+promise_test(function(t) {
+ var scope = 'resources/simple.html?referrerPolicy';
+ var frame;
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ test(() => {
+ assert_equals(frame.contentDocument.body.textContent, 'ReferrerPolicy: strict-origin-when-cross-origin');
+ }, 'Service Worker should respond to fetch with the default referrer policy');
+ // First, run the referrer policy tests without passing a referrer in RequestInit.
+ return run_referrer_policy_tests(frame, undefined, frame.contentDocument.location.href,
+ frame.contentDocument.location.origin);
+ })
+ .then(function() {
+ // Now, run the referrer policy tests while passing a referrer in RequestInit.
+ var referrer = get_host_info()['HTTPS_ORIGIN'] + base_path() + 'resources/fake-referrer';
+ return run_referrer_policy_tests(frame, 'fake-referrer', referrer,
+ frame.contentDocument.location.origin);
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Service Worker responds to fetch event with the referrer policy');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html
new file mode 100644
index 0000000000..05e2210524
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent.respondWith() argument type test.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var resolve_test_done;
+
+var test_done_promise = new Promise(function(resolve) {
+ resolve_test_done = resolve;
+ });
+
+// Called by the child frame.
+function notify_test_done(result) {
+ resolve_test_done(result);
+}
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-event-respond-with-argument-iframe.html';
+ var script = 'resources/fetch-event-respond-with-argument-worker.js';
+ var frame;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return test_done_promise;
+ })
+ .then(function(result) {
+ frame.remove();
+ assert_equals(result, 'PASS');
+ });
+ }, 'respondWith() takes either a Response or a promise that resolves ' +
+ 'with a Response. Other values should raise a network error.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html
new file mode 100644
index 0000000000..932f9030c5
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a response whose body is being loaded from the network by chunks</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER = 'resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js';
+const SCOPE = 'resources/fetch-event-respond-with-body-loaded-in-chunk-iframe.html';
+
+promise_test(async t => {
+ var reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+ add_completion_callback(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+ let iframe = await with_iframe(SCOPE);
+ t.add_cleanup(() => iframe.remove());
+
+ let response = await iframe.contentWindow.fetch('body-in-chunk');
+ assert_equals(await response.text(), 'TEST_TRICKLE\nTEST_TRICKLE\nTEST_TRICKLE\nTEST_TRICKLE\n');
+}, 'Respond by chunks with a Response being loaded');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html
new file mode 100644
index 0000000000..645a29c9b4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a new Response</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+ 'resources/fetch-event-respond-with-custom-response-worker.js';
+const SCOPE =
+ 'resources/blank.html';
+
+// Register a service worker, then create an iframe at url.
+function iframeTest(url, callback, name) {
+ return promise_test(async t => {
+ const reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+ add_completion_callback(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+ const iframe = await with_iframe(url);
+ const iwin = iframe.contentWindow;
+ t.add_cleanup(() => iframe.remove());
+ await callback(t, iwin);
+ }, name);
+}
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=string');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a string');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=blob');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a blob');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=buffer');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a buffer');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=buffer-view');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a buffer-view');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=form-data');
+ const data = await response.formData();
+ assert_equals(data.get('result'), 'PASS');
+}, 'Subresource built from form-data');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=search-params');
+ assert_equals(await response.text(), 'result=PASS');
+}, 'Subresource built from search-params');
+
+// As above, but navigations
+
+iframeTest(SCOPE + '?type=string', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a string');
+
+iframeTest(SCOPE + '?type=blob', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a blob');
+
+iframeTest(SCOPE + '?type=buffer', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a buffer');
+
+iframeTest(SCOPE + '?type=buffer-view', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a buffer-view');
+
+// Note: not testing form data for a navigation as the boundary header is lost.
+
+iframeTest(SCOPE + '?type=search-params', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'result=PASS');
+}, 'Navigation resource built from search-params');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html
new file mode 100644
index 0000000000..505cef2972
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith streams data to an intercepted fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+ 'resources/fetch-event-respond-with-partial-stream-worker.js';
+const SCOPE =
+ 'resources/fetch-event-respond-with-partial-stream-iframe.html';
+
+promise_test(async t => {
+ let reg = await service_worker_unregister_and_register(t, WORKER, SCOPE)
+ add_completion_callback(() => reg.unregister());
+
+ await wait_for_state(t, reg.installing, 'activated');
+
+ let frame = await with_iframe(SCOPE);
+ t.add_cleanup(_ => frame.remove());
+
+ let response = await frame.contentWindow.fetch('partial-stream.txt');
+
+ let reader = response.body.getReader();
+
+ let encoder = new TextEncoder();
+ let decoder = new TextDecoder();
+
+ let expected = 'partial-stream-content';
+ let encodedExpected = encoder.encode(expected);
+ let received = '';
+ let encodedReceivedLength = 0;
+
+ // Accumulate response data from the service worker. We do this as a loop
+ // to allow the browser the flexibility of rebuffering if it chooses. We
+ // do expect to get the partial data within the test timeout period, though.
+ // The spec is a bit vague at the moment about this, but it seems reasonable
+ // that the browser should not stall the response stream when the service
+ // worker has only written a partial result, but not closed the stream.
+ while (encodedReceivedLength < encodedExpected.length) {
+ let chunk = await reader.read();
+ assert_false(chunk.done, 'partial body stream should not be closed yet');
+
+ encodedReceivedLength += chunk.value.length;
+ received += decoder.decode(chunk.value);
+ }
+
+ // Note, the spec may allow some re-buffering between the service worker
+ // and the outer intercepted fetch. We could relax this exact chunk value
+ // match if necessary. The goal, though, is to ensure the outer fetch is
+ // not completely blocked until the service worker body is closed.
+ assert_equals(received, expected,
+ 'should receive partial content through service worker interception');
+
+ reg.active.postMessage('done');
+
+ await reader.closed;
+
+ }, 'respondWith() streams data to an intercepted fetch()');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html
new file mode 100644
index 0000000000..4544a9e08f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a response built from a ReadableStream</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER = 'resources/fetch-event-respond-with-readable-stream-chunk-worker.js';
+const SCOPE = 'resources/fetch-event-respond-with-readable-stream-chunk-iframe.html';
+
+promise_test(async t => {
+ var reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+ add_completion_callback(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+ let iframe = await with_iframe(SCOPE);
+ t.add_cleanup(() => iframe.remove());
+
+ let response = await iframe.contentWindow.fetch('body-stream');
+ assert_equals(await response.text(), 'chunk #1 chunk #2 chunk #3 chunk #4');
+}, 'Respond by chunks with a Response built from a ReadableStream');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html
new file mode 100644
index 0000000000..4651258e6a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a response built from a ReadableStream</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+ 'resources/fetch-event-respond-with-readable-stream-worker.js';
+const SCOPE =
+ 'resources/blank.html';
+
+// Register a service worker, then create an iframe at url.
+function iframeTest(url, callback, name) {
+ return promise_test(async t => {
+ const reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+ add_completion_callback(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+ const iframe = await with_iframe(url);
+ const iwin = iframe.contentWindow;
+ t.add_cleanup(() => iframe.remove());
+ await callback(t, iwin);
+ }, name);
+}
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?stream');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a ReadableStream');
+
+iframeTest(SCOPE + '?stream', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Main resource built from a ReadableStream');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?stream&delay');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a ReadableStream - delayed');
+
+iframeTest(SCOPE + '?stream&delay', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Main resource built from a ReadableStream - delayed');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?stream&use-fetch-stream');
+ assert_equals(await response.text(), 'PASS\n');
+}, 'Subresource built from a ReadableStream - fetch stream');
+
+iframeTest(SCOPE + '?stream&use-fetch-stream', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS\n');
+}, 'Main resource built from a ReadableStream - fetch stream');
+
+for (const enqueue of [true, false]) {
+ const withStream = enqueue ? 'with nonempty stream' : 'with empty stream';
+ iframeTest(SCOPE, async (t, iwin) => {
+ const id = token();
+ let response = await iwin.fetch(`?stream&observe-cancel&id=${id}&enqueue=${enqueue}`);
+ response.body.cancel();
+
+ // Wait for a while to avoid a race between the cancel handling and the
+ // second fetch request.
+ await new Promise(r => step_timeout(r, 10));
+
+ response = await iwin.fetch(`?stream&query-cancel&id=${id}`);
+ assert_equals(await response.text(), 'cancelled');
+ }, `Cancellation in the page should be observable in the service worker ${withStream}`);
+
+ iframeTest(SCOPE, async (t, iwin) => {
+ const id = token();
+ const controller = new AbortController();
+ let response = await iwin.fetch(`?stream&observe-cancel&id=${id}&enqueue=${enqueue}`, {
+ signal: controller.signal
+ });
+ controller.abort();
+
+ // Wait for a while to avoid a race between the cancel handling and the
+ // second fetch request.
+ await new Promise(r => step_timeout(r, 10));
+
+ response = await iwin.fetch(`?stream&query-cancel&id=${id}`);
+ assert_equals(await response.text(), 'cancelled');
+ }, `Abort in the page should be observable in the service worker ${withStream}`);
+}
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html
new file mode 100644
index 0000000000..2a44811461
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with response body having invalid chunks</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+ 'resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js';
+const SCOPE =
+ 'resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html';
+
+// Called by the iframe when it has the reader promise we should watch.
+var set_reader_promise;
+let reader_promise = new Promise(resolve => set_reader_promise = resolve);
+
+var set_fetch_promise;
+let fetch_promise = new Promise(resolve => set_fetch_promise = resolve);
+
+// This test creates an controlled iframe that makes a fetch request. The
+// service worker returns a response with a body stream containing an invalid
+// chunk.
+promise_test(async t => {
+ // Start off the process.
+ let errorConstructor;
+ await service_worker_unregister_and_register(t, WORKER, SCOPE)
+ .then(reg => {
+ add_completion_callback(() => reg.unregister());
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(() => with_iframe(SCOPE))
+ .then(frame => {
+ t.add_cleanup(() => frame.remove())
+ errorConstructor = frame.contentWindow.TypeError;
+ });
+
+ await promise_rejects_js(t, errorConstructor, reader_promise,
+ "read() should be rejected");
+ // Fetch should complete properly, because the reader error is caught in
+ // the subframe. That is, there should be no errors _other_ than the
+ // reader!
+ return fetch_promise;
+ }, 'Response with a ReadableStream having non-Uint8Array chunks should be transferred as errored');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html
new file mode 100644
index 0000000000..31fd616b6d
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var script =
+ 'resources/fetch-event-respond-with-stops-propagation-worker.js';
+ var scope = 'resources/simple.html';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ t.add_cleanup(function() { frame.remove(); });
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) { resolve(e.data); }
+ });
+ var worker = frame.contentWindow.navigator.serviceWorker.controller;
+
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+ .then(function(message) {
+ assert_equals(message, 'PASS');
+ })
+ }, 'respondWith() invokes stopImmediatePropagation()');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html
new file mode 100644
index 0000000000..d98fb22ff4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-event-throws-after-respond-with-iframe.html';
+ var workerscript = 'resources/respond-then-throw-worker.js';
+ var iframe;
+ return service_worker_unregister_and_register(t, workerscript, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated')
+ .then(() => reg.active);
+ })
+ .then(function(worker) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(e) {
+ assert_equals(e.data, 'SYNC', ' Should receive sync message.');
+ channel.port1.postMessage('ACK');
+ }
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ // The iframe will only be loaded after the sync is completed.
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ assert_true(frame.contentDocument.body.innerHTML.includes("intercepted"));
+ })
+ }, 'Fetch event handler throws after a successful respondWith()');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html
new file mode 100644
index 0000000000..15a2e95bd3
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/fetch-event-within-sw-worker.js';
+
+function wait(ms) {
+ return new Promise(r => setTimeout(r, ms));
+}
+
+function reset() {
+ for (const iframe of [...document.querySelectorAll('.test-iframe')]) {
+ iframe.remove();
+ }
+ return navigator.serviceWorker.getRegistrations().then(registrations => {
+ return Promise.all(registrations.map(r => r.unregister()));
+ }).then(() => caches.keys()).then(cacheKeys => {
+ return Promise.all(cacheKeys.map(c => caches.delete(c)));
+ });
+}
+
+add_completion_callback(reset);
+
+function regReady(reg) {
+ return new Promise((resolve, reject) => {
+ if (reg.active) {
+ resolve();
+ return;
+ }
+ const nextWorker = reg.waiting || reg.installing;
+
+ nextWorker.addEventListener('statechange', () => {
+ if (nextWorker.state == 'redundant') {
+ reject(Error(`Service worker failed to install`));
+ return;
+ }
+ if (nextWorker.state == 'activated') {
+ resolve();
+ }
+ });
+ });
+}
+
+function getCookies() {
+ return new Map(
+ document.cookie
+ .split(/;/g)
+ .map(c => c.trim().split('=').map(s => s.trim()))
+ );
+}
+
+function registerSwAndOpenFrame() {
+ return reset().then(() => navigator.serviceWorker.register(worker, {scope: 'resources/'}))
+ .then(reg => regReady(reg))
+ .then(() => with_iframe('resources/simple.html'));
+}
+
+function raceBroadcastAndCookie(channel, cookie) {
+ const initialCookie = getCookies().get(cookie);
+ let done = false;
+
+ return Promise.race([
+ new Promise(resolve => {
+ const bc = new BroadcastChannel(channel);
+ bc.onmessage = () => {
+ bc.close();
+ resolve('broadcast');
+ };
+ }),
+ (function checkCookie() {
+ // Stop polling if the broadcast channel won
+ if (done == true) return;
+ if (getCookies().get(cookie) != initialCookie) return 'cookie';
+
+ return wait(200).then(checkCookie);
+ }())
+ ]).then(val => {
+ done = true;
+ return val;
+ });
+}
+
+promise_test(() => {
+ return Notification.requestPermission().then(permission => {
+ if (permission != "granted") {
+ throw Error('You must allow notifications for this origin before running this test.');
+ }
+ return registerSwAndOpenFrame();
+ }).then(iframe => {
+ return Promise.resolve().then(() => {
+ // In this test, the service worker will ping the 'icon-request' channel
+ // if it intercepts a request for 'notification_icon.py'. If the request
+ // reaches the server it sets the 'notification' cookie to the value given
+ // in the URL. "raceBroadcastAndCookie" monitors both and returns which
+ // happens first.
+ const race = raceBroadcastAndCookie('icon-request', 'notification');
+ const notification = new iframe.contentWindow.Notification('test', {
+ icon: `notification_icon.py?set-cookie-notification=${Math.random()}`
+ });
+ notification.close();
+
+ return race.then(winner => {
+ assert_equals(winner, 'broadcast', 'The service worker intercepted the from-window notification icon request');
+ });
+ }).then(() => {
+ // Similar race to above, but this time the service worker requests the
+ // notification.
+ const race = raceBroadcastAndCookie('icon-request', 'notification');
+ iframe.contentWindow.fetch(`show-notification?set-cookie-notification=${Math.random()}`);
+
+ return race.then(winner => {
+ assert_equals(winner, 'broadcast', 'The service worker intercepted the from-service-worker notification icon request');
+ });
+ })
+ });
+}, `Notification requests intercepted both from window and SW`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event-within-sw.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event-within-sw.https.html
new file mode 100644
index 0000000000..0b52b18305
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event-within-sw.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+const worker = 'resources/fetch-event-within-sw-worker.js';
+
+async function registerSwAndOpenFrame(t) {
+ const registration = await navigator.serviceWorker.register(
+ worker, { scope: 'resources/' });
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe('resources/simple.html');
+ t.add_cleanup(() => frame.remove());
+ return frame;
+}
+
+async function deleteCaches() {
+ const cacheKeys = await caches.keys();
+ await Promise.all(cacheKeys.map(c => caches.delete(c)));
+}
+
+promise_test(async t => {
+ t.add_cleanup(deleteCaches);
+
+ const iframe = await registerSwAndOpenFrame(t);
+ const fetchText =
+ await iframe.contentWindow.fetch('sample.txt').then(r => r.text());
+
+ const cache = await iframe.contentWindow.caches.open('test');
+ await cache.add('sample.txt');
+
+ const response = await cache.match('sample.txt');
+ const cacheText = await (response ? response.text() : 'cache match failed');
+ assert_equals(fetchText, 'intercepted', 'fetch intercepted');
+ assert_equals(cacheText, 'intercepted', 'cache.add intercepted');
+}, 'Service worker intercepts requests from window');
+
+promise_test(async t => {
+ const iframe = await registerSwAndOpenFrame(t);
+ const [fetchText, cacheText] = await Promise.all([
+ iframe.contentWindow.fetch('sample.txt-inner-fetch').then(r => r.text()),
+ iframe.contentWindow.fetch('sample.txt-inner-cache').then(r => r.text())
+ ]);
+ assert_equals(fetchText, 'Hello world\n', 'fetch within SW not intercepted');
+ assert_equals(cacheText, 'Hello world\n',
+ 'cache.add within SW not intercepted');
+}, 'Service worker does not intercept fetch/cache requests within service ' +
+ 'worker');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event.https.h2.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event.https.h2.html
new file mode 100644
index 0000000000..5cd381ec98
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event.https.h2.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/fetch-event-test-worker.js';
+
+const method = 'POST';
+const duplex = 'half';
+
+function createBody(t) {
+ const rs = new ReadableStream({start(c) {
+ c.enqueue('i a');
+ c.enqueue('m the request');
+ step_timeout(t.step_func(() => {
+ c.enqueue(' body');
+ c.close();
+ }, 10));
+ }});
+ return rs.pipeThrough(new TextEncoderStream());
+}
+
+promise_test(async t => {
+ const scope = 'resources/';
+ const registration =
+ await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // This will happen after all other tests
+ promise_test(t => {
+ return registration.unregister();
+ }, 'restore global state');
+}, 'global setup');
+
+// Test that the service worker can read FetchEvent#body when it is made from
+// a ReadableStream. It responds with request body it read.
+promise_test(async t => {
+ const body = createBody(t);
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ const response = await frame.contentWindow.fetch('simple.html?request-body', {
+ method, body, duplex});
+ assert_equals(response.status, 200, 'status');
+ const text = await response.text();
+ assert_equals(text, 'i am the request body', 'body');
+}, 'The streaming request body is readable in the service worker.');
+
+// Network fallback
+promise_test(async t => {
+ const body = createBody(t);
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?ignore" so that the service worker falls back to
+ // echo-content.h2.py.
+ const echo_url = '/fetch/api/resources/echo-content.h2.py?ignore';
+ const response =
+ await frame.contentWindow.fetch(echo_url, { method, body, duplex});
+ assert_equals(response.status, 200, 'status');
+ const text = await response.text();
+ assert_equals(text, 'i am the request body', 'body');
+}, 'Network fallback for streaming upload.');
+
+// When the streaming body is used in the service worker, network fallback
+// fails.
+promise_test(async t => {
+ const body = createBody(t);
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ const echo_url = '/fetch/api/resources/echo-content.h2.py?use-and-ignore';
+ const w = frame.contentWindow;
+ await promise_rejects_js(t, w.TypeError, w.fetch(echo_url, {
+ method, body, duplex}));
+}, 'When the streaming request body is used, network fallback fails.');
+
+// When the streaming body is used by clone() in the service worker, network
+// fallback succeeds.
+promise_test(async t => {
+ const body = createBody(t);
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?clone-and-ignore" so that the service worker falls back to
+ // echo-content.h2.py.
+ const echo_url = '/fetch/api/resources/echo-content.h2.py?clone-and-ignore';
+ const response = await frame.contentWindow.fetch(echo_url, {
+ method, body, duplex});
+ assert_equals(response.status, 200, 'status');
+ const text = await response.text();
+ assert_equals(text, 'i am the request body', 'body');
+}, 'Running clone() in the service worker does not prevent network fallback.');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-event.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-event.https.html
new file mode 100644
index 0000000000..ce53f3c9bf
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event.https.html
@@ -0,0 +1,1000 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker = 'resources/fetch-event-test-worker.js';
+function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+}
+
+promise_test(async t => {
+ const scope = 'resources/';
+ const registration =
+ await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // This will happen after all other tests
+ promise_test(t => {
+ return registration.unregister();
+ }, 'restore global state');
+ }, 'global setup');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?headers';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ const headers = JSON.parse(frame.contentDocument.body.textContent);
+ const header_names = {};
+ for (const [name, value] of headers) {
+ header_names[name] = true;
+ }
+
+ assert_true(
+ header_names.hasOwnProperty('accept'),
+ 'request includes "Accept" header as inserted by Fetch'
+ );
+ });
+ }, 'Service Worker headers in the request of a fetch event');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?string';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Test string',
+ 'Service Worker should respond to fetch with a test string');
+ assert_equals(
+ frame.contentDocument.contentType,
+ 'text/plain',
+ 'The content type of the response created with a string should be text/plain');
+ assert_equals(
+ frame.contentDocument.characterSet,
+ 'UTF-8',
+ 'The character set of the response created with a string should be UTF-8');
+ });
+ }, 'Service Worker responds to fetch event with string');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?string';
+ var frame;
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.fetch(page_url + "#foo")
+ })
+ .then(function(response) { return response.text() })
+ .then(function(text) {
+ assert_equals(
+ text,
+ 'Test string',
+ 'Service Worker should respond to fetch with a test string');
+ });
+ }, 'Service Worker responds to fetch event using request fragment with string');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?blob';
+ return with_iframe(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Test blob',
+ 'Service Worker should respond to fetch with a test string');
+ });
+ }, 'Service Worker responds to fetch event with blob body');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?referrer';
+ return with_iframe(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Referrer: ' + document.location.href,
+ 'Service Worker should respond to fetch with the referrer URL');
+ });
+ }, 'Service Worker responds to fetch event with the referrer URL');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?clientId';
+ var frame;
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Client ID Not Found',
+ 'Service Worker should respond to fetch with a client id');
+ return frame.contentWindow.fetch('resources/other.html?clientId');
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ assert_equals(
+ response_text.substr(0, 15),
+ 'Client ID Found',
+ 'Service Worker should respond to fetch with an existing client id');
+ });
+ }, 'Service Worker responds to fetch event with an existing client id');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?resultingClientId';
+ const expected_found = 'Resulting Client ID Found';
+ const expected_not_found = 'Resulting Client ID Not Found';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent.substr(0, expected_found.length),
+ expected_found,
+ 'Service Worker should respond with an existing resulting client id for non-subresource requests');
+ return frame.contentWindow.fetch('resources/other.html?resultingClientId');
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ assert_equals(
+ response_text.substr(0),
+ expected_not_found,
+ 'Service Worker should respond with an empty resulting client id for subresource requests');
+ });
+ }, 'Service Worker responds to fetch event with the correct resulting client id');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?ignore';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'Here\'s a simple html file.\n',
+ 'Response should come from fallback to native fetch');
+ });
+ }, 'Service Worker does not respond to fetch event');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?null';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ '',
+ 'Response should be the empty string');
+ });
+ }, 'Service Worker responds to fetch event with null response body');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?fetch';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'Here\'s an other html file.\n',
+ 'Response should come from fetched other file');
+ });
+ }, 'Service Worker fetches other file in fetch event');
+
+// Creates a form and an iframe and does a form submission that navigates the
+// frame to |action_url|. Returns the frame after navigation.
+function submit_form(action_url) {
+ return new Promise(resolve => {
+ const frame = document.createElement('iframe');
+ frame.name = 'post-frame';
+ document.body.appendChild(frame);
+ const form = document.createElement('form');
+ form.target = frame.name;
+ form.action = action_url;
+ form.method = 'post';
+ const input1 = document.createElement('input');
+ input1.type = 'text';
+ input1.value = 'testValue1';
+ input1.name = 'testName1'
+ form.appendChild(input1);
+ const input2 = document.createElement('input');
+ input2.type = 'text';
+ input2.value = 'testValue2';
+ input2.name = 'testName2'
+ form.appendChild(input2);
+ document.body.appendChild(form);
+ frame.onload = function() {
+ form.remove();
+ resolve(frame);
+ };
+ form.submit();
+ });
+}
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?form-post';
+ return submit_form(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'POST:application/x-www-form-urlencoded:' +
+ 'testName1=testValue1&testName2=testValue2');
+ });
+ }, 'Service Worker responds to fetch event with POST form');
+
+promise_test(t => {
+ // Add '?ignore' so the service worker falls back to network.
+ const page_url = 'resources/echo-content.py?ignore';
+ return submit_form(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'testName1=testValue1&testName2=testValue2');
+ });
+ }, 'Service Worker falls back to network in fetch event with POST form');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?multiple-respond-with';
+ return with_iframe(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ '(0)(1)[InvalidStateError](2)[InvalidStateError]',
+ 'Multiple calls of respondWith must throw InvalidStateErrors.');
+ });
+ }, 'Multiple calls of respondWith must throw InvalidStateErrors');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?used-check';
+ var first_frame;
+ return with_iframe(page_url)
+ .then(function(frame) {
+ assert_equals(frame.contentDocument.body.textContent,
+ 'Here\'s an other html file.\n',
+ 'Response should come from fetched other file');
+ first_frame = frame;
+ t.add_cleanup(() => { first_frame.remove(); });
+ return with_iframe(page_url);
+ })
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ // When we access to the page_url in the second time, the content of the
+ // response is generated inside the ServiceWorker. The body contains
+ // the value of bodyUsed of the first response which is already
+ // consumed by FetchEvent.respondWith method.
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'bodyUsed: true',
+ 'event.respondWith must set the used flag.');
+ });
+ }, 'Service Worker event.respondWith must set the used flag');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?fragment-check';
+ var fragment = '#/some/fragment';
+ var first_frame;
+ return with_iframe(page_url + fragment)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Fragment Found :' + fragment,
+ 'Service worker should expose URL fragments in request.');
+ });
+ }, 'Service Worker should expose FetchEvent URL fragments.');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?cache';
+ var frame;
+ var cacheTypes = [
+ undefined, 'default', 'no-store', 'reload', 'no-cache', 'force-cache', 'only-if-cached'
+ ];
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentWindow.document.body.textContent, 'default');
+ var tests = cacheTypes.map(function(type) {
+ return new Promise(function(resolve, reject) {
+ var init = {cache: type};
+ if (type === 'only-if-cached') {
+ // For privacy reasons, for the time being, only-if-cached
+ // requires the mode to be same-origin.
+ init.mode = 'same-origin';
+ }
+ return frame.contentWindow.fetch(page_url + '=' + type, init)
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ var expected = (type === undefined) ? 'default' : type;
+ assert_equals(response_text, expected,
+ 'Service Worker should respond to fetch with the correct type');
+ })
+ .then(resolve)
+ .catch(reject);
+ });
+ });
+ return Promise.all(tests);
+ })
+ .then(function() {
+ return new Promise(function(resolve, reject) {
+ frame.addEventListener('load', function onLoad() {
+ frame.removeEventListener('load', onLoad);
+ try {
+ assert_equals(frame.contentWindow.document.body.textContent,
+ 'no-cache');
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ frame.contentWindow.location.reload();
+ });
+ });
+ }, 'Service Worker responds to fetch event with the correct cache types');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?eventsource';
+ var frame;
+
+ function test_eventsource(opts) {
+ return new Promise(function(resolve, reject) {
+ var eventSource = new frame.contentWindow.EventSource(page_url, opts);
+ eventSource.addEventListener('message', function(msg) {
+ eventSource.close();
+ try {
+ var data = JSON.parse(msg.data);
+ assert_equals(data.mode, 'cors',
+ 'EventSource should make CORS requests.');
+ assert_equals(data.cache, 'no-store',
+ 'EventSource should bypass the http cache.');
+ var expectedCredentials = opts.withCredentials ? 'include'
+ : 'same-origin';
+ assert_equals(data.credentials, expectedCredentials,
+ 'EventSource should pass correct credentials mode.');
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ eventSource.addEventListener('error', function(e) {
+ eventSource.close();
+ reject('The EventSource fired an error event.');
+ });
+ });
+ }
+
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return test_eventsource({ withCredentials: false });
+ })
+ .then(function() {
+ return test_eventsource({ withCredentials: true });
+ });
+ }, 'Service Worker should intercept EventSource');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?integrity';
+ var frame;
+ var integrity_metadata = 'gs0nqru8KbsrIt5YToQqS9fYao4GQJXtcId610g7cCU=';
+
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ // A request has associated integrity metadata (a string).
+ // Unless stated otherwise, it is the empty string.
+ assert_equals(
+ frame.contentDocument.body.textContent, '');
+
+ return frame.contentWindow.fetch(page_url, {'integrity': integrity_metadata});
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(response_text, integrity_metadata, 'integrity');
+ });
+ }, 'Service Worker responds to fetch event with the correct integrity_metadata');
+
+// Test that the service worker can read FetchEvent#body when it is a string.
+// It responds with request body it read.
+promise_test(t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/simple.html?ignore-for-request-body-string';
+ let frame;
+
+ return with_iframe(page_url)
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.fetch('simple.html?request-body', {
+ method: 'POST',
+ body: 'i am the request body'
+ });
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(response_text, 'i am the request body');
+ });
+ }, 'FetchEvent#body is a string');
+
+// Test that the service worker can read FetchEvent#body when it is made from
+// a ReadableStream. It responds with request body it read.
+promise_test(async t => {
+ const rs = new ReadableStream({start(c) {
+ c.enqueue('i a');
+ c.enqueue('m the request');
+ step_timeout(t.step_func(() => {
+ c.enqueue(' body');
+ c.close();
+ }, 10));
+ }});
+
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ const res = await frame.contentWindow.fetch('simple.html?request-body', {
+ method: 'POST',
+ body: rs.pipeThrough(new TextEncoderStream()),
+ duplex: 'half',
+ });
+ assert_equals(await res.text(), 'i am the request body');
+ }, 'FetchEvent#body is a ReadableStream');
+
+// Test that the request body is sent to network upon network fallback,
+// for a string body.
+promise_test(t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+ let frame;
+
+ return with_iframe(page_url)
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?ignore" so the service worker falls back to echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?ignore';
+ return frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: 'i am the request body'
+ });
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(
+ response_text,
+ 'i am the request body',
+ 'the network fallback request should include the request body');
+ });
+ }, 'FetchEvent#body is a string and is passed to network fallback');
+
+// Test that the request body is sent to network upon network fallback,
+// for a ReadableStream body.
+promise_test(async t => {
+ const rs = new ReadableStream({start(c) {
+ c.enqueue('i a');
+ c.enqueue('m the request');
+ t.step_timeout(t.step_func(() => {
+ c.enqueue(' body');
+ c.close();
+ }, 10));
+ }});
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?ignore" so the service worker falls back to echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?ignore';
+ const w = frame.contentWindow;
+ await promise_rejects_js(t, w.TypeError, w.fetch(echo_url, {
+ method: 'POST',
+ body: rs
+ }));
+ }, 'FetchEvent#body is a none Uint8Array ReadableStream and is passed to a service worker');
+
+// Test that the request body is sent to network upon network fallback even when
+// the request body is used in the service worker, for a string body.
+promise_test(async t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?use-and-ignore" so the service worker falls back to echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?use-and-ignore';
+ const response = await frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: 'i am the request body'
+ });
+ const text = await response.text();
+ assert_equals(
+ text,
+ 'i am the request body',
+ 'the network fallback request should include the request body');
+ }, 'FetchEvent#body is a string, used and passed to network fallback');
+
+// Test that the request body is sent to network upon network fallback even when
+// the request body is used by clone() in the service worker, for a string body.
+promise_test(async t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?clone-and-ignore" so the service worker falls back to
+ // echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?clone-and-ignore';
+ const response = await frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: 'i am the request body'
+ });
+ const text = await response.text();
+ assert_equals(
+ text,
+ 'i am the request body',
+ 'the network fallback request should include the request body');
+ }, 'FetchEvent#body is a string, cloned and passed to network fallback');
+
+// Test that the service worker can read FetchEvent#body when it is a blob.
+// It responds with request body it read.
+promise_test(t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/simple.html?ignore-for-request-body-blob';
+ let frame;
+
+ return with_iframe(page_url)
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ const blob = new Blob(['it\'s me the blob', ' ', 'and more blob!']);
+ return frame.contentWindow.fetch('simple.html?request-body', {
+ method: 'POST',
+ body: blob
+ });
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(response_text, 'it\'s me the blob and more blob!');
+ });
+ }, 'FetchEvent#body is a blob');
+
+// Test that the request body is sent to network upon network fallback,
+// for a blob body.
+promise_test(t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/simple.html?ignore-for-request-body-fallback-blob';
+ let frame;
+
+ return with_iframe(page_url)
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ const blob = new Blob(['it\'s me the blob', ' ', 'and more blob!']);
+ // Add "?ignore" so the service worker falls back to echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?ignore';
+ return frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: blob
+ });
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(
+ response_text,
+ 'it\'s me the blob and more blob!',
+ 'the network fallback request should include the request body');
+ });
+ }, 'FetchEvent#body is a blob and is passed to network fallback');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?keepalive';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, 'false');
+ const response = await frame.contentWindow.fetch(page_url, {keepalive: true});
+ const text = await response.text();
+ assert_equals(text, 'true');
+ }, 'Service Worker responds to fetch event with the correct keepalive value');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isReloadNavigation';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = true');
+ }, 'FetchEvent#request.isReloadNavigation is true (location.reload())');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isReloadNavigation';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(0);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = true');
+ }, 'FetchEvent#request.isReloadNavigation is true (history.go(0))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isReloadNavigation';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ const form = frame.contentDocument.createElement('form');
+ form.method = 'POST';
+ form.name = 'form';
+ form.action = new Request(page_url).url;
+ frame.contentDocument.body.appendChild(form);
+ form.submit();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = POST, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = POST, isReloadNavigation = true');
+ }, 'FetchEvent#request.isReloadNavigation is true (POST + location.reload())');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isReloadNavigation';
+ const anotherUrl = new Request('resources/simple.html').url;
+ let frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = anotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(0);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = true');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(1);
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ }, 'FetchEvent#request.isReloadNavigation is true (with history traversal)');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = anotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(-1))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(anotherUrl);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = page_url;
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(1))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(anotherUrl);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = page_url;
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(0);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ }, 'FetchEvent#request.isHistoryNavigation is false (with history.go(0))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(anotherUrl);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = page_url;
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ }, 'FetchEvent#request.isHistoryNavigation is false (with location.reload)');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const oneAnotherUrl = new Request('resources/simple.html?ignore2').url;
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = anotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = oneAnotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-2);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(-2))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const oneAnotherUrl = new Request('resources/simple.html?ignore2').url;
+ const frame = await with_iframe(anotherUrl);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = oneAnotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = page_url;
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-2);
+ });
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(2);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(2))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ const form = frame.contentDocument.createElement('form');
+ form.method = 'POST';
+ form.name = 'form';
+ form.action = new Request(page_url).url;
+ frame.contentDocument.body.appendChild(form);
+ form.submit();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = POST, isHistoryNavigation = false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = anotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = POST, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (POST + history.go(-1))');
+
+// When service worker responds with a Response, no XHR upload progress
+// events are delivered.
+promise_test(async t => {
+ const page_url = 'resources/simple.html?ignore-for-request-body-string';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open('POST', 'simple.html?request-body');
+ xhr.upload.addEventListener('progress', t.unreached_func('progress'));
+ xhr.upload.addEventListener('error', t.unreached_func('error'));
+ xhr.upload.addEventListener('abort', t.unreached_func('abort'));
+ xhr.upload.addEventListener('timeout', t.unreached_func('timeout'));
+ xhr.upload.addEventListener('load', t.unreached_func('load'));
+ xhr.upload.addEventListener('loadend', t.unreached_func('loadend'));
+ xhr.send('i am the request body');
+
+ await new Promise((resolve) => xhr.addEventListener('load', resolve));
+ }, 'XHR upload progress events for response coming from SW');
+
+// Upload progress events should be delivered for the network fallback case.
+promise_test(async t => {
+ const page_url = 'resources/simple.html?ignore-for-request-body-string';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+
+ let progress = false;
+ let load = false;
+ let loadend = false;
+
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open('POST', '/fetch/api/resources/echo-content.py?ignore');
+ xhr.upload.addEventListener('progress', () => progress = true);
+ xhr.upload.addEventListener('error', t.unreached_func('error'));
+ xhr.upload.addEventListener('abort', t.unreached_func('abort'));
+ xhr.upload.addEventListener('timeout', t.unreached_func('timeout'));
+ xhr.upload.addEventListener('load', () => load = true);
+ xhr.upload.addEventListener('loadend', () => loadend = true);
+ xhr.send('i am the request body');
+
+ await new Promise((resolve) => xhr.addEventListener('load', resolve));
+ assert_true(progress, 'progress');
+ assert_true(load, 'load');
+ assert_true(loadend, 'loadend');
+ }, 'XHR upload progress events for network fallback');
+
+promise_test(async t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?clone-and-ignore" so the service worker falls back to
+ // echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?status=421';
+ const response = await frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: 'text body'
+ });
+ assert_equals(response.status, 421);
+ const text = await response.text();
+ assert_equals(
+ text,
+ 'text body. Request was sent 1 times.',
+ 'the network fallback request should include the request body');
+ }, 'Fetch with POST with text on sw 421 response should not be retried.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-frame-resource.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-frame-resource.https.html
new file mode 100644
index 0000000000..a33309f34f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-frame-resource.https.html
@@ -0,0 +1,236 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch for the frame loading.</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker = 'resources/fetch-rewrite-worker.js';
+var path = base_path() + 'resources/fetch-access-control.py';
+var host_info = get_host_info();
+
+function getLoadedObject(win, contentFunc, closeFunc) {
+ return new Promise(function(resolve) {
+ function done(contentString) {
+ var result = null;
+ // fetch-access-control.py returns a string like "report( <json> )".
+ // Eval the returned string with a report functionto get the json
+ // object.
+ try {
+ function report(obj) { result = obj };
+ eval(contentString);
+ } catch(e) {
+ // just resolve null if we get unexpected page content
+ }
+ closeFunc(win);
+ resolve(result);
+ }
+
+ // We can't catch the network error on window. So we use the timer.
+ var timeout = setTimeout(function() {
+ // Failure pages are considered cross-origin in some browsers. This
+ // means you cannot even .resolve() the window because the check for
+ // the .then property will throw. Instead, treat cross-origin
+ // failure pages as the empty string which will fail to parse as the
+ // expected json result.
+ var content = '';
+ try {
+ content = contentFunc(win);
+ } catch(e) {
+ // use default empty string for cross-domain window
+ }
+ done(content);
+ }, 10000);
+
+ win.onload = function() {
+ clearTimeout(timeout);
+ let content = '';
+ try {
+ content = contentFunc(win);
+ } catch(e) {
+ // use default empty string for cross-domain window (see above)
+ }
+ done(content);
+ };
+ });
+}
+
+function getLoadedFrameAsObject(frame) {
+ return getLoadedObject(frame, function(f) {
+ return f.contentDocument.body.textContent;
+ }, function(f) {
+ f.parentNode.removeChild(f);
+ });
+}
+
+function getLoadedWindowAsObject(win) {
+ return getLoadedObject(win, function(w) {
+ return w.document.body.textContent
+ }, function(w) {
+ w.close();
+ });
+}
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/frame-basic';
+ var frame;
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ frame = document.createElement('iframe');
+ frame.src =
+ scope + '?url=' +
+ encodeURIComponent(host_info['HTTPS_ORIGIN'] + path);
+ document.body.appendChild(frame);
+ return getLoadedFrameAsObject(frame);
+ })
+ .then(function(result) {
+ assert_equals(
+ result.jsonpResult,
+ 'success',
+ 'Basic type response could be loaded in the iframe.');
+ frame.remove();
+ });
+ }, 'Basic type response could be loaded in the iframe.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/frame-cors';
+ var frame;
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ frame = document.createElement('iframe');
+ frame.src =
+ scope + '?mode=cors&url=' +
+ encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path +
+ '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true');
+ document.body.appendChild(frame);
+ return getLoadedFrameAsObject(frame);
+ })
+ .then(function(result) {
+ assert_equals(
+ result.jsonpResult,
+ 'success',
+ 'CORS type response could be loaded in the iframe.');
+ frame.remove();
+ });
+ }, 'CORS type response could be loaded in the iframe.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/frame-opaque';
+ var frame;
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ frame = document.createElement('iframe');
+ frame.src =
+ scope + '?mode=no-cors&url=' +
+ encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path);
+ document.body.appendChild(frame);
+ return getLoadedFrameAsObject(frame);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ null,
+ 'Opaque type response could not be loaded in the iframe.');
+ frame.remove();
+ });
+ }, 'Opaque type response could not be loaded in the iframe.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/window-basic';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ var win = window.open(
+ scope + '?url=' +
+ encodeURIComponent(host_info['HTTPS_ORIGIN'] + path));
+ return getLoadedWindowAsObject(win);
+ })
+ .then(function(result) {
+ assert_equals(
+ result.jsonpResult,
+ 'success',
+ 'Basic type response could be loaded in the new window.');
+ });
+ }, 'Basic type response could be loaded in the new window.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/window-cors';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ var win = window.open(
+ scope + '?mode=cors&url=' +
+ encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path +
+ '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true'));
+ return getLoadedWindowAsObject(win);
+ })
+ .then(function(result) {
+ assert_equals(
+ result.jsonpResult,
+ 'success',
+ 'CORS type response could be loaded in the new window.');
+ });
+ }, 'CORS type response could be loaded in the new window.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/window-opaque';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ var win = window.open(
+ scope + '?mode=no-cors&url=' +
+ encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path));
+ return getLoadedWindowAsObject(win);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ null,
+ 'Opaque type response could not be loaded in the new window.');
+ });
+ }, 'Opaque type response could not be loaded in the new window.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-header-visibility.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-header-visibility.https.html
new file mode 100644
index 0000000000..1f4813c4f8
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-header-visibility.https.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<title>Service Worker: Visibility of headers during fetch.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+ var worker = 'resources/fetch-rewrite-worker.js';
+ var path = base_path() + 'resources/fetch-access-control.py';
+ var host_info = get_host_info();
+ var frame;
+
+ promise_test(function(t) {
+ var scope = 'resources/fetch-header-visibility-iframe.html';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ frame = document.createElement('iframe');
+ frame.src = scope;
+ document.body.appendChild(frame);
+
+ // Resolve a promise when we recieve 2 success messages
+ return new Promise(function(resolve, reject) {
+ var remaining = 4;
+ function onMessage(e) {
+ if (e.data == 'PASS') {
+ remaining--;
+ if (remaining == 0) {
+ resolve();
+ } else {
+ return;
+ }
+ } else {
+ reject(e.data);
+ }
+
+ window.removeEventListener('message', onMessage);
+ }
+ window.addEventListener('message', onMessage);
+ });
+ })
+ .then(function(result) {
+ frame.remove();
+ });
+ }, 'Visibility of defaulted headers during interception');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html
new file mode 100644
index 0000000000..0e8fa93b32
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>Service Worker: Mixed content of fetch()</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body></body>
+<script>
+async_test(function(t) {
+ var host_info = get_host_info();
+ window.addEventListener('message', t.step_func(on_message), false);
+ with_iframe(
+ host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/fetch-mixed-content-iframe.html?target=inscope');
+ function on_message(e) {
+ assert_equals(e.data.results, 'finish');
+ t.done();
+ }
+ }, 'Verify Mixed content of fetch() in a Service Worker');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html
new file mode 100644
index 0000000000..391dc5d2c1
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>Service Worker: Mixed content of fetch()</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body></body>
+<script>
+async_test(function(t) {
+ var host_info = get_host_info();
+ window.addEventListener('message', t.step_func(on_message), false);
+ with_iframe(
+ host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/fetch-mixed-content-iframe.html?target=outscope');
+ function on_message(e) {
+ assert_equals(e.data.results, 'finish');
+ t.done();
+ }
+ }, 'Verify Mixed content of fetch() in a Service Worker');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-base-url.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-base-url.https.html
new file mode 100644
index 0000000000..467a66cee4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-base-url.https.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<title>Service Worker: CSS's base URL must be the response URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+const SCOPE = 'resources/fetch-request-css-base-url-iframe.html';
+const SCRIPT = 'resources/fetch-request-css-base-url-worker.js';
+let worker;
+
+var signalMessage;
+function getNextMessage() {
+ return new Promise(resolve => { signalMessage = resolve; });
+}
+
+promise_test(async (t) => {
+ const registration = await service_worker_unregister_and_register(
+ t, SCRIPT, SCOPE);
+ worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+}, 'global setup');
+
+// Creates a test concerning the base URL of a stylesheet. It loads a
+// stylesheet from a controlled page. The stylesheet makes a subresource
+// request for an image. The service worker messages back the details of the
+// image request in order to test the base URL.
+//
+// The request URL for the stylesheet is under "resources/request-url-path/".
+// The service worker may respond in a way such that the response URL is
+// different to the request URL.
+function base_url_test(params) {
+ promise_test(async (t) => {
+ let frame;
+ t.add_cleanup(() => {
+ if (frame)
+ frame.remove();
+ });
+
+ // Ask the service worker to message this page once it gets the request
+ // for the image.
+ let channel = new MessageChannel();
+ const sawPong = getNextMessage();
+ channel.port1.onmessage = (event) => {
+ signalMessage(event.data);
+ };
+ worker.postMessage({port:channel.port2},[channel.port2]);
+
+ // It sends a pong back immediately. This ping/pong protocol helps deflake
+ // the test for browsers where message/fetch ordering isn't guaranteed.
+ assert_equals('pong', await sawPong);
+
+ // Load the frame which will load the stylesheet that makes the image
+ // request.
+ const sawResult = getNextMessage();
+ frame = await with_iframe(params.framePath);
+ const result = await sawResult;
+
+ // Test the image request.
+ const base = new URL('.', document.location).href;
+ assert_equals(result.url,
+ base + params.expectImageRequestPath,
+ 'request');
+ assert_equals(result.referrer,
+ base + params.expectImageRequestReferrer,
+ 'referrer');
+ }, params.description);
+}
+
+const cssFile = 'fetch-request-css-base-url-style.css';
+
+base_url_test({
+ framePath: SCOPE + '?fetch',
+ expectImageRequestPath: 'resources/sample.png',
+ expectImageRequestReferrer: `resources/${cssFile}?fetch`,
+ description: 'base URL when service worker does respondWith(fetch(responseUrl)).'});
+
+base_url_test({
+ framePath: SCOPE + '?newResponse',
+ expectImageRequestPath: 'resources/request-url-path/sample.png',
+ expectImageRequestReferrer: `resources/request-url-path/${cssFile}?newResponse`,
+ description: 'base URL when service worker does respondWith(new Response()).'});
+
+// Cleanup step: this must be the last promise_test.
+promise_test(async (t) => {
+ return service_worker_unregister(t, SCOPE);
+}, 'cleanup global state');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html
new file mode 100644
index 0000000000..d9c1c7f5df
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<title>Service Worker: Cross-origin CSS files fetched via SW.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+function getElementColorInFrame(frame, id) {
+ var element = frame.contentDocument.getElementById(id);
+ var style = frame.contentWindow.getComputedStyle(element, '');
+ return style['color'];
+}
+
+promise_test(async t => {
+ var SCOPE =
+ 'resources/fetch-request-css-cross-origin';
+ var SCRIPT =
+ 'resources/fetch-request-css-cross-origin-worker.js';
+ let registration = await service_worker_unregister_and_register(
+ t, SCRIPT, SCOPE);
+ promise_test(async t => {
+ await registration.unregister();
+ }, 'cleanup global state');
+
+ await wait_for_state(t, registration.installing, 'activated');
+}, 'setup global state');
+
+promise_test(async t => {
+ const EXPECTED_COLOR = 'rgb(0, 0, 255)';
+ const PAGE =
+ 'resources/fetch-request-css-cross-origin-mime-check-iframe.html';
+
+ const f = await with_iframe(PAGE);
+ t.add_cleanup(() => {f.remove(); });
+ assert_equals(
+ getElementColorInFrame(f, 'crossOriginCss'),
+ EXPECTED_COLOR,
+ 'The color must be overridden by cross origin CSS.');
+ assert_equals(
+ getElementColorInFrame(f, 'crossOriginHtml'),
+ EXPECTED_COLOR,
+ 'The color must not be overridden by cross origin non CSS file.');
+ assert_equals(
+ getElementColorInFrame(f, 'sameOriginCss'),
+ EXPECTED_COLOR,
+ 'The color must be overridden by same origin CSS.');
+ assert_equals(
+ getElementColorInFrame(f, 'sameOriginHtml'),
+ EXPECTED_COLOR,
+ 'The color must be overridden by same origin non CSS file.');
+ assert_equals(
+ getElementColorInFrame(f, 'synthetic'),
+ EXPECTED_COLOR,
+ 'The color must be overridden by synthetic CSS.');
+}, 'MIME checking of CSS resources fetched via service worker when Content-Type is not set.');
+
+promise_test(async t => {
+ const PAGE =
+ 'resources/fetch-request-css-cross-origin-read-contents.html';
+
+ const f = await with_iframe(PAGE);
+ t.add_cleanup(() => {f.remove(); });
+ assert_throws_dom('SecurityError', f.contentWindow.DOMException, () => {
+ f.contentDocument.styleSheets[0].cssRules[0].cssText;
+ });
+ assert_equals(
+ f.contentDocument.styleSheets[1].cssRules[0].cssText,
+ '#crossOriginCss { color: blue; }',
+ 'cross-origin CORS approved response');
+ assert_equals(
+ f.contentDocument.styleSheets[2].cssRules[0].cssText,
+ '#sameOriginCss { color: blue; }',
+ 'same-origin response');
+ assert_equals(
+ f.contentDocument.styleSheets[3].cssRules[0].cssText,
+ '#synthetic { color: blue; }',
+ 'service worker generated response');
+ }, 'Same-origin policy for access to CSS resources fetched via service worker');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-images.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-images.https.html
new file mode 100644
index 0000000000..586dea2613
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-css-images.https.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent for css image</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+var SCOPE = 'resources/fetch-request-resources-iframe.https.html';
+var SCRIPT = 'resources/fetch-request-resources-worker.js';
+var host_info = get_host_info();
+var LOCAL_URL =
+ host_info['HTTPS_ORIGIN'] + base_path() + 'resources/sample?test';
+var REMOTE_URL =
+ host_info['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/sample?test';
+
+function css_image_test(expected_results, frame, url, type,
+ expected_mode, expected_credentials) {
+ expected_results[url] = {
+ url: url,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ message: 'CSSImage load (url:' + url + ' type:' + type + ')'
+ };
+ return frame.contentWindow.load_css_image(url, type);
+}
+
+function css_image_set_test(expected_results, frame, url, type,
+ expected_mode, expected_credentials) {
+ expected_results[url] = {
+ url: url,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ message: 'CSSImageSet load (url:' + url + ' type:' + type + ')'
+ };
+ return frame.contentWindow.load_css_image_set(url, type);
+}
+
+function waitForWorker(worker) {
+ return new Promise(function(resolve) {
+ var channel = new MessageChannel();
+ channel.port1.addEventListener('message', function(msg) {
+ if (msg.data.ready) {
+ resolve(channel);
+ }
+ });
+ channel.port1.start();
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+}
+
+function create_message_promise(channel, expected_results, worker, scope) {
+ return new Promise(function(resolve) {
+ channel.port1.addEventListener('message', function(msg) {
+ var result = msg.data;
+ if (!expected_results[result.url]) {
+ return;
+ }
+ resolve(result);
+ });
+ }).then(function(result) {
+ var expected = expected_results[result.url];
+ assert_equals(
+ result.mode, expected.mode,
+ 'mode of ' + expected.message + ' must be ' +
+ expected.mode + '.');
+ assert_equals(
+ result.credentials, expected.credentials,
+ 'credentials of ' + expected.message + ' must be ' +
+ expected.credentials + '.');
+ delete expected_results[result.url];
+ });
+}
+
+promise_test(function(t) {
+ var scope = SCOPE + "?img=backgroundImage";
+ var expected_results = {};
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame = f;
+ return waitForWorker(worker);
+ })
+ .then(function(channel) {
+ css_image_test(expected_results, frame, LOCAL_URL + Date.now(),
+ 'backgroundImage', 'no-cors', 'include');
+ css_image_test(expected_results, frame, REMOTE_URL + Date.now(),
+ 'backgroundImage', 'no-cors', 'include');
+
+ return Promise.all([
+ create_message_promise(channel, expected_results, worker, scope),
+ create_message_promise(channel, expected_results, worker, scope)
+ ]);
+ });
+ }, 'Verify FetchEvent for css image (backgroundImage).');
+
+promise_test(function(t) {
+ var scope = SCOPE + "?img=shapeOutside";
+ var expected_results = {};
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame = f;
+ return waitForWorker(worker);
+ })
+ .then(function(channel) {
+ css_image_test(expected_results, frame, LOCAL_URL + Date.now(),
+ 'shapeOutside', 'cors', 'same-origin');
+ css_image_test(expected_results, frame, REMOTE_URL + Date.now(),
+ 'shapeOutside', 'cors', 'same-origin');
+
+ return Promise.all([
+ create_message_promise(channel, expected_results, worker, scope),
+ create_message_promise(channel, expected_results, worker, scope)
+ ]);
+ });
+ }, 'Verify FetchEvent for css image (shapeOutside).');
+
+promise_test(function(t) {
+ var scope = SCOPE + "?img_set=backgroundImage";
+ var expected_results = {};
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();;
+ });
+ frame = f;
+ return waitForWorker(worker);
+ })
+ .then(function(channel) {
+ css_image_set_test(expected_results, frame, LOCAL_URL + Date.now(),
+ 'backgroundImage', 'no-cors', 'include');
+ css_image_set_test(expected_results, frame, REMOTE_URL + Date.now(),
+ 'backgroundImage', 'no-cors', 'include');
+
+ return Promise.all([
+ create_message_promise(channel, expected_results, worker, scope),
+ create_message_promise(channel, expected_results, worker, scope)
+ ]);
+ });
+ }, 'Verify FetchEvent for css image-set (backgroundImage).');
+
+promise_test(function(t) {
+ var scope = SCOPE + "?img_set=shapeOutside";
+ var expected_results = {};
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame = f;
+ return waitForWorker(worker);
+ })
+ .then(function(channel) {
+ css_image_set_test(expected_results, frame, LOCAL_URL + Date.now(),
+ 'shapeOutside', 'cors', 'same-origin');
+ css_image_set_test(expected_results, frame, REMOTE_URL + Date.now(),
+ 'shapeOutside', 'cors', 'same-origin');
+
+ return Promise.all([
+ create_message_promise(channel, expected_results, worker, scope),
+ create_message_promise(channel, expected_results, worker, scope)
+ ]);
+ });
+ }, 'Verify FetchEvent for css image-set (shapeOutside).');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-fallback.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-fallback.https.html
new file mode 100644
index 0000000000..a29f31d127
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-fallback.https.html
@@ -0,0 +1,282 @@
+<!DOCTYPE html>
+<title>Service Worker: the fallback behavior of FetchEvent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function get_fetched_urls(worker) {
+ return new Promise(function(resolve) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(msg) { resolve(msg); };
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+}
+
+function check_urls(worker, expected_requests) {
+ return get_fetched_urls(worker)
+ .then(function(msg) {
+ var requests = msg.data.requests;
+ assert_object_equals(requests, expected_requests);
+ });
+}
+
+var path = new URL(".", window.location).pathname;
+var SCOPE = 'resources/fetch-request-fallback-iframe.html';
+var SCRIPT = 'resources/fetch-request-fallback-worker.js';
+var host_info = get_host_info();
+var BASE_URL = host_info['HTTPS_ORIGIN'] +
+ path + 'resources/fetch-access-control.py?';
+var BASE_PNG_URL = BASE_URL + 'PNGIMAGE&';
+var OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] +
+ path + 'resources/fetch-access-control.py?';
+var OTHER_BASE_PNG_URL = OTHER_BASE_URL + 'PNGIMAGE&';
+var REDIRECT_URL = host_info['HTTPS_ORIGIN'] +
+ path + 'resources/redirect.py?Redirect=';
+var register;
+
+promise_test(function(t) {
+ var registration;
+ var worker;
+
+ register = service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(r) {
+ registration = r;
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ // This test should not be considered complete until after the service
+ // worker has been unregistered. Currently, `testharness.js` does not
+ // support asynchronous global "tear down" logic, so this must be
+ // expressed using a dedicated `promise_test`. Because the other
+ // sub-tests in this file are declared synchronously, this test will be
+ // the final test executed.
+ promise_test(function(t) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+ return registration.unregister();
+ }, 'restore global state');
+
+ return {frame: frame, worker: worker};
+ });
+
+ return register;
+ }, 'initialize global state');
+
+function promise_frame_test(body, desc) {
+ promise_test(function(test) {
+ return register.then(function(result) {
+ return body(test, result.frame, result.worker);
+ });
+ }, desc);
+}
+
+promise_frame_test(function(t, frame, worker) {
+ return check_urls(
+ worker,
+ [{
+ url: host_info['HTTPS_ORIGIN'] + path + SCOPE,
+ mode: 'navigate'
+ }]);
+ }, 'The SW must intercept the request for a main resource.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.xhr(BASE_URL)
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: BASE_URL, mode: 'cors' }]);
+ });
+ }, 'The SW must intercept the request of same origin XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.Error,
+ frame.contentWindow.xhr(OTHER_BASE_URL),
+ 'SW fallbacked CORS-unsupported other origin XHR should fail.')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_URL, mode: 'cors' }]);
+ });
+ }, 'The SW must intercept the request of CORS-unsupported other origin XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.xhr(OTHER_BASE_URL + 'ACAOrigin=*')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_URL + 'ACAOrigin=*', mode: 'cors' }]);
+ })
+ }, 'The SW must intercept the request of CORS-supported other origin XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.xhr(
+ REDIRECT_URL + encodeURIComponent(BASE_URL))
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(BASE_URL),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request of redirected XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.Error,
+ frame.contentWindow.xhr(
+ REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL)),
+ 'SW fallbacked XHR which is redirected to CORS-unsupported ' +
+ 'other origin should fail.')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for XHR which is' +
+ ' redirected to CORS-unsupported other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.xhr(
+ REDIRECT_URL +
+ encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'))
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL +
+ encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for XHR which is ' +
+ 'redirected to CORS-supported other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(BASE_PNG_URL, '')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: BASE_PNG_URL, mode: 'no-cors' }]);
+ });
+ }, 'The SW must intercept the request for image.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(OTHER_BASE_PNG_URL, '')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_PNG_URL, mode: 'no-cors' }]);
+ });
+ }, 'The SW must intercept the request for other origin image.');
+
+promise_frame_test(function(t, frame, worker) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.Error,
+ frame.contentWindow.load_image(OTHER_BASE_PNG_URL, 'anonymous'),
+ 'SW fallbacked CORS-unsupported other origin image request ' +
+ 'should fail.')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_PNG_URL, mode: 'cors' }]);
+ })
+ }, 'The SW must intercept the request for CORS-unsupported other ' +
+ 'origin image.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(
+ OTHER_BASE_PNG_URL + 'ACAOrigin=*', 'anonymous')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_PNG_URL + 'ACAOrigin=*', mode: 'cors' }]);
+ });
+ }, 'The SW must intercept the request for CORS-supported other ' +
+ 'origin image.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(
+ REDIRECT_URL + encodeURIComponent(BASE_PNG_URL), '')
+ .catch(function() {
+ assert_unreached(
+ 'SW fallbacked redirected image request should succeed.');
+ })
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(BASE_PNG_URL),
+ mode: 'no-cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for redirected ' +
+ 'image resource.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(
+ REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL), '')
+ .catch(function() {
+ assert_unreached(
+ 'SW fallbacked image request which is redirected to ' +
+ 'other origin should succeed.');
+ })
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL),
+ mode: 'no-cors'
+ }]);
+ })
+ }, 'The SW must intercept only the first request for image ' +
+ 'resource which is redirected to other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.Error,
+ frame.contentWindow.load_image(
+ REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL),
+ 'anonymous'),
+ 'SW fallbacked image request which is redirected to ' +
+ 'CORS-unsupported other origin should fail.')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for image ' +
+ 'resource which is redirected to CORS-unsupported other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(
+ REDIRECT_URL +
+ encodeURIComponent(OTHER_BASE_PNG_URL + 'ACAOrigin=*'),
+ 'anonymous')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL +
+ encodeURIComponent(OTHER_BASE_PNG_URL + 'ACAOrigin=*'),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for image ' +
+ 'resource which is redirected to CORS-supported other origin.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html
new file mode 100644
index 0000000000..03b7d35761
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<title>Service Worker: the headers of FetchEvent shouldn't contain freshness headers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-request-no-freshness-headers-iframe.html';
+ var SCRIPT = 'resources/fetch-request-no-freshness-headers-worker.js';
+ var worker;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ return new Promise(function(resolve) {
+ frame.onload = function() {
+ resolve(frame);
+ };
+ frame.contentWindow.location.reload();
+ });
+ })
+ .then(function(frame) {
+ return new Promise(function(resolve) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func(function(msg) {
+ frame.remove();
+ resolve(msg);
+ });
+ worker.postMessage(
+ {port: channel.port2}, [channel.port2]);
+ });
+ })
+ .then(function(msg) {
+ var freshness_headers = {
+ 'if-none-match': true,
+ 'if-modified-since': true
+ };
+ msg.data.requests.forEach(function(request) {
+ request.headers.forEach(function(header) {
+ assert_false(
+ !!freshness_headers[header[0]],
+ header[0] + ' header must not be set in the ' +
+ 'FetchEvent\'s request. (url = ' + request.url + ')');
+ });
+ })
+ });
+ }, 'The headers of FetchEvent shouldn\'t contain freshness headers.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-redirect.https.html
new file mode 100644
index 0000000000..5ce015b421
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-redirect.https.html
@@ -0,0 +1,385 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent for resources</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/media.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var test_scope = ""
+function assert_resolves(promise, description) {
+ return promise.then(
+ () => test(() => {}, description + " - " + test_scope),
+ (e) => test(() => { throw e; }, description + " - " + test_scope)
+ );
+}
+
+function assert_rejects(promise, description) {
+ return promise.then(
+ () => test(() => { assert_unreached(); }, description + " - " + test_scope),
+ () => test(() => {}, description + " - " + test_scope)
+ );
+}
+
+function iframe_test(url, timeout_enabled) {
+ return new Promise(function(resolve, reject) {
+ var frame = document.createElement('iframe');
+ frame.src = url;
+ if (timeout_enabled) {
+ // We can't catch the network error on iframe. So we use the timer for
+ // failure detection.
+ var timer = setTimeout(function() {
+ reject(new Error('iframe load timeout'));
+ frame.remove();
+ }, 10000);
+ }
+ frame.onload = function() {
+ if (timeout_enabled)
+ clearTimeout(timer);
+ try {
+ if (frame.contentDocument.body.textContent == 'Hello world\n')
+ resolve();
+ else
+ reject(new Error('content mismatch'));
+ } catch (e) {
+ // Chrome treats iframes that failed to load due to a network error as
+ // having a different origin, so accessing contentDocument throws an
+ // error. Other browsers might have different behavior.
+ reject(new Error(e));
+ }
+ frame.remove();
+ };
+ document.body.appendChild(frame);
+ });
+}
+
+promise_test(function(t) {
+ test_scope = "default";
+
+ var SCOPE = 'resources/fetch-request-redirect-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect=';
+ var IMAGE_URL = base_path() + 'resources/square.png';
+ var AUDIO_URL = getAudioURI("/media/sound_5");
+ var XHR_URL = base_path() + 'resources/simple.txt';
+ var HTML_URL = base_path() + 'resources/sample.html';
+
+ var REDIRECT_TO_IMAGE_URL = REDIRECT_URL + encodeURIComponent(IMAGE_URL);
+ var REDIRECT_TO_AUDIO_URL = REDIRECT_URL + encodeURIComponent(AUDIO_URL);
+ var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL);
+ var REDIRECT_TO_HTML_URL = REDIRECT_URL + encodeURIComponent(HTML_URL);
+
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(() => service_worker_unregister(t, SCOPE));
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(async function(f) {
+ frame = f;
+ // XMLHttpRequest tests.
+ await assert_resolves(frame.contentWindow.xhr(XHR_URL),
+ 'Normal XHR should succeed.');
+ await assert_resolves(frame.contentWindow.xhr(REDIRECT_TO_XHR_URL),
+ 'Redirected XHR should succeed.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&redirect-mode=follow'),
+ 'Redirected XHR with Request.redirect=follow should succeed.');
+ await assert_rejects(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&redirect-mode=error'),
+ 'Redirected XHR with Request.redirect=error should fail.');
+ await assert_rejects(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&redirect-mode=manual'),
+ 'Redirected XHR with Request.redirect=manual should fail.');
+
+ // Image loading tests.
+ await assert_resolves(frame.contentWindow.load_image(IMAGE_URL),
+ 'Normal image resource should be loaded.');
+ await assert_resolves(
+ frame.contentWindow.load_image(REDIRECT_TO_IMAGE_URL),
+ 'Redirected image resource should be loaded.');
+ await assert_resolves(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) +
+ '&redirect-mode=follow'),
+ 'Loading redirected image with Request.redirect=follow should' +
+ ' succeed.');
+ await assert_rejects(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) +
+ '&redirect-mode=error'),
+ 'Loading redirected image with Request.redirect=error should ' +
+ 'fail.');
+ await assert_rejects(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) +
+ '&redirect-mode=manual'),
+ 'Loading redirected image with Request.redirect=manual should' +
+ ' fail.');
+
+ // Audio loading tests.
+ await assert_resolves(frame.contentWindow.load_audio(AUDIO_URL),
+ 'Normal audio resource should be loaded.');
+ await assert_resolves(
+ frame.contentWindow.load_audio(REDIRECT_TO_AUDIO_URL),
+ 'Redirected audio resource should be loaded.');
+ await assert_resolves(
+ frame.contentWindow.load_audio(
+ './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) +
+ '&redirect-mode=follow'),
+ 'Loading redirected audio with Request.redirect=follow should' +
+ ' succeed.');
+ await assert_rejects(
+ frame.contentWindow.load_audio(
+ './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) +
+ '&redirect-mode=error'),
+ 'Loading redirected audio with Request.redirect=error should ' +
+ 'fail.');
+ await assert_rejects(
+ frame.contentWindow.load_audio(
+ './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) +
+ '&redirect-mode=manual'),
+ 'Loading redirected audio with Request.redirect=manual should' +
+ ' fail.');
+
+ // Iframe tests.
+ await assert_resolves(iframe_test(HTML_URL),
+ 'Normal iframe loading should succeed.');
+ await assert_resolves(
+ iframe_test(REDIRECT_TO_HTML_URL),
+ 'Normal redirected iframe loading should succeed.');
+ await assert_rejects(
+ iframe_test(SCOPE + '?url=' +
+ encodeURIComponent(REDIRECT_TO_HTML_URL) +
+ '&redirect-mode=follow',
+ true /* timeout_enabled */),
+ 'Redirected iframe loading with Request.redirect=follow should'+
+ ' fail.');
+ await assert_rejects(
+ iframe_test(SCOPE + '?url=' +
+ encodeURIComponent(REDIRECT_TO_HTML_URL) +
+ '&redirect-mode=error',
+ true /* timeout_enabled */),
+ 'Redirected iframe loading with Request.redirect=error should '+
+ 'fail.');
+ await assert_resolves(
+ iframe_test(SCOPE + '?url=' +
+ encodeURIComponent(REDIRECT_TO_HTML_URL) +
+ '&redirect-mode=manual',
+ true /* timeout_enabled */),
+ 'Redirected iframe loading with Request.redirect=manual should'+
+ ' succeed.');
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Verify redirect mode of Fetch API and ServiceWorker FetchEvent.');
+
+// test for reponse.redirected
+promise_test(function(t) {
+ test_scope = "redirected";
+
+ var SCOPE = 'resources/fetch-request-redirect-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect=';
+ var XHR_URL = base_path() + 'resources/simple.txt';
+ var IMAGE_URL = base_path() + 'resources/square.png';
+
+ var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL);
+
+ var host_info = get_host_info();
+
+ var CROSS_ORIGIN_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_URL;
+
+ var REDIRECT_TO_CROSS_ORIGIN = REDIRECT_URL +
+ encodeURIComponent(CROSS_ORIGIN_URL + '?ACAOrigin=*');
+
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(() => service_worker_unregister(t, SCOPE));
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(async function(f) {
+ frame = f;
+ // XMLHttpRequest tests.
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(XHR_URL) +
+ '&expected_redirected=false' +
+ '&expected_resolves=true'),
+ 'Normal XHR should be resolved and response should not be ' +
+ 'redirected.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&expected_redirected=true' +
+ '&expected_resolves=true'),
+ 'Redirected XHR should be resolved and response should be ' +
+ 'redirected.');
+
+ // tests for request's mode = cors
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(XHR_URL) +
+ '&mode=cors' +
+ '&expected_redirected=false' +
+ '&expected_resolves=true'),
+ 'Normal XHR should be resolved and response should not be ' +
+ 'redirected even with CORS mode.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&mode=cors' +
+ '&redirect-mode=follow' +
+ '&expected_redirected=true' +
+ '&expected_resolves=true'),
+ 'Redirected XHR should be resolved and response.redirected ' +
+ 'should be redirected with CORS mode.');
+
+ // tests for request's mode = no-cors
+ // The response.redirect should be false since we will not add
+ // redirected url list when redirect-mode is not follow.
+ await assert_rejects(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&mode=no-cors' +
+ '&redirect-mode=manual' +
+ '&expected_redirected=false' +
+ '&expected_resolves=false'),
+ 'Redirected XHR should be reject and response should be ' +
+ 'redirected with NO-CORS mode and redirect-mode=manual.');
+
+ // tests for redirecting to a cors
+ await assert_resolves(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_CROSS_ORIGIN) +
+ '&mode=no-cors' +
+ '&redirect-mode=follow' +
+ '&expected_redirected=false' +
+ '&expected_resolves=true'),
+ 'Redirected CORS image should be reject and response should ' +
+ 'not be redirected with NO-CORS mode.');
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Verify redirected of Response(Fetch API) and ServiceWorker FetchEvent.');
+
+// test for reponse.redirected after cached
+promise_test(function(t) {
+ test_scope = "cache";
+
+ var SCOPE = 'resources/fetch-request-redirect-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect=';
+ var XHR_URL = base_path() + 'resources/simple.txt';
+ var IMAGE_URL = base_path() + 'resources/square.png';
+
+ var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL);
+
+ var host_info = get_host_info();
+
+ var CROSS_ORIGIN_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_URL;
+
+ var REDIRECT_TO_CROSS_ORIGIN = REDIRECT_URL +
+ encodeURIComponent(CROSS_ORIGIN_URL + '?ACAOrigin=*');
+
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(() => service_worker_unregister(t, SCOPE));
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(async function(f) {
+ frame = f;
+ // XMLHttpRequest tests.
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(XHR_URL) +
+ '&expected_redirected=false' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Normal XHR should be resolved and response should not be ' +
+ 'redirected.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&expected_redirected=true' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Redirected XHR should be resolved and response should be ' +
+ 'redirected.');
+
+ // tests for request's mode = cors
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(XHR_URL) +
+ '&mode=cors' +
+ '&expected_redirected=false' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Normal XHR should be resolved and response should not be ' +
+ 'redirected even with CORS mode.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&mode=cors' +
+ '&redirect-mode=follow' +
+ '&expected_redirected=true' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Redirected XHR should be resolved and response.redirected ' +
+ 'should be redirected with CORS mode.');
+
+ // tests for request's mode = no-cors
+ // The response.redirect should be false since we will not add
+ // redirected url list when redirect-mode is not follow.
+ await assert_rejects(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&mode=no-cors' +
+ '&redirect-mode=manual' +
+ '&expected_redirected=false' +
+ '&expected_resolves=false' +
+ '&cache'),
+ 'Redirected XHR should be reject and response should be ' +
+ 'redirected with NO-CORS mode and redirect-mode=manual.');
+
+ // tests for redirecting to a cors
+ await assert_resolves(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_CROSS_ORIGIN) +
+ '&mode=no-cors' +
+ '&redirect-mode=follow' +
+ '&expected_redirected=false' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Redirected CORS image should be reject and response should ' +
+ 'not be redirected with NO-CORS mode.');
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Verify redirected of Response(Fetch API), Cache API and ServiceWorker ' +
+ 'FetchEvent.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-resources.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-resources.https.html
new file mode 100644
index 0000000000..b4680c3ccd
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-resources.https.html
@@ -0,0 +1,302 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent for resources</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+let url_count = 0;
+const expected_results = {};
+
+function add_promise_to_test(url)
+{
+ const expected = expected_results[url];
+ return new Promise((resolve) => {
+ expected.resolve = resolve;
+ });
+}
+
+function image_test(frame, url, cross_origin, expected_mode,
+ expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ cross_origin: cross_origin,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'image',
+ message: `Image load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_image(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+function script_test(frame, url, cross_origin, expected_mode,
+ expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ cross_origin: cross_origin,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'script',
+ message: `Script load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_script(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+function css_test(frame, url, cross_origin, expected_mode,
+ expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ cross_origin: cross_origin,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'style',
+ message: `CSS load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_css(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+function font_face_test(frame, url, expected_mode, expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ url: actual_url,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'font',
+ message: `FontFace load (url: ${actual_url})`
+ };
+ frame.contentWindow.load_font(actual_url);
+ return add_promise_to_test(actual_url);
+}
+
+function script_integrity_test(frame, url, integrity, expected_integrity) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ url: actual_url,
+ mode: 'no-cors',
+ credentials: 'include',
+ redirect: 'follow',
+ integrity: expected_integrity,
+ destination: 'script',
+ message: `Script load (url:${actual_url})`
+ };
+ frame.contentWindow.load_script_with_integrity(actual_url, integrity);
+ return add_promise_to_test(actual_url);
+}
+
+function css_integrity_test(frame, url, integrity, expected_integrity) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ url: actual_url,
+ mode: 'no-cors',
+ credentials: 'include',
+ redirect: 'follow',
+ integrity: expected_integrity,
+ destination: 'style',
+ message: `CSS load (url:${actual_url})`
+ };
+ frame.contentWindow.load_css_with_integrity(actual_url, integrity);
+ return add_promise_to_test(actual_url);
+}
+
+function fetch_test(frame, url, mode, credentials,
+ expected_mode, expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: '',
+ message: `fetch (url:${actual_url} mode:${mode} ` +
+ `credentials:${credentials})`
+ };
+ frame.contentWindow.fetch(
+ new Request(actual_url, {mode: mode, credentials: credentials}));
+ return add_promise_to_test(actual_url);
+}
+
+function audio_test(frame, url, cross_origin,
+ expected_mode, expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'audio',
+ message: `Audio load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_audio(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+
+function video_test(frame, url, cross_origin,
+ expected_mode, expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'video',
+ message: `Video load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_video(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+promise_test(async t => {
+ const SCOPE = 'resources/fetch-request-resources-iframe.https.html';
+ const SCRIPT = 'resources/fetch-request-resources-worker.js';
+ const host_info = get_host_info();
+ const LOCAL_URL =
+ host_info['HTTPS_ORIGIN'] + base_path() + 'resources/sample?test';
+ const REMOTE_URL =
+ host_info['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/sample?test';
+
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+
+ await new Promise((resolve, reject) => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func(msg => {
+ if (msg.data.ready) {
+ resolve();
+ return;
+ }
+ const result = msg.data;
+ const expected = expected_results[result.url];
+ if (!expected) {
+ return;
+ }
+ test(() => {
+ assert_equals(
+ result.mode, expected.mode,
+ `mode of must be ${expected.mode}.`);
+ assert_equals(
+ result.credentials, expected.credentials,
+ `credentials of ${expected.message} must be ` +
+ `${expected.credentials}.`);
+ assert_equals(
+ result.redirect, expected.redirect,
+ `redirect mode of ${expected.message} must be ` +
+ `${expected.redirect}.`);
+ assert_equals(
+ result.integrity, expected.integrity,
+ `integrity of ${expected.message} must be ` +
+ `${expected.integrity}.`);
+ assert_equals(
+ result.destination, expected.destination,
+ `destination of ${expected.message} must be ` +
+ `${expected.destination}.`);
+ }, expected.message);
+ expected.resolve();
+ delete expected_results[result.url];
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+
+ const f = await with_iframe(SCOPE);
+ t.add_cleanup(() => f.remove());
+
+ await image_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await image_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await css_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await css_test(f, REMOTE_URL, '', 'no-cors', 'include');
+
+ await image_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await image_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await image_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await image_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await image_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+ await script_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await script_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await script_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await script_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await script_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await script_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+ await css_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await css_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await css_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await css_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await css_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await css_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+ await font_face_test(f, LOCAL_URL, 'cors', 'same-origin');
+ await font_face_test(f, REMOTE_URL, 'cors', 'same-origin');
+
+ await script_integrity_test(f, LOCAL_URL, ' ', ' ');
+ await script_integrity_test(
+ f, LOCAL_URL,
+ 'This is not a valid integrity because it has no dashes',
+ 'This is not a valid integrity because it has no dashes');
+ await script_integrity_test(f, LOCAL_URL, 'sha256-', 'sha256-');
+ await script_integrity_test(f, LOCAL_URL, 'sha256-foo?123', 'sha256-foo?123');
+ await script_integrity_test(f, LOCAL_URL, 'sha256-foo sha384-abc ',
+ 'sha256-foo sha384-abc ');
+ await script_integrity_test(f, LOCAL_URL, 'sha256-foo sha256-abc',
+ 'sha256-foo sha256-abc');
+
+ await css_integrity_test(f, LOCAL_URL, ' ', ' ');
+ await css_integrity_test(
+ f, LOCAL_URL,
+ 'This is not a valid integrity because it has no dashes',
+ 'This is not a valid integrity because it has no dashes');
+ await css_integrity_test(f, LOCAL_URL, 'sha256-', 'sha256-');
+ await css_integrity_test(f, LOCAL_URL, 'sha256-foo?123', 'sha256-foo?123');
+ await css_integrity_test(f, LOCAL_URL, 'sha256-foo sha384-abc ',
+ 'sha256-foo sha384-abc ');
+ await css_integrity_test(f, LOCAL_URL, 'sha256-foo sha256-abc',
+ 'sha256-foo sha256-abc');
+
+ await fetch_test(f, LOCAL_URL, 'same-origin', 'omit', 'same-origin', 'omit');
+ await fetch_test(f, LOCAL_URL, 'same-origin', 'same-origin',
+ 'same-origin', 'same-origin');
+ await fetch_test(f, LOCAL_URL, 'same-origin', 'include',
+ 'same-origin', 'include');
+ await fetch_test(f, LOCAL_URL, 'no-cors', 'omit', 'no-cors', 'omit');
+ await fetch_test(f, LOCAL_URL, 'no-cors', 'same-origin',
+ 'no-cors', 'same-origin');
+ await fetch_test(f, LOCAL_URL, 'no-cors', 'include', 'no-cors', 'include');
+ await fetch_test(f, LOCAL_URL, 'cors', 'omit', 'cors', 'omit');
+ await fetch_test(f, LOCAL_URL, 'cors', 'same-origin', 'cors', 'same-origin');
+ await fetch_test(f, LOCAL_URL, 'cors', 'include', 'cors', 'include');
+ await fetch_test(f, REMOTE_URL, 'no-cors', 'omit', 'no-cors', 'omit');
+ await fetch_test(f, REMOTE_URL, 'no-cors', 'same-origin', 'no-cors', 'same-origin');
+ await fetch_test(f, REMOTE_URL, 'no-cors', 'include', 'no-cors', 'include');
+ await fetch_test(f, REMOTE_URL, 'cors', 'omit', 'cors', 'omit');
+ await fetch_test(f, REMOTE_URL, 'cors', 'same-origin', 'cors', 'same-origin');
+ await fetch_test(f, REMOTE_URL, 'cors', 'include', 'cors', 'include');
+
+ await audio_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await audio_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await audio_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await audio_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await audio_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await audio_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+ await video_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await video_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await video_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await video_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await video_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await video_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+}, 'Verify FetchEvent for resources.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js
new file mode 100644
index 0000000000..e6c0213928
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js
@@ -0,0 +1,19 @@
+// META: script=resources/test-helpers.sub.js
+
+"use strict";
+
+promise_test(async t => {
+ const url = "resources/fetch-request-xhr-sync-error-worker.js";
+ const scope = "resources/fetch-request-xhr-sync-iframe.html";
+
+ const registration = await service_worker_unregister_and_register(t, url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-1.txt"));
+ assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-2.txt"));
+ assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-3.txt"));
+}, "Verify synchronous XMLHttpRequest always throws a NetworkError for ReadableStream errors");
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html
new file mode 100644
index 0000000000..9f18096aa2
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>Service Worker: Synchronous XHR on Worker is intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test((t) => {
+ const url = 'resources/fetch-request-xhr-sync-on-worker-worker.js';
+ const scope = 'resources/fetch-request-xhr-sync-on-worker-scope/';
+ const non_existent_file = 'non-existent-file.txt';
+
+ // In Chromium, the service worker scope matching for workers is based on
+ // the URL of the parent HTML. So this test creates an iframe which is
+ // controlled by the service worker first, and creates a worker from the
+ // iframe.
+ return service_worker_unregister_and_register(t, url, scope)
+ .then((registration) => {
+ t.add_cleanup(() => registration.unregister());
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => { return with_iframe(scope + 'iframe_page'); })
+ .then((frame) => {
+ t.add_cleanup(() => frame.remove());
+ return frame.contentWindow.performSyncXHROnWorker(non_existent_file);
+ })
+ .then((result) => {
+ assert_equals(
+ result.status,
+ 200,
+ 'HTTP response status code for intercepted request'
+ );
+ assert_equals(
+ result.responseText,
+ 'Response from service worker',
+ 'HTTP response text for intercepted request'
+ );
+ });
+ }, 'Verify SyncXHR on Worker is intercepted');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html
new file mode 100644
index 0000000000..ec27fb8983
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<title>Service Worker: Synchronous XHR is intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var url = 'resources/fetch-request-xhr-sync-worker.js';
+ var scope = 'resources/fetch-request-xhr-sync-iframe.html';
+ var non_existent_file = 'non-existent-file.txt';
+
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(frame) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ return new Promise(function(resolve, reject) {
+ t.step_timeout(function() {
+ var xhr;
+ try {
+ xhr = frame.contentWindow.performSyncXHR(non_existent_file);
+ resolve(xhr);
+ } catch (err) {
+ reject(err);
+ }
+ }, 0);
+ })
+ })
+ .then(function(xhr) {
+ assert_equals(
+ xhr.status,
+ 200,
+ 'HTTP response status code for intercepted request'
+ );
+ assert_equals(
+ xhr.responseText,
+ 'Response from service worker',
+ 'HTTP response text for intercepted request'
+ );
+ });
+ }, 'Verify SyncXHR is intercepted');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr.https.html
new file mode 100644
index 0000000000..37a457393b
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-request-xhr.https.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<title>Service Worker: the body of FetchEvent using XMLHttpRequest</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe-sub"></script>
+<script>
+let frame;
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScope = 'resources/fetch-request-xhr-iframe.https.html';
+ const kScript = 'resources/fetch-request-xhr-worker.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ promise_test(() => {
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => { f.remove(); });
+ });
+ }, 'initialize global state');
+
+// Run the tests.
+promise_test(t => {
+ return frame.contentWindow.get_header_test();
+ }, 'event.request has the expected headers for same-origin GET.');
+
+promise_test(t => {
+ return frame.contentWindow.post_header_test();
+ }, 'event.request has the expected headers for same-origin POST.');
+
+promise_test(t => {
+ return frame.contentWindow.cross_origin_get_header_test();
+ }, 'event.request has the expected headers for cross-origin GET.');
+
+promise_test(t => {
+ return frame.contentWindow.cross_origin_post_header_test();
+ }, 'event.request has the expected headers for cross-origin POST.');
+
+promise_test(t => {
+ return frame.contentWindow.string_test();
+ }, 'FetchEvent#request.body contains XHR request data (string)');
+
+promise_test(t => {
+ return frame.contentWindow.blob_test();
+ }, 'FetchEvent#request.body contains XHR request data (blob)');
+
+promise_test(t => {
+ return frame.contentWindow.custom_method_test();
+ }, 'FetchEvent#request.method is set to XHR method');
+
+promise_test(t => {
+ return frame.contentWindow.options_method_test();
+ }, 'XHR using OPTIONS method');
+
+promise_test(t => {
+ return frame.contentWindow.form_data_test();
+ }, 'XHR with form data');
+
+promise_test(t => {
+ return frame.contentWindow.mode_credentials_test();
+ }, 'XHR with mode/credentials set');
+
+promise_test(t => {
+ return frame.contentWindow.data_url_test();
+ }, 'XHR to data URL');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-response-taint.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-response-taint.https.html
new file mode 100644
index 0000000000..8e190f4850
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-response-taint.https.html
@@ -0,0 +1,223 @@
+<!DOCTYPE html>
+<title>Service Worker: Tainting of responses fetched via SW.</title>
+<!-- This test makes a large number of requests sequentially. -->
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+var BASE_ORIGIN = host_info.HTTPS_ORIGIN;
+var OTHER_ORIGIN = host_info.HTTPS_REMOTE_ORIGIN;
+var BASE_URL = BASE_ORIGIN + base_path() +
+ 'resources/fetch-access-control.py?';
+var OTHER_BASE_URL = OTHER_ORIGIN + base_path() +
+ 'resources/fetch-access-control.py?';
+
+function frame_fetch(frame, url, mode, credentials) {
+ var foreignPromise = frame.contentWindow.fetch(
+ new Request(url, {mode: mode, credentials: credentials}))
+
+ // Event loops should be shared between contexts of similar origin, not all
+ // browsers adhere to this expectation at the time of this writing. Incorrect
+ // behavior in this regard can interfere with test execution when the
+ // provided iframe is removed from the document.
+ //
+ // WPT maintains a test dedicated the expected treatment of event loops, so
+ // the following workaround is acceptable in this context.
+ return Promise.resolve(foreignPromise);
+}
+
+var login_and_register;
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-response-taint-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var registration;
+
+ login_and_register = login_https(t, host_info.HTTPS_ORIGIN, host_info.HTTPS_REMOTE_ORIGIN)
+ .then(function() {
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ })
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(f) {
+ // This test should not be considered complete until after the
+ // service worker has been unregistered. Currently, `testharness.js`
+ // does not support asynchronous global "tear down" logic, so this
+ // must be expressed using a dedicated `promise_test`. Because the
+ // other sub-tests in this file are declared synchronously, this
+ // test will be the final test executed.
+ promise_test(function(t) {
+ f.remove();
+ return registration.unregister();
+ }, 'restore global state');
+
+ return f;
+ });
+ return login_and_register;
+ }, 'initialize global state');
+
+function ng_test(url, mode, credentials) {
+ promise_test(function(t) {
+ return login_and_register
+ .then(function(frame) {
+ var fetchRequest = frame_fetch(frame, url, mode, credentials);
+ return promise_rejects_js(t, frame.contentWindow.TypeError, fetchRequest);
+ });
+ }, 'url:\"' + url + '\" mode:\"' + mode +
+ '\" credentials:\"' + credentials + '\" should fail.');
+}
+
+function ok_test(url, mode, credentials, expected_type, expected_username) {
+ promise_test(function() {
+ return login_and_register.then(function(frame) {
+ return frame_fetch(frame, url, mode, credentials)
+ })
+ .then(function(res) {
+ assert_equals(res.type, expected_type, 'response type');
+ return res.text();
+ })
+ .then(function(text) {
+ if (expected_type == 'opaque') {
+ assert_equals(text, '');
+ } else {
+ return new Promise(function(resolve) {
+ var report = resolve;
+ // text must contain report() call.
+ eval(text);
+ })
+ .then(function(result) {
+ assert_equals(result.username, expected_username);
+ });
+ }
+ });
+ }, 'fetching url:\"' + url + '\" mode:\"' + mode +
+ '\" credentials:\"' + credentials + '\" should ' +
+ 'succeed.');
+}
+
+function build_rewrite_url(origin, url, mode, credentials) {
+ return origin + '/?url=' + encodeURIComponent(url) + '&mode=' + mode +
+ '&credentials=' + credentials + '&';
+}
+
+function for_each_origin_mode_credentials(callback) {
+ [BASE_ORIGIN, OTHER_ORIGIN].forEach(function(origin) {
+ ['same-origin', 'no-cors', 'cors'].forEach(function(mode) {
+ ['omit', 'same-origin', 'include'].forEach(function(credentials) {
+ callback(origin, mode, credentials);
+ });
+ });
+ });
+}
+
+ok_test(BASE_URL, 'same-origin', 'omit', 'basic', 'undefined');
+ok_test(BASE_URL, 'same-origin', 'same-origin', 'basic', 'username2s');
+ok_test(BASE_URL, 'same-origin', 'include', 'basic', 'username2s');
+ok_test(BASE_URL, 'no-cors', 'omit', 'basic', 'undefined');
+ok_test(BASE_URL, 'no-cors', 'same-origin', 'basic', 'username2s');
+ok_test(BASE_URL, 'no-cors', 'include', 'basic', 'username2s');
+ok_test(BASE_URL, 'cors', 'omit', 'basic', 'undefined');
+ok_test(BASE_URL, 'cors', 'same-origin', 'basic', 'username2s');
+ok_test(BASE_URL, 'cors', 'include', 'basic', 'username2s');
+ng_test(OTHER_BASE_URL, 'same-origin', 'omit');
+ng_test(OTHER_BASE_URL, 'same-origin', 'same-origin');
+ng_test(OTHER_BASE_URL, 'same-origin', 'include');
+ok_test(OTHER_BASE_URL, 'no-cors', 'omit', 'opaque');
+ok_test(OTHER_BASE_URL, 'no-cors', 'same-origin', 'opaque');
+ok_test(OTHER_BASE_URL, 'no-cors', 'include', 'opaque');
+ng_test(OTHER_BASE_URL, 'cors', 'omit');
+ng_test(OTHER_BASE_URL, 'cors', 'same-origin');
+ng_test(OTHER_BASE_URL, 'cors', 'include');
+ok_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'omit', 'cors', 'undefined');
+ok_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'same-origin', 'cors',
+ 'undefined');
+ng_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'include');
+ok_test(OTHER_BASE_URL + 'ACAOrigin=' + BASE_ORIGIN + '&ACACredentials=true',
+ 'cors', 'include', 'cors', 'username1s')
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, BASE_URL, 'same-origin', 'omit');
+ // Fetch to the other origin with same-origin mode should fail.
+ if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+ ng_test(url, mode, credentials);
+ } else {
+ // The response type from the SW should be basic
+ ok_test(url, mode, credentials, 'basic', 'undefined');
+ }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, BASE_URL, 'same-origin', 'same-origin');
+
+ // Fetch to the other origin with same-origin mode should fail.
+ if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+ ng_test(url, mode, credentials);
+ } else {
+ // The response type from the SW should be basic.
+ ok_test(url, mode, credentials, 'basic', 'username2s');
+ }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, OTHER_BASE_URL, 'same-origin', 'omit');
+ // The response from the SW should be an error.
+ ng_test(url, mode, credentials);
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, OTHER_BASE_URL, 'no-cors', 'omit');
+
+ // SW can respond only to no-cors requests.
+ if (mode != 'no-cors') {
+ ng_test(url, mode, credentials);
+ } else {
+ // The response type from the SW should be opaque.
+ ok_test(url, mode, credentials, 'opaque');
+ }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'omit');
+
+ // Fetch to the other origin with same-origin mode should fail.
+ if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+ ng_test(url, mode, credentials);
+ } else if (origin == BASE_ORIGIN && mode == 'same-origin') {
+ // Cors type response to a same-origin mode request should fail
+ ng_test(url, mode, credentials);
+ } else {
+ // The response from the SW should be cors.
+ ok_test(url, mode, credentials, 'cors', 'undefined');
+ }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin,
+ OTHER_BASE_URL + 'ACAOrigin=' + BASE_ORIGIN +
+ '&ACACredentials=true',
+ 'cors', 'include');
+ // Fetch to the other origin with same-origin mode should fail.
+ if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+ ng_test(url, mode, credentials);
+ } else if (origin == BASE_ORIGIN && mode == 'same-origin') {
+ // Cors type response to a same-origin mode request should fail
+ ng_test(url, mode, credentials);
+ } else {
+ // The response from the SW should be cors.
+ ok_test(url, mode, credentials, 'cors', 'username1s');
+ }
+});
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-response-xhr.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-response-xhr.https.html
new file mode 100644
index 0000000000..891eb02942
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-response-xhr.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Service Worker: the response of FetchEvent using XMLHttpRequest</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-response-xhr-iframe.https.html';
+ var SCRIPT = 'resources/fetch-response-xhr-worker.js';
+ var host_info = get_host_info();
+
+ window.addEventListener('message', t.step_func(on_message), false);
+ function on_message(e) {
+ assert_equals(e.data.results, 'foo, bar');
+ e.source.postMessage('ACK', host_info['HTTPS_ORIGIN']);
+ }
+
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ var channel;
+
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ channel = new MessageChannel();
+ var onPortMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+
+ frame.contentWindow.postMessage('START',
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+
+ return onPortMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/fetch-waits-for-activate.https.html b/testing/web-platform/tests/service-workers/service-worker/fetch-waits-for-activate.https.html
new file mode 100644
index 0000000000..7c888450f0
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-waits-for-activate.https.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch Event Waits for Activate Event</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const worker_url = 'resources/fetch-waits-for-activate-worker.js';
+const normalized_worker_url = normalizeURL(worker_url);
+const worker_scope = 'resources/fetch-waits-for-activate/';
+
+// Resolves with the Service Worker's registration once it's reached the
+// "activating" state. (The Service Worker should remain "activating" until
+// explicitly told advance to the "activated" state).
+async function registerAndWaitForActivating(t) {
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, worker_scope);
+ t.add_cleanup(() => service_worker_unregister(t, worker_scope));
+
+ await wait_for_state(t, registration.installing, 'activating');
+
+ return registration;
+}
+
+// Attempts to ensure that the "Handle Fetch" algorithm has reached the step
+//
+// "If activeWorker’s state is "activating", wait for activeWorker’s state to
+// become "activated"."
+//
+// by waiting for some time to pass.
+//
+// WARNING: whether the algorithm has reached that step isn't directly
+// observable, so this is best effort and can race. Note that this can only
+// result in false positives (where the algorithm hasn't reached that step yet
+// and any functional events haven't actually been handled by the Service
+// Worker).
+async function ensureFunctionalEventsAreWaiting(registration) {
+ await (new Promise(resolve => { setTimeout(resolve, 1000); }));
+
+ assert_equals(registration.active.scriptURL, normalized_worker_url,
+ 'active worker should be present');
+ assert_equals(registration.active.state, 'activating',
+ 'active worker should be in activating state');
+}
+
+promise_test(async t => {
+ const registration = await registerAndWaitForActivating(t);
+
+ let frame = null;
+ t.add_cleanup(() => {
+ if (frame) {
+ frame.remove();
+ }
+ });
+
+ // This should block until we message the worker to tell it to complete
+ // the activate event.
+ const frameLoadPromise = with_iframe(worker_scope).then(function(f) {
+ frame = f;
+ });
+
+ await ensureFunctionalEventsAreWaiting(registration);
+ assert_equals(frame, null, 'frame should not be loaded');
+
+ registration.active.postMessage('ACTIVATE');
+
+ await frameLoadPromise;
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalized_worker_url,
+ 'frame should now be loaded and controlled');
+ assert_equals(registration.active.state, 'activated',
+ 'active worker should be in activated state');
+}, 'Navigation fetch events should wait for the activate event to complete.');
+
+promise_test(async t => {
+ const frame = await with_iframe(worker_scope);
+ t.add_cleanup(() => { frame.remove(); });
+
+ const registration = await registerAndWaitForActivating(t);
+
+ // Make the Service Worker control the frame so the frame can perform an
+ // intercepted fetch.
+ await (new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalized_worker_url, 'frame should be controlled');
+ resolve();
+ };
+
+ registration.active.postMessage('CLAIM');
+ }));
+
+ const fetch_url = `${worker_scope}non/existent/path`;
+ const expected_fetch_result = 'Hello world';
+ let fetch_promise_settled = false;
+
+ // This should block until we message the worker to tell it to complete
+ // the activate event.
+ const fetchPromise = frame.contentWindow.fetch(fetch_url, {
+ method: 'POST',
+ body: expected_fetch_result,
+ }).then(response => {
+ fetch_promise_settled = true;
+ return response;
+ });
+
+ await ensureFunctionalEventsAreWaiting(registration);
+ assert_false(fetch_promise_settled,
+ "fetch()-ing a Service Worker-controlled scope shouldn't have " +
+ "settled yet");
+
+ registration.active.postMessage('ACTIVATE');
+
+ const response = await fetchPromise;
+ assert_equals(await response.text(), expected_fetch_result,
+ "Service Worker should have responded to request to" +
+ fetch_url)
+ assert_equals(registration.active.state, 'activated',
+ 'active worker should be in activated state');
+}, 'Subresource fetch events should wait for the activate event to complete.');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/getregistration.https.html b/testing/web-platform/tests/service-workers/service-worker/getregistration.https.html
new file mode 100644
index 0000000000..634c2efa12
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/getregistration.https.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+async_test(function(t) {
+ var documentURL = 'no-such-worker';
+ navigator.serviceWorker.getRegistration(documentURL)
+ .then(function(value) {
+ assert_equals(value, undefined,
+ 'getRegistration should resolve with undefined');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'getRegistration');
+
+promise_test(function(t) {
+ var scope = 'resources/scope/getregistration/normal';
+ var registration;
+ return service_worker_unregister_and_register(t, 'resources/empty-worker.js',
+ scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(value) {
+ assert_equals(
+ value, registration,
+ 'getRegistration should resolve to the same registration object');
+ });
+ }, 'Register then getRegistration');
+
+promise_test(function(t) {
+ var scope = 'resources/scope/getregistration/url-with-fragment';
+ var documentURL = scope + '#ref';
+ var registration;
+ return service_worker_unregister_and_register(t, 'resources/empty-worker.js',
+ scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return navigator.serviceWorker.getRegistration(documentURL);
+ })
+ .then(function(value) {
+ assert_equals(
+ value, registration,
+ 'getRegistration should resolve to the same registration object');
+ });
+ }, 'Register then getRegistration with a URL having a fragment');
+
+async_test(function(t) {
+ var documentURL = 'http://example.com/';
+ navigator.serviceWorker.getRegistration(documentURL)
+ .then(function() {
+ assert_unreached(
+ 'getRegistration with an out of origin URL should fail');
+ }, function(reason) {
+ assert_equals(
+ reason.name, 'SecurityError',
+ 'getRegistration with an out of origin URL should fail');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'getRegistration with a cross origin URL');
+
+async_test(function(t) {
+ var scope = 'resources/scope/getregistration/register-unregister';
+ service_worker_unregister_and_register(t, 'resources/empty-worker.js',
+ scope)
+ .then(function(registration) {
+ return registration.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(value) {
+ assert_equals(value, undefined,
+ 'getRegistration should resolve with undefined');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then Unregister then getRegistration');
+
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/getregistration/register-unregister';
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/empty-worker.js', scope
+ );
+
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const frameNav = frame.contentWindow.navigator;
+ await registration.unregister();
+ const value = await frameNav.serviceWorker.getRegistration(scope);
+
+ assert_equals(value, undefined, 'getRegistration should resolve with undefined');
+}, 'Register then Unregister then getRegistration in controlled iframe');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/getregistrations.https.html b/testing/web-platform/tests/service-workers/service-worker/getregistrations.https.html
new file mode 100644
index 0000000000..3a9b9a2331
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/getregistrations.https.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<title>Service Worker: getRegistrations()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+// Purge the existing registrations for the origin.
+// getRegistrations() is used in order to avoid adding additional complexity
+// e.g. adding an internal function.
+promise_test(async () => {
+ const registrations = await navigator.serviceWorker.getRegistrations();
+ await Promise.all(registrations.map(r => r.unregister()));
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(
+ value, [],
+ 'getRegistrations should resolve with an empty array.');
+}, 'registrations are not returned following unregister');
+
+promise_test(async t => {
+ const scope = 'resources/scope/getregistrations/normal';
+ const script = 'resources/empty-worker.js';
+ const registrations = [
+ await service_worker_unregister_and_register(t, script, scope)];
+ t.add_cleanup(() => registrations[0].unregister());
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(value, registrations,
+ 'getRegistrations should resolve with an array of registrations');
+}, 'Register then getRegistrations');
+
+promise_test(async t => {
+ const scope1 = 'resources/scope/getregistrations/scope1';
+ const scope2 = 'resources/scope/getregistrations/scope2';
+ const scope3 = 'resources/scope/getregistrations/scope12';
+
+ const script = 'resources/empty-worker.js';
+ t.add_cleanup(() => service_worker_unregister(t, scope1));
+ t.add_cleanup(() => service_worker_unregister(t, scope2));
+ t.add_cleanup(() => service_worker_unregister(t, scope3));
+
+ const registrations = [
+ await service_worker_unregister_and_register(t, script, scope1),
+ await service_worker_unregister_and_register(t, script, scope2),
+ await service_worker_unregister_and_register(t, script, scope3),
+ ];
+
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(value, registrations);
+}, 'Register multiple times then getRegistrations');
+
+promise_test(async t => {
+ const scope = 'resources/scope/getregistrations/register-unregister';
+ const script = 'resources/empty-worker.js';
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ await registration.unregister();
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(
+ value, [], 'getRegistrations should resolve with an empty array.');
+}, 'Register then Unregister then getRegistrations');
+
+promise_test(async t => {
+ const scope = 'resources/scope/getregistrations/register-unregister-controlled';
+ const script = 'resources/empty-worker.js';
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Create a frame controlled by the service worker and unregister the
+ // worker.
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+ await registration.unregister();
+
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(
+ value, [],
+ 'getRegistrations should resolve with an empty array.');
+ assert_equals(registration.installing, null);
+ assert_equals(registration.waiting, null);
+ assert_equals(registration.active.state, 'activated');
+}, 'Register then Unregister with controlled frame then getRegistrations');
+
+promise_test(async t => {
+ const host_info = get_host_info();
+ // Rewrite the url to point to remote origin.
+ const frame_same_origin_url = new URL("resources/frame-for-getregistrations.html", window.location);
+ const frame_url = host_info['HTTPS_REMOTE_ORIGIN'] + frame_same_origin_url.pathname;
+ const scope = 'resources/scope-for-getregistrations';
+ const script = 'resources/empty-worker.js';
+
+ // Loads an iframe and waits for 'ready' message from it to resolve promise.
+ // Caller is responsible for removing frame.
+ function with_iframe_ready(url) {
+ return new Promise(resolve => {
+ const frame = document.createElement('iframe');
+ frame.src = url;
+ window.addEventListener('message', function onMessage(e) {
+ window.removeEventListener('message', onMessage);
+ if (e.data == 'ready') {
+ resolve(frame);
+ }
+ });
+ document.body.appendChild(frame);
+ });
+ }
+
+ // We need this special frame loading function because the frame is going
+ // to register it's own service worker and there is the possibility that that
+ // register() finishes after the register() for the same domain later in the
+ // test. So we have to wait until the cross origin register() is done, and not
+ // just until the frame loads.
+ const frame = await with_iframe_ready(frame_url);
+ t.add_cleanup(async () => {
+ // Wait until the cross-origin worker is unregistered.
+ let resolve;
+ const channel = new MessageChannel();
+ channel.port1.onmessage = e => {
+ if (e.data == 'unregistered')
+ resolve();
+ };
+ frame.contentWindow.postMessage('unregister', '*', [channel.port2]);
+ await new Promise(r => { resolve = r; });
+
+ frame.remove();
+ });
+
+ const registrations = [
+ await service_worker_unregister_and_register(t, script, scope)];
+ t.add_cleanup(() => registrations[0].unregister());
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(
+ value, registrations,
+ 'getRegistrations should only return same origin registrations.');
+}, 'getRegistrations promise resolves only with same origin registrations.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/global-serviceworker.https.any.js b/testing/web-platform/tests/service-workers/service-worker/global-serviceworker.https.any.js
new file mode 100644
index 0000000000..19d77847c4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/global-serviceworker.https.any.js
@@ -0,0 +1,53 @@
+// META: title=serviceWorker on service worker global
+// META: global=serviceworker
+
+test(() => {
+ assert_equals(registration.installing, null, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active, null, 'registration.active');
+ assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+ assert_equals(serviceWorker.state, 'parsed', 'serviceWorker.state');
+ assert_readonly(self, 'serviceWorker', `self.serviceWorker is read only`);
+}, 'First run');
+
+// Cache this for later tests.
+const initialServiceWorker = self.serviceWorker;
+
+async_test((t) => {
+ assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+ serviceWorker.postMessage({ messageTest: true });
+
+ // The rest of the test runs once this receives the above message.
+ addEventListener('message', t.step_func((event) => {
+ // Ignore unrelated messages.
+ if (!event.data.messageTest) return;
+ assert_equals(event.source, serviceWorker, 'event.source');
+ t.done();
+ }));
+}, 'Can post message to self during startup');
+
+// The test is registered now so there isn't a race condition when collecting tests, but the asserts
+// don't happen until the 'install' event fires.
+async_test((t) => {
+ addEventListener('install', t.step_func_done(() => {
+ assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+ assert_equals(serviceWorker, initialServiceWorker, `self.serviceWorker hasn't changed`);
+ assert_equals(registration.installing, serviceWorker, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active, null, 'registration.active');
+ assert_equals(serviceWorker.state, 'installing', 'serviceWorker.state');
+ }));
+}, 'During install');
+
+// The test is registered now so there isn't a race condition when collecting tests, but the asserts
+// don't happen until the 'activate' event fires.
+async_test((t) => {
+ addEventListener('activate', t.step_func_done(() => {
+ assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+ assert_equals(serviceWorker, initialServiceWorker, `self.serviceWorker hasn't changed`);
+ assert_equals(registration.installing, null, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active, serviceWorker, 'registration.active');
+ assert_equals(serviceWorker.state, 'activating', 'serviceWorker.state');
+ }));
+}, 'During activate');
diff --git a/testing/web-platform/tests/service-workers/service-worker/historical.https.any.js b/testing/web-platform/tests/service-workers/service-worker/historical.https.any.js
new file mode 100644
index 0000000000..20b3ddfbf7
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/historical.https.any.js
@@ -0,0 +1,5 @@
+// META: global=serviceworker
+
+test((t) => {
+ assert_false('targetClientId' in FetchEvent.prototype)
+}, 'targetClientId should not be on FetchEvent');
diff --git a/testing/web-platform/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html b/testing/web-platform/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html
new file mode 100644
index 0000000000..5626237dcc
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>register on a secure page after redirect from an non-secure url</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+var host_info = get_host_info();
+
+// Loads a non-secure url in a new window, which redirects to |target_url|.
+// That page then registers a service worker, and messages back with the result.
+// Returns a promise that resolves with the result.
+function redirect_and_register(target_url) {
+ var redirect_url = host_info.HTTP_REMOTE_ORIGIN + base_path() +
+ 'resources/redirect.py?Redirect=';
+ var child = window.open(redirect_url + encodeURIComponent(target_url));
+ return new Promise(resolve => {
+ window.addEventListener('message', e => resolve(e.data));
+ })
+ .then(function(result) {
+ child.close();
+ return result;
+ });
+}
+
+promise_test(function(t) {
+ var target_url = window.location.origin + base_path() +
+ 'resources/http-to-https-redirect-and-register-iframe.html';
+
+ return redirect_and_register(target_url)
+ .then(result => {
+ assert_equals(result, 'OK');
+ });
+ }, 'register on a secure page after redirect from an non-secure url');
+
+promise_test(function(t) {
+ var target_url = host_info.HTTP_REMOTE_ORIGIN + base_path() +
+ 'resources/http-to-https-redirect-and-register-iframe.html';
+
+ return redirect_and_register(target_url)
+ .then(result => {
+ assert_equals(result, 'FAIL: navigator.serviceWorker is undefined');
+ });
+ }, 'register on a non-secure page after redirect from an non-secure url');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html b/testing/web-platform/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html
new file mode 100644
index 0000000000..e63f6b348a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+let expected = ['immutable', 'immutable', 'immutable', 'immutable', 'immutable'];
+
+promise_test(t =>
+ navigator.serviceWorker.register('resources/immutable-prototype-serviceworker.js', {scope: './resources/'})
+ .then(registration => {
+ let worker = registration.installing || registration.waiting || registration.active;
+ let channel = new MessageChannel()
+ worker.postMessage(channel.port2, [channel.port2]);
+ let resolve;
+ let promise = new Promise(r => resolve = r);
+ channel.port1.onmessage = resolve;
+ return promise.then(result => assert_array_equals(expected, result.data));
+ }),
+'worker prototype chain should be immutable');
+</script>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/import-scripts-cross-origin.https.html b/testing/web-platform/tests/service-workers/service-worker/import-scripts-cross-origin.https.html
new file mode 100644
index 0000000000..773708a9fb
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/import-scripts-cross-origin.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: cross-origin</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ const scope = 'resources/import-scripts-cross-origin';
+ await service_worker_unregister(t, scope);
+ let reg = await navigator.serviceWorker.register(
+ 'resources/import-scripts-cross-origin-worker.sub.js', { scope: scope });
+ t.add_cleanup(_ => reg.unregister());
+ assert_not_equals(reg.installing, null, 'worker is installing');
+ }, 'importScripts() supports cross-origin requests');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/import-scripts-data-url.https.html b/testing/web-platform/tests/service-workers/service-worker/import-scripts-data-url.https.html
new file mode 100644
index 0000000000..f0922193dd
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/import-scripts-data-url.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: data: URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ const scope = 'resources/import-scripts-data-url';
+ await service_worker_unregister(t, scope);
+ let reg = await navigator.serviceWorker.register(
+ 'resources/import-scripts-data-url-worker.js', { scope: scope });
+ t.add_cleanup(_ => reg.unregister());
+ assert_not_equals(reg.installing, null, 'worker is installing');
+ }, 'importScripts() supports data URLs');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/import-scripts-mime-types.https.html b/testing/web-platform/tests/service-workers/service-worker/import-scripts-mime-types.https.html
new file mode 100644
index 0000000000..1679831d0f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/import-scripts-mime-types.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: MIME types</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+/**
+ * Test that a Service Worker's importScript() only accepts valid MIME types.
+ */
+let serviceWorker = null;
+
+promise_test(async t => {
+ const scope = 'resources/import-scripts-mime-types';
+ const registration = await service_worker_unregister_and_register(t,
+ 'resources/import-scripts-mime-types-worker.js', scope);
+
+ add_completion_callback(() => { registration.unregister(); });
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ serviceWorker = registration.active;
+}, 'Global setup');
+
+promise_test(async t => {
+ await fetch_tests_from_worker(serviceWorker);
+}, 'Fetch importScripts tests from service worker')
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/import-scripts-redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/import-scripts-redirect.https.html
new file mode 100644
index 0000000000..07ea49439e
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/import-scripts-redirect.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: redirect</title>
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ const scope = 'resources/import-scripts-redirect';
+ await service_worker_unregister(t, scope);
+ let reg = await navigator.serviceWorker.register(
+ 'resources/import-scripts-redirect-worker.js', { scope: scope });
+ assert_not_equals(reg.installing, null, 'worker is installing');
+ await reg.unregister();
+ }, 'importScripts() supports redirects');
+
+promise_test(async t => {
+ const scope = 'resources/import-scripts-redirect';
+ await service_worker_unregister(t, scope);
+ let reg = await navigator.serviceWorker.register(
+ 'resources/import-scripts-redirect-worker.js', { scope: scope });
+ assert_not_equals(reg.installing, null, 'before update');
+ await wait_for_state(t, reg.installing, 'activated');
+ await Promise.all([
+ wait_for_update(t, reg),
+ reg.update()
+ ]);
+ assert_not_equals(reg.installing, null, 'after update');
+ await reg.unregister();
+ },
+ "an imported script redirects, and the body changes during the update check");
+
+promise_test(async t => {
+ const key = token();
+ const scope = 'resources/import-scripts-redirect';
+ await service_worker_unregister(t, scope);
+ let reg = await navigator.serviceWorker.register(
+ `resources/import-scripts-redirect-on-second-time-worker.js?Key=${key}`,
+ { scope });
+ t.add_cleanup(() => reg.unregister());
+
+ assert_not_equals(reg.installing, null, 'before update');
+ await wait_for_state(t, reg.installing, 'activated');
+ await Promise.all([
+ wait_for_update(t, reg),
+ reg.update()
+ ]);
+ assert_not_equals(reg.installing, null, 'after update');
+ },
+ "an imported script doesn't redirect initially, then redirects during " +
+ "the update check and the body changes");
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/import-scripts-resource-map.https.html b/testing/web-platform/tests/service-workers/service-worker/import-scripts-resource-map.https.html
new file mode 100644
index 0000000000..4742bd0126
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/import-scripts-resource-map.https.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Tests for importScripts: script resource map</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+ <script>
+ // This test registers a worker that imports a script multiple times. The
+ // script should be stored on the first import and thereafter that stored
+ // script should be loaded. The worker asserts that the stored script was
+ // loaded; if the assert fails then registration fails.
+
+ promise_test(async t => {
+ const SCOPE = "resources/import-scripts-resource-map";
+ const SCRIPT = "resources/import-scripts-resource-map-worker.js";
+ await service_worker_unregister(t, SCOPE);
+ const registration = await navigator.serviceWorker.register(SCRIPT, {
+ scope: SCOPE
+ });
+ await registration.unregister();
+ }, "import the same script URL multiple times");
+
+ promise_test(async t => {
+ const SCOPE = "resources/import-scripts-diff-resource-map";
+ const SCRIPT = "resources/import-scripts-diff-resource-map-worker.js";
+ await service_worker_unregister(t, SCOPE);
+ const registration = await navigator.serviceWorker.register(SCRIPT, {
+ scope: SCOPE
+ });
+ await registration.unregister();
+ }, "call importScripts() with multiple arguments");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/import-scripts-updated-flag.https.html b/testing/web-platform/tests/service-workers/service-worker/import-scripts-updated-flag.https.html
new file mode 100644
index 0000000000..09b4496aa0
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/import-scripts-updated-flag.https.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: import scripts updated flag</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This test registers a worker that calls importScripts at various stages of
+// service worker lifetime. The sub-tests trigger subsequent `importScript`
+// invocations via the `message` event.
+
+var register;
+
+function post_and_wait_for_reply(worker, message) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => { resolve(e.data); };
+ worker.postMessage(message);
+ });
+}
+
+promise_test(function(t) {
+ const scope = 'resources/import-scripts-updated-flag';
+ let registration;
+
+ register = service_worker_unregister_and_register(
+ t, 'resources/import-scripts-updated-flag-worker.js', scope)
+ .then(r => {
+ registration = r;
+ add_completion_callback(() => { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ // This test should not be considered complete until after the
+ // service worker has been unregistered. Currently, `testharness.js`
+ // does not support asynchronous global "tear down" logic, so this
+ // must be expressed using a dedicated `promise_test`. Because the
+ // other sub-tests in this file are declared synchronously, this test
+ // will be the final test executed.
+ promise_test(function(t) {
+ return registration.unregister();
+ });
+
+ return registration.active;
+ });
+
+ return register;
+ }, 'initialize global state');
+
+promise_test(t => {
+ return register
+ .then(function(worker) {
+ return post_and_wait_for_reply(worker, 'root-and-message');
+ })
+ .then(result => {
+ assert_equals(result.error, null);
+ assert_equals(result.value, 'root-and-message');
+ });
+ }, 'import script previously imported at worker evaluation time');
+
+promise_test(t => {
+ return register
+ .then(function(worker) {
+ return post_and_wait_for_reply(worker, 'install-and-message');
+ })
+ .then(result => {
+ assert_equals(result.error, null);
+ assert_equals(result.value, 'install-and-message');
+ });
+ }, 'import script previously imported at worker install time');
+
+promise_test(t => {
+ return register
+ .then(function(worker) {
+ return post_and_wait_for_reply(worker, 'message');
+ })
+ .then(result => {
+ assert_equals(result.error, 'NetworkError');
+ assert_equals(result.value, null);
+ });
+ }, 'import script not previously imported');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/indexeddb.https.html b/testing/web-platform/tests/service-workers/service-worker/indexeddb.https.html
new file mode 100644
index 0000000000..be9be4968f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/indexeddb.https.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<title>Service Worker: Indexed DB</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function readDB() {
+ return new Promise(function(resolve, reject) {
+ var openRequest = indexedDB.open('db');
+
+ openRequest.onerror = reject;
+ openRequest.onsuccess = function() {
+ var db = openRequest.result;
+ var tx = db.transaction('store');
+ var store = tx.objectStore('store');
+ var getRequest = store.get('key');
+
+ getRequest.onerror = function() {
+ db.close();
+ reject(getRequest.error);
+ };
+ getRequest.onsuccess = function() {
+ db.close();
+ resolve(getRequest.result);
+ };
+ };
+ });
+}
+
+function send(worker, action) {
+ return new Promise(function(resolve, reject) {
+ var messageChannel = new MessageChannel();
+ messageChannel.port1.onmessage = function(event) {
+ if (event.data.type === 'error') {
+ reject(event.data.reason);
+ }
+
+ resolve();
+ };
+
+ worker.postMessage(
+ {action: action, port: messageChannel.port2},
+ [messageChannel.port2]);
+ });
+}
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html';
+
+ return service_worker_unregister_and_register(
+ t, 'resources/indexeddb-worker.js', scope)
+ .then(function(registration) {
+ var worker = registration.installing;
+
+ promise_test(function() {
+ return registration.unregister();
+ }, 'clean up: registration');
+
+ return send(worker, 'create')
+ .then(function() {
+ promise_test(function() {
+ return new Promise(function(resolve, reject) {
+ var delete_request = indexedDB.deleteDatabase('db');
+
+ delete_request.onsuccess = resolve;
+ delete_request.onerror = reject;
+ });
+ }, 'clean up: database');
+ })
+ .then(readDB)
+ .then(function(value) {
+ assert_equals(
+ value, 'value',
+ 'The get() result should match what the worker put().');
+ });
+ });
+ }, 'Verify Indexed DB operation in a Service Worker');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/install-event-type.https.html b/testing/web-platform/tests/service-workers/service-worker/install-event-type.https.html
new file mode 100644
index 0000000000..7e74af85c3
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/install-event-type.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_install_event(worker) {
+ return new Promise(function(resolve) {
+ worker.addEventListener('statechange', function(event) {
+ if (worker.state == 'installed')
+ resolve(true);
+ else if (worker.state == 'redundant')
+ resolve(false);
+ });
+ });
+}
+
+promise_test(function(t) {
+ var script = 'resources/install-event-type-worker.js';
+ var scope = 'resources/install-event-type';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ return wait_for_install_event(registration.installing);
+ })
+ .then(function(did_install) {
+ assert_true(did_install, 'The worker was installed');
+ })
+ }, 'install event type');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/installing.https.html b/testing/web-platform/tests/service-workers/service-worker/installing.https.html
new file mode 100644
index 0000000000..0f257b6aba
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/installing.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.installing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/empty-worker.js';
+const SCOPE = 'resources/blank.html';
+
+// "installing" is set
+promise_test(async t => {
+
+ t.add_cleanup(async() => {
+ if (frame)
+ frame.remove();
+ if (registration)
+ await registration.unregister();
+ });
+
+ await service_worker_unregister(t, SCOPE);
+ const frame = await with_iframe(SCOPE);
+ const registration =
+ await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ const container = frame.contentWindow.navigator.serviceWorker;
+ assert_equals(container.controller, null, 'controller');
+ assert_equals(registration.active, null, 'registration.active');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.installing.scriptURL, normalizeURL(SCRIPT),
+ 'registration.installing.scriptURL');
+ // FIXME: Add a test for a frame created after installation.
+ // Should the existing frame ("frame") block activation?
+}, 'installing is set');
+
+// Tests that The ServiceWorker objects returned from installing attribute getter
+// that represent the same service worker are the same objects.
+promise_test(async t => {
+ const registration1 =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ const registration2 = await navigator.serviceWorker.getRegistration(SCOPE);
+ assert_equals(registration1.installing, registration2.installing,
+ 'ServiceWorkerRegistration.installing should return the ' +
+ 'same object');
+ await registration1.unregister();
+}, 'The ServiceWorker objects returned from installing attribute getter that ' +
+ 'represent the same service worker are the same objects');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/interface-requirements-sw.https.html b/testing/web-platform/tests/service-workers/service-worker/interface-requirements-sw.https.html
new file mode 100644
index 0000000000..eef868c889
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/interface-requirements-sw.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>Service Worker Global Scope Interfaces</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+// interface-requirements-worker.sub.js checks additional interface
+// requirements, on top of the basic IDL that is validated in
+// service-workers/idlharness.any.js
+service_worker_test(
+ 'resources/interface-requirements-worker.sub.js',
+ 'Interfaces and attributes in ServiceWorkerGlobalScope');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/invalid-blobtype.https.html b/testing/web-platform/tests/service-workers/service-worker/invalid-blobtype.https.html
new file mode 100644
index 0000000000..1c5920fb03
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/invalid-blobtype.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: respondWith with header value containing a null byte</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/invalid-blobtype-iframe.https.html';
+ var SCRIPT = 'resources/invalid-blobtype-worker.js';
+ var host_info = get_host_info();
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ var channel = new MessageChannel();
+ var onMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ return onMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/invalid-header.https.html b/testing/web-platform/tests/service-workers/service-worker/invalid-header.https.html
new file mode 100644
index 0000000000..1bc9769790
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/invalid-header.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Service Worker: respondWith with header value containing a null byte</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/invalid-header-iframe.https.html';
+ var SCRIPT = 'resources/invalid-header-worker.js';
+ var host_info = get_host_info();
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ var channel = new MessageChannel();
+ var onMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ return onMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/iso-latin1-header.https.html b/testing/web-platform/tests/service-workers/service-worker/iso-latin1-header.https.html
new file mode 100644
index 0000000000..c27a5f48a5
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/iso-latin1-header.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: respondWith with header value containing an ISO Latin 1 (ISO-8859-1 Character Set) string</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/iso-latin1-header-iframe.html';
+ var SCRIPT = 'resources/iso-latin1-header-worker.js';
+ var host_info = get_host_info();
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ var channel = new MessageChannel();
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ var onMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ return onMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/local-url-inherit-controller.https.html b/testing/web-platform/tests/service-workers/service-worker/local-url-inherit-controller.https.html
new file mode 100644
index 0000000000..6702abcadb
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/local-url-inherit-controller.https.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<title>Service Worker: local URL windows and workers inherit controller</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/local-url-inherit-controller-worker.js';
+const SCOPE = 'resources/local-url-inherit-controller-frame.html';
+
+async function doAsyncTest(t, opts) {
+ let name = `${opts.scheme}-${opts.child}-${opts.check}`;
+ let scope = SCOPE + '?name=' + name;
+ let reg = await service_worker_unregister_and_register(t, SCRIPT, scope);
+ add_completion_callback(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ let frame = await with_iframe(scope);
+ add_completion_callback(_ => frame.remove());
+ assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null,
+ 'frame should be controlled');
+
+ let result = await frame.contentWindow.checkChildController(opts);
+ result = result.data;
+
+ let expect = 'unexpected';
+ if (opts.check === 'controller') {
+ expect = opts.expect === 'inherit'
+ ? frame.contentWindow.navigator.serviceWorker.controller.scriptURL
+ : null;
+ } else if (opts.check === 'fetch') {
+ // The service worker FetchEvent handler will provide an "intercepted"
+ // body. If the local URL ends up with an opaque origin and is not
+ // intercepted then it will get an opaque Response. In that case it
+ // should see an empty string body.
+ expect = opts.expect === 'intercept' ? 'intercepted' : '';
+ }
+
+ assert_equals(result, expect,
+ `${opts.scheme} URL ${opts.child} should ${opts.expect} ${opts.check}`);
+}
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'blob',
+ child: 'iframe',
+ check: 'controller',
+ expect: 'inherit',
+ });
+}, 'Same-origin blob URL iframe should inherit service worker controller.');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'blob',
+ child: 'iframe',
+ check: 'fetch',
+ expect: 'intercept',
+ });
+}, 'Same-origin blob URL iframe should intercept fetch().');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'blob',
+ child: 'worker',
+ check: 'controller',
+ expect: 'inherit',
+ });
+}, 'Same-origin blob URL worker should inherit service worker controller.');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'blob',
+ child: 'worker',
+ check: 'fetch',
+ expect: 'intercept',
+ });
+}, 'Same-origin blob URL worker should intercept fetch().');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'data',
+ child: 'iframe',
+ check: 'fetch',
+ expect: 'not intercept',
+ });
+}, 'Data URL iframe should not intercept fetch().');
+
+promise_test(function(t) {
+ // Data URLs should result in an opaque origin and should probably not
+ // have access to a cross-origin service worker. See:
+ //
+ // https://github.com/w3c/ServiceWorker/issues/1262
+ //
+ return doAsyncTest(t, {
+ scheme: 'data',
+ child: 'worker',
+ check: 'controller',
+ expect: 'not inherit',
+ });
+}, 'Data URL worker should not inherit service worker controller.');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'data',
+ child: 'worker',
+ check: 'fetch',
+ expect: 'not intercept',
+ });
+}, 'Data URL worker should not intercept fetch().');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/mime-sniffing.https.html b/testing/web-platform/tests/service-workers/service-worker/mime-sniffing.https.html
new file mode 100644
index 0000000000..8175bcdf87
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/mime-sniffing.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<title>Service Worker: MIME sniffing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ const SCOPE = 'resources/blank.html?mime-sniffing';
+ const SCRIPT = 'resources/mime-sniffing-worker.js';
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(registration => {
+ add_completion_callback(() => registration.unregister());
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(_ => with_iframe(SCOPE))
+ .then(frame => {
+ add_completion_callback(() => frame.remove());
+ assert_equals(frame.contentWindow.document.body.innerText, 'test');
+ const h1 = frame.contentWindow.document.getElementById('testid');
+ assert_equals(h1.innerText,'test');
+ });
+ }, 'The response from service worker should be correctly MIME siniffed.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/current/current.https.html b/testing/web-platform/tests/service-workers/service-worker/multi-globals/current/current.https.html
new file mode 100644
index 0000000000..82a48d4099
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/current/current.https.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Current page used as a test helper</title>
diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/current/test-sw.js b/testing/web-platform/tests/service-workers/service-worker/multi-globals/current/test-sw.js
new file mode 100644
index 0000000000..e673292f2c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/current/test-sw.js
@@ -0,0 +1 @@
+// Service worker for current/ \ No newline at end of file
diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html b/testing/web-platform/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html
new file mode 100644
index 0000000000..4585f15b0f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>Incumbent page used as a test helper</title>
+
+<iframe src="../current/current.https.html" id="c"></iframe>
+<iframe src="../relevant/relevant.https.html" id="r"></iframe>
+
+<script>
+'use strict';
+
+const current = document.querySelector('#c').contentWindow;
+const relevant = document.querySelector('#r').contentWindow;
+
+window.testRegister = options => {
+ return current.navigator.serviceWorker.register.call(relevant.navigator.serviceWorker, 'test-sw.js', options);
+};
+
+window.testGetRegistration = () => {
+ return current.navigator.serviceWorker.getRegistration.call(relevant.navigator.serviceWorker, 'test-sw.js');
+};
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js b/testing/web-platform/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js
new file mode 100644
index 0000000000..e2a0e93b58
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js
@@ -0,0 +1 @@
+// Service worker for incumbent/ \ No newline at end of file
diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html b/testing/web-platform/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html
new file mode 100644
index 0000000000..44f42eda49
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Relevant page used as a test helper</title>
diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js b/testing/web-platform/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js
new file mode 100644
index 0000000000..ff44cdf086
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js
@@ -0,0 +1 @@
+// Service worker for relevant/ \ No newline at end of file
diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/test-sw.js b/testing/web-platform/tests/service-workers/service-worker/multi-globals/test-sw.js
new file mode 100644
index 0000000000..ce3c940ece
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/test-sw.js
@@ -0,0 +1 @@
+// Service worker for / \ No newline at end of file
diff --git a/testing/web-platform/tests/service-workers/service-worker/multi-globals/url-parsing.https.html b/testing/web-platform/tests/service-workers/service-worker/multi-globals/url-parsing.https.html
new file mode 100644
index 0000000000..b9dfe36343
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/multi-globals/url-parsing.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<title>register()/getRegistration() URL parsing, with multiple globals in play</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-container-register-method">
+<link rel="help" href="https://w3c.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-container-getregistration-method">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<script src="/resources/testharness.js"></script>
+<script src="../resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+
+<!-- This is the entry global -->
+
+<iframe src="incumbent/incumbent.https.html"></iframe>
+
+<script>
+'use strict';
+
+const loadPromise = new Promise(resolve => {
+ window.addEventListener('load', () => resolve());
+});
+
+promise_test(t => {
+ let registration;
+
+ return loadPromise.then(() => {
+ return frames[0].testRegister();
+ }).then(r => {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ }).then(_ => {
+ assert_equals(registration.active.scriptURL, normalizeURL('relevant/test-sw.js'), 'the script URL should be parsed against the relevant global');
+ assert_equals(registration.scope, normalizeURL('relevant/'), 'the default scope URL should be parsed against the parsed script URL');
+
+ return registration.unregister();
+ });
+}, 'register should use the relevant global of the object it was called on to resolve the script URL and the default scope URL');
+
+promise_test(t => {
+ let registration;
+
+ return loadPromise.then(() => {
+ return frames[0].testRegister({ scope: 'scope' });
+ }).then(r => {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ }).then(_ => {
+ assert_equals(registration.active.scriptURL, normalizeURL('relevant/test-sw.js'), 'the script URL should be parsed against the relevant global');
+ assert_equals(registration.scope, normalizeURL('relevant/scope'), 'the given scope URL should be parsed against the relevant global');
+
+ return registration.unregister();
+ });
+}, 'register should use the relevant global of the object it was called on to resolve the script URL and the given scope URL');
+
+promise_test(t => {
+ let registration;
+
+ return loadPromise.then(() => {
+ return navigator.serviceWorker.register(normalizeURL('relevant/test-sw.js'));
+ }).then(r => {
+ registration = r;
+ return frames[0].testGetRegistration();
+ })
+ .then(gottenRegistration => {
+ assert_not_equals(registration, null, 'the registration should not be null');
+ assert_not_equals(gottenRegistration, null, 'the registration from the other frame should not be null');
+ assert_equals(gottenRegistration.scope, registration.scope,
+ 'the retrieved registration\'s scope should be equal to the original\'s scope');
+
+ return registration.unregister();
+ });
+}, 'getRegistration should use the relevant global of the object it was called on to resolve the script URL');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/multipart-image.https.html b/testing/web-platform/tests/service-workers/service-worker/multipart-image.https.html
new file mode 100644
index 0000000000..00c20d25f9
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/multipart-image.https.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<title>Tests for cross-origin multipart image returned by service worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+// This tests loading a multipart image via service worker. The service worker responds with
+// an opaque or a non-opaque response. The content of opaque response should not be readable.
+
+const script = 'resources/multipart-image-worker.js';
+const scope = 'resources/multipart-image-iframe.html';
+let frame;
+
+function check_image_data(data) {
+ assert_equals(data[0], 255);
+ assert_equals(data[1], 0);
+ assert_equals(data[2], 0);
+ assert_equals(data[3], 255);
+}
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ promise_test(() => {
+ if (frame) {
+ frame.remove();
+ }
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => with_iframe(scope))
+ .then(f => {
+ frame = f;
+ });
+ }, 'initialize global state');
+
+promise_test(t => {
+ return frame.contentWindow.load_multipart_image('same-origin-multipart-image')
+ .then(img => frame.contentWindow.get_image_data(img))
+ .then(img_data => {
+ check_image_data(img_data.data);
+ });
+ }, 'same-origin multipart image via SW should be readable');
+
+promise_test(t => {
+ return frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-cors-approved')
+ .then(img => frame.contentWindow.get_image_data(img))
+ .then(img_data => {
+ check_image_data(img_data.data);
+ });
+ }, 'cross-origin multipart image via SW with approved CORS should be readable');
+
+promise_test(t => {
+ return frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-no-cors')
+ .then(img => {
+ assert_throws_dom('SecurityError', frame.contentWindow.DOMException,
+ () => frame.contentWindow.get_image_data(img));
+ });
+ }, 'cross-origin multipart image with no-cors via SW should not be readable');
+
+promise_test(t => {
+ const promise = frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-cors-rejected');
+ return promise_rejects_dom(t, 'NetworkError', frame.contentWindow.DOMException, promise);
+ }, 'cross-origin multipart image via SW with rejected CORS should fail to load');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/multiple-register.https.html b/testing/web-platform/tests/service-workers/service-worker/multiple-register.https.html
new file mode 100644
index 0000000000..752e132fc1
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/multiple-register.https.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker_url = 'resources/empty-worker.js';
+
+async_test(function(t) {
+ var scope = 'resources/scope/subsequent-register-from-same-window';
+ var registration;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(worker_url, { scope: scope });
+ })
+ .then(function(new_registration) {
+ assert_equals(new_registration, registration,
+ 'register should resolve to the same registration');
+ assert_equals(new_registration.active, registration.active,
+ 'register should resolve to the same worker');
+ assert_equals(new_registration.active.state, 'activated',
+ 'the worker should be in state "activated"');
+ return registration.unregister();
+ })
+ .then(function() { t.done(); })
+ .catch(unreached_rejection(t));
+}, 'Subsequent registrations resolve to the same registration object');
+
+async_test(function(t) {
+ var scope = 'resources/scope/subsequent-register-from-different-iframe';
+ var frame;
+ var registration;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() { return with_iframe('resources/404.py'); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ 'empty-worker.js',
+ { scope: 'scope/subsequent-register-from-different-iframe' });
+ })
+ .then(function(new_registration) {
+ assert_not_equals(
+ registration, new_registration,
+ 'register should resolve to a different registration');
+ assert_equals(
+ registration.scope, new_registration.scope,
+ 'registrations should have the same scope');
+
+ assert_equals(
+ registration.installing, null,
+ 'installing worker should be null');
+ assert_equals(
+ new_registration.installing, null,
+ 'installing worker should be null');
+ assert_equals(
+ registration.waiting, null,
+ 'waiting worker should be null')
+ assert_equals(
+ new_registration.waiting, null,
+ 'waiting worker should be null')
+
+ assert_not_equals(
+ registration.active, new_registration.active,
+ 'registration should have a different active worker');
+ assert_equals(
+ registration.active.scriptURL,
+ new_registration.active.scriptURL,
+ 'active workers should have the same script URL');
+ assert_equals(
+ registration.active.state,
+ new_registration.active.state,
+ 'active workers should be in the same state');
+
+ frame.remove();
+ return registration.unregister();
+ })
+ .then(function() { t.done(); })
+ .catch(unreached_rejection(t));
+}, 'Subsequent registrations from a different iframe resolve to the ' +
+ 'different registration object but they refer to the same ' +
+ 'registration and workers');
+
+async_test(function(t) {
+ var scope = 'resources/scope/concurrent-register';
+
+ service_worker_unregister(t, scope)
+ .then(function() {
+ var promises = [];
+ for (var i = 0; i < 10; ++i) {
+ promises.push(navigator.serviceWorker.register(worker_url,
+ { scope: scope }));
+ }
+ return Promise.all(promises);
+ })
+ .then(function(registrations) {
+ registrations.forEach(function(registration) {
+ assert_equals(registration, registrations[0],
+ 'register should resolve to the same registration');
+ });
+ return registrations[0].unregister();
+ })
+ .then(function() { t.done(); })
+ .catch(unreached_rejection(t));
+}, 'Concurrent registrations resolve to the same registration object');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/multiple-update.https.html b/testing/web-platform/tests/service-workers/service-worker/multiple-update.https.html
new file mode 100644
index 0000000000..6a83f73a05
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/multiple-update.https.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!-- In Bug 1217367, we will try to merge update events for same registration
+ if possible. This testcase is used to make sure the optimization algorithm
+ doesn't go wrong. -->
+<title>Service Worker: Trigger multiple updates</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var script = 'resources/update-nocookie-worker.py';
+ var scope = 'resources/scope/update';
+ var expected_url = normalizeURL(script);
+ var registration;
+
+ return service_worker_unregister_and_register(t, expected_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ // Test single update works before triggering multiple update events
+ return Promise.all([registration.update(),
+ wait_for_update(t, registration)]);
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'new installing should be set after update resolves.');
+ assert_equals(registration.waiting, null,
+ 'waiting should still be null after update resolves.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after update found.');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing should be null after installing.');
+ if (registration.waiting) {
+ assert_equals(registration.waiting.scriptURL, expected_url,
+ 'waiting should be set after installing.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after installing.');
+ return wait_for_state(t, registration.waiting, 'activated');
+ }
+ })
+ .then(function() {
+ // Test triggering multiple update events at the same time.
+ var promiseList = [];
+ const burstUpdateCount = 10;
+ for (var i = 0; i < burstUpdateCount; i++) {
+ promiseList.push(registration.update());
+ }
+ promiseList.push(wait_for_update(t, registration));
+ return Promise.all(promiseList);
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'new installing should be set after update resolves.');
+ assert_equals(registration.waiting, null,
+ 'waiting should still be null after update resolves.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after update found.');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing should be null after installing.');
+ if (registration.waiting) {
+ assert_equals(registration.waiting.scriptURL, expected_url,
+ 'waiting should be set after installing.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after installing.');
+ return wait_for_state(t, registration.waiting, 'activated');
+ }
+ })
+ .then(function() {
+ // Test update still works after handling update event burst.
+ return Promise.all([registration.update(),
+ wait_for_update(t, registration)]);
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'new installing should be set after update resolves.');
+ assert_equals(registration.waiting, null,
+ 'waiting should be null after activated.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after update found.');
+ });
+ }, 'Trigger multiple updates.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigate-window.https.html b/testing/web-platform/tests/service-workers/service-worker/navigate-window.https.html
new file mode 100644
index 0000000000..46d32a48a0
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigate-window.https.html
@@ -0,0 +1,151 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigate a Window</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+var BASE_URL = host_info['HTTPS_ORIGIN'] + base_path();
+
+function wait_for_message(msg) {
+ return new Promise(function(resolve, reject) {
+ window.addEventListener('message', function onMsg(evt) {
+ if (evt.data.type === msg) {
+ resolve();
+ }
+ });
+ });
+}
+
+function with_window(url) {
+ var win = window.open(url);
+ return wait_for_message('LOADED').then(_ => win);
+}
+
+function navigate_window(win, url) {
+ win.location = url;
+ return wait_for_message('LOADED').then(_ => win);
+}
+
+function reload_window(win) {
+ win.location.reload();
+ return wait_for_message('LOADED').then(_ => win);
+}
+
+function go_back(win) {
+ win.history.back();
+ return wait_for_message('PAGESHOW').then(_ => win);
+}
+
+function go_forward(win) {
+ win.history.forward();
+ return wait_for_message('PAGESHOW').then(_ => win);
+}
+
+function get_clients(win, sw, opts) {
+ return new Promise((resolve, reject) => {
+ win.navigator.serviceWorker.addEventListener('message', function onMsg(evt) {
+ win.navigator.serviceWorker.removeEventListener('message', onMsg);
+ if (evt.data.type === 'success') {
+ resolve(evt.data.detail);
+ } else {
+ reject(evt.data.detail);
+ }
+ });
+ sw.postMessage({ type: 'GET_CLIENTS', opts: (opts || {}) });
+ });
+}
+
+function compare_urls(a, b) {
+ return a.url < b.url ? -1 : b.url < a.url ? 1 : 0;
+}
+
+function validate_window(win, url, opts) {
+ return win.navigator.serviceWorker.getRegistration(url)
+ .then(reg => {
+ // In order to compare service worker instances we need to
+ // make sure the DOM object is owned by the same global; the
+ // opened window in this case.
+ assert_equals(win.navigator.serviceWorker.controller, reg.active,
+ 'window should be controlled by service worker');
+ return get_clients(win, reg.active, opts);
+ })
+ .then(resultList => {
+ // We should always see our controlled window.
+ var expected = [
+ { url: url, frameType: 'auxiliary' }
+ ];
+ // If we are including uncontrolled windows, then we might see the
+ // test window itself and the test harness.
+ if (opts.includeUncontrolled) {
+ expected.push({ url: BASE_URL + 'navigate-window.https.html',
+ frameType: 'auxiliary' });
+ expected.push({
+ url: host_info['HTTPS_ORIGIN'] + '/testharness_runner.html',
+ frameType: 'top-level' });
+ }
+
+ assert_equals(resultList.length, expected.length,
+ 'expected number of clients');
+
+ expected.sort(compare_urls);
+ resultList.sort(compare_urls);
+
+ for (var i = 0; i < resultList.length; ++i) {
+ assert_equals(resultList[i].url, expected[i].url,
+ 'client should have expected url');
+ assert_equals(resultList[i].frameType, expected[i].frameType,
+ 'client should have expected frame type');
+ }
+ return win;
+ })
+}
+
+promise_test(function(t) {
+ var worker = BASE_URL + 'resources/navigate-window-worker.js';
+ var scope = BASE_URL + 'resources/loaded.html?navigate-window-controlled';
+ var url1 = scope + '&q=1';
+ var url2 = scope + '&q=2';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(reg => wait_for_state(t, reg.installing, 'activated') )
+ .then(___ => with_window(url1))
+ .then(win => validate_window(win, url1, { includeUncontrolled: false }))
+ .then(win => navigate_window(win, url2))
+ .then(win => validate_window(win, url2, { includeUncontrolled: false }))
+ .then(win => go_back(win))
+ .then(win => validate_window(win, url1, { includeUncontrolled: false }))
+ .then(win => go_forward(win))
+ .then(win => validate_window(win, url2, { includeUncontrolled: false }))
+ .then(win => reload_window(win))
+ .then(win => validate_window(win, url2, { includeUncontrolled: false }))
+ .then(win => win.close())
+ .catch(unreached_rejection(t))
+ .then(___ => service_worker_unregister(t, scope))
+ }, 'Clients.matchAll() should not show an old window as controlled after ' +
+ 'it navigates.');
+
+promise_test(function(t) {
+ var worker = BASE_URL + 'resources/navigate-window-worker.js';
+ var scope = BASE_URL + 'resources/loaded.html?navigate-window-uncontrolled';
+ var url1 = scope + '&q=1';
+ var url2 = scope + '&q=2';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(reg => wait_for_state(t, reg.installing, 'activated') )
+ .then(___ => with_window(url1))
+ .then(win => validate_window(win, url1, { includeUncontrolled: true }))
+ .then(win => navigate_window(win, url2))
+ .then(win => validate_window(win, url2, { includeUncontrolled: true }))
+ .then(win => go_back(win))
+ .then(win => validate_window(win, url1, { includeUncontrolled: true }))
+ .then(win => go_forward(win))
+ .then(win => validate_window(win, url2, { includeUncontrolled: true }))
+ .then(win => reload_window(win))
+ .then(win => validate_window(win, url2, { includeUncontrolled: true }))
+ .then(win => win.close())
+ .catch(unreached_rejection(t))
+ .then(___ => service_worker_unregister(t, scope))
+ }, 'Clients.matchAll() should not show an old window after it navigates.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-headers.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-headers.https.html
new file mode 100644
index 0000000000..a4b52035e2
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-headers.https.html
@@ -0,0 +1,819 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Navigation Post Request Origin Header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const script = new URL('./resources/fetch-rewrite-worker.js', self.location);
+const base = './resources/navigation-headers-server.py';
+const scope = base + '?with-sw';
+let registration;
+
+async function post_and_get_headers(t, form_host, method, swaction,
+ redirect_hosts=[]) {
+ if (swaction === 'navpreload') {
+ assert_true('navigationPreload' in registration,
+ 'navigation preload must be supported');
+ }
+ let target_string;
+ if (swaction === 'no-sw') {
+ target_string = base + '?no-sw';
+ } else if (swaction === 'fallback') {
+ target_string = `${scope}&ignore`;
+ } else {
+ target_string = `${scope}&${swaction}`;
+ }
+ let target = new URL(target_string, self.location);
+
+ for (let i = redirect_hosts.length - 1; i >= 0; --i) {
+ const redirect_url = new URL('./resources/redirect.py', self.location);
+ redirect_url.hostname = redirect_hosts[i];
+ redirect_url.search = `?Status=307&Redirect=${encodeURIComponent(target)}`;
+ target = redirect_url;
+ }
+
+ let popup_url_path;
+ if (method === 'GET') {
+ popup_url_path = './resources/location-setter.html';
+ } else if (method === 'POST') {
+ popup_url_path = './resources/form-poster.html';
+ }
+
+ const popup_url = new URL(popup_url_path, self.location);
+ popup_url.hostname = form_host;
+ popup_url.search = `?target=${encodeURIComponent(target.href)}`;
+
+ const message_promise = new Promise(resolve => {
+ self.addEventListener('message', evt => {
+ resolve(evt.data);
+ });
+ });
+
+ const frame = await with_iframe(popup_url);
+ t.add_cleanup(() => frame.remove());
+
+ return await message_promise;
+}
+
+const SAME_ORIGIN = new URL(self.location.origin);
+const SAME_SITE = new URL(get_host_info().HTTPS_REMOTE_ORIGIN);
+const CROSS_SITE = new URL(get_host_info().HTTPS_NOTSAMESITE_ORIGIN);
+
+promise_test(async t => {
+ registration = await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ if (registration.navigationPreload)
+ await registration.navigationPreload.enable();
+}, 'Setup service worker');
+
+//
+// Origin and referer headers
+//
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with navpreload service worker sets correct ' +
+ 'origin and referer headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'GET Navigation, same-origin with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result.origin, SAME_SITE.origin, 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'POST Navigation, same-site with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result.origin, SAME_SITE.origin, 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'POST Navigation, same-site with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'fallback');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'fallback');
+ assert_equals(result.origin, SAME_SITE.origin, 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'POST Navigation, same-site with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with navpreload service worker sets correct ' +
+ 'origin and referer headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'change-request');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'GET Navigation, same-site with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'change-request');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-site with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result.origin, CROSS_SITE.origin, 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'POST Navigation, cross-site with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result.origin, CROSS_SITE.origin, 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'POST Navigation, cross-site with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'fallback');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'fallback');
+ assert_equals(result.origin, CROSS_SITE.origin, 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'POST Navigation, cross-site with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with navpreload service worker sets correct ' +
+ 'origin and referer headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'change-request');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'GET Navigation, cross-site with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'change-request');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, cross-site with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+//
+// Origin and referer header tests using redirects
+//
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw', [SAME_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and no service worker ' +
+ 'sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough', [SAME_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and passthrough service ' +
+ 'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback', [SAME_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and fallback service ' +
+ 'worker sets correct origin and referer headers.');
+
+// There is no navpreload case because it does not work with POST requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request', [SAME_SITE.hostname]);
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and change-request service ' +
+ 'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw', [CROSS_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and no service worker ' +
+ 'sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough', [CROSS_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and passthrough service ' +
+ 'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback', [CROSS_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and fallback service ' +
+ 'worker sets correct origin and referer headers.');
+
+// There is no navpreload case because it does not work with POST requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request', [CROSS_SITE.hostname]);
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and change-request service ' +
+ 'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and no service worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and passthrough service worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and fallback service worker sets correct origin and referer headers.');
+
+// There is no navpreload case because it does not work with POST requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and change-request service worker sets correct origin and referer headers.');
+
+//
+// Sec-Fetch-* Headers (separated since not all browsers implement them)
+//
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with navpreload service worker sets correct ' +
+ 'sec-fetch headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with navpreload service worker sets correct ' +
+ 'sec-fetch headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with navpreload service worker sets correct ' +
+ 'sec-fetch headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+//
+// Sec-Fetch-* header tests using redirects
+//
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and no service worker ' +
+ 'sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and passthrough service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and fallback service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and navpreload service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and change-request service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and no service worker ' +
+ 'sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and passthrough service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and fallback service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and navpreload service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and change-request service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and no service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and passthrough service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and fallback service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and navpreload service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and change-request service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ await registration.unregister();
+}, 'Cleanup service worker');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html
new file mode 100644
index 0000000000..ec74282ac3
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload with chunked encoding</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/broken-chunked-encoding-worker.js';
+ var scope = 'resources/broken-chunked-encoding-scope.asis';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'PASS: preloadResponse resolved');
+ });
+ }, 'FetchEvent#preloadResponse resolves even if the body is sent with broken chunked encoding.');
+
+promise_test(t => {
+ var script = 'resources/broken-chunked-encoding-worker.js';
+ var scope = 'resources/chunked-encoding-scope.py?use_broken_body';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'PASS: preloadResponse resolved');
+ });
+ }, 'FetchEvent#preloadResponse resolves even if the body is sent with broken chunked encoding with some delays');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html
new file mode 100644
index 0000000000..830ce32cea
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload with chunked encoding</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/chunked-encoding-worker.js';
+ var scope = 'resources/chunked-encoding-scope.py';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ '0123456789');
+ });
+ }, 'Navigation Preload must work with chunked encoding.');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html
new file mode 100644
index 0000000000..7e8aacdd36
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload empty response body</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/empty-preload-response-body-worker.js';
+ var scope = 'resources/empty-preload-response-body-scope.html';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ '[]');
+ });
+ }, 'Navigation Preload empty response body.');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/get-state.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/get-state.https.html
new file mode 100644
index 0000000000..08e2f4976c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/get-state.https.html
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>NavigationPreloadManager.getState</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script src="resources/helpers.js"></script>
+<body>
+<script>
+function post_and_wait_for_reply(worker, message) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => { resolve(e.data); };
+ worker.postMessage(message);
+ });
+}
+
+promise_test(t => {
+ const scope = '../resources/get-state';
+ const script = '../resources/empty-worker.js';
+ var np;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ np = r.navigationPreload;
+ add_completion_callback(() => r.unregister());
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => np.getState())
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'true', 'default state');
+ return np.enable();
+ })
+ .then(result => {
+ assert_equals(result, undefined,
+ 'enable() should resolve to undefined');
+ return np.getState();
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, true, 'true',
+ 'state after enable()');
+ return np.disable();
+ })
+ .then(result => {
+ assert_equals(result, undefined,
+ 'disable() should resolve to undefined');
+ return np.getState();
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'true',
+ 'state after disable()');
+ return np.setHeaderValue('dreams that cannot be');
+ })
+ .then(result => {
+ assert_equals(result, undefined,
+ 'setHeaderValue() should resolve to undefined');
+ return np.getState();
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'dreams that cannot be',
+ 'state after setHeaderValue()');
+ return np.setHeaderValue('').then(() => np.getState());
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, false, '',
+ 'after setHeaderValue to empty string');
+ return np.setHeaderValue(null).then(() => np.getState());
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'null',
+ 'after setHeaderValue to null');
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue('what\uDC00\uD800this'),
+ 'setHeaderValue() should throw if passed surrogates');
+ })
+ .then(() => {
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue('zer\0o'),
+ 'setHeaderValue() should throw if passed \\0');
+ })
+ .then(() => {
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue('\rcarriage'),
+ 'setHeaderValue() should throw if passed \\r');
+ })
+ .then(() => {
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue('newline\n'),
+ 'setHeaderValue() should throw if passed \\n');
+ })
+ .then(() => {
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue(),
+ 'setHeaderValue() should throw if passed undefined');
+ })
+ .then(() => np.enable().then(() => np.getState()))
+ .then(state => {
+ expect_navigation_preload_state(state, true, 'null',
+ 'enable() should not change header');
+ });
+ }, 'getState');
+
+// This test sends commands to a worker to call enable()/disable()/getState().
+// It checks the results from the worker and verifies that they match the
+// navigation preload state accessible from the page.
+promise_test(t => {
+ const scope = 'resources/get-state-worker';
+ const script = 'resources/get-state-worker.js';
+ var worker;
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ registration = r;
+ add_completion_callback(() => registration.unregister());
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(() => {
+ // Call getState().
+ return post_and_wait_for_reply(worker, 'getState');
+ })
+ .then(data => {
+ return Promise.all([data, registration.navigationPreload.getState()]);
+ })
+ .then(states => {
+ expect_navigation_preload_state(states[0], false, 'true',
+ 'default state (from worker)');
+ expect_navigation_preload_state(states[1], false, 'true',
+ 'default state (from page)');
+ // Call enable() and then getState().
+ return post_and_wait_for_reply(worker, 'enable');
+ })
+ .then(data => {
+ assert_equals(data, undefined, 'enable() should resolve to undefined');
+ return Promise.all([
+ post_and_wait_for_reply(worker, 'getState'),
+ registration.navigationPreload.getState()
+ ]);
+ })
+ .then(states => {
+ expect_navigation_preload_state(states[0], true, 'true',
+ 'state after enable() (from worker)');
+ expect_navigation_preload_state(states[1], true, 'true',
+ 'state after enable() (from page)');
+ // Call disable() and then getState().
+ return post_and_wait_for_reply(worker, 'disable');
+ })
+ .then(data => {
+ assert_equals(data, undefined,
+ '.disable() should resolve to undefined');
+ return Promise.all([
+ post_and_wait_for_reply(worker, 'getState'),
+ registration.navigationPreload.getState()
+ ]);
+ })
+ .then(states => {
+ expect_navigation_preload_state(states[0], false, 'true',
+ 'state after disable() (from worker)');
+ expect_navigation_preload_state(states[1], false, 'true',
+ 'state after disable() (from page)');
+ return post_and_wait_for_reply(worker, 'setHeaderValue');
+ })
+ .then(data => {
+ assert_equals(data, undefined,
+ '.setHeaderValue() should resolve to undefined');
+ return Promise.all([
+ post_and_wait_for_reply(worker, 'getState'),
+ registration.navigationPreload.getState()]);
+ })
+ .then(states => {
+ expect_navigation_preload_state(
+ states[0], false, 'insightful',
+ 'state after setHeaderValue() (from worker)');
+ expect_navigation_preload_state(
+ states[1], false, 'insightful',
+ 'state after setHeaderValue() (from page)');
+ });
+ }, 'getState from a worker');
+
+// This tests navigation preload API when there is no active worker. It calls
+// the API from the main page and then from the worker itself.
+promise_test(t => {
+ const scope = 'resources/wait-for-activate-worker';
+ const script = 'resources/wait-for-activate-worker.js';
+ var registration;
+ var np;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ registration = r;
+ np = registration.navigationPreload;
+ add_completion_callback(() => registration.unregister());
+ return Promise.all([
+ promise_rejects_dom(
+ t, 'InvalidStateError', np.enable(),
+ 'enable should reject if there is no active worker'),
+ promise_rejects_dom(
+ t, 'InvalidStateError', np.disable(),
+ 'disable should reject if there is no active worker'),
+ promise_rejects_dom(
+ t, 'InvalidStateError', np.setHeaderValue('umm'),
+ 'setHeaderValue should reject if there is no active worker')]);
+ })
+ .then(() => np.getState())
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'true',
+ 'state before activation');
+ return post_and_wait_for_reply(registration.installing, 'ping');
+ })
+ .then(result => assert_equals(result, 'PASS'));
+ }, 'no active worker');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html
new file mode 100644
index 0000000000..392e5c14dc
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>ServiceWorker: navigator.serviceWorker.navigationPreload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script src="resources/helpers.js"></script>
+<script>
+promise_test(async t => {
+ const SCRIPT = '../resources/empty-worker.js';
+ const SCOPE = '../resources/navigationpreload';
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ const navigationPreload = registration.navigationPreload;
+ assert_true(navigationPreload instanceof NavigationPreloadManager,
+ 'ServiceWorkerRegistration.navigationPreload');
+ await registration.unregister();
+}, "The navigationPreload attribute must return service worker " +
+ "registration's NavigationPreloadManager object.");
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/redirect.https.html
new file mode 100644
index 0000000000..5970f053e3
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/redirect.https.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload redirect response</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+
+function check_opaqueredirect(response_info, scope) {
+ assert_equals(response_info.type, 'opaqueredirect');
+ assert_equals(response_info.url, '' + new URL(scope, location));
+ assert_equals(response_info.status, 0);
+ assert_equals(response_info.ok, false);
+ assert_equals(response_info.statusText, '');
+ assert_equals(response_info.headers.length, 0);
+}
+
+function redirect_response_test(t, scope, expected_body, expected_urls) {
+ var script = 'resources/redirect-worker.js';
+ var registration;
+ var message_resolvers = [];
+ function wait_for_message(count) {
+ var promises = [];
+ message_resolvers = [];
+ for (var i = 0; i < count; ++i) {
+ promises.push(new Promise(resolve => message_resolvers.push(resolve)));
+ }
+ return promises;
+ }
+ function on_message(e) {
+ var resolve = message_resolvers.shift();
+ if (resolve)
+ resolve(e.data);
+ }
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(reg => {
+ registration = reg;
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope + '&base'))
+ .then(frame => {
+ assert_equals(frame.contentDocument.body.textContent, 'OK');
+ frame.contentWindow.navigator.serviceWorker.onmessage = on_message;
+ return Promise.all(wait_for_message(expected_urls.length)
+ .concat(with_iframe(scope)));
+ })
+ .then(results => {
+ var frame = results[expected_urls.length];
+ assert_equals(frame.contentDocument.body.textContent, expected_body);
+ for (var i = 0; i < expected_urls.length; ++i) {
+ check_opaqueredirect(results[i], expected_urls[i]);
+ }
+ frame.remove();
+ return registration.unregister();
+ });
+}
+
+promise_test(t => {
+ return redirect_response_test(
+ t,
+ 'resources/redirect-scope.py?type=normal',
+ 'redirected\n',
+ ['resources/redirect-scope.py?type=normal']);
+ }, 'Navigation Preload redirect response.');
+
+promise_test(t => {
+ return redirect_response_test(
+ t,
+ 'resources/redirect-scope.py?type=no-location',
+ '',
+ ['resources/redirect-scope.py?type=no-location']);
+ }, 'Navigation Preload no-location redirect response.');
+
+promise_test(t => {
+ return redirect_response_test(
+ t,
+ 'resources/redirect-scope.py?type=no-location-with-body',
+ 'BODY',
+ ['resources/redirect-scope.py?type=no-location-with-body']);
+ }, 'Navigation Preload no-location redirect response with body.');
+
+promise_test(t => {
+ return redirect_response_test(
+ t,
+ 'resources/redirect-scope.py?type=redirect-to-scope',
+ 'redirected\n',
+ ['resources/redirect-scope.py?type=redirect-to-scope',
+ 'resources/redirect-scope.py?type=redirect-to-scope2',
+ 'resources/redirect-scope.py?type=redirect-to-scope3',]);
+ }, 'Navigation Preload redirect to the same scope.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/request-headers.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/request-headers.https.html
new file mode 100644
index 0000000000..0964201021
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/request-headers.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload request headers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/request-headers-worker.js';
+ var scope = 'resources/request-headers-scope.py';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ var headers = JSON.parse(frame.contentDocument.body.textContent);
+ assert_true(
+ 'SERVICE-WORKER-NAVIGATION-PRELOAD' in headers,
+ 'The Navigation Preload request must specify a ' +
+ '"Service-Worker-Navigation-Preload" header.');
+ assert_array_equals(
+ headers['SERVICE-WORKER-NAVIGATION-PRELOAD'],
+ ['hello'],
+ 'The Navigation Preload request must specify the correct value ' +
+ 'for the "Service-Worker-Navigation-Preload" header.');
+ assert_true(
+ 'UPGRADE-INSECURE-REQUESTS' in headers,
+ 'The Navigation Preload request must specify an ' +
+ '"Upgrade-Insecure-Requests" header.');
+ assert_array_equals(
+ headers['UPGRADE-INSECURE-REQUESTS'],
+ ['1'],
+ 'The Navigation Preload request must specify the correct value ' +
+ 'for the "Upgrade-Insecure-Requests" header.');
+ });
+ }, 'Navigation Preload request headers.');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html
new file mode 100644
index 0000000000..468a1f51e8
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload Resource Timing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+
+function check_timing_entry(entry, url, decodedBodySize, encodedBodySize) {
+ assert_equals(entry.name, url, 'The entry name of '+ url);
+
+ assert_equals(
+ entry.entryType, 'resource',
+ 'The entryType of preload response timing entry must be "resource' +
+ '" :' + url);
+ assert_equals(
+ entry.initiatorType, 'navigation',
+ 'The initiatorType of preload response timing entry must be ' +
+ '"navigation":' + url);
+
+ // If the server returns the redirect response, |decodedBodySize| is null and
+ // |entry.decodedBodySize| should be 0. Otherwise |entry.decodedBodySize| must
+ // same as |decodedBodySize|
+ assert_equals(
+ entry.decodedBodySize, Number(decodedBodySize),
+ 'decodedBodySize must same as the decoded size in the server:' + url);
+
+ // If the server returns the redirect response, |encodedBodySize| is null and
+ // |entry.encodedBodySize| should be 0. Otherwise |entry.encodedBodySize| must
+ // same as |encodedBodySize|
+ assert_equals(
+ entry.encodedBodySize, Number(encodedBodySize),
+ 'encodedBodySize must same as the encoded size in the server:' + url);
+
+ assert_greater_than(
+ entry.transferSize, entry.decodedBodySize,
+ 'transferSize must greater then encodedBodySize.');
+
+ assert_greater_than(entry.startTime, 0, 'startTime of ' + url);
+ assert_greater_than_equal(entry.fetchStart, entry.startTime,
+ 'fetchStart >= startTime of ' + url);
+ assert_greater_than_equal(entry.domainLookupStart, entry.fetchStart,
+ 'domainLookupStart >= fetchStart of ' + url);
+ assert_greater_than_equal(entry.domainLookupEnd, entry.domainLookupStart,
+ 'domainLookupEnd >= domainLookupStart of ' + url);
+ assert_greater_than_equal(entry.connectStart, entry.domainLookupEnd,
+ 'connectStart >= domainLookupEnd of ' + url);
+ assert_greater_than_equal(entry.connectEnd, entry.connectStart,
+ 'connectEnd >= connectStart of ' + url);
+ assert_greater_than_equal(entry.requestStart, entry.connectEnd,
+ 'requestStart >= connectEnd of ' + url);
+ assert_greater_than_equal(entry.responseStart, entry.requestStart,
+ 'domainLookupStart >= requestStart of ' + url);
+ assert_greater_than_equal(entry.responseEnd, entry.responseStart,
+ 'responseEnd >= responseStart of ' + url);
+ assert_greater_than(entry.duration, 0, 'duration of ' + url);
+}
+
+promise_test(t => {
+ var script = 'resources/resource-timing-worker.js';
+ var scope = 'resources/resource-timing-scope.py';
+ var registration;
+ var frames = [];
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(reg => {
+ registration = reg;
+ add_completion_callback(_ => registration.unregister());
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(_ => with_iframe(scope + '?type=normal'))
+ .then(frame => {
+ frames.push(frame);
+ return with_iframe(scope + '?type=redirect');
+ })
+ .then(frame => {
+ frames.push(frame);
+ frames.forEach(frame => {
+ var result = JSON.parse(frame.contentDocument.body.textContent);
+ assert_equals(
+ result.timingEntries.length, 1,
+ 'performance.getEntriesByName() must returns one ' +
+ 'PerformanceResourceTiming entry for the navigation preload.');
+ var entry = result.timingEntries[0];
+ check_timing_entry(entry, frame.src, result.decodedBodySize,
+ result.encodedBodySize);
+ frame.remove();
+ });
+ return registration.unregister();
+ });
+ }, 'Navigation Preload Resource Timing.');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis
new file mode 100644
index 0000000000..2a719536fb
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis
@@ -0,0 +1,6 @@
+HTTP/1.1 200 OK
+Content-type: text/html; charset=UTF-8
+Transfer-encoding: chunked
+
+hello
+world
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js
new file mode 100644
index 0000000000..7a453e4055
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js
@@ -0,0 +1,11 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse
+ .then(
+ _ => new Response('PASS: preloadResponse resolved'),
+ _ => new Response('FAIL: preloadResponse rejected')));
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py
new file mode 100644
index 0000000000..659c4d8cdf
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py
@@ -0,0 +1,19 @@
+import time
+
+def main(request, response):
+ use_broken_body = b'use_broken_body' in request.GET
+
+ response.add_required_headers = False
+ response.writer.write_status(200)
+ response.writer.write_header(b"Content-type", b"text/html; charset=UTF-8")
+ response.writer.write_header(b"Transfer-encoding", b"chunked")
+ response.writer.end_headers()
+
+ for idx in range(10):
+ if use_broken_body:
+ response.writer.write(u"%s\n%s\n" % (len(str(idx)), idx))
+ else:
+ response.writer.write(u"%s\r\n%s\r\n" % (len(str(idx)), idx))
+ time.sleep(0.001)
+
+ response.writer.write(u"0\r\n\r\n")
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js
new file mode 100644
index 0000000000..f30e5ed274
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js
@@ -0,0 +1,8 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse);
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/cookie.py b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/cookie.py
new file mode 100644
index 0000000000..30a1dd498a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/cookie.py
@@ -0,0 +1,20 @@
+def main(request, response):
+ """
+ Returns a response with a Set-Cookie header based on the query params.
+ The body will be "1" if the cookie is present in the request and `drop` parameter is "0",
+ otherwise the body will be "0".
+ """
+ same_site = request.GET.first(b"same-site")
+ cookie_name = request.GET.first(b"cookie-name")
+ drop = request.GET.first(b"drop")
+ cookie_in_request = b"0"
+ cookie = b"%s=1; Secure; SameSite=%s" % (cookie_name, same_site)
+
+ if drop == b"1":
+ cookie += b"; Max-Age=0"
+
+ if request.cookies.get(cookie_name):
+ cookie_in_request = request.cookies[cookie_name].value
+
+ headers = [(b'Content-Type', b'text/html'), (b'Set-Cookie', cookie)]
+ return (200, headers, cookie_in_request)
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js
new file mode 100644
index 0000000000..48c14b7916
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(
+ event.preloadResponse
+ .then(res => res.text())
+ .then(text => {
+ return new Response(
+ '<body>[' + text + ']</body>',
+ {headers: [['content-type', 'text/html']]});
+ }));
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js
new file mode 100644
index 0000000000..a14ffb4faa
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js
@@ -0,0 +1,21 @@
+// This worker listens for commands from the page and messages back
+// the result.
+
+function handle(message) {
+ const np = self.registration.navigationPreload;
+ switch (message) {
+ case 'getState':
+ return np.getState();
+ case 'enable':
+ return np.enable();
+ case 'disable':
+ return np.disable();
+ case 'setHeaderValue':
+ return np.setHeaderValue('insightful');
+ }
+ return Promise.reject('bad message');
+}
+
+self.addEventListener('message', e => {
+ e.waitUntil(handle(e.data).then(result => e.source.postMessage(result)));
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/helpers.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/helpers.js
new file mode 100644
index 0000000000..86f0c0916e
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/helpers.js
@@ -0,0 +1,5 @@
+function expect_navigation_preload_state(state, enabled, header, desc) {
+ assert_equals(Object.keys(state).length, 2, desc + ': # of keys');
+ assert_equals(state.enabled, enabled, desc + ': enabled');
+ assert_equals(state.headerValue, header, desc + ': header');
+}
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js
new file mode 100644
index 0000000000..6e1ab23290
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js
@@ -0,0 +1,3 @@
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse);
+});
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html
new file mode 100644
index 0000000000..f9bfce5e89
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body>redirected</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py
new file mode 100644
index 0000000000..84a97e594b
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py
@@ -0,0 +1,38 @@
+def main(request, response):
+ if b"base" in request.GET:
+ return [(b"Content-Type", b"text/html")], b"OK"
+ type = request.GET.first(b"type")
+
+ if type == b"normal":
+ response.status = 302
+ response.headers.append(b"Location", b"redirect-redirected.html")
+ response.headers.append(b"Custom-Header", b"hello")
+ return b""
+
+ if type == b"no-location":
+ response.status = 302
+ response.headers.append(b"Content-Type", b"text/html")
+ response.headers.append(b"Custom-Header", b"hello")
+ return b""
+
+ if type == b"no-location-with-body":
+ response.status = 302
+ response.headers.append(b"Content-Type", b"text/html")
+ response.headers.append(b"Custom-Header", b"hello")
+ return b"<body>BODY</body>"
+
+ if type == b"redirect-to-scope":
+ response.status = 302
+ response.headers.append(b"Location",
+ b"redirect-scope.py?type=redirect-to-scope2")
+ return b""
+ if type == b"redirect-to-scope2":
+ response.status = 302
+ response.headers.append(b"Location",
+ b"redirect-scope.py?type=redirect-to-scope3")
+ return b""
+ if type == b"redirect-to-scope3":
+ response.status = 302
+ response.headers.append(b"Location", b"redirect-redirected.html")
+ response.headers.append(b"Custom-Header", b"hello")
+ return b""
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js
new file mode 100644
index 0000000000..1b55f2ef0d
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js
@@ -0,0 +1,35 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+function get_response_info(r) {
+ var info = {
+ type: r.type,
+ url: r.url,
+ status: r.status,
+ ok: r.ok,
+ statusText: r.statusText,
+ headers: []
+ };
+ r.headers.forEach((value, name) => { info.headers.push([value, name]); });
+ return info;
+}
+
+function post_to_page(data) {
+ return self.clients.matchAll()
+ .then(clients => clients.forEach(client => client.postMessage(data)));
+}
+
+self.addEventListener('fetch', event => {
+ event.respondWith(
+ event.preloadResponse
+ .then(
+ res => {
+ if (res.url.includes("base")) {
+ return res;
+ }
+ return post_to_page(get_response_info(res)).then(_ => res);
+ },
+ err => new Response(err.toString())));
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py
new file mode 100644
index 0000000000..5bab5b01f3
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py
@@ -0,0 +1,14 @@
+import json
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ normalized = dict()
+
+ for key, values in dict(request.headers).items():
+ values = [isomorphic_decode(value) for value in values]
+ normalized[isomorphic_decode(key.upper())] = values
+
+ response.headers.append(b"Content-Type", b"text/html")
+
+ return json.dumps(normalized)
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js
new file mode 100644
index 0000000000..1006cf2791
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js
@@ -0,0 +1,10 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ Promise.all[
+ self.registration.navigationPreload.enable(),
+ self.registration.navigationPreload.setHeaderValue('hello')]);
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse);
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py
new file mode 100644
index 0000000000..856f9dbc2a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py
@@ -0,0 +1,19 @@
+import zlib
+
+def main(request, response):
+ type = request.GET.first(b"type")
+
+ if type == "normal":
+ content = b"This is Navigation Preload Resource Timing test."
+ output = zlib.compress(content, 9)
+ headers = [(b"Content-type", b"text/plain"),
+ (b"Content-Encoding", b"deflate"),
+ (b"X-Decoded-Body-Size", len(content)),
+ (b"X-Encoded-Body-Size", len(output)),
+ (b"Content-Length", len(output))]
+ return headers, output
+
+ if type == b"redirect":
+ response.status = 302
+ response.headers.append(b"Location", b"redirect-redirected.html")
+ return b""
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js
new file mode 100644
index 0000000000..fac0d8de2a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js
@@ -0,0 +1,37 @@
+async function wait_for_performance_entries(url) {
+ let entries = performance.getEntriesByName(url);
+ if (entries.length > 0) {
+ return entries;
+ }
+ return new Promise((resolve) => {
+ new PerformanceObserver((list) => {
+ const entries = list.getEntriesByName(url);
+ if (entries.length > 0) {
+ resolve(entries);
+ }
+ }).observe({ entryTypes: ['resource'] });
+ });
+}
+
+self.addEventListener('activate', event => {
+ event.waitUntil(self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ let headers;
+ event.respondWith(
+ event.preloadResponse
+ .then(response => {
+ headers = response.headers;
+ return response.text()
+ })
+ .then(_ => wait_for_performance_entries(event.request.url))
+ .then(entries =>
+ new Response(
+ JSON.stringify({
+ decodedBodySize: headers.get('X-Decoded-Body-Size'),
+ encodedBodySize: headers.get('X-Encoded-Body-Size'),
+ timingEntries: entries
+ }),
+ {headers: {'Content-Type': 'text/html'}})));
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html
new file mode 100644
index 0000000000..a28b61261e
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body>samesite</body>
+<script>
+onmessage = (e) => {
+ if (e.data === "GetBody") {
+ parent.postMessage("samesite", '*');
+ }
+}
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html
new file mode 100644
index 0000000000..51fdc9ec74
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload Same Site SW registrator</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/test-helpers.sub.js"></script>
+<script>
+
+/**
+ * This is a helper file to register/unregister service worker in a same-site
+ * iframe.
+ **/
+
+async function messageToParent(msg) {
+ parent.postMessage(msg, '*');
+}
+
+onmessage = async (e) => {
+ // t is a , but the helper function needs a test object.
+ let t = {
+ step_func: (func) => func,
+ };
+ if (e.data === "Register") {
+ let reg = await service_worker_unregister_and_register(t, "samesite-worker.js", ".");
+ let worker = reg.installing;
+ await wait_for_state(t, worker, 'activated');
+ await messageToParent("SW Registered");
+ } else if (e.data == "Unregister") {
+ await service_worker_unregister(t, ".");
+ await messageToParent("SW Unregistered");
+ }
+}
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js
new file mode 100644
index 0000000000..f30e5ed274
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js
@@ -0,0 +1,8 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse);
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js
new file mode 100644
index 0000000000..87791d2e48
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js
@@ -0,0 +1,40 @@
+// This worker remains in the installing phase so that the
+// navigation preload API can be tested when there is no
+// active worker.
+importScripts('/resources/testharness.js');
+importScripts('helpers.js');
+
+function expect_rejection(promise) {
+ return promise.then(
+ () => { return Promise.reject('unexpected fulfillment'); },
+ err => { assert_equals('InvalidStateError', err.name); });
+}
+
+function test_before_activation() {
+ const np = self.registration.navigationPreload;
+ return expect_rejection(np.enable())
+ .then(() => expect_rejection(np.disable()))
+ .then(() => expect_rejection(np.setHeaderValue('hi')))
+ .then(() => np.getState())
+ .then(state => expect_navigation_preload_state(
+ state, false, 'true', 'state should be the default'))
+ .then(() => 'PASS')
+ .catch(err => 'FAIL: ' + err);
+}
+
+var resolve_done_promise;
+var done_promise = new Promise(resolve => { resolve_done_promise = resolve; });
+
+// Run the test once the page messages this worker.
+self.addEventListener('message', e => {
+ e.waitUntil(test_before_activation()
+ .then(result => {
+ e.source.postMessage(result);
+ resolve_done_promise();
+ }));
+ });
+
+// Don't become the active worker until the test is done.
+self.addEventListener('install', e => {
+ e.waitUntil(done_promise);
+ });
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html
new file mode 100644
index 0000000000..a860d95456
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>Navigation Preload: SameSite cookies</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const scope = 'resources/cookie.py';
+const script = 'resources/navigation-preload-worker.js';
+
+async function drop_cookie(t, same_site, cookie) {
+ const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=1');
+ t.add_cleanup(() => frame.remove());
+}
+
+async function same_site_cookies_test(t, same_site, cookie) {
+ // Remove the cookie before the first visit.
+ await drop_cookie(t, same_site, cookie);
+
+ {
+ const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=0');
+ t.add_cleanup(() => frame.remove());
+ // The body will be 0 because this is the first visit.
+ assert_equals(frame.contentDocument.body.textContent, '0', 'first visit');
+ }
+
+ {
+ const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=0');
+ t.add_cleanup(() => frame.remove());
+ // The body will be 1 because this is the second visit.
+ assert_equals(frame.contentDocument.body.textContent, '1', 'second visit');
+ }
+
+ // Remove the cookie after the test.
+ t.add_cleanup(() => drop_cookie(t, same_site, cookie));
+}
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ promise_test(t => registration.unregister(), 'Unregister a service worker.');
+
+ await wait_for_state(t, registration.installing, 'activated');
+ await registration.navigationPreload.enable();
+}, 'Set up a service worker for navigation preload tests.');
+
+promise_test(async t => {
+ await same_site_cookies_test(t, 'None', 'cookie-key-none');
+}, 'Navigation Preload for same site cookies (None).');
+
+promise_test(async t => {
+ await same_site_cookies_test(t, 'Strict', 'cookie-key-strict');
+}, 'Navigation Preload for same site cookies (Strict).');
+
+promise_test(async t => {
+ await same_site_cookies_test(t, 'Lax', 'cookie-key-lax');
+}, 'Navigation Preload for same site cookies (Lax).');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html
new file mode 100644
index 0000000000..633da9926a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload for same site iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<body></body>
+<script>
+
+const SAME_SITE = get_host_info().HTTPS_REMOTE_ORIGIN;
+const RESOURCES_DIR = "/service-workers/service-worker/navigation-preload/resources/";
+
+/**
+ * This test is used for testing the NavigationPreload works in a same site iframe.
+ * The test scenario is
+ * 1. Create a same site iframe to register service worker and wait for it be activated
+ * 2. Create a same site iframe which be intercepted by the service worker.
+ * 3. Once the iframe is loaded, service worker should set the page through the preload response.
+ * And checking if the iframe's body content is expected.
+ * 4. Unregister the service worker.
+ * 5. remove created iframes.
+ */
+
+promise_test(async (t) => {
+ let resolver;
+ let checkValue = false;
+ window.onmessage = (e) => {
+ if (checkValue) {
+ assert_equals(e.data, "samesite");
+ checkValue = false;
+ }
+ resolver();
+ };
+
+ let helperIframe = document.createElement("iframe");
+ helperIframe.src = SAME_SITE + RESOURCES_DIR + "samesite-sw-helper.html";
+ document.body.appendChild(helperIframe);
+
+ await new Promise(resolve => {
+ resolver = resolve;
+ helperIframe.onload = async () => {
+ helperIframe.contentWindow.postMessage("Register", '*');
+ }
+ });
+
+ let sameSiteIframe = document.createElement("iframe");
+ sameSiteIframe.src = SAME_SITE + RESOURCES_DIR + "samesite-iframe.html";
+ document.body.appendChild(sameSiteIframe);
+ await new Promise(resolve => {
+ resolver = resolve;
+ sameSiteIframe.onload = async() => {
+ checkValue = true;
+ sameSiteIframe.contentWindow.postMessage("GetBody", '*')
+ }
+ });
+
+ await new Promise(resolve => {
+ resolver = resolve;
+ helperIframe.contentWindow.postMessage("Unregister", '*')
+ });
+
+ helperIframe.remove();
+ sameSiteIframe.remove();
+ });
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-body.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-body.https.html
new file mode 100644
index 0000000000..0441c610b1
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-body.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigation redirection must clear body</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<meta charset="utf-8">
+<body>
+<form id="test-form" method="POST" style="display: none;">
+ <input type="submit" id="submit-button" />
+</form>
+<script>
+promise_test(function(t) {
+ var scope = 'resources/navigation-redirect-body.py';
+ var script = 'resources/navigation-redirect-body-worker.js';
+ var registration;
+ var frame = document.createElement('frame');
+ var form = document.getElementById('test-form');
+ var submit_button = document.getElementById('submit-button');
+
+ frame.src = 'about:blank';
+ frame.name = 'target_frame';
+ frame.id = 'frame';
+ document.body.appendChild(frame);
+ t.add_cleanup(function() { document.body.removeChild(frame); });
+
+ form.action = scope;
+ form.target = 'target_frame';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var frame_load_promise = new Promise(function(resolve) {
+ frame.addEventListener('load', function() {
+ resolve(frame.contentWindow.document.body.innerText);
+ }, false);
+ });
+ submit_button.click();
+ return frame_load_promise;
+ })
+ .then(function(text) {
+ var request_uri = decodeURIComponent(text);
+ assert_equals(
+ request_uri,
+ '/service-workers/service-worker/resources/navigation-redirect-body.py?redirect');
+ return registration.unregister();
+ });
+ }, 'Navigation redirection must clear body');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-resolution.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-resolution.https.html
new file mode 100644
index 0000000000..59e1cafec3
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-resolution.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigation Redirect Resolution</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+function make_absolute(url) {
+ return new URL(url, location).toString();
+}
+
+const script = 'resources/fetch-rewrite-worker.js';
+
+function redirect_result_test(scope, expected_url, description) {
+ promise_test(async t => {
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ t.add_cleanup(() => {
+ return service_worker_unregister(t, scope);
+ })
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // The navigation to |scope| will be resolved by a fetch to |redirect_url|
+ // which returns a relative Location header. If it is resolved relative to
+ // |scope|, the result will be navigate-redirect-resolution/blank.html. If
+ // relative to |redirect_url|, it will be resources/blank.html. The latter
+ // is correct.
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => { iframe.remove(); });
+ assert_equals(iframe.contentWindow.location.href,
+ make_absolute(expected_url));
+ }, description);
+}
+
+// |redirect_url| serves a relative redirect to resources/blank.html.
+const redirect_url = 'resources/redirect.py?Redirect=blank.html';
+
+// |scope_base| does not exist but will be replaced with a fetch of
+// |redirect_url| by fetch-rewrite-worker.js.
+const scope_base = 'resources/subdir/navigation-redirect-resolution?' +
+ 'redirect-mode=manual&url=' +
+ encodeURIComponent(make_absolute(redirect_url));
+
+// When the Service Worker forwards the result of |redirect_url| as an
+// opaqueredirect response, the redirect uses the response's URL list as the
+// base URL, not the request.
+redirect_result_test(scope_base, 'resources/blank.html',
+ 'test relative opaqueredirect');
+
+// The response's base URL should be preserved across CacheStorage and clone.
+redirect_result_test(scope_base + '&cache=1', 'resources/blank.html',
+ 'test relative opaqueredirect with CacheStorage');
+redirect_result_test(scope_base + '&clone=1', 'resources/blank.html',
+ 'test relative opaqueredirect with clone');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-to-http.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-to-http.https.html
new file mode 100644
index 0000000000..d4d2788c58
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect-to-http.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Service Worker: Service Worker can receive HTTP opaqueredirect response.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<meta charset="utf-8">
+<body></body>
+<script>
+async_test(function(t) {
+ var frame_src = get_host_info()['HTTPS_ORIGIN'] + base_path() +
+ 'resources/navigation-redirect-to-http-iframe.html';
+ function on_message(e) {
+ assert_equals(e.data.results, 'OK');
+ t.done();
+ }
+
+ window.addEventListener('message', t.step_func(on_message), false);
+
+ with_iframe(frame_src)
+ .then(function(frame) {
+ t.add_cleanup(function() { frame.remove(); });
+ });
+ }, 'Verify Service Worker can receive HTTP opaqueredirect response.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect.https.html
new file mode 100644
index 0000000000..d7d3d5259a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-redirect.https.html
@@ -0,0 +1,846 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigation redirection</title>
+<meta name="timeout" content="long">
+<!-- empty variant tests document.location and intercepted URLs -->
+<meta name="variant" content="">
+<!-- client variant tests the Clients API (resultingClientId and Client.url) -->
+<meta name="variant" content="?client">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const host_info = get_host_info();
+
+// This test registers three Service Workers at SCOPE1, SCOPE2 and
+// OTHER_ORIGIN_SCOPE. And checks the redirected page's URL and the requests
+// which are intercepted by Service Worker while loading redirect page.
+const BASE_URL = host_info['HTTPS_ORIGIN'] + base_path();
+const OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + base_path();
+
+const SCOPE1 = BASE_URL + 'resources/navigation-redirect-scope1.py?';
+const SCOPE2 = BASE_URL + 'resources/navigation-redirect-scope2.py?';
+const OUT_SCOPE = BASE_URL + 'resources/navigation-redirect-out-scope.py?';
+const SCRIPT = 'resources/redirect-worker.js';
+
+const OTHER_ORIGIN_IFRAME_URL =
+ OTHER_BASE_URL + 'resources/navigation-redirect-other-origin.html';
+const OTHER_ORIGIN_SCOPE =
+ OTHER_BASE_URL + 'resources/navigation-redirect-scope1.py?';
+const OTHER_ORIGIN_OUT_SCOPE =
+ OTHER_BASE_URL + 'resources/navigation-redirect-out-scope.py?';
+
+let registrations;
+let workers;
+let other_origin_frame;
+let message_resolvers = {};
+let next_message_id = 0;
+
+promise_test(async t => {
+ // In this frame we register a service worker at OTHER_ORIGIN_SCOPE.
+ // And will use this frame to communicate with the worker.
+ other_origin_frame = await with_iframe(OTHER_ORIGIN_IFRAME_URL);
+
+ // Register same-origin service workers.
+ registrations = await Promise.all([
+ service_worker_unregister_and_register(t, SCRIPT, SCOPE1),
+ service_worker_unregister_and_register(t, SCRIPT, SCOPE2)]);
+
+ // Wait for all workers to activate.
+ workers = registrations.map(get_effective_worker);
+ return Promise.all([
+ wait_for_state(t, workers[0], 'activated'),
+ wait_for_state(t, workers[1], 'activated'),
+ // This promise will resolve when |wait_for_worker_promise|
+ // in OTHER_ORIGIN_IFRAME_URL resolves.
+ send_to_iframe(other_origin_frame, {command: 'wait_for_worker'})]);
+}, 'initialize global state');
+
+function get_effective_worker(registration) {
+ if (registration.active)
+ return registration.active;
+ if (registration.waiting)
+ return registration.waiting;
+ if (registration.installing)
+ return registration.installing;
+}
+
+async function check_all_intercepted_urls(expected_urls) {
+ const urls = [];
+ urls.push(await get_intercepted_urls(workers[0]));
+ urls.push(await get_intercepted_urls(workers[1]));
+ // Gets the request URLs which are intercepted by OTHER_ORIGIN_SCOPE's
+ // SW. This promise will resolve when get_request_infos() in
+ // OTHER_ORIGIN_IFRAME_URL resolves.
+ const request_infos = await send_to_iframe(other_origin_frame,
+ {command: 'get_request_infos'});
+ urls.push(request_infos.map(info => { return info.url; }));
+
+ assert_object_equals(urls, expected_urls, 'Intercepted URLs should match.');
+}
+
+// Checks |clients| returned from a worker. Only the client matching
+// |expected_final_client_tag| should be found. Returns true if a client was
+// found. Note that the final client is not necessarily found by this worker,
+// if the client is cross-origin.
+//
+// |clients| is an object like:
+// {x: {found: true, id: id1, url: url1}, b: {found: false}}
+function check_clients(clients,
+ expected_id,
+ expected_url,
+ expected_final_client_tag,
+ worker_name) {
+ let found = false;
+ Object.keys(clients).forEach(key => {
+ const info = clients[key];
+ if (info.found) {
+ assert_true(!!expected_final_client_tag,
+ `${worker_name} client tag exists`);
+ assert_equals(key, expected_final_client_tag,
+ `${worker_name} client tag matches`);
+ assert_equals(info.id, expected_id, `${worker_name} client id`);
+ assert_equals(info.url, expected_url, `${worker_name} client url`);
+ found = true;
+ }
+ });
+ return found;
+}
+
+function check_resulting_client_ids(infos, expected_infos, actual_ids, worker) {
+ assert_equals(infos.length, expected_infos.length,
+ `request length for ${worker}`);
+ for (var i = 0; i < infos.length; i++) {
+ const tag = expected_infos[i].resultingClientIdTag;
+ const url = expected_infos[i].url;
+ const actual_id = infos[i].resultingClientId;
+ const expected_id = actual_ids[tag];
+ assert_equals(typeof(actual_id), 'string',
+ `resultingClientId for ${url} request to ${worker}`);
+ if (expected_id) {
+ assert_equals(actual_id, expected_id,
+ `resultingClientId for ${url} request to ${worker}`);
+ } else {
+ actual_ids[tag] = actual_id;
+ }
+ }
+}
+
+// Creates an iframe and navigates to |url|, which is expected to start a chain
+// of redirects.
+// - |expected_last_url| is the expected window.location after the
+// navigation.
+//
+// - |expected_request_infos| is the expected requests that the service workers
+// were dispatched fetch events for. The format is:
+// [
+// [
+// // Requests received by workers[0].
+// {url: url1, resultingClientIdTag: 'a'},
+// {url: url2, resultingClientIdTag: 'a'}
+// ],
+// [
+// // Requests received by workers[1].
+// {url: url3, resultingClientIdTag: 'a'}
+// ],
+// [
+// // Requests received by the cross-origin worker.
+// {url: url4, resultingClientIdTag: 'x'}
+// {url: url5, resultingClientIdTag: 'x'}
+// ]
+// ]
+// Here, |url| is |event.request.url| and |resultingClientIdTag| represents
+// |event.resultingClientId|. Since the actual client ids are not known
+// beforehand, the expectation isn't the literal expected value, but all equal
+// tags must map to the same actual id.
+//
+// - |expected_final_client_tag| is the resultingClientIdTag that is
+// expected to map to the created client's id. This is null if there
+// is no such tag, which can happen when the final request was a cross-origin
+// redirect to out-scope, so no worker received a fetch event whose
+// resultingClientId is the id of the resulting client.
+//
+// In the example above:
+// - workers[0] receives two requests with the same resultingClientId.
+// - workers[1] receives one request also with that resultingClientId.
+// - The cross-origin worker receives two requests with the same
+// resultingClientId which differs from the previous one.
+// - Assuming |expected_final_client_tag| is 'x', then the created
+// client has the id seen by the cross-origin worker above.
+function redirect_test(url,
+ expected_last_url,
+ expected_request_infos,
+ expected_final_client_tag,
+ test_name) {
+ promise_test(async t => {
+ const frame = await with_iframe(url);
+ t.add_cleanup(() => { frame.remove(); });
+
+ // Switch on variant.
+ if (document.location.search == '?client') {
+ return client_variant_test(url, expected_last_url, expected_request_infos,
+ expected_final_client_tag, test_name);
+ }
+
+ return default_variant_test(url, expected_last_url, expected_request_infos,
+ frame, test_name);
+ }, test_name);
+}
+
+// The default variant tests the request interception chain and
+// resulting document.location.
+async function default_variant_test(url,
+ expected_last_url,
+ expected_request_infos,
+ frame,
+ test_name) {
+ const expected_intercepted_urls = expected_request_infos.map(
+ requests_for_worker => {
+ return requests_for_worker.map(info => {
+ return info.url;
+ });
+ });
+ await check_all_intercepted_urls(expected_intercepted_urls);
+ const last_url = await send_to_iframe(frame, 'getLocation');
+ assert_equals(last_url, expected_last_url, 'Last URL should match.');
+}
+
+// The "client" variant tests the Clients API using resultingClientId.
+async function client_variant_test(url,
+ expected_last_url,
+ expected_request_infos,
+ expected_final_client_tag,
+ test_name) {
+ // Request infos is an array like:
+ // [
+ // [{url: url1, resultingClientIdTag: tag1}],
+ // [{url: url2, resultingClientIdTag: tag2}],
+ // [{url: url3: resultingClientIdTag: tag3}]
+ // ]
+ const requestInfos = await get_all_request_infos();
+
+ // We check the actual infos against the expected ones, and learn the
+ // actual ids as we go.
+ const actual_ids = {};
+ check_resulting_client_ids(requestInfos[0],
+ expected_request_infos[0],
+ actual_ids,
+ 'worker0');
+ check_resulting_client_ids(requestInfos[1],
+ expected_request_infos[1],
+ actual_ids,
+ 'worker1');
+ check_resulting_client_ids(requestInfos[2],
+ expected_request_infos[2],
+ actual_ids,
+ 'crossOriginWorker');
+
+ // Now |actual_ids| maps tag to actual id:
+ // {x: id1, b: id2, c: id3}
+ // Ask each worker to try to resolve the actual ids to clients.
+ // Only |expected_final_client_tag| should resolve to a client.
+ const client_infos = await get_all_clients(actual_ids);
+
+ // Client infos is an object like:
+ // {
+ // worker0: {x: {found: true, id: id1, url: url1}, b: {found: false}},
+ // worker1: {x: {found: true, id: id1, url: url1}},
+ // crossOriginWorker: {x: {found: false}}, {b: {found: false}}
+ // }
+ //
+ // Now check each client info. check_clients() verifies each info: only
+ // |expected_final_client_tag| should ever be found and the found client
+ // should have the expected url and id. A wrinkle is that not all workers
+ // will find the client, if they are cross-origin to the client. This
+ // means check_clients() trivially passes if no clients are found. So
+ // additionally check that at least one worker found the client (|found|),
+ // if that was expected (|expect_found|).
+ let found = false;
+ const expect_found = !!expected_final_client_tag;
+ const expected_id = actual_ids[expected_final_client_tag];
+ found = check_clients(client_infos.worker0,
+ expected_id,
+ expected_last_url,
+ expected_final_client_tag,
+ 'worker0');
+ found = check_clients(client_infos.worker1,
+ expected_id,
+ expected_last_url,
+ expected_final_client_tag,
+ 'worker1') || found;
+ found = check_clients(client_infos.crossOriginWorker,
+ expected_id,
+ expected_last_url,
+ expected_final_client_tag,
+ 'crossOriginWorker') || found;
+ assert_equals(found, expect_found, 'client found');
+
+ if (!expect_found) {
+ // TODO(falken): Ask the other origin frame if it has a client of the
+ // expected URL.
+ }
+}
+
+window.addEventListener('message', on_message, false);
+
+function on_message(e) {
+ if (e.origin != host_info['HTTPS_REMOTE_ORIGIN'] &&
+ e.origin != host_info['HTTPS_ORIGIN'] ) {
+ console.error('invalid origin: ' + e.origin);
+ return;
+ }
+ var resolve = message_resolvers[e.data.id];
+ delete message_resolvers[e.data.id];
+ resolve(e.data.result);
+}
+
+function send_to_iframe(frame, message) {
+ var message_id = next_message_id++;
+ return new Promise(resolve => {
+ message_resolvers[message_id] = resolve;
+ frame.contentWindow.postMessage(
+ {id: message_id, message},
+ '*');
+ });
+}
+
+async function get_all_clients(actual_ids) {
+ const client_infos = {};
+ client_infos['worker0'] = await get_clients(workers[0], actual_ids);
+ client_infos['worker1'] = await get_clients(workers[1], actual_ids);
+ client_infos['crossOriginWorker'] =
+ await send_to_iframe(other_origin_frame,
+ {command: 'get_clients', actual_ids});
+ return client_infos;
+}
+
+function get_clients(worker, actual_ids) {
+ return new Promise(resolve => {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = (msg) => {
+ resolve(msg.data.clients);
+ };
+ worker.postMessage({command: 'getClients', actual_ids, port: channel.port2},
+ [channel.port2]);
+ });
+}
+
+// Returns an array of the URLs that |worker| received fetch events for:
+// [url1, url2]
+async function get_intercepted_urls(worker) {
+ const infos = await get_request_infos(worker);
+ return infos.map(info => { return info.url; });
+}
+
+// Returns the requests that |worker| received fetch events for. The return
+// value is an array of format:
+// [
+// {url: url1, resultingClientId: id},
+// {url: url2, resultingClientId: id}
+// ]
+function get_request_infos(worker) {
+ return new Promise(resolve => {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = (msg) => {
+ resolve(msg.data.requestInfos);
+ };
+ worker.postMessage({command: 'getRequestInfos', port: channel.port2},
+ [channel.port2]);
+ });
+}
+
+// Returns an array of the requests the workers received fetch events for:
+// [
+// // Requests from workers[0].
+// [
+// {url: url1, resultingClientIdTag: tag1},
+// {url: url2, resultingClientIdTag: tag1}
+// ],
+//
+// // Requests from workers[1].
+// [{url: url3, resultingClientIdTag: tag2}],
+//
+// // Requests from the cross-origin worker.
+// []
+// ]
+async function get_all_request_infos() {
+ const request_infos = [];
+ request_infos.push(await get_request_infos(workers[0]));
+ request_infos.push(await get_request_infos(workers[1]));
+ request_infos.push(await send_to_iframe(other_origin_frame,
+ {command: 'get_request_infos'}));
+ return request_infos;
+}
+
+let url;
+let url1;
+let url2;
+
+// Normal redirect (from out-scope to in-scope).
+url = SCOPE1;
+redirect_test(
+ OUT_SCOPE + 'url=' + encodeURIComponent(url),
+ url,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Normal redirect to same-origin scope.');
+
+
+url = SCOPE1 + '#ref';
+redirect_test(
+ OUT_SCOPE + 'url=' + encodeURIComponent(SCOPE1) + '#ref',
+ url,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Normal redirect to same-origin scope with a hash fragment.');
+
+url = SCOPE1 + '#ref2';
+redirect_test(
+ OUT_SCOPE + 'url=' + encodeURIComponent(url) + '#ref',
+ url,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Normal redirect to same-origin scope with different hash fragments.');
+
+url = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ OUT_SCOPE + 'url=' + encodeURIComponent(url),
+ url,
+ [[], [], [{url, resultingClientIdTag: 'x'}]],
+ 'x',
+ 'Normal redirect to other-origin scope.');
+
+// SW fallbacked redirect. SW doesn't handle the fetch request.
+url = SCOPE1 + 'url=' + encodeURIComponent(OUT_SCOPE);
+redirect_test(
+ url,
+ OUT_SCOPE,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-fallbacked redirect to same-origin out-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fallbacked redirect to same-origin same-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1) + '#ref';
+url2 = SCOPE1 + '#ref';
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fallbacked redirect to same-origin same-scope with a hash fragment.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1 + '#ref2') + '#ref';
+url2 = SCOPE1 + '#ref2';
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fallbacked redirect to same-origin same-scope with different hash ' +
+ 'fragments.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'SW-fallbacked redirect to same-origin other-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'SW-fallbacked redirect to other-origin out-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}]
+ ],
+ 'x',
+ 'SW-fallbacked redirect to other-origin in-scope.');
+
+
+url3 = SCOPE1;
+url2 = OTHER_ORIGIN_SCOPE + 'url=' + encodeURIComponent(url3);
+url1 = SCOPE1 + 'url=' + encodeURIComponent(url2);
+redirect_test(
+ url1,
+ url3,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'a'},
+ {url: url3, resultingClientIdTag: 'x'}
+ ],
+ [],
+ [{url: url2, resultingClientIdTag: 'b'}]
+ ],
+ 'x',
+ 'SW-fallbacked redirect to other-origin and back to same-origin.');
+
+// SW generated redirect.
+// SW: event.respondWith(Response.redirect(params['url']));
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE);
+url2 = OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-generated redirect to same-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE) + '#ref';
+url2 = OUT_SCOPE + '#ref';
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-generated redirect to same-origin out-scope with a hash fragment.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE + '#ref2') + '#ref';
+url2 = OUT_SCOPE + '#ref2';
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-generated redirect to same-origin out-scope with different hash ' +
+ 'fragments.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-generated redirect to same-origin same-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'SW-generated redirect to same-origin other-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'SW-generated redirect to other-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}]
+ ],
+ 'x',
+ 'SW-generated redirect to other-origin in-scope.');
+
+
+// SW fetched redirect.
+// SW: event.respondWith(fetch(event.request));
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OUT_SCOPE)
+url2 = OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-fetched redirect to same-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fetched redirect to same-origin same-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'SW-fetched redirect to same-origin other-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'SW-fetched redirect to other-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}]
+ ],
+ 'x',
+ 'SW-fetched redirect to other-origin in-scope.');
+
+
+// SW responds with a fetch from a different url.
+// SW: event.respondWith(fetch(params['url']));
+url2 = SCOPE1;
+url1 = SCOPE1 + 'sw=fetch-url&url=' + encodeURIComponent(url2);
+redirect_test(
+ url1,
+ url1,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fetched response from different URL, same-origin same-scope.');
+
+
+// Opaque redirect.
+// SW: event.respondWith(fetch(
+// new Request(event.request.url, {redirect: 'manual'})));
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OUT_SCOPE);
+url2 = OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Redirect to same-origin out-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'Redirect to same-origin same-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'Redirect to same-origin other-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'Redirect to other-origin out-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}]
+ ],
+ 'x',
+ 'Redirect to other-origin in-scope with opaque redirect response.');
+
+url= SCOPE1 + 'sw=manual&noLocationRedirect';
+redirect_test(
+ url, url, [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'No location redirect response.');
+
+
+// Opaque redirect passed through Cache.
+// SW responds with an opaque redirectresponse from the Cache API.
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(OUT_SCOPE);
+url2 = OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Redirect to same-origin out-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'Redirect to same-origin same-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'Redirect to same-origin other-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' +
+ encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'Redirect to other-origin out-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' +
+ encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ ],
+ 'x',
+ 'Redirect to other-origin in-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url = SCOPE1 + 'sw=manualThroughCache&noLocationRedirect';
+redirect_test(
+ url,
+ url,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'No location redirect response via Cache.');
+
+// Clean up the test environment. This promise_test() needs to be the last one.
+promise_test(async t => {
+ registrations.forEach(async registration => {
+ if (registration)
+ await registration.unregister();
+ });
+ await send_to_iframe(other_origin_frame, {command: 'unregister'});
+ other_origin_frame.remove();
+}, 'clean up global state');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-sets-cookie.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-sets-cookie.https.html
new file mode 100644
index 0000000000..7f6c756f55
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-sets-cookie.https.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Navigation setting cookies</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/cookies/resources/cookie-helper.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const scopepath = '/cookies/resources/setSameSite.py?with-sw';
+
+async function unregister_service_worker(origin) {
+ let target_url = origin +
+ '/service-workers/service-worker/resources/unregister-rewrite-worker.html' +
+ '?scopepath=' + encodeURIComponent(scopepath);
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('SW-UNREGISTERED');
+ } finally {
+ w.close();
+ }
+}
+
+async function register_service_worker(origin) {
+ let target_url = origin +
+ '/service-workers/service-worker/resources/register-rewrite-worker.html' +
+ '?scopepath=' + encodeURIComponent(scopepath);
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('SW-REGISTERED');
+ } finally {
+ w.close();
+ }
+}
+
+async function clear_cookies(origin) {
+ let target_url = origin + '/cookies/samesite/resources/puppet.html';
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('READY');
+ w.postMessage({ type: 'drop' }, '*');
+ await wait_for_message('drop-complete');
+ } finally {
+ w.close();
+ }
+}
+
+// The following tests are adapted from /cookies/samesite/setcookie-navigation.https.html
+
+// Asserts that cookies are present or not present (according to `expectation`)
+// in the cookie string `cookies` with the correct names and value.
+function assert_cookies_present(cookies, value, expected_cookie_names, expectation) {
+ for (name of expected_cookie_names) {
+ let re = new RegExp("(?:^|; )" + name + "=" + value + "(?:$|;)");
+ let assertion = expectation ? assert_true : assert_false;
+ assertion(re.test(cookies), "`" + name + "=" + value + "` in cookies");
+ }
+}
+
+// Navigate from ORIGIN to |origin_to|, expecting the navigation to set SameSite
+// cookies on |origin_to|.
+function navigate_test(method, origin_to, query, title) {
+ promise_test(async function(t) {
+ // The cookies don't need to be cleared on each run because |value| is
+ // a new random value on each run, so on each run we are overwriting and
+ // checking for a cookie with a different random value.
+ let value = query + "&" + Math.random();
+ let url_from = SECURE_ORIGIN + "/cookies/samesite/resources/navigate.html"
+ let url_to = origin_to + "/cookies/resources/setSameSite.py?" + value;
+ var w = window.open(url_from);
+ await wait_for_message('READY', SECURE_ORIGIN);
+ assert_equals(SECURE_ORIGIN, window.origin);
+ assert_equals(SECURE_ORIGIN, w.origin);
+ let command = (method === "POST") ? "post-form" : "navigate";
+ w.postMessage({ type: command, url: url_to }, "*");
+ let message = await wait_for_message('COOKIES_SET', origin_to);
+ let samesite_cookie_names = ['samesite_strict', 'samesite_lax', 'samesite_none', 'samesite_unspecified'];
+ assert_cookies_present(message.data.cookies, value, samesite_cookie_names, true);
+ w.close();
+ }, title);
+}
+
+promise_test(async t => {
+ await register_service_worker(SECURE_ORIGIN);
+ await register_service_worker(SECURE_CROSS_SITE_ORIGIN);
+}, 'Setup service workers');
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw&ignore",
+ "Same-site top-level navigation with fallback service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&ignore",
+ "Cross-site top-level navigation with fallback service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_ORIGIN, "with-sw&ignore",
+ "Same-site top-level POST with fallback service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw&ignore",
+ "Cross-site top-level with fallback service worker POST should be able to set SameSite=* cookies.");
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw",
+ "Same-site top-level navigation with passthrough service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw",
+ "Cross-site top-level navigation with passthrough service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_ORIGIN, "with-sw",
+ "Same-site top-level POST with passthrough service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw",
+ "Cross-site top-level with passthrough service worker POST should be able to set SameSite=* cookies.");
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw&navpreload",
+ "Same-site top-level navigation with navpreload service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&navpreload",
+ "Cross-site top-level navigation with navpreload service worker should be able to set SameSite=* cookies.");
+// navpreload not supported with POST method
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw&change-request",
+ "Same-site top-level navigation with change-request service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&change-request",
+ "Cross-site top-level navigation with change-request service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_ORIGIN, "with-sw&change-request",
+ "Same-site top-level POST with change-request service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw&change-request",
+ "Cross-site top-level with change-request service worker POST should be able to set SameSite=* cookies.");
+
+promise_test(async t => {
+ await unregister_service_worker(SECURE_ORIGIN);
+ await unregister_service_worker(SECURE_CROSS_SITE_ORIGIN);
+ await clear_cookies(SECURE_ORIGIN);
+ await clear_cookies(SECURE_CROSS_SITE_ORIGIN);
+}, 'Cleanup service workers');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-timing-extended.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-timing-extended.https.html
new file mode 100644
index 0000000000..acb02c6fe1
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-timing-extended.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+const timingEventOrder = [
+ 'startTime',
+ 'workerStart',
+ 'fetchStart',
+ 'requestStart',
+ 'responseStart',
+ 'responseEnd',
+];
+
+function navigate_in_frame(frame, url) {
+ frame.contentWindow.location = url;
+ return new Promise((resolve) => {
+ frame.addEventListener('load', () => {
+ const timing = frame.contentWindow.performance.getEntriesByType('navigation')[0];
+ const {timeOrigin} = frame.contentWindow.performance;
+ resolve({
+ workerStart: timing.workerStart + timeOrigin,
+ fetchStart: timing.fetchStart + timeOrigin
+ })
+ });
+ });
+}
+
+const worker_url = 'resources/navigation-timing-worker-extended.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/timings/dummy.html';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activating');
+ const frame = await with_iframe('resources/empty.html');
+ t.add_cleanup(() => frame.remove());
+
+ const [timingFromEntry, timingFromWorker] = await Promise.all([
+ navigate_in_frame(frame, scope),
+ new Promise(resolve => {
+ window.addEventListener('message', m => {
+ resolve(m.data)
+ })
+ })])
+
+ assert_greater_than(timingFromWorker.activateWorkerEnd, timingFromEntry.workerStart,
+ 'workerStart marking should not wait for worker activation to finish');
+ assert_greater_than(timingFromEntry.fetchStart, timingFromWorker.activateWorkerEnd,
+ 'fetchStart should be marked once the worker is activated');
+ assert_greater_than(timingFromWorker.handleFetchEvent, timingFromEntry.fetchStart,
+ 'fetchStart should be marked before the Fetch event handler is called');
+}, 'Service worker controlled navigation timing');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-timing.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-timing.https.html
new file mode 100644
index 0000000000..75cab40458
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-timing.https.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+const timingEventOrder = [
+ 'startTime',
+ 'workerStart',
+ 'fetchStart',
+ 'requestStart',
+ 'responseStart',
+ 'responseEnd',
+];
+
+function verify(timing) {
+ for (let i = 0; i < timingEventOrder.length - 1; i++) {
+ assert_true(timing[timingEventOrder[i]] <= timing[timingEventOrder[i + 1]],
+ `Expected ${timingEventOrder[i]} <= ${timingEventOrder[i + 1]}`);
+ }
+}
+
+function navigate_in_frame(frame, url) {
+ frame.contentWindow.location = url;
+ return new Promise((resolve) => {
+ frame.addEventListener('load', () => {
+ const timing = frame.contentWindow.performance.getEntriesByType('navigation')[0];
+ resolve(timing);
+ });
+ });
+}
+
+const worker_url = 'resources/navigation-timing-worker.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/empty.html';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const timing = await navigate_in_frame(frame, scope);
+ assert_greater_than(timing.workerStart, 0);
+ verify(timing);
+}, 'Service worker controlled navigation timing');
+
+promise_test(async (t) => {
+ const scope = 'resources/empty.html?network-fallback';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const timing = await navigate_in_frame(frame, scope);
+ assert_greater_than(timing.workerStart, 0);
+ verify(timing);
+}, 'Service worker controlled navigation timing network fallback');
+
+promise_test(async (t) => {
+ const scope = 'resources/redirect.py?Redirect=empty.html';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const timing = await navigate_in_frame(frame, scope);
+ verify(timing);
+ // Additional checks for redirected navigation.
+ assert_true(timing.redirectStart <= timing.redirectEnd,
+ 'Expected redirectStart <= redirectEnd');
+ assert_true(timing.redirectEnd <= timing.fetchStart,
+ 'Expected redirectEnd <= fetchStart');
+}, 'Service worker controlled navigation timing redirect');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/nested-blob-url-workers.https.html b/testing/web-platform/tests/service-workers/service-worker/nested-blob-url-workers.https.html
new file mode 100644
index 0000000000..7269cbb701
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/nested-blob-url-workers.https.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Service Worker: nested blob URL worker clients</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/simple-intercept-worker.js';
+const SCOPE = 'resources/';
+const RESOURCE = 'resources/simple.txt';
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-blob-url-workers.html');
+}, 'Nested blob URL workers should be intercepted by a service worker.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-worker-created-from-blob-url-worker.html');
+}, 'Nested worker created from a blob URL worker should be intercepted by a service worker.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-blob-url-worker-created-from-worker.html');
+}, 'Nested blob URL worker created from a worker should be intercepted by a service worker.');
+
+async function runTest(t, iframe_url) {
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ const frame = await with_iframe(iframe_url);
+ t.add_cleanup(_ => frame.remove());
+ assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ null, 'frame should be controlled');
+
+ const response_text = await frame.contentWindow.fetch_in_worker(RESOURCE);
+ assert_equals(response_text, 'intercepted by service worker',
+ 'fetch() should be intercepted.');
+}
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/next-hop-protocol.https.html b/testing/web-platform/tests/service-workers/service-worker/next-hop-protocol.https.html
new file mode 100644
index 0000000000..7a907438d5
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/next-hop-protocol.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Verify nextHopProtocol is set correctly</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+async function getNextHopProtocol(frame, url) {
+ let final_url = new URL(url, self.location).href;
+ await frame.contentWindow.fetch(final_url).then(r => r.text());
+ let entryList = frame.contentWindow.performance.getEntriesByName(final_url);
+ let entry = entryList[entryList.length - 1];
+ return entry.nextHopProtocol;
+}
+
+async function runTest(t, base_url, expected_protocol) {
+ const scope = 'resources/empty.html?next-hop-protocol';
+ const script = 'resources/fetch-rewrite-worker.js';
+ let frame;
+
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(async _ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ frame = await with_iframe(scope);
+ t.add_cleanup(_ => frame.remove());
+
+ assert_equals(await getNextHopProtocol(frame, `${base_url}?generate-png`),
+ '', 'nextHopProtocol is not set on synthetic response');
+ assert_equals(await getNextHopProtocol(frame, `${base_url}?ignore`),
+ expected_protocol, 'nextHopProtocol is set on fallback');
+ assert_equals(await getNextHopProtocol(frame, `${base_url}`),
+ expected_protocol, 'nextHopProtocol is set on pass-through');
+ assert_equals(await getNextHopProtocol(frame, `${base_url}?cache`),
+ expected_protocol, 'nextHopProtocol is set on cached response');
+}
+
+promise_test(async (t) => {
+ return runTest(t, 'resources/empty.js', 'http/1.1');
+}, 'nextHopProtocol reports H1 correctly when routed via a service worker.');
+
+// This may be expected to fail if the WPT infrastructure does not fully
+// support H2 protocol testing yet.
+promise_test(async (t) => {
+ return runTest(t, 'resources/empty.h2.js', 'h2');
+}, 'nextHopProtocol reports H2 correctly when routed via a service worker.');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js b/testing/web-platform/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js
new file mode 100644
index 0000000000..f7c2ef37b8
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js
@@ -0,0 +1,7 @@
+// META: global=serviceworker-module
+
+// This is imported to ensure import('./basic-module-2.js') fails even if
+// it has been previously statically imported.
+import './resources/basic-module-2.js';
+
+import './resources/no-dynamic-import.js';
diff --git a/testing/web-platform/tests/service-workers/service-worker/no-dynamic-import.any.js b/testing/web-platform/tests/service-workers/service-worker/no-dynamic-import.any.js
new file mode 100644
index 0000000000..25b370b709
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/no-dynamic-import.any.js
@@ -0,0 +1,3 @@
+// META: global=serviceworker
+
+importScripts('resources/no-dynamic-import.js');
diff --git a/testing/web-platform/tests/service-workers/service-worker/onactivate-script-error.https.html b/testing/web-platform/tests/service-workers/service-worker/onactivate-script-error.https.html
new file mode 100644
index 0000000000..f5e80bb9a4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/onactivate-script-error.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_install(worker) {
+ return new Promise(function(resolve, reject) {
+ worker.addEventListener('statechange', function(event) {
+ if (worker.state == 'installed')
+ resolve();
+ else if (worker.state == 'redundant')
+ reject();
+ });
+ });
+}
+
+function wait_for_activate(worker) {
+ return new Promise(function(resolve, reject) {
+ worker.addEventListener('statechange', function(event) {
+ if (worker.state == 'activated')
+ resolve();
+ else if (worker.state == 'redundant')
+ reject();
+ });
+ });
+}
+
+function make_test(name, script) {
+ promise_test(function(t) {
+ var scope = script;
+ var registration;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+
+ t.add_cleanup(function() {
+ return r.unregister();
+ });
+
+ return wait_for_install(registration.installing);
+ })
+ .then(function() {
+ // Activate should succeed regardless of script errors.
+ return wait_for_activate(registration.waiting);
+ });
+ }, name);
+}
+
+[
+ {
+ name: 'activate handler throws an error',
+ script: 'resources/onactivate-throw-error-worker.js',
+ },
+ {
+ name: 'activate handler throws an error, error handler does not cancel',
+ script: 'resources/onactivate-throw-error-with-empty-onerror-worker.js',
+ },
+ {
+ name: 'activate handler dispatches an event that throws an error',
+ script: 'resources/onactivate-throw-error-from-nested-event-worker.js',
+ },
+ {
+ name: 'activate handler throws an error that is cancelled',
+ script: 'resources/onactivate-throw-error-then-cancel-worker.js',
+ },
+ {
+ name: 'activate handler throws an error and prevents default',
+ script: 'resources/onactivate-throw-error-then-prevent-default-worker.js',
+ }
+].forEach(function(test_case) {
+ make_test(test_case.name, test_case.script);
+ });
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/oninstall-script-error.https.html b/testing/web-platform/tests/service-workers/service-worker/oninstall-script-error.https.html
new file mode 100644
index 0000000000..fe7f6e9012
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/oninstall-script-error.https.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_install_event(worker) {
+ return new Promise(function(resolve) {
+ worker.addEventListener('statechange', function(event) {
+ if (worker.state == 'installed')
+ resolve(true);
+ else if (worker.state == 'redundant')
+ resolve(false);
+ });
+ });
+}
+
+function make_test(name, script, expect_install) {
+ promise_test(function(t) {
+ var scope = script;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ return wait_for_install_event(registration.installing);
+ })
+ .then(function(did_install) {
+ assert_equals(did_install, expect_install,
+ 'The worker was installed');
+ })
+ }, name);
+}
+
+[
+ {
+ name: 'install handler throws an error',
+ script: 'resources/oninstall-throw-error-worker.js',
+ expect_install: true
+ },
+ {
+ name: 'install handler throws an error, error handler does not cancel',
+ script: 'resources/oninstall-throw-error-with-empty-onerror-worker.js',
+ expect_install: true
+ },
+ {
+ name: 'install handler dispatches an event that throws an error',
+ script: 'resources/oninstall-throw-error-from-nested-event-worker.js',
+ expect_install: true
+ },
+ {
+ name: 'install handler throws an error in the waitUntil',
+ script: 'resources/oninstall-waituntil-throw-error-worker.js',
+ expect_install: false
+ },
+
+ // The following two cases test what happens when the ServiceWorkerGlobalScope
+ // 'error' event handler cancels the resulting error event. Since the
+ // original 'install' event handler through, the installation should still
+ // be stopped in this case. See:
+ // https://github.com/slightlyoff/ServiceWorker/issues/778
+ {
+ name: 'install handler throws an error that is cancelled',
+ script: 'resources/oninstall-throw-error-then-cancel-worker.js',
+ expect_install: true
+ },
+ {
+ name: 'install handler throws an error and prevents default',
+ script: 'resources/oninstall-throw-error-then-prevent-default-worker.js',
+ expect_install: true
+ }
+].forEach(function(test_case) {
+ make_test(test_case.name, test_case.script, test_case.expect_install);
+ });
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/opaque-response-preloaded.https.html b/testing/web-platform/tests/service-workers/service-worker/opaque-response-preloaded.https.html
new file mode 100644
index 0000000000..417aa4ebec
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/opaque-response-preloaded.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Opaque responses should not be reused for XHRs</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const WORKER =
+ 'resources/opaque-response-preloaded-worker.js';
+
+var done;
+
+// These test that the browser does not inappropriately use a cached opaque
+// response for a request that is not no-cors. The test opens a controlled
+// iframe that uses link rel=preload to issue a same-origin no-cors request.
+// The service worker responds to the request with an opaque response. Then the
+// iframe does an XHR (not no-cors) to that URL again. The request should fail.
+promise_test(t => {
+ const SCOPE =
+ 'resources/opaque-response-being-preloaded-xhr.html';
+ const promise = new Promise(resolve => done = resolve);
+
+ return service_worker_unregister_and_register(t, WORKER, SCOPE)
+ .then(reg => {
+ add_completion_callback(() => reg.unregister());
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(() => with_iframe(SCOPE))
+ .then(frame => t.add_cleanup(() => frame.remove() ))
+ .then(() => promise)
+ .then(result => assert_equals(result, 'PASS'));
+ }, 'Opaque responses should not be reused for XHRs, loading case');
+
+promise_test(t => {
+ const SCOPE =
+ 'resources/opaque-response-preloaded-xhr.html';
+ const promise = new Promise(resolve => done = resolve);
+
+ return service_worker_unregister_and_register(t, WORKER, SCOPE)
+ .then(reg => {
+ add_completion_callback(() => reg.unregister());
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(() => with_iframe(SCOPE))
+ .then(frame => t.add_cleanup(() => frame.remove() ))
+ .then(() => promise)
+ .then(result => assert_equals(result, 'PASS'));
+ }, 'Opaque responses should not be reused for XHRs, done case');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/opaque-script.https.html b/testing/web-platform/tests/service-workers/service-worker/opaque-script.https.html
new file mode 100644
index 0000000000..7d2121855d
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/opaque-script.https.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<title>Cache Storage: verify scripts loaded from cache_storage are marked opaque</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+'use strict';
+
+const SW_URL = 'resources/opaque-script-sw.js';
+const BASE_SCOPE = './resources/opaque-script-frame.html';
+const SAME_ORIGIN_BASE = new URL('./resources/', self.location.href).href;
+const CROSS_ORIGIN_BASE = new URL('./resources/',
+ get_host_info().HTTPS_REMOTE_ORIGIN + base_path()).href;
+
+function wait_for_error() {
+ return new Promise(resolve => {
+ self.addEventListener('message', function messageHandler(evt) {
+ if (evt.data.type !== 'ErrorEvent')
+ return;
+ self.removeEventListener('message', messageHandler);
+ resolve(evt.data.msg);
+ });
+ });
+}
+
+// Load an iframe that dynamically adds a script tag that is
+// same/cross origin and large/small. It then calls a function
+// defined in that loaded script that throws an unhandled error.
+// The resulting message exposed in the global onerror handler
+// is reported back from this function. Opaque cross origin
+// scripts should not expose the details of the uncaught exception.
+async function get_error_message(t, mode, size) {
+ const script_base = mode === 'same-origin' ? SAME_ORIGIN_BASE
+ : CROSS_ORIGIN_BASE;
+ const script = script_base + `opaque-script-${size}.js`;
+ const scope = BASE_SCOPE + `?script=${script}`;
+ const reg = await service_worker_unregister_and_register(t, SW_URL, scope);
+ t.add_cleanup(_ => reg.unregister());
+ assert_true(!!reg.installing);
+ await wait_for_state(t, reg.installing, 'activated');
+ const error_promise = wait_for_error();
+ const f = await with_iframe(scope);
+ t.add_cleanup(_ => f.remove());
+ const error = await error_promise;
+ return error;
+}
+
+promise_test(async t => {
+ const error = await get_error_message(t, 'same-origin', 'small');
+ assert_true(error.includes('Intentional error'));
+}, 'Verify small same-origin cache_storage scripts are not opaque.');
+
+promise_test(async t => {
+ const error = await get_error_message(t, 'same-origin', 'large');
+ assert_true(error.includes('Intentional error'));
+}, 'Verify large same-origin cache_storage scripts are not opaque.');
+
+promise_test(async t => {
+ const error = await get_error_message(t, 'cross-origin', 'small');
+ assert_false(error.includes('Intentional error'));
+}, 'Verify small cross-origin cache_storage scripts are opaque.');
+
+promise_test(async t => {
+ const error = await get_error_message(t, 'cross-origin', 'large');
+ assert_false(error.includes('Intentional error'));
+}, 'Verify large cross-origin cache_storage scripts are opaque.');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/partitioned-claim.tentative.https.html b/testing/web-platform/tests/service-workers/service-worker/partitioned-claim.tentative.https.html
new file mode 100644
index 0000000000..1f42c528e0
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/partitioned-claim.tentative.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+This test creates a iframe in a first-party context and then registers a
+service worker (such that the iframe client is unclaimed).
+A third-party iframe is then created which has its SW call clients.claim()
+and then the test checks that the 1p iframe was not claimed int he process.
+Finally the test has its SW call clients.claim() and confirms the 1p iframe is
+claimed.
+
+<script>
+promise_test(async t => {
+ const script = './resources/partitioned-storage-sw.js';
+ const scope = './resources/partitioned-';
+
+ // Add a 1p iframe.
+ const wait_frame_url = new URL(
+ './resources/partitioned-service-worker-iframe-claim.html?1p-mode',
+ self.location);
+
+ const frame = await with_iframe(wait_frame_url, false);
+ t.add_cleanup(async () => {
+ frame.remove();
+ });
+
+ // Add service worker to this 1P context.
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Register the message listener.
+ self.addEventListener('message', messageEventHandler);
+
+ // Now we need to create a third-party iframe whose SW will claim it and then
+ // the iframe will postMessage that its serviceWorker.controller state has
+ // changed.
+ const third_party_iframe_url = new URL(
+ './resources/partitioned-service-worker-iframe-claim.html?3p-mode',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+ // Create the 3p window (which will in turn create the iframe with the SW)
+ // and await on its data.
+ const frame_3p_data = await loadAndReturnSwData(t, third_party_iframe_url,
+ 'window');
+ assert_equals(frame_3p_data.status, "success",
+ "3p iframe was successfully claimed");
+
+ // Confirm that the 1p iframe wasn't claimed at the same time.
+ const controller_1p_iframe = makeMessagePromise();
+ frame.contentWindow.postMessage({type: "get-controller"});
+ const controller_1p_iframe_data = await controller_1p_iframe;
+ assert_equals(controller_1p_iframe_data.controller, null,
+ "Test iframe client isn't claimed yet.");
+
+
+ // Tell the SW to claim.
+ const claimed_1p_iframe = makeMessagePromise();
+ reg.active.postMessage({type: "claim"});
+ const claimed_1p_iframe_data = await claimed_1p_iframe;
+
+ assert_equals(claimed_1p_iframe_data.status, "success",
+ "iframe client was successfully claimed.");
+
+}, "ServiceWorker's clients.claim() is partitioned");
+</script>
+
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html b/testing/web-platform/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html
new file mode 100644
index 0000000000..5f6371cb42
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Partitioned Cookies</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+</head>
+
+<!--
+ This test exercises partitioned service workers' interaction with partitioned cookies.
+ Partitioned service workers should only be able to interact with partitioned cookies whose
+ partition key matches the worker's partition.
+-->
+
+<body>
+<script>
+
+promise_test(async t => {
+ const script = './resources/partitioned-cookies-sw.js'
+ const scope = './resources/partitioned-cookies-'
+ const absolute_scope = new URL(scope, window.location).href;
+
+ // Set a Partitioned cookie.
+ document.cookie = '__Host-partitioned=123; Secure; Path=/; SameSite=None; Partitioned;';
+ assert_true(document.cookie.includes('__Host-partitioned=123'));
+
+ // Set an unpartitioned cookie.
+ document.cookie = 'unpartitioned=456; Secure; Path=/; SameSite=None;';
+ assert_true(document.cookie.includes('unpartitioned=456'));
+
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ t.add_cleanup(() => reg.unregister());
+
+ // on_message will be reassigned below based on the expected reply from the service worker.
+ let on_message;
+ self.addEventListener('message', ev => on_message(ev));
+ navigator.serviceWorker.addEventListener('message', evt => {
+ self.postMessage(evt.data, '*');
+ });
+
+ const retrieved_registrations =
+ await navigator.serviceWorker.getRegistrations();
+ // It's possible that other tests have left behind other service workers.
+ // This steps filters those other SWs out.
+ const filtered_registrations =
+ retrieved_registrations.filter(reg => reg.scope == absolute_scope);
+
+ // First test that the worker script started correctly and message passing is enabed.
+ let resolve_wait_promise;
+ let wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ let got;
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({type: 'test_message'});
+ await wait_promise;
+ assert_true(got.ok, 'Message passing');
+
+ // Test that the partitioned cookie is available to this worker via HTTP.
+ wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({type: 'echo_cookies_http'});
+ await wait_promise;
+ assert_true(got.ok, 'Get cookies');
+ assert_true(got.cookies.includes('__Host-partitioned'), 'Can access partitioned cookie via HTTP');
+ assert_true(got.cookies.includes('unpartitioned'), 'Can access unpartitioned cookie via HTTP');
+
+ // Test that the partitioned cookie is available to this worker via CookieStore API.
+ wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({type: 'echo_cookies_js'});
+ await wait_promise;
+ assert_true(got.ok, 'Get cookies');
+ assert_true(got.cookies.includes('__Host-partitioned'), 'Can access partitioned cookie via JS');
+ assert_true(got.cookies.includes('unpartitioned'), 'Can access unpartitioned cookie via JS');
+
+ // Test that the partitioned cookie is not available to this worker in HTTP
+ // requests from importScripts.
+ wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({type: 'echo_cookies_import'});
+ await wait_promise;
+ assert_true(got.ok, 'Get cookies');
+ assert_true(got.cookies.includes('__Host-partitioned'), 'Can access partitioned cookie via importScripts');
+ assert_true(got.cookies.includes('unpartitioned'), 'Can access unpartitioned cookie via importScripts');
+
+ const popup = window.open(
+ new URL(
+ `./resources/partitioned-cookies-3p-window.html?origin=${
+ encodeURIComponent(self.location.origin)}`,
+ get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname));
+ await fetch_tests_from_window(popup);
+});
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html b/testing/web-platform/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html
new file mode 100644
index 0000000000..7c4d4f1e02
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+This test loads a SW in a first-party context and gets the SW's (randomly)
+generated ID. It does the same thing for the SW but in a third-party context
+and then confirms that the IDs are different.
+
+<script>
+promise_test(async t => {
+ const script = './resources/partitioned-storage-sw.js'
+ const scope = './resources/partitioned-'
+ const absoluteScope = new URL(scope, window.location).href;
+
+ // Add service worker to this 1P context.
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Register the message listener.
+ self.addEventListener('message', messageEventHandler);
+
+ // Open an iframe that will create a promise within the SW.
+ // The query param is there to track which request the service worker is
+ // handling.
+ //
+ // This promise is necessary to prevent the service worker from being
+ // shutdown during the test which would cause a new ID to be generated
+ // and thus invalidate the test.
+ const wait_frame_url = new URL(
+ './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame',
+ self.location);
+
+ // We don't really need the data the SW sent us from this request
+ // but we can use the ID to confirm the SW wasn't shut down during the
+ // test.
+ const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url,
+ 'iframe');
+
+ // Now we need to create a third-party iframe that will send us its SW's
+ // ID.
+ const third_party_iframe_url = new URL(
+ './resources/partitioned-service-worker-third-party-iframe-getRegistrations.html',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+ // Create the 3p window (which will in turn create the iframe with the SW)
+ // and await on its data.
+ const frame_3p_ID = await loadAndReturnSwData(t, third_party_iframe_url,
+ 'window');
+
+ // Now get this frame's SW's ID.
+ const frame_1p_ID_promise = makeMessagePromise();
+
+ const retrieved_registrations =
+ await navigator.serviceWorker.getRegistrations();
+ // It's possible that other tests have left behind other service workers.
+ // This steps filters those other SWs out.
+ const filtered_registrations =
+ retrieved_registrations.filter(reg => reg.scope == absoluteScope);
+
+ // Register a listener on the service worker container and then forward to
+ // the self event listener so we can reuse the existing message promise
+ // function.
+ navigator.serviceWorker.addEventListener('message', evt => {
+ self.postMessage(evt.data, '*');
+ });
+
+ filtered_registrations[0].active.postMessage({type: "get-id"});
+
+ const frame_1p_ID = await frame_1p_ID_promise;
+
+ // First check that the SW didn't shutdown during the run of the test.
+ // (Note: We're not using assert_equals because random values make it
+ // difficult to use a test expectations file.)
+ assert_true(wait_frame_1p_data.ID === frame_1p_ID.ID,
+ "1p SW didn't shutdown");
+ // Now check that the 1p and 3p IDs differ.
+ assert_false(frame_1p_ID.ID === frame_3p_ID.ID,
+ "1p SW ID matches 3p SW ID");
+
+ // Finally, for clean up, resolve the SW's promise so it stops waiting.
+ const resolve_frame_url = new URL(
+ './resources/partitioned-resolve.fakehtml?From1pFrame', self.location);
+
+ // We don't care about the data.
+ await loadAndReturnSwData(t, resolve_frame_url, 'iframe');
+
+}, "ServiceWorker's getRegistrations() is partitioned");
+
+
+</script>
+
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html b/testing/web-platform/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html
new file mode 100644
index 0000000000..46beec819c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+This test loads a SW in a first-party context and gets has the SW send
+its list of clients from client.matchAll(). It does the same thing for the
+SW in a third-party context as well and confirms that each SW see's the correct
+clients and that they don't see eachother's clients.
+
+<script>
+promise_test(async t => {
+
+ const script = './resources/partitioned-storage-sw.js'
+ const scope = './resources/partitioned-'
+
+ // Add service worker to this 1P context.
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Register the message listener.
+ self.addEventListener('message', messageEventHandler);
+
+ // Create a third-party iframe that will send us its SW's clients.
+ const third_party_iframe_url = new URL(
+ './resources/partitioned-service-worker-third-party-iframe-matchAll.html',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+ const {urls_list: frame_3p_urls_list} = await loadAndReturnSwData(t,
+ third_party_iframe_url, 'window');
+
+ // Register a listener on the service worker container and then forward to
+ // the self event listener so we can reuse the existing message promise
+ // function.
+ navigator.serviceWorker.addEventListener('message', evt => {
+ self.postMessage(evt.data, '*');
+ });
+
+ const frame_1p_data_promise = makeMessagePromise();
+
+ reg.active.postMessage({type: "get-match-all"});
+
+ const {urls_list: frame_1p_urls_list} = await frame_1p_data_promise;
+
+ // If partitioning is working, the 1p and 3p SWs should only see a single
+ // client.
+ assert_equals(frame_3p_urls_list.length, 1);
+ assert_equals(frame_1p_urls_list.length, 1);
+ // Confirm that the expected URL was seen by each.
+ assert_equals(frame_3p_urls_list[0], third_party_iframe_url.toString(),
+ "3p SW has the correct client url.");
+ assert_equals(frame_1p_urls_list[0], window.location.href,
+ "1P SW has the correct client url.");
+}, "ServiceWorker's matchAll() is partitioned");
+
+
+</script>
+
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/service-workers/service-worker/partitioned.tentative.https.html b/testing/web-platform/tests/service-workers/service-worker/partitioned.tentative.https.html
new file mode 100644
index 0000000000..17a375f9c7
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/partitioned.tentative.https.html
@@ -0,0 +1,188 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+ <!-- Debugging text for both test cases -->
+ The 3p iframe's postMessage:
+ <p id="iframe_response">No message received</p>
+
+ The nested iframe's postMessage:
+ <p id="nested_iframe_response">No message received</p>
+
+<script>
+promise_test(async t => {
+ const script = './resources/partitioned-storage-sw.js'
+ const scope = './resources/partitioned-'
+
+ // Add service worker to this 1P context. wait_for_state() and
+ // service_worker_unregister_and_register() are helper functions
+ // for creating test ServiceWorkers defined in:
+ // service-workers/service-worker/resources/test-helpers.sub.js
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Registers the message listener with messageEventHandler(), defined in:
+ // service-workers/service-worker/resources/partitioned-utils.js
+ self.addEventListener('message', messageEventHandler);
+
+ // Open an iframe that will create a promise within the SW.
+ // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+ // `waitUntilResolved.fakehtml`: URL scope that creates the promise.
+ // `?From1pFrame`: query param that tracks which request the service worker is
+ // handling.
+ const wait_frame_url = new URL(
+ './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame',
+ self.location);
+
+ // Loads a child iframe with wait_frame_url as the content and returns
+ // a promise for the data messaged from the loaded iframe.
+ // loadAndReturnSwData() defined in:
+ // service-workers/service-worker/resources/partitioned-utils.js:
+ const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url,
+ 'iframe');
+ assert_equals(wait_frame_1p_data.source, 'From1pFrame',
+ 'The data for the 1p frame came from the wrong source');
+
+ // Now create a 3p iframe that will try to resolve the SW in a 3p context.
+ const third_party_iframe_url = new URL(
+ './resources/partitioned-service-worker-third-party-iframe.html',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+ // loadAndReturnSwData() creates a HTTPS_NOTSAMESITE_ORIGIN or 3p `window`
+ // element which embeds an iframe with the ServiceWorker and returns
+ // a promise of the data messaged from that frame.
+ const frame_3p_data = await loadAndReturnSwData(t, third_party_iframe_url, 'window');
+ assert_equals(frame_3p_data.source, 'From3pFrame',
+ 'The data for the 3p frame came from the wrong source');
+
+ // Print some debug info to the main frame.
+ document.getElementById("iframe_response").innerHTML =
+ "3p iframe's has_pending: " + frame_3p_data.has_pending + " source: " +
+ frame_3p_data.source + ". ";
+
+ // Now do the same for the 1p iframe.
+ // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+ // `resolve.fakehtml`: URL scope that resolves the promise.
+ const resolve_frame_url = new URL(
+ './resources/partitioned-resolve.fakehtml?From1pFrame', self.location);
+
+ const frame_1p_data = await loadAndReturnSwData(t, resolve_frame_url,
+ 'iframe');
+ assert_equals(frame_1p_data.source, 'From1pFrame',
+ 'The data for the 1p frame came from the wrong source');
+ // Both the 1p frames should have been serviced by the same service worker ID.
+ // If this isn't the case then that means the SW could have been deactivated
+ // which invalidates the test.
+ assert_equals(frame_1p_data.ID, wait_frame_1p_data.ID,
+ 'The 1p frames were serviced by different service workers.');
+
+ document.getElementById("iframe_response").innerHTML +=
+ "1p iframe's has_pending: " + frame_1p_data.has_pending + " source: " +
+ frame_1p_data.source;
+
+ // If partitioning is working correctly then only the 1p iframe should see
+ // (and resolve) its SW's promise. Additionally the two frames should see
+ // different IDs.
+ assert_true(frame_1p_data.has_pending,
+ 'The 1p iframe saw a pending promise in the service worker.');
+ assert_false(frame_3p_data.has_pending,
+ 'The 3p iframe saw a pending promise in the service worker.');
+ assert_not_equals(frame_1p_data.ID, frame_3p_data.ID,
+ 'The frames were serviced by the same service worker thread.');
+}, 'Services workers under different top-level sites are partitioned.');
+
+// Optional Test: Checking for partitioned ServiceWorkers in an A->B->A
+// (nested-iframes with cross-site ancestor) scenario.
+promise_test(async t => {
+ const script = './resources/partitioned-storage-sw.js'
+ const scope = './resources/partitioned-'
+
+ // Add service worker to this 1P context. wait_for_state() and
+ // service_worker_unregister_and_register() are helper functions
+ // for creating test ServiceWorkers defined in:
+ // service-workers/service-worker/resources/test-helpers.sub.js
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Registers the message listener with messageEventHandler(), defined in:
+ // service-workers/service-worker/resources/partitioned-utils.js
+ self.addEventListener('message', messageEventHandler);
+
+ // Open an iframe that will create a promise within the SW.
+ // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+ // `waitUntilResolved.fakehtml`: URL scope that creates the promise.
+ // `?From1pFrame`: query param that tracks which request the service worker is
+ // handling.
+ const wait_frame_url = new URL(
+ './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame',
+ self.location);
+
+ // Load a child iframe with wait_frame_url as the content.
+ // loadAndReturnSwData() defined in:
+ // service-workers/service-worker/resources/partitioned-utils.js:
+ const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url,
+ 'iframe');
+ assert_equals(wait_frame_1p_data.source, 'From1pFrame',
+ 'The data for the 1p frame came from the wrong source');
+
+ // Now create a set of nested iframes in the configuration A1->B->A2
+ // where B is cross-site and A2 is same-site to this top-level
+ // site (A1). The innermost iframe of the nested iframes (A2) will
+ // create an additional iframe to finally resolve the ServiceWorker.
+ const nested_iframe_url = new URL(
+ './resources/partitioned-service-worker-nested-iframe-parent.html',
+ get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname);
+
+ // Create the nested iframes (which will in turn create the iframe
+ // with the ServiceWorker) and await on receiving its data.
+ const nested_iframe_data = await loadAndReturnSwData(t, nested_iframe_url, 'iframe');
+ assert_equals(nested_iframe_data.source, 'FromNestedFrame',
+ 'The data for the nested iframe frame came from the wrong source');
+
+ // Print some debug info to the main frame.
+ document.getElementById("nested_iframe_response").innerHTML =
+ "Nested iframe's has_pending: " + nested_iframe_data.has_pending + " source: " +
+ nested_iframe_data.source + ". ";
+
+ // Now do the same for the 1p iframe.
+ // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+ // `resolve.fakehtml`: URL scope that resolves the promise.
+ const resolve_frame_url = new URL(
+ './resources/partitioned-resolve.fakehtml?From1pFrame', self.location);
+
+ const frame_1p_data = await loadAndReturnSwData(t, resolve_frame_url,
+ 'iframe');
+ assert_equals(frame_1p_data.source, 'From1pFrame',
+ 'The data for the 1p frame came from the wrong source');
+ // Both the 1p frames should have been serviced by the same service worker ID.
+ // If this isn't the case then that means the SW could have been deactivated
+ // which invalidates the test.
+ assert_equals(frame_1p_data.ID, wait_frame_1p_data.ID,
+ 'The 1p frames were serviced by different service workers.');
+
+ document.getElementById("nested_iframe_response").innerHTML +=
+ "1p iframe's has_pending: " + frame_1p_data.has_pending + " source: " +
+ frame_1p_data.source;
+
+ // If partitioning is working correctly then only the 1p iframe should see
+ // (and resolve) its SW's promise. Additionally, the innermost iframe of
+ // the nested iframes (A2 in the configuration A1->B->A2) should have a
+ // different service worker ID than the 1p (A1) frame.
+ assert_true(frame_1p_data.has_pending,
+ 'The 1p iframe saw a pending promise in the service worker.');
+ assert_false(nested_iframe_data.has_pending,
+ 'The 3p iframe saw a pending promise in the service worker.');
+ assert_not_equals(frame_1p_data.ID, nested_iframe_data.ID,
+ 'The frames were serviced by the same service worker thread.');
+}, 'Services workers with cross-site ancestors are partitioned.');
+
+</script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/service-workers/service-worker/performance-timeline.https.html b/testing/web-platform/tests/service-workers/service-worker/performance-timeline.https.html
new file mode 100644
index 0000000000..e56e6fe416
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/performance-timeline.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+service_worker_test(
+ 'resources/performance-timeline-worker.js',
+ 'Test Performance Timeline API in Service Worker');
+
+// The purpose of this test is to verify that service worker overhead
+// is included in the Performance API's timing information.
+promise_test(t => {
+ let script = 'resources/empty-but-slow-worker.js';
+ let scope = 'resources/sample.txt?slow-sw-timing';
+ let url = new URL(scope, window.location).href;
+ let slowURL = url + '&slow';
+ let frame;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(reg => {
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(f => {
+ frame = f;
+ return frame.contentWindow.fetch(url).then(r => r && r.text());
+ })
+ .then(_ => {
+ return frame.contentWindow.fetch(slowURL).then(r => r && r.text());
+ })
+ .then(_ => {
+ function elapsed(u) {
+ let entry = frame.contentWindow.performance.getEntriesByName(u);
+ return entry[0] ? entry[0].duration : undefined;
+ }
+ let urlTime = elapsed(url);
+ let slowURLTime = elapsed(slowURL);
+ // Verify the request slowed by the service worker is indeed measured
+ // to be slower. Note, we compare to smaller delay instead of the exact
+ // delay amount to avoid making the test racy under automation.
+ assert_greater_than(slowURLTime, urlTime + 1000,
+ 'Slow service worker request should measure increased delay.');
+ frame.remove();
+ })
+}, 'empty service worker fetch event included in performance timings');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/postmessage-blob-url.https.html b/testing/web-platform/tests/service-workers/service-worker/postmessage-blob-url.https.html
new file mode 100644
index 0000000000..16fddd57b8
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/postmessage-blob-url.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage Blob URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ let script = 'resources/postmessage-blob-url.js';
+ let scope = 'resources/blank.html';
+ let registration;
+ let blobText = 'Blob text';
+ let blob;
+ let blobUrl;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ add_completion_callback(() => r.unregister());
+ registration = r;
+ let worker = registration.installing;
+ blob = new Blob([blobText]);
+ blobUrl = URL.createObjectURL(blob);
+ return new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => { resolve(e.data); }
+ worker.postMessage(blobUrl);
+ });
+ })
+ .then(response => {
+ assert_equals(response, 'Worker reply:' + blobText);
+ URL.revokeObjectURL(blobUrl);
+ return registration.unregister();
+ });
+ }, 'postMessage Blob URL to a ServiceWorker');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html b/testing/web-platform/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html
new file mode 100644
index 0000000000..117def9eb2
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage from waiting serviceworker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+function echo(worker, data) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.addEventListener('message', function onMsg(evt) {
+ navigator.serviceWorker.removeEventListener('message', onMsg);
+ resolve(evt);
+ });
+ worker.postMessage(data);
+ });
+}
+
+promise_test(t => {
+ let script = 'resources/echo-message-to-source-worker.js';
+ let scope = 'resources/client-postmessage-from-wait-serviceworker';
+ let registration;
+ let frame;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(swr => {
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ registration = swr;
+ return wait_for_state(t, registration.installing, 'activated');
+ }).then(_ => {
+ return with_iframe(scope);
+ }).then(f => {
+ frame = f;
+ return navigator.serviceWorker.register(script + '?update', { scope: scope })
+ }).then(swr => {
+ assert_equals(swr, registration, 'should be same registration');
+ return wait_for_state(t, registration.installing, 'installed');
+ }).then(_ => {
+ return echo(registration.waiting, 'waiting');
+ }).then(evt => {
+ assert_equals(evt.source, registration.waiting,
+ 'message event source should be correct');
+ return echo(registration.active, 'active');
+ }).then(evt => {
+ assert_equals(evt.source, registration.active,
+ 'message event source should be correct');
+ frame.remove();
+ });
+}, 'Client.postMessage() from waiting serviceworker.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html b/testing/web-platform/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html
new file mode 100644
index 0000000000..29c056080c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage via MessagePort to Client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/postmessage-msgport-to-client-worker.js';
+ var scope = 'resources/blank.html';
+ var port;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(() => registration.unregister());
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => with_iframe(scope))
+ .then(frame => {
+ t.add_cleanup(() => frame.remove());
+ return new Promise(resolve => {
+ var w = frame.contentWindow;
+ w.navigator.serviceWorker.onmessage = resolve;
+ w.navigator.serviceWorker.controller.postMessage('ping');
+ });
+ })
+ .then(e => {
+ port = e.ports[0];
+ port.postMessage({value: 1});
+ port.postMessage({value: 2});
+ port.postMessage({done: true});
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ assert_equals(e.data.ack, 'Acking value: 1');
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ assert_equals(e.data.ack, 'Acking value: 2');
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => { assert_true(e.data.done, 'done'); });
+ }, 'postMessage MessagePorts from ServiceWorker to Client');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html b/testing/web-platform/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html
new file mode 100644
index 0000000000..83e5f4540d
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage to Client (message queue)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// This function creates a message listener that captures all messages
+// sent to this window and matches them with corresponding requests.
+// This frees test code from having to use clunky constructs just to
+// avoid race conditions, since the relative order of message and
+// request arrival doesn't matter.
+function create_message_listener(t) {
+ const listener = {
+ messages: new Set(),
+ requests: new Set(),
+ waitFor: function(predicate) {
+ for (const event of this.messages) {
+ // If a message satisfying the predicate has already
+ // arrived, it gets matched to this request.
+ if (predicate(event)) {
+ this.messages.delete(event);
+ return Promise.resolve(event);
+ }
+ }
+
+ // If no match was found, the request is stored and a
+ // promise is returned.
+ const request = { predicate };
+ const promise = new Promise(resolve => request.resolve = resolve);
+ this.requests.add(request);
+ return promise;
+ }
+ };
+ window.onmessage = t.step_func(event => {
+ for (const request of listener.requests) {
+ // If the new message matches a stored request's
+ // predicate, the request's promise is resolved with this
+ // message.
+ if (request.predicate(event)) {
+ listener.requests.delete(request);
+ request.resolve(event);
+ return;
+ }
+ };
+
+ // No outstanding request for this message, store it in case
+ // it's requested later.
+ listener.messages.add(event);
+ });
+ return listener;
+}
+
+async function service_worker_register_and_activate(t, script, scope) {
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+ return worker;
+}
+
+// Add an iframe (parent) whose document contains a nested iframe
+// (child), then set the child's src attribute to child_url and return
+// its Window (without waiting for it to finish loading).
+async function with_nested_iframes(t, child_url) {
+ const parent = await with_iframe('resources/nested-iframe-parent.html?role=parent');
+ t.add_cleanup(() => parent.remove());
+ const child = parent.contentWindow.document.getElementById('child');
+ child.setAttribute('src', child_url);
+ return child.contentWindow;
+}
+
+// Returns a predicate matching a fetch message with the specified
+// key.
+function fetch_message(key) {
+ return event => event.data.type === 'fetch' && event.data.key === key;
+}
+
+// Returns a predicate matching a ping message with the specified
+// payload.
+function ping_message(data) {
+ return event => event.data.type === 'ping' && event.data.data === data;
+}
+
+// A client message queue test is a testharness.js test with some
+// additional setup:
+// 1. A listener (see create_message_listener)
+// 2. An active service worker
+// 3. Two nested iframes
+// 4. A state transition function that controls the order of events
+// during the test
+function client_message_queue_test(url, test_function, description) {
+ promise_test(async t => {
+ t.listener = create_message_listener(t);
+
+ const script = 'resources/stalling-service-worker.js';
+ const scope = 'resources/';
+ t.service_worker = await service_worker_register_and_activate(t, script, scope);
+
+ // We create two nested iframes such that both are controlled by
+ // the newly installed service worker.
+ const child_url = url + '?role=child';
+ t.frame = await with_nested_iframes(t, child_url);
+
+ t.state_transition = async function(from, to, scripts) {
+ // A state transition begins with the child's parser
+ // fetching a script due to a <script> tag. The request
+ // arrives at the service worker, which notifies the
+ // parent, which in turn notifies the test. Note that the
+ // event loop keeps spinning while the parser is waiting.
+ const request = await this.listener.waitFor(fetch_message(to));
+
+ // The test instructs the service worker to send two ping
+ // messages through the Client interface: first to the
+ // child, then to the parent.
+ this.service_worker.postMessage(from);
+
+ // When the parent receives the ping message, it forwards
+ // it to the test. Assuming that messages to both child
+ // and parent are mapped to the same task queue (this is
+ // not [yet] required by the spec), receiving this message
+ // guarantees that the child has already dispatched its
+ // message if it was allowed to do so.
+ await this.listener.waitFor(ping_message(from));
+
+ // Finally, reply to the service worker's fetch
+ // notification with the script it should use as the fetch
+ // request's response. This is a defensive mechanism that
+ // ensures the child's parser really is blocked until the
+ // test is ready to continue.
+ request.ports[0].postMessage([`state = '${to}';`].concat(scripts));
+ };
+
+ await test_function(t);
+ }, description);
+}
+
+function client_message_queue_enable_test(
+ install_script,
+ start_script,
+ earliest_dispatch,
+ description)
+{
+ function assert_state_less_than_equal(state1, state2, explanation) {
+ const states = ['init', 'install', 'start', 'finish', 'loaded'];
+ const index1 = states.indexOf(state1);
+ const index2 = states.indexOf(state2);
+ if (index1 > index2)
+ assert_unreached(explanation);
+ }
+
+ client_message_queue_test('enable-client-message-queue.html', async t => {
+ // While parsing the child's document, the child transitions
+ // from the 'init' state all the way to the 'finish' state.
+ // Once parsing is finished it would enter the final 'loaded'
+ // state. All but the last transition require assitance from
+ // the test.
+ await t.state_transition('init', 'install', [install_script]);
+ await t.state_transition('install', 'start', [start_script]);
+ await t.state_transition('start', 'finish', []);
+
+ // Wait for all messages to get dispatched on the child's
+ // ServiceWorkerContainer and then verify that each message
+ // was dispatched after |earliest_dispatch|.
+ const report = await t.frame.report;
+ ['init', 'install', 'start'].forEach(state => {
+ const explanation = `Message sent in state '${state}' was dispatched in '${report[state]}', should be dispatched no earlier than '${earliest_dispatch}'`;
+ assert_state_less_than_equal(earliest_dispatch,
+ report[state],
+ explanation);
+ });
+ }, description);
+}
+
+const empty_script = ``;
+
+const add_event_listener =
+ `navigator.serviceWorker.addEventListener('message', handle_message);`;
+
+const set_onmessage = `navigator.serviceWorker.onmessage = handle_message;`;
+
+const start_messages = `navigator.serviceWorker.startMessages();`;
+
+client_message_queue_enable_test(add_event_listener, empty_script, 'loaded',
+ 'Messages from ServiceWorker to Client only received after DOMContentLoaded event.');
+
+client_message_queue_enable_test(add_event_listener, start_messages, 'start',
+ 'Messages from ServiceWorker to Client only received after calling startMessages().');
+
+client_message_queue_enable_test(set_onmessage, empty_script, 'install',
+ 'Messages from ServiceWorker to Client only received after setting onmessage.');
+
+const resolve_manual_promise = `resolve_manual_promise();`
+
+async function test_microtasks_when_client_message_queue_enabled(t, scripts) {
+ await t.state_transition('init', 'start', scripts.concat([resolve_manual_promise]));
+ let result = await t.frame.result;
+ assert_equals(result[0], 'microtask', 'The microtask was executed first.');
+ assert_equals(result[1], 'message', 'The message was dispatched.');
+}
+
+client_message_queue_test('message-vs-microtask.html', t => {
+ return test_microtasks_when_client_message_queue_enabled(t, [
+ add_event_listener,
+ start_messages,
+ ]);
+}, 'Microtasks run before dispatching messages after calling startMessages().');
+
+client_message_queue_test('message-vs-microtask.html', t => {
+ return test_microtasks_when_client_message_queue_enabled(t, [set_onmessage]);
+}, 'Microtasks run before dispatching messages after setting onmessage.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/postmessage-to-client.https.html b/testing/web-platform/tests/service-workers/service-worker/postmessage-to-client.https.html
new file mode 100644
index 0000000000..f834a4bffe
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/postmessage-to-client.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage to Client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(async t => {
+ const script = 'resources/postmessage-to-client-worker.js';
+ const scope = 'resources/blank.html';
+
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+ const w = frame.contentWindow;
+
+ w.navigator.serviceWorker.controller.postMessage('ping');
+ let e = await new Promise(r => w.navigator.serviceWorker.onmessage = r);
+
+ assert_equals(e.constructor, w.MessageEvent,
+ 'message events should use MessageEvent interface.');
+ assert_equals(e.type, 'message', 'type should be "message".');
+ assert_false(e.bubbles, 'message events should not bubble.');
+ assert_false(e.cancelable, 'message events should not be cancelable.');
+ assert_equals(e.origin, location.origin,
+ 'origin of message should be origin of Service Worker.');
+ assert_equals(e.lastEventId, '',
+ 'lastEventId should be an empty string.');
+ assert_equals(e.source.constructor, w.ServiceWorker,
+ 'source should use ServiceWorker interface.');
+ assert_equals(e.source, w.navigator.serviceWorker.controller,
+ 'source should be the service worker that sent the message.');
+ assert_equals(e.ports.length, 0, 'ports should be an empty array.');
+ assert_equals(e.data, 'Sending message via clients');
+
+ e = await new Promise(r => w.navigator.serviceWorker.onmessage = r);
+ assert_equals(e.data, 'quit');
+}, 'postMessage from ServiceWorker to Client.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/postmessage.https.html b/testing/web-platform/tests/service-workers/service-worker/postmessage.https.html
new file mode 100644
index 0000000000..7abb3022f9
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/postmessage.https.html
@@ -0,0 +1,202 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/postmessage-worker.js';
+ var scope = 'resources/blank.html';
+ var registration;
+ var worker;
+ var port;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ t.add_cleanup(() => r.unregister());
+ registration = r;
+ worker = registration.installing;
+
+ var messageChannel = new MessageChannel();
+ port = messageChannel.port1;
+ return new Promise(resolve => {
+ port.onmessage = resolve;
+ worker.postMessage({port: messageChannel.port2},
+ [messageChannel.port2]);
+ worker.postMessage({value: 1});
+ worker.postMessage({value: 2});
+ worker.postMessage({done: true});
+ });
+ })
+ .then(e => {
+ assert_equals(e.data, 'Acking value: 1');
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ assert_equals(e.data, 'Acking value: 2');
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ assert_equals(e.data, 'quit');
+ return registration.unregister(scope);
+ });
+ }, 'postMessage to a ServiceWorker (and back via MessagePort)');
+
+promise_test(t => {
+ var script = 'resources/postmessage-transferables-worker.js';
+ var scope = 'resources/blank.html';
+ var sw = navigator.serviceWorker;
+
+ var message = 'Hello, world!';
+ var text_encoder = new TextEncoder;
+ var text_decoder = new TextDecoder;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ t.add_cleanup(() => r.unregister());
+
+ var ab = text_encoder.encode(message);
+ assert_equals(ab.byteLength, message.length);
+ r.installing.postMessage(ab, [ab.buffer]);
+ assert_equals(text_decoder.decode(ab), '');
+ assert_equals(ab.byteLength, 0);
+
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the transferred array buffer.
+ assert_equals(e.data.content, message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the array buffer sent back from
+ // ServiceWorker via Client.postMessage.
+ assert_equals(text_decoder.decode(e.data), message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify that the array buffer on ServiceWorker is neutered.
+ assert_equals(e.data.content, '');
+ assert_equals(e.data.byteLength, 0);
+ });
+ }, 'postMessage a transferable ArrayBuffer between ServiceWorker and Client');
+
+promise_test(t => {
+ var script = 'resources/postmessage-transferables-worker.js';
+ var scope = 'resources/blank.html';
+ var message = 'Hello, world!';
+ var text_encoder = new TextEncoder;
+ var text_decoder = new TextDecoder;
+ var port;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ t.add_cleanup(() => r.unregister());
+
+ var channel = new MessageChannel;
+ port = channel.port1;
+ r.installing.postMessage(undefined, [channel.port2]);
+
+ var ab = text_encoder.encode(message);
+ assert_equals(ab.byteLength, message.length);
+ port.postMessage(ab, [ab.buffer]);
+ assert_equals(text_decoder.decode(ab), '');
+ assert_equals(ab.byteLength, 0);
+
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the transferred array buffer.
+ assert_equals(e.data.content, message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the array buffer sent back from
+ // ServiceWorker via Client.postMessage.
+ assert_equals(text_decoder.decode(e.data), message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify that the array buffer on ServiceWorker is neutered.
+ assert_equals(e.data.content, '');
+ assert_equals(e.data.byteLength, 0);
+ });
+ }, 'postMessage a transferable ArrayBuffer between ServiceWorker and Client' +
+ ' over MessagePort');
+
+ promise_test(t => {
+ var script = 'resources/postmessage-dictionary-transferables-worker.js';
+ var scope = 'resources/blank.html';
+ var sw = navigator.serviceWorker;
+
+ var message = 'Hello, world!';
+ var text_encoder = new TextEncoder;
+ var text_decoder = new TextDecoder;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ t.add_cleanup(() => r.unregister());
+
+ var ab = text_encoder.encode(message);
+ assert_equals(ab.byteLength, message.length);
+ r.installing.postMessage(ab, {transfer: [ab.buffer]});
+ assert_equals(text_decoder.decode(ab), '');
+ assert_equals(ab.byteLength, 0);
+
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the transferred array buffer.
+ assert_equals(e.data.content, message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the array buffer sent back from
+ // ServiceWorker via Client.postMessage.
+ assert_equals(text_decoder.decode(e.data), message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify that the array buffer on ServiceWorker is neutered.
+ assert_equals(e.data.content, '');
+ assert_equals(e.data.byteLength, 0);
+ });
+ }, 'postMessage with dictionary a transferable ArrayBuffer between ServiceWorker and Client');
+
+ promise_test(async t => {
+ const firstScript = 'resources/postmessage-echo-worker.js?one';
+ const secondScript = 'resources/postmessage-echo-worker.js?two';
+ const scope = 'resources/';
+
+ const registration = await service_worker_unregister_and_register(t, firstScript, scope);
+ t.add_cleanup(() => registration.unregister());
+ const firstWorker = registration.installing;
+
+ const messagePromise = new Promise(resolve => {
+ navigator.serviceWorker.addEventListener('message', (event) => {
+ resolve(event.data);
+ }, {once: true});
+ });
+
+ await wait_for_state(t, firstWorker, 'activated');
+ await navigator.serviceWorker.register(secondScript, {scope});
+ const secondWorker = registration.installing;
+ await wait_for_state(t, firstWorker, 'redundant');
+
+ // postMessage() to a redundant worker should be dropped silently.
+ // Historically, this threw an exception.
+ firstWorker.postMessage('firstWorker');
+
+ // To test somewhat that it was not received, send a message to another
+ // worker and check that we get a reply for that one.
+ secondWorker.postMessage('secondWorker');
+ const data = await messagePromise;
+ assert_equals(data, 'secondWorker');
+ }, 'postMessage to a redundant worker');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/ready.https.window.js b/testing/web-platform/tests/service-workers/service-worker/ready.https.window.js
new file mode 100644
index 0000000000..6c4e270682
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/ready.https.window.js
@@ -0,0 +1,223 @@
+// META: title=Service Worker: navigator.serviceWorker.ready
+// META: script=resources/test-helpers.sub.js
+
+test(() => {
+ assert_equals(
+ navigator.serviceWorker.ready,
+ navigator.serviceWorker.ready,
+ 'repeated access to ready without intervening registrations should return the same Promise object'
+ );
+}, 'ready returns the same Promise object');
+
+promise_test(async t => {
+ const frame = await with_iframe('resources/blank.html?uncontrolled');
+ t.add_cleanup(() => frame.remove());
+
+ const promise = frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(
+ Object.getPrototypeOf(promise),
+ frame.contentWindow.Promise.prototype,
+ 'the Promise should be in the context of the related document'
+ );
+}, 'ready returns a Promise object in the context of the related document');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const scope = 'resources/blank.html?ready-controlled';
+ const expectedURL = normalizeURL(url);
+ const registration = await service_worker_unregister_and_register(t, url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const readyReg = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(readyReg.installing, null, 'installing should be null');
+ assert_equals(readyReg.waiting, null, 'waiting should be null');
+ assert_equals(readyReg.active.scriptURL, expectedURL, 'active after ready should not be null');
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller,
+ readyReg.active,
+ 'the controller should be the active worker'
+ );
+ assert_in_array(
+ readyReg.active.state,
+ ['activating', 'activated'],
+ '.ready should be resolved when the registration has an active worker'
+ );
+}, 'ready on a controlled document');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const scope = 'resources/blank.html?ready-potential-controlled';
+ const expected_url = normalizeURL(url);
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const registration = await navigator.serviceWorker.register(url, { scope });
+ t.add_cleanup(() => registration.unregister());
+
+ const readyReg = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(readyReg.installing, null, 'installing should be null');
+ assert_equals(readyReg.waiting, null, 'waiting should be null.')
+ assert_equals(readyReg.active.scriptURL, expected_url, 'active after ready should not be null');
+ assert_in_array(
+ readyReg.active.state,
+ ['activating', 'activated'],
+ '.ready should be resolved when the registration has an active worker'
+ );
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller,
+ null,
+ 'uncontrolled document should not have a controller'
+ );
+}, 'ready on a potential controlled document');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const scope = 'resources/blank.html?ready-installing';
+
+ await service_worker_unregister(t, scope);
+
+ const frame = await with_iframe(scope);
+ const promise = frame.contentWindow.navigator.serviceWorker.ready;
+ navigator.serviceWorker.register(url, { scope });
+ const registration = await promise;
+
+ t.add_cleanup(async () => {
+ await registration.unregister();
+ frame.remove();
+ });
+
+ assert_equals(registration.installing, null, 'installing should be null');
+ assert_equals(registration.waiting, null, 'waiting should be null');
+ assert_not_equals(registration.active, null, 'active after ready should not be null');
+ assert_in_array(
+ registration.active.state,
+ ['activating', 'activated'],
+ '.ready should be resolved when the registration has an active worker'
+ );
+}, 'ready on an iframe whose parent registers a new service worker');
+
+promise_test(async t => {
+ const scope = 'resources/register-iframe.html';
+ const frame = await with_iframe(scope);
+
+ const registration = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ t.add_cleanup(async () => {
+ await registration.unregister();
+ frame.remove();
+ });
+
+ assert_equals(registration.installing, null, 'installing should be null');
+ assert_equals(registration.waiting, null, 'waiting should be null');
+ assert_not_equals(registration.active, null, 'active after ready should not be null');
+ assert_in_array(
+ registration.active.state,
+ ['activating', 'activated'],
+ '.ready should be resolved with "active worker"'
+ );
+ }, 'ready on an iframe that installs a new service worker');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const matchedScope = 'resources/blank.html?ready-after-match';
+ const longerMatchedScope = 'resources/blank.html?ready-after-match-longer';
+
+ await service_worker_unregister(t, matchedScope);
+ await service_worker_unregister(t, longerMatchedScope);
+
+ const frame = await with_iframe(longerMatchedScope);
+ const registration = await navigator.serviceWorker.register(url, { scope: matchedScope });
+
+ t.add_cleanup(async () => {
+ await registration.unregister();
+ frame.remove();
+ });
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const longerRegistration = await navigator.serviceWorker.register(url, { scope: longerMatchedScope });
+
+ t.add_cleanup(() => longerRegistration.unregister());
+
+ const readyReg = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(
+ readyReg.scope,
+ normalizeURL(longerMatchedScope),
+ 'longer matched registration should be returned'
+ );
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller,
+ null,
+ 'controller should be null'
+ );
+}, 'ready after a longer matched registration registered');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const matchedScope = 'resources/blank.html?ready-after-resolve';
+ const longerMatchedScope = 'resources/blank.html?ready-after-resolve-longer';
+ const registration = await service_worker_unregister_and_register(t, url, matchedScope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe(longerMatchedScope);
+ t.add_cleanup(() => frame.remove());
+
+ const readyReg1 = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(
+ readyReg1.scope,
+ normalizeURL(matchedScope),
+ 'matched registration should be returned'
+ );
+
+ const longerReg = await navigator.serviceWorker.register(url, { scope: longerMatchedScope });
+ t.add_cleanup(() => longerReg.unregister());
+
+ const readyReg2 = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(
+ readyReg2.scope,
+ normalizeURL(matchedScope),
+ 'ready should only be resolved once'
+ );
+}, 'access ready after it has been resolved');
+
+promise_test(async t => {
+ const url1 = 'resources/empty-worker.js';
+ const url2 = url1 + '?2';
+ const matchedScope = 'resources/blank.html?ready-after-unregister';
+ const reg1 = await service_worker_unregister_and_register(t, url1, matchedScope);
+ t.add_cleanup(() => reg1.unregister());
+
+ await wait_for_state(t, reg1.installing, 'activating');
+
+ const frame = await with_iframe(matchedScope);
+ t.add_cleanup(() => frame.remove());
+
+ await reg1.unregister();
+
+ // Ready promise should be pending, waiting for a new registration to arrive
+ const readyPromise = frame.contentWindow.navigator.serviceWorker.ready;
+
+ const reg2 = await navigator.serviceWorker.register(url2, { scope: matchedScope });
+ t.add_cleanup(() => reg2.unregister());
+
+ const readyReg = await readyPromise;
+
+ // Wait for registration update, since it comes from another global, the states are racy.
+ await wait_for_state(t, reg2.installing || reg2.waiting || reg2.active, 'activated');
+
+ assert_equals(readyReg.active.scriptURL, reg2.active.scriptURL, 'Resolves with the second registration');
+ assert_not_equals(reg1, reg2, 'Registrations should be different');
+}, 'resolve ready after unregistering');
diff --git a/testing/web-platform/tests/service-workers/service-worker/redirected-response.https.html b/testing/web-platform/tests/service-workers/service-worker/redirected-response.https.html
new file mode 100644
index 0000000000..71b35d0e12
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/redirected-response.https.html
@@ -0,0 +1,471 @@
+<!DOCTYPE html>
+<title>Service Worker: Redirected response</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests redirect behavior. It calls fetch_method(url, fetch_option) and tests
+// the resulting response against the expected values. It also adds the
+// response to |cache| and checks the cached response matches the expected
+// values.
+//
+// |options|: a dictionary of parameters for the test
+// |options.url|: the URL to fetch
+// |options.fetch_option|: the options passed to |fetch_method|
+// |options.fetch_method|: the method used to fetch. Useful for testing an
+// iframe's fetch() vs. this page's fetch().
+// |options.expected_type|: The value of response.type
+// |options.expected_redirected|: The value of response.redirected
+// |options.expected_intercepted_urls|: The list of intercepted request URLs.
+function redirected_test(options) {
+ return options.fetch_method.call(null, options.url, options.fetch_option).then(response => {
+ let cloned_response = response.clone();
+ assert_equals(
+ response.type, options.expected_type,
+ 'The response type of response must match. URL: ' + options.url);
+ assert_equals(
+ cloned_response.type, options.expected_type,
+ 'The response type of cloned response must match. URL: ' + options.url);
+ assert_equals(
+ response.redirected, options.expected_redirected,
+ 'The redirected flag of response must match. URL: ' + options.url);
+ assert_equals(
+ cloned_response.redirected, options.expected_redirected,
+ 'The redirected flag of cloned response must match. URL: ' + options.url);
+ if (options.expected_response_url) {
+ assert_equals(
+ cloned_response.url, options.expected_response_url,
+ 'The URL does not meet expectation. URL: ' + options.url);
+ }
+ return cache.put(options.url, response);
+ })
+ .then(_ => cache.match(options.url))
+ .then(response => {
+ assert_equals(
+ response.type, options.expected_type,
+ 'The response type of response in CacheStorage must match. ' +
+ 'URL: ' + options.url);
+ assert_equals(
+ response.redirected, options.expected_redirected,
+ 'The redirected flag of response in CacheStorage must match. ' +
+ 'URL: ' + options.url);
+ return check_intercepted_urls(options.expected_intercepted_urls);
+ });
+}
+
+async function take_intercepted_urls() {
+ const message = new Promise((resolve) => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = msg => { resolve(msg.data.requestInfos); };
+ worker.postMessage({command: 'getRequestInfos', port: channel.port2},
+ [channel.port2]);
+ });
+ const request_infos = await message;
+ return request_infos.map(info => { return info.url; });
+}
+
+function check_intercepted_urls(expected_urls) {
+ return take_intercepted_urls().then((urls) => {
+ assert_object_equals(urls, expected_urls, 'Intercepted URLs matching.');
+ });
+}
+
+function setup_and_clean() {
+ // To prevent interference from previous tests, take the intercepted URLs from
+ // the service worker.
+ return setup.then(() => take_intercepted_urls());
+}
+
+
+let host_info = get_host_info();
+const REDIRECT_URL = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/redirect.py?Redirect=';
+const TARGET_URL = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/simple.txt?';
+const REDIRECT_TO_TARGET_URL = REDIRECT_URL + encodeURIComponent(TARGET_URL);
+let frame;
+let cache;
+let setup;
+let worker;
+
+promise_test(t => {
+ const SCOPE = 'resources/blank.html?redirected-response';
+ const SCRIPT = 'resources/redirect-worker.js';
+ const CACHE_NAME = 'service-workers/service-worker/redirected-response';
+ setup = service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(registration => {
+ promise_test(
+ () => registration.unregister(),
+ 'restore global state (service worker registration)');
+ worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(_ => self.caches.open(CACHE_NAME))
+ .then(c => {
+ cache = c;
+ promise_test(
+ () => self.caches.delete(CACHE_NAME),
+ 'restore global state (caches)');
+ return with_iframe(SCOPE);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => f.remove());
+ return check_intercepted_urls(
+ [host_info['HTTPS_ORIGIN'] + base_path() + SCOPE]);
+ });
+ return setup;
+ }, 'initialize global state (service worker registration and caches)');
+
+// ===============================================================
+// Tests for requests that are out-of-scope of the service worker.
+// ===============================================================
+promise_test(t => setup_and_clean()
+ .then(() => redirected_test({url: TARGET_URL,
+ fetch_option: {},
+ fetch_method: self.fetch,
+ expected_type: 'basic',
+ expected_redirected: false,
+ expected_intercepted_urls: []})),
+ 'mode: "follow", non-intercepted request, no server redirect');
+
+promise_test(t => setup_and_clean()
+ .then(() => redirected_test({url: REDIRECT_TO_TARGET_URL,
+ fetch_option: {},
+ fetch_method: self.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: []})),
+ 'mode: "follow", non-intercepted request');
+
+promise_test(t => setup_and_clean()
+ .then(() => redirected_test({url: REDIRECT_TO_TARGET_URL + '&manual',
+ fetch_option: {redirect: 'manual'},
+ fetch_method: self.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: []})),
+ 'mode: "manual", non-intercepted request');
+
+promise_test(t => setup_and_clean()
+ .then(() => promise_rejects_js(
+ t, TypeError,
+ self.fetch(REDIRECT_TO_TARGET_URL + '&error',
+ {redirect:'error'}),
+ 'The redirect response from the server should be treated as' +
+ ' an error when the redirect flag of request was \'error\'.'))
+ .then(() => check_intercepted_urls([])),
+ 'mode: "error", non-intercepted request');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = TARGET_URL + '&sw=fetch';
+ return redirected_test({url: url,
+ fetch_option: {},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "follow", no mode change, no server redirect');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a redirected response.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=follow&sw=fetch';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'follow'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "follow", no mode change');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=error&sw=follow';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The redirected response from the service worker should be ' +
+ 'treated as an error when the redirect flag of request was ' +
+ '\'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", mode change: "follow"');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=manual&sw=follow';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'manual'}),
+ 'The redirected response from the service worker should be ' +
+ 'treated as an error when the redirect flag of request was ' +
+ '\'manual\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "manual", mode change: "follow"');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns an opaqueredirect response.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=follow&sw=manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'follow'}),
+ 'The opaqueredirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'follow\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "follow", mode change: "manual"');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=error&sw=manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The opaqueredirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", mode change: "manual"');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=manual&sw=manual';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'manual'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]});
+ }),
+ 'mode: "manual", no mode change');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=follow&sw=gen';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'follow'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: [url, TARGET_URL]})
+ }),
+ 'mode: "follow", generated redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=error&sw=gen';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The generated redirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", generated redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=manual&sw=gen';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'manual'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "manual", generated redirect response');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response manually with the Response
+// constructor.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=follow&sw=gen-manual';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'follow'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: [url, TARGET_URL]})
+ }),
+ 'mode: "follow", manually-generated redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=error&sw=gen-manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The generated redirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", manually-generated redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=manual&sw=gen-manual';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'manual'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "manual", manually-generated redirect response');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response with a relative location header.
+// Generated responses do not have URLs, so this should fail to resolve.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=blank.html' +
+ '&original-redirect-mode=follow&sw=gen-manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'follow'}),
+ 'Following the generated redirect response from the service worker '+
+ 'should result fail.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "follow", generated relative redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=blank.html' +
+ '&original-redirect-mode=error&sw=gen-manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The generated redirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", generated relative redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=blank.html' +
+ '&original-redirect-mode=manual&sw=gen-manual';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'manual'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "manual", generated relative redirect response');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response. And the fetch follows the
+// redirection multiple times.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ // The Fetch spec says: "If request’s redirect count is twenty, return a
+ // network error." https://fetch.spec.whatwg.org/#http-redirect-fetch
+ // So fetch can follow the redirect response 20 times.
+ let urls = [TARGET_URL];
+ for (let i = 0; i < 20; ++i) {
+ urls.unshift(host_info['HTTPS_ORIGIN'] + '/sample?sw=gen&url=' +
+ encodeURIComponent(urls[0]));
+
+ }
+ return redirected_test({url: urls[0],
+ fetch_option: {redirect: 'follow'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: urls})
+ }),
+ 'Fetch should follow the redirect response 20 times');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ let urls = [TARGET_URL];
+ // The Fetch spec says: "If request’s redirect count is twenty, return a
+ // network error." https://fetch.spec.whatwg.org/#http-redirect-fetch
+ // So fetch can't follow the redirect response 21 times.
+ for (let i = 0; i < 21; ++i) {
+ urls.unshift(host_info['HTTPS_ORIGIN'] + '/sample?sw=gen&url=' +
+ encodeURIComponent(urls[0]));
+
+ }
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(urls[0], {redirect: 'follow'}),
+ 'Fetch should not follow the redirect response 21 times.')
+ .then(() => {
+ urls.pop();
+ return check_intercepted_urls(urls)
+ });
+ }),
+ 'Fetch should not follow the redirect response 21 times.');
+
+// =======================================================
+// A test for verifying the url of a service-worker-redirected request is
+// propagated to the outer response.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() + 'sample?url=' +
+ encodeURIComponent(TARGET_URL) +'&sw=fetch-url';
+ return redirected_test({url: url,
+ fetch_option: {},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: false,
+ expected_intercepted_urls: [url],
+ expected_response_url: TARGET_URL});
+ }),
+ 'The URL for the service worker redirected request should be propagated to ' +
+ 'response.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/referer.https.html b/testing/web-platform/tests/service-workers/service-worker/referer.https.html
new file mode 100644
index 0000000000..0957e4c533
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/referer.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: check referer of fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/referer-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var host_info = get_host_info();
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ var channel = new MessageChannel();
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ var onMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ return onMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the referer');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/referrer-policy-header.https.html b/testing/web-platform/tests/service-workers/service-worker/referrer-policy-header.https.html
new file mode 100644
index 0000000000..784343e6d8
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/referrer-policy-header.https.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<title>Service Worker: check referer of fetch() with Referrer Policy</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+const SCOPE = 'resources/referrer-policy-iframe.html';
+const SCRIPT = 'resources/fetch-rewrite-worker-referrer-policy.js';
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ t.add_cleanup(() => registration.unregister(),
+ 'Remove registration as a cleanup');
+
+ const full_scope_url = new URL(SCOPE, location.href);
+ const redirect_to = `${full_scope_url.href}?ignore=true`;
+ const frame = await with_iframe(
+ `${SCOPE}?pipe=status(302)|header(Location,${redirect_to})|` +
+ 'header(Referrer-Policy,origin)');
+ assert_equals(frame.contentDocument.referrer,
+ full_scope_url.origin + '/');
+ t.add_cleanup(() => frame.remove());
+}, 'Referrer for a main resource redirected with referrer-policy (origin) ' +
+ 'should only have origin.');
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE, `{type: 'module'}`);
+ await wait_for_state(t, registration.installing, 'activated');
+ t.add_cleanup(() => registration.unregister(),
+ 'Remove registration as a cleanup');
+
+ const full_scope_url = new URL(SCOPE, location.href);
+ const redirect_to = `${full_scope_url.href}?ignore=true`;
+ const frame = await with_iframe(
+ `${SCOPE}?pipe=status(302)|header(Location,${redirect_to})|` +
+ 'header(Referrer-Policy,origin)');
+ assert_equals(frame.contentDocument.referrer,
+ full_scope_url.origin + '/');
+ t.add_cleanup(() => frame.remove());
+}, 'Referrer for a main resource redirected with a module script with referrer-policy (origin) ' +
+ 'should only have origin.');
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ t.add_cleanup(() => registration.unregister(),
+ 'Remove registration as a cleanup');
+
+ const host_info = get_host_info();
+ const frame = await with_iframe(SCOPE);
+ const channel = new MessageChannel();
+ t.add_cleanup(() => frame.remove());
+ const e = await new Promise(resolve => {
+ channel.port1.onmessage = resolve;
+ frame.contentWindow.postMessage(
+ {}, host_info['HTTPS_ORIGIN'], [channel.port2]);
+ });
+ assert_equals(e.data.results, 'finish');
+}, 'Referrer for fetch requests initiated from a service worker with ' +
+ 'referrer-policy (origin) should only have origin.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html b/testing/web-platform/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html
new file mode 100644
index 0000000000..65c60a11db
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Service Worker: check referrer of top-level script fetch</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+
+async function get_toplevel_script_headers(worker) {
+ worker.postMessage("getHeaders");
+ return new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ });
+}
+
+promise_test(async (t) => {
+ const script = "resources/test-request-headers-worker.py";
+ const scope = "resources/blank.html";
+ const host_info = get_host_info();
+
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ await wait_for_state(t, registration.installing, "activated");
+
+ const expected_referrer = host_info["HTTPS_ORIGIN"] + location.pathname;
+
+ // Check referrer for register().
+ const register_headers = await get_toplevel_script_headers(registration.active);
+ assert_equals(register_headers["referer"], expected_referrer, "referrer of register()");
+
+ // Check referrer for update().
+ await registration.update();
+ await wait_for_state(t, registration.installing, "installed");
+ const update_headers = await get_toplevel_script_headers(registration.waiting);
+ assert_equals(update_headers["referer"], expected_referrer, "referrer of update()");
+}, "Referrer of the top-level script fetch should be the document URL");
+
+promise_test(async (t) => {
+ const script = "resources/test-request-headers-worker.py";
+ const scope = "resources/blank.html";
+ const host_info = get_host_info();
+
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope, {type: 'module'});
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ await wait_for_state(t, registration.installing, "activated");
+
+ const expected_referrer = host_info["HTTPS_ORIGIN"] + location.pathname;
+
+ // Check referrer for register().
+ const register_headers = await get_toplevel_script_headers(registration.active);
+ assert_equals(register_headers["referer"], expected_referrer, "referrer of register()");
+
+ // Check referrer for update().
+ await registration.update();
+ await wait_for_state(t, registration.installing, "installed");
+ const update_headers = await get_toplevel_script_headers(registration.waiting);
+ assert_equals(update_headers["referer"], expected_referrer, "referrer of update()");
+}, "Referrer of the module script fetch should be the document URL");
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/register-closed-window.https.html b/testing/web-platform/tests/service-workers/service-worker/register-closed-window.https.html
new file mode 100644
index 0000000000..9c1b639bb7
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/register-closed-window.https.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>Service Worker: Register() on Closed Window</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+var host_info = get_host_info();
+var frameURL = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/register-closed-window-iframe.html';
+
+async_test(function(t) {
+ var frame;
+ with_iframe(frameURL).then(function(f) {
+ frame = f;
+ return new Promise(function(resolve) {
+ window.addEventListener('message', function messageHandler(evt) {
+ window.removeEventListener('message', messageHandler);
+ resolve(evt.data);
+ });
+ frame.contentWindow.postMessage('START', '*');
+ });
+ }).then(function(result) {
+ assert_equals(result, 'OK', 'frame should complete without crashing');
+ frame.remove();
+ t.done();
+ }).catch(unreached_rejection(t));
+}, 'Call register() on ServiceWorkerContainer owned by closed window.');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/register-default-scope.https.html b/testing/web-platform/tests/service-workers/service-worker/register-default-scope.https.html
new file mode 100644
index 0000000000..1d86548eb5
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/register-default-scope.https.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<title>register() and scope</title>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var script_url = new URL(script, location.href);
+ var expected_scope = new URL('./', script_url).href;
+ return service_worker_unregister(t, expected_scope)
+ .then(function() {
+ return navigator.serviceWorker.register('resources/empty-worker.js');
+ }).then(function(registration) {
+ assert_equals(registration.scope, expected_scope,
+ 'The default scope should be URL("./", script_url)');
+ return registration.unregister();
+ }).then(function() {
+ t.done();
+ });
+ }, 'default scope');
+
+promise_test(function(t) {
+ // This script must be different than the 'default scope' test, or else
+ // the scopes will collide.
+ var script = 'resources/empty.js';
+ var script_url = new URL(script, location.href);
+ var expected_scope = new URL('./', script_url).href;
+ return service_worker_unregister(t, expected_scope)
+ .then(function() {
+ return navigator.serviceWorker.register('resources/empty.js',
+ { scope: undefined });
+ }).then(function(registration) {
+ assert_equals(registration.scope, expected_scope,
+ 'The default scope should be URL("./", script_url)');
+ return registration.unregister();
+ }).then(function() {
+ t.done();
+ });
+ }, 'undefined scope');
+
+promise_test(function(t) {
+ var script = 'resources/simple-fetch-worker.js';
+ var script_url = new URL(script, location.href);
+ var expected_scope = new URL('./', script_url).href;
+ return service_worker_unregister(t, expected_scope)
+ .then(function() {
+ return navigator.serviceWorker.register('resources/empty.js',
+ { scope: null });
+ })
+ .then(
+ function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, registration.scope);
+ });
+
+ assert_unreached('register should fail');
+ },
+ function(error) {
+ assert_equals(error.name, 'SecurityError',
+ 'passing a null scope should be interpreted as ' +
+ 'scope="null" which violates the path restriction');
+ t.done();
+ });
+ }, 'null scope');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html b/testing/web-platform/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html
new file mode 100644
index 0000000000..6eb00f3071
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html
@@ -0,0 +1,233 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var script1 = normalizeURL('resources/empty-worker.js');
+var script2 = normalizeURL('resources/empty-worker.js?new');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-new-script-concurrently';
+ var register_promise1;
+ var register_promise2;
+
+ service_worker_unregister(t, scope)
+ .then(function() {
+ register_promise1 = navigator.serviceWorker.register(script1,
+ {scope: scope});
+ register_promise2 = navigator.serviceWorker.register(script2,
+ {scope: scope});
+ return register_promise1;
+ })
+ .then(function(registration) {
+ assert_equals(registration.installing.scriptURL, script1,
+ 'on first register, first script should be installing');
+ assert_equals(registration.waiting, null,
+ 'on first register, waiting should be null');
+ assert_equals(registration.active, null,
+ 'on first register, active should be null');
+ return register_promise2;
+ })
+ .then(function(registration) {
+ assert_equals(
+ registration.installing.scriptURL, script2,
+ 'on second register, second script should be installing');
+ // Spec allows racing: the first register may have finished
+ // or the second one could have terminated the installing worker.
+ assert_true(registration.waiting == null ||
+ registration.waiting.scriptURL == script1,
+ 'on second register, .waiting should be null or the ' +
+ 'first script');
+ assert_true(registration.active == null ||
+ (registration.waiting == null &&
+ registration.active.scriptURL == script1),
+ 'on second register, .active should be null or the ' +
+ 'first script');
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register different scripts concurrently');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-then-register-new-script';
+ var registration;
+
+ service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on activated, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on activated, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on activated, the first script should be active');
+ return navigator.serviceWorker.register(script2, {scope:scope});
+ })
+ .then(function(r) {
+ registration = r;
+ assert_equals(registration.installing.scriptURL, script2,
+ 'on second register, the second script should be ' +
+ 'installing');
+ assert_equals(registration.waiting, null,
+ 'on second register, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on second register, the first script should be ' +
+ 'active');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on installed, installing should be null');
+ assert_equals(registration.waiting.scriptURL, script2,
+ 'on installed, the second script should be waiting');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on installed, the first script should be active');
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then register new script URL');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-then-register-new-script-404';
+ var registration;
+
+ service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on activated, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on activated, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on activated, the first script should be active');
+ return navigator.serviceWorker.register('this-will-404.js',
+ {scope:scope});
+ })
+ .then(
+ function() { assert_unreached('register should reject'); },
+ function(error) {
+ assert_equals(registration.installing, null,
+ 'on rejected, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on rejected, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on rejected, the first script should be active');
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then register new script URL that 404s');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-then-register-new-script-reject-install';
+ var reject_script = normalizeURL('resources/reject-install-worker.js');
+ var registration;
+
+ service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on activated, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on activated, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on activated, the first script should be active');
+ return navigator.serviceWorker.register(reject_script, {scope:scope});
+ })
+ .then(function(r) {
+ registration = r;
+ assert_equals(registration.installing.scriptURL, reject_script,
+ 'on update, the second script should be installing');
+ assert_equals(registration.waiting, null,
+ 'on update, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on update, the first script should be active');
+ return wait_for_state(t, registration.installing, 'redundant');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on redundant, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on redundant, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on redundant, the first script should be active');
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then register new script that does not install');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-new-script-controller';
+ var iframe;
+ var registration;
+
+ service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ iframe = frame;
+ return navigator.serviceWorker.register(script2, { scope: scope })
+ })
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ var sw_container = iframe.contentWindow.navigator.serviceWorker;
+ assert_equals(sw_container.controller.scriptURL, script1,
+ 'the old version should control the old doc');
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ var sw_container = frame.contentWindow.navigator.serviceWorker;
+ assert_equals(sw_container.controller.scriptURL, script1,
+ 'the old version should control a new doc');
+ var onactivated_promise = wait_for_state(t,
+ registration.waiting,
+ 'activated');
+ frame.remove();
+ iframe.remove();
+ return onactivated_promise;
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ var sw_container = frame.contentWindow.navigator.serviceWorker;
+ assert_equals(sw_container.controller.scriptURL, script2,
+ 'the new version should control a new doc');
+ frame.remove();
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register same-scope new script url effect on controller');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html b/testing/web-platform/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html
new file mode 100644
index 0000000000..0920b5cb22
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<title>Service Worker: Register wait-forever-in-install-worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var bad_script = 'resources/wait-forever-in-install-worker.js';
+ var good_script = 'resources/empty-worker.js';
+ var scope = 'resources/wait-forever-in-install-worker';
+ var other_scope = 'resources/wait-forever-in-install-worker-other';
+ var registration;
+ var registerPromise;
+
+ return navigator.serviceWorker.register(bad_script, {scope: scope})
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ assert_equals(registration.installing.scriptURL,
+ normalizeURL(bad_script));
+
+ // This register job should not start until the first
+ // register for the same scope completes.
+ registerPromise =
+ navigator.serviceWorker.register(good_script, {scope: scope});
+
+ // In order to test that the above register does not complete
+ // we will perform a register() on a different scope. The
+ // assumption here is that the previous register call would
+ // have completed in the same timeframe if it was able to do
+ // so.
+ return navigator.serviceWorker.register(good_script,
+ {scope: other_scope});
+ })
+ .then(function(swr) {
+ return swr.unregister();
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL,
+ normalizeURL(bad_script));
+ registration.installing.postMessage('STOP_WAITING');
+ return registerPromise;
+ })
+ .then(function(swr) {
+ assert_equals(registration.installing.scriptURL,
+ normalizeURL(good_script));
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ }, 'register worker that calls waitUntil with a promise that never ' +
+ 'resolves in oninstall');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-basic.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-basic.https.html
new file mode 100644
index 0000000000..759b4244a2
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/registration-basic.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (basic)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const script = 'resources/registration-worker.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/registration/normal';
+ const registration = await navigator.serviceWorker.register(script, {scope});
+ t.add_cleanup(() => registration.unregister());
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+}, 'Registering normal scope');
+
+promise_test(async (t) => {
+ const scope = 'resources/registration/scope-with-fragment#ref';
+ const registration = await navigator.serviceWorker.register(script, {scope});
+ t.add_cleanup(() => registration.unregister());
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+ assert_equals(
+ registration.scope,
+ normalizeURL('resources/registration/scope-with-fragment'),
+ 'A fragment should be removed from scope');
+}, 'Registering scope with fragment');
+
+promise_test(async (t) => {
+ const scope = 'resources/';
+ const registration = await navigator.serviceWorker.register(script, {scope})
+ t.add_cleanup(() => registration.unregister());
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+}, 'Registering same scope as the script directory');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-end-to-end.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-end-to-end.https.html
new file mode 100644
index 0000000000..1af4582d38
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/registration-end-to-end.https.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<title>Service Worker: registration end-to-end</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var t = async_test('Registration: end-to-end');
+t.step(function() {
+
+ var scope = 'resources/in-scope/';
+ var serviceWorkerStates = [];
+ var lastServiceWorkerState = '';
+ var receivedMessageFromPort = '';
+
+ assert_true(navigator.serviceWorker instanceof ServiceWorkerContainer);
+ assert_equals(typeof navigator.serviceWorker.register, 'function');
+ assert_equals(typeof navigator.serviceWorker.getRegistration, 'function');
+
+ service_worker_unregister_and_register(
+ t, 'resources/end-to-end-worker.js', scope)
+ .then(onRegister)
+ .catch(unreached_rejection(t));
+
+ function sendMessagePort(worker, from) {
+ var messageChannel = new MessageChannel();
+ worker.postMessage({from:from, port:messageChannel.port2}, [messageChannel.port2]);
+ return messageChannel.port1;
+ }
+
+ function onRegister(registration) {
+ var sw = registration.installing;
+ serviceWorkerStates.push(sw.state);
+ lastServiceWorkerState = sw.state;
+
+ var sawMessage = new Promise(t.step_func(function(resolve) {
+ sendMessagePort(sw, 'registering doc').onmessage = t.step_func(function (e) {
+ receivedMessageFromPort = e.data;
+ resolve();
+ });
+ }));
+
+ var sawActive = new Promise(t.step_func(function(resolve) {
+ sw.onstatechange = t.step_func(function() {
+ serviceWorkerStates.push(sw.state);
+
+ switch (sw.state) {
+ case 'installed':
+ assert_equals(lastServiceWorkerState, 'installing');
+ break;
+ case 'activating':
+ assert_equals(lastServiceWorkerState, 'installed');
+ break;
+ case 'activated':
+ assert_equals(lastServiceWorkerState, 'activating');
+ break;
+ default:
+ // We won't see 'redundant' because onstatechange is
+ // overwritten before calling unregister.
+ assert_unreached('Unexpected state: ' + sw.state);
+ }
+
+ lastServiceWorkerState = sw.state;
+ if (sw.state === 'activated')
+ resolve();
+ });
+ }));
+
+ Promise.all([sawMessage, sawActive]).then(t.step_func(function() {
+ assert_array_equals(serviceWorkerStates,
+ ['installing', 'installed', 'activating', 'activated'],
+ 'Service worker should pass through all states');
+
+ assert_equals(receivedMessageFromPort, 'Ack for: registering doc');
+
+ var sawRedundant = new Promise(t.step_func(function(resolve) {
+ sw.onstatechange = t.step_func(function() {
+ assert_equals(sw.state, 'redundant');
+ resolve();
+ });
+ }));
+ registration.unregister();
+ sawRedundant.then(t.step_func(function() {
+ t.done();
+ }));
+ }));
+ }
+});
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-events.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-events.https.html
new file mode 100644
index 0000000000..5bcfd66846
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/registration-events.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<title>Service Worker: registration events</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var scope = 'resources/in-scope/';
+ return service_worker_unregister_and_register(
+ t, 'resources/events-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return onRegister(registration.installing);
+ });
+
+ function sendMessagePort(worker, from) {
+ var messageChannel = new MessageChannel();
+ worker.postMessage({from:from, port:messageChannel.port2}, [messageChannel.port2]);
+ return messageChannel.port1;
+ }
+
+ function onRegister(sw) {
+ return new Promise(function(resolve) {
+ sw.onstatechange = function() {
+ if (sw.state === 'activated')
+ resolve();
+ };
+ }).then(function() {
+ return new Promise(function(resolve) {
+ sendMessagePort(sw, 'registering doc').onmessage = resolve;
+ });
+ }).then(function(e) {
+ assert_array_equals(e.data.events,
+ ['install', 'activate'],
+ 'Worker should see install then activate events');
+ });
+ }
+}, 'Registration: events');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-iframe.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-iframe.https.html
new file mode 100644
index 0000000000..ae39ddfea3
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/registration-iframe.https.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Registration for iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// Set script url and scope url relative to the iframe's document's url. Assert
+// the implementation parses the urls against the iframe's document's url.
+async_test(function(t) {
+ const url = 'resources/blank.html';
+ const iframe_scope = 'registration-with-valid-scope';
+ const scope = normalizeURL('resources/' + iframe_scope);
+ const iframe_script = 'empty-worker.js';
+ const script = normalizeURL('resources/' + iframe_script);
+ var frame;
+ var registration;
+
+ service_worker_unregister(t, scope)
+ .then(function() { return with_iframe(url); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ iframe_script,
+ { scope: iframe_scope });
+ })
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.scope, scope,
+ 'registration\'s scope must be parsed against the ' +
+ '"relevant global object"');
+ assert_equals(registration.active.scriptURL, script,
+ 'worker\'s scriptURL must be parsed against the ' +
+ '"relevant global object"');
+ return registration.unregister();
+ })
+ .then(function() {
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'register method should use the "relevant global object" to parse its ' +
+ 'scriptURL and scope - normal case');
+
+// Set script url and scope url relative to the parent frame's document's url.
+// Assert the implementation throws a TypeError exception.
+async_test(function(t) {
+ const url = 'resources/blank.html';
+ const iframe_scope = 'resources/registration-with-scope-to-non-existing-url';
+ const scope = normalizeURL('resources/' + iframe_scope);
+ const script = 'resources/empty-worker.js';
+ var frame;
+ var registration;
+
+ service_worker_unregister(t, scope)
+ .then(function() { return with_iframe(url); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ script,
+ { scope: iframe_scope });
+ })
+ .then(
+ function() {
+ assert_unreached('register() should reject');
+ },
+ function(e) {
+ assert_equals(e.name, 'TypeError',
+ 'register method with scriptURL and scope parsed to ' +
+ 'nonexistent location should reject with TypeError');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'register method should use the "relevant global object" to parse its ' +
+ 'scriptURL and scope - error case');
+
+// Set the scope url to a non-subdirectory of the script url. Assert the
+// implementation throws a SecurityError exception.
+async_test(function(t) {
+ const url = 'resources/blank.html';
+ const scope = 'registration-with-disallowed-scope';
+ const iframe_scope = '../' + scope;
+ const script = 'empty-worker.js';
+ var frame;
+ var registration;
+
+ service_worker_unregister(t, scope)
+ .then(function() { return with_iframe(url); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ script,
+ { scope: iframe_scope });
+ })
+ .then(
+ function() {
+ assert_unreached('register() should reject');
+ },
+ function(e) {
+ assert_equals(e.name, 'SecurityError',
+ 'The scope set to a non-subdirectory of the scriptURL ' +
+ 'should reject with SecurityError');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'A scope url should start with the given script url');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-mime-types.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-mime-types.https.html
new file mode 100644
index 0000000000..3a21aac5c7
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/registration-mime-types.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (MIME types)</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-mime-types.js"></script>
+<script>
+registration_tests_mime_types((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-schedule-job.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-schedule-job.https.html
new file mode 100644
index 0000000000..25d758ee8f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/registration-schedule-job.https.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name=timeout content=long>
+<title>Service Worker: Schedule Job algorithm</title>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests for https://w3c.github.io/ServiceWorker/#schedule-job-algorithm
+// Non-equivalent register jobs should not be coalesced.
+const scope = 'resources/';
+const script1 = 'resources/empty.js';
+const script2 = 'resources/empty.js?change';
+
+async function cleanup() {
+ const registration = await navigator.serviceWorker.getRegistration(scope);
+ if (registration)
+ await registration.unregister();
+}
+
+function absolute_url(url) {
+ return new URL(url, self.location).toString();
+}
+
+// Test that a change to `script` starts a new register job.
+promise_test(async t => {
+ await cleanup();
+ t.add_cleanup(cleanup);
+
+ // Make a registration.
+ const registration = await
+ navigator.serviceWorker.register(script1, {scope});
+
+ // Schedule two more register jobs.
+ navigator.serviceWorker.register(script1, {scope});
+ await navigator.serviceWorker.register(script2, {scope});
+
+ // The jobs should not have been coalesced.
+ const worker = get_newest_worker(registration);
+ assert_equals(worker.scriptURL, absolute_url(script2));
+}, 'different scriptURL');
+
+// Test that a change to `updateViaCache` starts a new register job.
+promise_test(async t => {
+ await cleanup();
+ t.add_cleanup(cleanup);
+
+ // Check defaults.
+ const registration = await
+ navigator.serviceWorker.register(script1, {scope});
+ assert_equals(registration.updateViaCache, 'imports');
+
+ // Schedule two more register jobs.
+ navigator.serviceWorker.register(script1, {scope});
+ await navigator.serviceWorker.register(script1, {scope,
+ updateViaCache: 'none'});
+
+ // The jobs should not have been coalesced.
+ assert_equals(registration.updateViaCache, 'none');
+}, 'different updateViaCache');
+
+// Test that a change to `type` starts a new register job.
+promise_test(async t => {
+ await cleanup();
+ t.add_cleanup(cleanup);
+
+ const scriptForTypeCheck = 'resources/type-check-worker.js';
+ // Check defaults.
+ const registration = await
+ navigator.serviceWorker.register(scriptForTypeCheck, {scope});
+
+ let worker_type = await new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ // The jobs should not have been coalesced. get_newest_worker() helps the
+ // test fail with stable output on browers that incorrectly coalesce
+ // register jobs, since then sometimes registration is not a new worker as
+ // expected.
+ const worker = get_newest_worker(registration);
+ // The argument of postMessage doesn't matter for this case.
+ worker.postMessage('');
+ });
+
+ assert_equals(worker_type, 'classic');
+
+ // Schedule two more register jobs.
+ navigator.serviceWorker.register(scriptForTypeCheck, {scope});
+ await navigator.serviceWorker.register(scriptForTypeCheck, {scope, type: 'module'});
+
+ worker_type = await new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ // The jobs should not have been coalesced. get_newest_worker() helps the
+ // test fail with stable output on browers that incorrectly coalesce
+ // register jobs, since then sometimes registration is not a new worker as
+ // expected.
+ const worker = get_newest_worker(registration);
+ // The argument of postMessage doesn't matter for this case.
+ worker.postMessage('');
+ });
+
+ assert_equals(worker_type, 'module');
+}, 'different type');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-scope-module-static-import.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-scope-module-static-import.https.html
new file mode 100644
index 0000000000..5c75295aed
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/registration-scope-module-static-import.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>Service Worker: Static imports from module top-level scripts shouldn't be affected by the service worker script path restriction</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// https://w3c.github.io/ServiceWorker/#path-restriction
+// is applied to top-level scripts in
+// https://w3c.github.io/ServiceWorker/#update-algorithm
+// but not to submodules imported from top-level scripts.
+async function runTest(t, script, scope) {
+ const script_url = new URL(script, location.href);
+ await service_worker_unregister(t, scope);
+ const registration = await
+ navigator.serviceWorker.register(script, {type: 'module'});
+ t.add_cleanup(_ => registration.unregister());
+ const msg = await new Promise(resolve => {
+ registration.installing.postMessage('ping');
+ navigator.serviceWorker.onmessage = resolve;
+ });
+ assert_equals(msg.data, 'pong');
+}
+
+promise_test(async t => {
+ await runTest(t,
+ 'resources/scope2/imported-module-script.js',
+ 'resources/scope2/');
+ }, 'imported-module-script.js works when used as top-level');
+
+promise_test(async t => {
+ await runTest(t,
+ 'resources/scope1/module-worker-importing-scope2.js',
+ 'resources/scope1/');
+ }, 'static imports to outside path restriction should be allowed');
+
+promise_test(async t => {
+ await runTest(t,
+ 'resources/scope1/module-worker-importing-redirect-to-scope2.js',
+ 'resources/scope1/');
+ }, 'static imports redirecting to outside path restriction should be allowed');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-scope.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-scope.https.html
new file mode 100644
index 0000000000..141875f584
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/registration-scope.https.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (scope)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-scope.js"></script>
+<script>
+registration_tests_scope((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-script-module.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-script-module.https.html
new file mode 100644
index 0000000000..9e39a1f75b
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/registration-script-module.https.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (module script)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-script.js"></script>
+<script>
+registration_tests_script(
+ (script, options) => navigator.serviceWorker.register(
+ script,
+ Object.assign({type: 'module'}, options)),
+ 'module');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-script-url.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-script-url.https.html
new file mode 100644
index 0000000000..bda61adb00
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/registration-script-url.https.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (scriptURL)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-script-url.js"></script>
+<script>
+registration_tests_script_url((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-script.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-script.https.html
new file mode 100644
index 0000000000..f1e51fd265
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/registration-script.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (script)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-script.js"></script>
+<script>
+registration_tests_script(
+ (script, options) => navigator.serviceWorker.register(script, options),
+ 'classic'
+);
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-security-error.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-security-error.https.html
new file mode 100644
index 0000000000..860c2d22ea
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/registration-security-error.https.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (SecurityError)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-security-error.js"></script>
+<script>
+registration_tests_security_error((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-service-worker-attributes.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-service-worker-attributes.https.html
new file mode 100644
index 0000000000..f7b52d5ddc
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/registration-service-worker-attributes.https.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+promise_test(function(t) {
+ var scope = 'resources/scope/installing-waiting-active-after-registration';
+ var worker_url = 'resources/empty-worker.js';
+ var expected_url = normalizeURL(worker_url);
+ var newest_worker;
+ var registration;
+
+ return service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return r.unregister();
+ });
+ registration = r;
+ newest_worker = registration.installing;
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'installing before updatefound');
+ assert_equals(registration.waiting, null,
+ 'waiting before updatefound');
+ assert_equals(registration.active, null,
+ 'active before updatefound');
+ return wait_for_update(t, registration);
+ })
+ .then(function() {
+ assert_equals(registration.installing, newest_worker,
+ 'installing after updatefound');
+ assert_equals(registration.waiting, null,
+ 'waiting after updatefound');
+ assert_equals(registration.active, null,
+ 'active after updatefound');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing after installed');
+ assert_equals(registration.waiting, newest_worker,
+ 'waiting after installed');
+ assert_equals(registration.active, null,
+ 'active after installed');
+ return wait_for_state(t, registration.waiting, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing after activated');
+ assert_equals(registration.waiting, null,
+ 'waiting after activated');
+ assert_equals(registration.active, newest_worker,
+ 'active after activated');
+ return Promise.all([
+ wait_for_state(t, registration.active, 'redundant'),
+ registration.unregister()
+ ]);
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing after redundant');
+ assert_equals(registration.waiting, null,
+ 'waiting after redundant');
+ // According to spec, Clear Registration runs Update State which is
+ // immediately followed by setting active to null, which means by the
+ // time the event loop turns and the Promise for statechange is
+ // resolved, this will be gone.
+ assert_equals(registration.active, null,
+ 'active should be null after redundant');
+ });
+ }, 'installing/waiting/active after registration');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/registration-updateviacache.https.html b/testing/web-platform/tests/service-workers/service-worker/registration-updateviacache.https.html
new file mode 100644
index 0000000000..b2f6bbc6f8
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/registration-updateviacache.https.html
@@ -0,0 +1,204 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration-updateViaCache</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+ const UPDATE_VIA_CACHE_VALUES = [undefined, 'imports', 'all', 'none'];
+ const SCRIPT_URL = 'resources/update-max-aged-worker.py';
+ const SCOPE = 'resources/blank.html';
+
+ async function cleanup() {
+ const reg = await navigator.serviceWorker.getRegistration(SCOPE);
+ if (!reg) return;
+ if (reg.scope == new URL(SCOPE, location).href) {
+ return reg.unregister();
+ };
+ }
+
+ function getScriptTimes(sw, testName) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.addEventListener('message', function listener(event) {
+ if (event.data.test !== testName) return;
+ navigator.serviceWorker.removeEventListener('message', listener);
+ resolve({
+ mainTime: event.data.mainTime,
+ importTime: event.data.importTime
+ });
+ });
+
+ sw.postMessage('');
+ });
+ }
+
+ // Test creating registrations & triggering an update.
+ for (const updateViaCache of UPDATE_VIA_CACHE_VALUES) {
+ const testName = `register-with-updateViaCache-${updateViaCache}`;
+
+ promise_test(async t => {
+ await cleanup();
+
+ const opts = {scope: SCOPE};
+
+ if (updateViaCache) opts.updateViaCache = updateViaCache;
+
+ const reg = await navigator.serviceWorker.register(
+ `${SCRIPT_URL}?test=${testName}`,
+ opts
+ );
+
+ assert_equals(reg.updateViaCache, updateViaCache || 'imports', "reg.updateViaCache");
+
+ const sw = reg.installing || reg.waiting || reg.active;
+ await wait_for_state(t, sw, 'activated');
+ const values = await getScriptTimes(sw, testName);
+ await reg.update();
+
+ if (updateViaCache == 'all') {
+ assert_equals(reg.installing, null, "No new service worker");
+ }
+ else {
+ const newWorker = reg.installing;
+ assert_true(!!newWorker, "New worker installing");
+ const newValues = await getScriptTimes(newWorker, testName);
+
+ if (!updateViaCache || updateViaCache == 'imports') {
+ assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+ assert_equals(values.importTime, newValues.importTime, "Imported script should be the same");
+ }
+ else if (updateViaCache == 'none') {
+ assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+ assert_not_equals(values.importTime, newValues.importTime, "Imported script should have updated");
+ }
+ else {
+ // We should have handled all of the possible values for updateViaCache.
+ // If this runs, something's gone very wrong.
+ throw Error(`Unexpected updateViaCache value: ${updateViaCache}`);
+ }
+ }
+
+ await cleanup();
+ }, testName);
+ }
+
+ // Test changing the updateViaCache value of an existing registration.
+ for (const updateViaCache1 of UPDATE_VIA_CACHE_VALUES) {
+ for (const updateViaCache2 of UPDATE_VIA_CACHE_VALUES) {
+ const testName = `register-with-updateViaCache-${updateViaCache1}-then-${updateViaCache2}`;
+
+ promise_test(async t => {
+ await cleanup();
+
+ const fullScriptUrl = `${SCRIPT_URL}?test=${testName}`;
+ let opts = {scope: SCOPE};
+ if (updateViaCache1) opts.updateViaCache = updateViaCache1;
+
+ const reg = await navigator.serviceWorker.register(fullScriptUrl, opts);
+
+ const sw = reg.installing;
+ await wait_for_state(t, sw, 'activated');
+ const values = await getScriptTimes(sw, testName);
+
+ const frame = await with_iframe(SCOPE);
+ const reg_in_frame = await frame.contentWindow.navigator.serviceWorker.getRegistration(normalizeURL(SCOPE));
+ assert_equals(reg_in_frame.updateViaCache, updateViaCache1 || 'imports', "reg_in_frame.updateViaCache");
+
+ opts = {scope: SCOPE};
+ if (updateViaCache2) opts.updateViaCache = updateViaCache2;
+
+ await navigator.serviceWorker.register(fullScriptUrl, opts);
+
+ const expected_updateViaCache = updateViaCache2 || 'imports';
+
+ assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache updated");
+ // If the update happens via the cache, the scripts will come back byte-identical.
+ // We bypass the byte-identical check if the script URL has changed, but not if
+ // only the updateViaCache value has changed.
+ if (updateViaCache2 == 'all') {
+ assert_equals(reg.installing, null, "No new service worker");
+ }
+ // If there's no change to the updateViaCache value, register should be a no-op.
+ // The default value should behave as 'imports'.
+ else if ((updateViaCache1 || 'imports') == (updateViaCache2 || 'imports')) {
+ assert_equals(reg.installing, null, "No new service worker");
+ }
+ else {
+ const newWorker = reg.installing;
+ assert_true(!!newWorker, "New worker installing");
+ const newValues = await getScriptTimes(newWorker, testName);
+
+ if (!updateViaCache2 || updateViaCache2 == 'imports') {
+ assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+ assert_equals(values.importTime, newValues.importTime, "Imported script should be the same");
+ }
+ else if (updateViaCache2 == 'none') {
+ assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+ assert_not_equals(values.importTime, newValues.importTime, "Imported script should have updated");
+ }
+ else {
+ // We should have handled all of the possible values for updateViaCache2.
+ // If this runs, something's gone very wrong.
+ throw Error(`Unexpected updateViaCache value: ${updateViaCache}`);
+ }
+ }
+
+ // Wait for all registration related tasks on |frame| to complete.
+ await wait_for_activation_on_sample_scope(t, frame.contentWindow);
+ // The updateViaCache change should have been propagated to all
+ // corresponding JS registration objects.
+ assert_equals(reg_in_frame.updateViaCache, expected_updateViaCache, "reg_in_frame.updateViaCache updated");
+ frame.remove();
+
+ await cleanup();
+ }, testName);
+ }
+ }
+
+ // Test accessing updateViaCache of an unregistered registration.
+ for (const updateViaCache of UPDATE_VIA_CACHE_VALUES) {
+ const testName = `access-updateViaCache-after-unregister-${updateViaCache}`;
+
+ promise_test(async t => {
+ await cleanup();
+
+ const opts = {scope: SCOPE};
+
+ if (updateViaCache) opts.updateViaCache = updateViaCache;
+
+ const reg = await navigator.serviceWorker.register(
+ `${SCRIPT_URL}?test=${testName}`,
+ opts
+ );
+
+ const expected_updateViaCache = updateViaCache || 'imports';
+ assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache");
+
+ await reg.unregister();
+
+ // Keep the original value.
+ assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache");
+
+ await cleanup();
+ }, testName);
+ }
+
+ promise_test(async t => {
+ await cleanup();
+ t.add_cleanup(cleanup);
+
+ const registration = await navigator.serviceWorker.register(
+ 'resources/empty.js',
+ {scope: SCOPE});
+ assert_equals(registration.updateViaCache, 'imports',
+ 'before update attempt');
+
+ const fail = navigator.serviceWorker.register(
+ 'resources/malformed-worker.py?parse-error',
+ {scope: SCOPE, updateViaCache: 'none'});
+ await promise_rejects_js(t, TypeError, fail);
+ assert_equals(registration.updateViaCache, 'imports',
+ 'after update attempt');
+ }, 'updateViaCache is not updated if register() rejects');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/rejections.https.html b/testing/web-platform/tests/service-workers/service-worker/rejections.https.html
new file mode 100644
index 0000000000..8002ad9a81
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/rejections.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>Service Worker: Rejection Types</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+(function() {
+ var t = async_test('Rejections are DOMExceptions');
+ t.step(function() {
+
+ navigator.serviceWorker.register('http://example.com').then(
+ t.step_func(function() { assert_unreached('Registration should fail'); }),
+ t.step_func(function(reason) {
+ assert_true(reason instanceof DOMException);
+ assert_true(reason instanceof Error);
+ t.done();
+ }));
+ });
+}());
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/request-end-to-end.https.html b/testing/web-platform/tests/service-workers/service-worker/request-end-to-end.https.html
new file mode 100644
index 0000000000..a39ceadd9f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/request-end-to-end.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent.request passed to onfetch</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(t => {
+ var url = 'resources/request-end-to-end-worker.js';
+ var scope = 'resources/blank.html';
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(r => {
+ add_completion_callback(() => { r.unregister(); });
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => { return with_iframe(scope); })
+ .then(frame => {
+ add_completion_callback(() => { frame.remove(); });
+
+ var result = JSON.parse(frame.contentDocument.body.textContent);
+ assert_equals(result.url, frame.src, 'request.url');
+ assert_equals(result.method, 'GET', 'request.method');
+ assert_equals(result.referrer, location.href, 'request.referrer');
+ assert_equals(result.mode, 'navigate', 'request.mode');
+ assert_equals(result.request_construct_error, '',
+ 'Constructing a Request with a Request whose mode ' +
+ 'is navigate and non-empty RequestInit must not throw a ' +
+ 'TypeError.')
+ assert_equals(result.credentials, 'include', 'request.credentials');
+ assert_equals(result.redirect, 'manual', 'request.redirect');
+ assert_equals(result.headers['user-agent'], undefined,
+ 'Default User-Agent header should not be passed to ' +
+ 'onfetch event.')
+ assert_equals(result.append_header_error, 'TypeError',
+ 'Appending a new header to the request must throw a ' +
+ 'TypeError.')
+ });
+ }, 'Test FetchEvent.request passed to onfetch');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/resource-timing-bodySize.https.html b/testing/web-platform/tests/service-workers/service-worker/resource-timing-bodySize.https.html
new file mode 100644
index 0000000000..5c2b1eba8c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resource-timing-bodySize.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const {REMOTE_ORIGIN} = get_host_info();
+
+/*
+ This test does the following:
+ - Loads a service worker
+ - Loads an iframe in the service worker's scope
+ - The service worker tries to fetch a resource which is either:
+ - constructed inside the service worker
+ - fetched from a different URL ny the service worker
+ - Streamed from a differend URL by the service worker
+ - Passes through
+ - By default the RT entry should have encoded/decoded body size. except for
+ the case where the response is an opaque pass-through.
+*/
+function test_scenario({tao, mode, name}) {
+ promise_test(async (t) => {
+ const uid = token();
+ const worker_url = `resources/fetch-response.js?uid=${uid}`;
+ const scope = `resources/fetch-response.html?uid=${uid}`;
+ const iframe = document.createElement('iframe');
+ const path = name === "passthrough" ? `element-timing/resources/TAOImage.py?origin=*&tao=${
+ tao === "pass" ? "wildcard" : "none"})}` : name;
+
+ iframe.src = `${scope}&path=${encodeURIComponent(
+ `${mode === "same-origin" ? "" : REMOTE_ORIGIN}/${path}`)}&mode=${mode}`;
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ t.add_cleanup(() => iframe.remove());
+ await wait_for_state(t, registration.installing, 'activated');
+ const waitForMessage = new Promise(resolve =>
+ window.addEventListener('message', ({data}) => resolve(data)));
+ document.body.appendChild(iframe);
+ const {buffer, entry} = await waitForMessage;
+ const expectPass = name !== "passthrough" || mode !== "no-cors";
+ assert_equals(buffer.byteLength, expectPass ? entry.decodedBodySize : 0);
+ assert_equals(buffer.byteLength, expectPass ? entry.encodedBodySize : 0);
+ }, `Response body size: ${name}, ${mode}, TAO ${tao}`);
+}
+for (const mode of ["cors", "no-cors", "same-origin"]) {
+ for (const tao of ["pass", "fail"])
+ for (const name of ['constructed', 'forward', 'stream', 'passthrough']) {
+ test_scenario({tao, mode, name});
+ }
+}
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/resource-timing-cross-origin.https.html b/testing/web-platform/tests/service-workers/service-worker/resource-timing-cross-origin.https.html
new file mode 100644
index 0000000000..2155d7ff6e
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resource-timing-cross-origin.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>This test validates Resource Timing for cross origin content fetched by Service Worker from an originally same-origin URL.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+</head>
+
+<body>
+<script>
+function test_sw_resource_timing({ mode }) {
+ promise_test(async t => {
+ const worker_url = `resources/worker-fetching-cross-origin.js?mode=${mode}`;
+ const scope = 'resources/iframe-with-image.html';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ const frame_performance = frame.contentWindow.performance;
+ // Check that there is one entry for which the timing allow check algorithm failed.
+ const entries = frame_performance.getEntriesByType('resource');
+ assert_equals(entries.length, 1);
+ const entry = entries[0];
+ assert_equals(entry.redirectStart, 0, 'redirectStart should be 0 in cross-origin request.');
+ assert_equals(entry.redirectEnd, 0, 'redirectEnd should be 0 in cross-origin request.');
+ assert_equals(entry.domainLookupStart, entry.fetchStart, 'domainLookupStart should be 0 in cross-origin request.');
+ assert_equals(entry.domainLookupEnd, entry.fetchStart, 'domainLookupEnd should be 0 in cross-origin request.');
+ assert_equals(entry.connectStart, entry.fetchStart, 'connectStart should be 0 in cross-origin request.');
+ assert_equals(entry.connectEnd, entry.fetchStart, 'connectEnd should be 0 in cross-origin request.');
+ assert_greater_than(entry.responseStart, entry.fetchStart, 'responseStart should be 0 in cross-origin request.');
+ assert_equals(entry.secureConnectionStart, entry.fetchStart, 'secureConnectionStart should be 0 in cross-origin request.');
+ assert_equals(entry.transferSize, 0, 'decodedBodySize should be 0 in cross-origin request.');
+ frame.remove();
+ await registration.unregister();
+ }, `Test that timing allow check fails when service worker changes origin from same to cross origin (${mode}).`);
+}
+
+test_sw_resource_timing({ mode: "cors" });
+test_sw_resource_timing({ mode: "no-cors" });
+
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html b/testing/web-platform/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html
new file mode 100644
index 0000000000..8d4f0be01a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test various interactions between fetch, service-workers and resource timing</title>
+<meta charset="utf-8" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<link rel="help" href="https://w3c.github.io/resource-timing/" >
+<!--
+ This test checks that the different properties in a PerformanceResourceTimingEntry
+ measure what they are supposed to measure according to spec.
+
+ It is achieved by generating programmatic delays and redirects inside a service worker,
+ and checking how the different metrics respond to the delays and redirects.
+
+ The deltas are not measured precisely, but rather relatively to the delay.
+ The delay needs to be long enough so that it's clear that what's measured is the test's
+ programmatic delay and not arbitrary system delays.
+-->
+</head>
+
+<body>
+<script>
+
+const delay = 200;
+const absolutePath = `${base_path()}/simple.txt`
+function toSequence({before, after, entry}) {
+ /*
+ The order of keys is the same as in this chart:
+ https://w3c.github.io/resource-timing/#attribute-descriptions
+ */
+ const keys = [
+ 'startTime',
+ 'redirectStart',
+ 'redirectEnd',
+ 'workerStart',
+ 'fetchStart',
+ 'connectStart',
+ 'requestStart',
+ 'responseStart',
+ 'responseEnd'
+ ];
+
+ let cursor = before;
+ const step = value => {
+ // A zero/null value, reflect that in the sequence
+ if (!value)
+ return value;
+
+ // Value is the same as before
+ if (value === cursor)
+ return "same";
+
+ // Oops, value is in the wrong place
+ if (value < cursor)
+ return "back";
+
+ // Delta is greater than programmatic delay, this is where the delay is measured.
+ if ((value - cursor) >= delay)
+ return "delay";
+
+ // Some small delta, probably measuring an actual networking stack delay
+ return "tick";
+ }
+
+ const res = keys.map(key => {
+ const value = step(entry[key]);
+ if (entry[key])
+ cursor = entry[key];
+ return [key, value];
+ });
+
+ return Object.fromEntries([...res, ['after', step(after)]]);
+}
+async function testVariant(t, variant) {
+ const worker_url = 'resources/fetch-variants-worker.js';
+ const url = encodeURIComponent(`simple.txt?delay=${delay}&variant=${variant}`);
+ const scope = `resources/iframe-with-fetch-variants.html?url=${url}`;
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+ const result = await new Promise(resolve => window.addEventListener('message', message => {
+ resolve(message.data);
+ }))
+
+ return toSequence(result);
+}
+
+promise_test(async t => {
+ const result = await testVariant(t, 'redirect');
+ assert_equals(result.redirectStart, 0);
+}, 'Redirects done from within a service-worker should not be exposed to client ResourceTiming');
+
+promise_test(async t => {
+ const result = await testVariant(t, 'forward');
+ assert_equals(result.connectStart, 'same');
+}, 'Connection info from within a service-worker should not be exposed to client ResourceTiming');
+
+promise_test(async t => {
+ const result = await testVariant(t, 'forward');
+ assert_not_equals(result.requestStart, 'back');
+}, 'requestStart should never be before fetchStart');
+
+promise_test(async t => {
+ const result = await testVariant(t, 'delay-after-fetch');
+ const whereIsDelayMeasured = Object.entries(result).find(r => r[1] === 'delay')[0];
+ assert_equals(whereIsDelayMeasured, 'responseStart');
+}, 'Delay from within service-worker (after internal fetching) should be accessible through `responseStart`');
+
+promise_test(async t => {
+ const result = await testVariant(t, 'delay-before-fetch');
+ const whereIsDelayMeasured = Object.entries(result).find(r => r[1] === 'delay')[0];
+ assert_equals(whereIsDelayMeasured, 'responseStart');
+}, 'Delay from within service-worker (before internal fetching) should be measured before responseStart in the client ResourceTiming entry');
+</script>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/resource-timing.sub.https.html b/testing/web-platform/tests/service-workers/service-worker/resource-timing.sub.https.html
new file mode 100644
index 0000000000..9808ae5ae1
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resource-timing.sub.https.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function resourceUrl(path) {
+ return "https://{{host}}:{{ports[https][0]}}" + base_path() + path;
+}
+
+function crossOriginUrl(path) {
+ return "https://{{hosts[alt][]}}:{{ports[https][0]}}" + base_path() + path;
+}
+
+// Verify existance of a PerformanceEntry and the order between the timings.
+//
+// |options| has these properties:
+// performance: Performance interface to verify existance of the entry.
+// resource: the path to the resource.
+// mode: 'cross-origin' to load the resource from third-party origin.
+// description: the description passed to each assertion.
+// should_no_performance_entry: no entry is expected to be recorded when it's
+// true.
+function verify(options) {
+ const url = options.mode === 'cross-origin' ? crossOriginUrl(options.resource)
+ : resourceUrl(options.resource);
+ const entryList = options.performance.getEntriesByName(url, 'resource');
+ if (options.should_no_performance_entry) {
+ // The performance timeline may not have an entry for a resource
+ // which failed to load.
+ assert_equals(entryList.length, 0, options.description);
+ return;
+ }
+
+ assert_equals(entryList.length, 1, options.description);
+ const entry = entryList[0];
+ assert_equals(entry.entryType, 'resource', options.description);
+
+ // workerStart is recorded between startTime and fetchStart.
+ assert_greater_than(entry.workerStart, 0, options.description);
+ assert_greater_than_equal(entry.workerStart, entry.startTime, options.description);
+ assert_less_than_equal(entry.workerStart, entry.fetchStart, options.description);
+
+ if (options.mode === 'cross-origin') {
+ assert_equals(entry.responseStart, 0, options.description);
+ assert_greater_than_equal(entry.responseEnd, entry.fetchStart, options.description);
+ } else {
+ assert_greater_than_equal(entry.responseStart, entry.fetchStart, options.description);
+ assert_greater_than_equal(entry.responseEnd, entry.responseStart, options.description);
+ }
+
+ // responseEnd follows fetchStart.
+ assert_greater_than(entry.responseEnd, entry.fetchStart, options.description);
+ // duration always has some value.
+ assert_greater_than(entry.duration, 0, options.description);
+
+ if (options.resource.indexOf('redirect.py') != -1) {
+ assert_less_than_equal(entry.workerStart, entry.redirectStart,
+ options.description);
+ } else {
+ assert_equals(entry.redirectStart, 0, options.description);
+ }
+}
+
+promise_test(async (t) => {
+ const worker_url = 'resources/resource-timing-worker.js';
+ const scope = 'resources/resource-timing-iframe.sub.html';
+
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const performance = frame.contentWindow.performance;
+ verify({
+ performance: performance,
+ resource: 'resources/sample.js',
+ mode: 'same-origin',
+ description: 'Generated response',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/empty.js',
+ mode: 'same-origin',
+ description: 'Network fallback',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/redirect.py?Redirect=empty.js',
+ mode: 'same-origin',
+ description: 'Redirect',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/square.png',
+ mode: 'same-origin',
+ description: 'Network fallback image',
+ });
+ // Test that worker start is available on cross-origin no-cors
+ // subresources.
+ verify({
+ performance: performance,
+ resource: 'resources/square.png',
+ mode: 'cross-origin',
+ description: 'Network fallback cross-origin image',
+ });
+
+ // Tests for resouces which failed to load.
+ verify({
+ performance: performance,
+ resource: 'resources/missing.jpg',
+ mode: 'same-origin',
+ description: 'Network fallback load failure',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/missing.jpg',
+ mode: 'cross-origin',
+ description: 'Network fallback cross-origin load failure',
+ });
+ // Tests for respondWith(fetch()).
+ verify({
+ performance: performance,
+ resource: 'resources/missing.jpg?SWRespondsWithFetch',
+ mode: 'same-origin',
+ description: 'Resource in iframe, nonexistent but responded with fetch to another.',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/sample.txt?SWFetched',
+ mode: 'same-origin',
+ description: 'Resource fetched as response from missing.jpg?SWRespondsWithFetch.',
+ should_no_performance_entry: true,
+ });
+ // Test for a normal resource that is unaffected by the Service Worker.
+ verify({
+ performance: performance,
+ resource: 'resources/empty-worker.js',
+ mode: 'same-origin',
+ description: 'Resource untouched by the Service Worker.',
+ });
+}, 'Controlled resource loads');
+
+test(() => {
+ const url = resourceUrl('resources/test-helpers.sub.js');
+ const entry = window.performance.getEntriesByName(url, 'resource')[0];
+ assert_equals(entry.workerStart, 0, 'Non-controlled');
+}, 'Non-controlled resource loads');
+
+</script>
diff --git a/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>
diff --git a/testing/web-platform/tests/service-workers/service-worker/respond-with-body-accessed-response.https.html b/testing/web-platform/tests/service-workers/service-worker/respond-with-body-accessed-response.https.html
new file mode 100644
index 0000000000..f6713d8921
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/respond-with-body-accessed-response.https.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<title>Service Worker responds with .body accessed response.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+
+promise_test(t => {
+ const SCOPE = 'resources/respond-with-body-accessed-response-iframe.html';
+ const SCRIPT = 'resources/respond-with-body-accessed-response-worker.js';
+
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(reg => {
+ promise_test(t => {
+ if (frame)
+ frame.remove();
+ return reg.unregister();
+ }, 'restore global state');
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(() => { return with_iframe(SCOPE); })
+ .then(f => { frame = f; });
+ }, 'initialize global state');
+
+const TEST_CASES = [
+ "type=basic",
+ "type=opaque",
+ "type=default",
+ "type=basic&clone=1",
+ "type=opaque&clone=1",
+ "type=default&clone=1",
+ "type=basic&clone=2",
+ "type=opaque&clone=2",
+ "type=default&clone=2",
+ "type=basic&passThroughCache=true",
+ "type=opaque&passThroughCache=true",
+ "type=default&passThroughCache=true",
+ "type=basic&clone=1&passThroughCache=true",
+ "type=opaque&clone=1&passThroughCache=true",
+ "type=default&clone=1&passThroughCache=true",
+ "type=basic&clone=2&passThroughCache=true",
+ "type=opaque&clone=2&passThroughCache=true",
+ "type=default&clone=2&passThroughCache=true",
+];
+
+TEST_CASES.forEach(param => {
+ promise_test(t => {
+ const url = 'TestRequest?' + param;
+ return frame.contentWindow.getJSONP(url)
+ .then(result => { assert_equals(result, 'OK'); });
+ }, 'test: ' + param);
+ });
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/same-site-cookies.https.html b/testing/web-platform/tests/service-workers/service-worker/same-site-cookies.https.html
new file mode 100644
index 0000000000..1d9b60d447
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/same-site-cookies.https.html
@@ -0,0 +1,496 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Same-site cookie behavior</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/cookies/resources/cookie-helper.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const COOKIE_VALUE = 'COOKIE_VALUE';
+
+function make_nested_url(nested_origins, target_url) {
+ for (let i = nested_origins.length - 1; i >= 0; --i) {
+ target_url = new URL(
+ `./resources/nested-parent.html?target=${encodeURIComponent(target_url)}`,
+ nested_origins[i] + self.location.pathname);
+ }
+ return target_url;
+}
+
+const scopepath = '/cookies/resources/postToParent.py?with-sw';
+
+async function unregister_service_worker(origin, nested_origins=[]) {
+ let target_url = origin +
+ '/service-workers/service-worker/resources/unregister-rewrite-worker.html' +
+ '?scopepath=' + encodeURIComponent(scopepath);
+ target_url = make_nested_url(nested_origins, target_url);
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('SW-UNREGISTERED');
+ } finally {
+ w.close();
+ }
+}
+
+async function register_service_worker(origin, nested_origins=[]) {
+ let target_url = origin +
+ '/service-workers/service-worker/resources/register-rewrite-worker.html' +
+ '?scopepath=' + encodeURIComponent(scopepath);
+ target_url = make_nested_url(nested_origins, target_url);
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('SW-REGISTERED');
+ } finally {
+ w.close();
+ }
+}
+
+async function run_test(t, origin, navaction, swaction, expected,
+ redirect_origins=[], nested_origins=[]) {
+ if (swaction === 'navpreload') {
+ assert_true('navigationPreload' in ServiceWorkerRegistration.prototype,
+ 'navigation preload must be supported');
+ }
+ const sw_param = swaction === 'no-sw' ? 'no-sw' : 'with-sw';
+ let action_param = '';
+ if (swaction === 'fallback') {
+ action_param = '&ignore';
+ } else if (swaction !== 'no-sw') {
+ action_param = '&' + swaction;
+ }
+ const navpreload_param = swaction === 'navpreload' ? '&navpreload' : '';
+ const change_request_param = swaction === 'change-request' ? '&change-request' : '';
+ const target_string = origin + `/cookies/resources/postToParent.py?` +
+ `${sw_param}${action_param}`
+ let target_url = new URL(target_string);
+
+ for (let i = redirect_origins.length - 1; i >= 0; --i) {
+ const redirect_url = new URL(
+ `./resources/redirect.py?Status=307&Redirect=${encodeURIComponent(target_url)}`,
+ redirect_origins[i] + self.location.pathname);
+ target_url = redirect_url;
+ }
+
+ if (navaction === 'window.open') {
+ target_url = new URL(
+ `./resources/window-opener.html?target=${encodeURIComponent(target_url)}`,
+ self.origin + self.location.pathname);
+ } else if (navaction === 'form post') {
+ target_url = new URL(
+ `./resources/form-poster.html?target=${encodeURIComponent(target_url)}`,
+ self.origin + self.location.pathname);
+ } else if (navaction === 'set location') {
+ target_url = new URL(
+ `./resources/location-setter.html?target=${encodeURIComponent(target_url)}`,
+ self.origin + self.location.pathname);
+ }
+
+ const w = window.open(make_nested_url(nested_origins, target_url));
+ t.add_cleanup(() => w.close());
+
+ const result = await wait_for_message('COOKIES');
+ verifySameSiteCookieState(expected, COOKIE_VALUE, result.data);
+}
+
+promise_test(async t => {
+ await resetSameSiteCookies(self.origin, COOKIE_VALUE);
+ await register_service_worker(self.origin);
+
+ await resetSameSiteCookies(SECURE_SUBDOMAIN_ORIGIN, COOKIE_VALUE);
+ await register_service_worker(SECURE_SUBDOMAIN_ORIGIN);
+
+ await resetSameSiteCookies(SECURE_CROSS_SITE_ORIGIN, COOKIE_VALUE);
+ await register_service_worker(SECURE_CROSS_SITE_ORIGIN);
+
+ await register_service_worker(self.origin,
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'Setup service workers');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with no service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with fallback');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with passthrough');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with change-request');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with navpreload');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'no-sw',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with no service worker');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'fallback',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with fallback');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'passthrough',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with passthrough');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with change-request');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'navpreload',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with navpreload');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'no-sw',
+ SameSiteStatus.LAX);
+}, 'cross-site, window.open with no service worker');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'fallback',
+ SameSiteStatus.LAX);
+}, 'cross-site, window.open with fallback');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'passthrough',
+ SameSiteStatus.LAX);
+}, 'cross-site, window.open with passthrough');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'cross-site, window.open with change-request');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'navpreload',
+ SameSiteStatus.LAX);
+}, 'cross-site, window.open with navpreload');
+
+//
+// window.open redirect tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with no service worker and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with fallback and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with passthrough and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with change-request and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with navpreload and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with no service worker and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with fallback and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with passthrough and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with change-request and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with navpreload and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with no service worker, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with fallback, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with passthrough, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with change-request, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with navpreload, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+//
+// Double-nested frame calling open.window() tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'no service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'fallback service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'passthrough service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'change-request service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'navpreload service worker');
+
+//
+// Double-nested frame setting location tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'no-sw',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'no service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'fallback',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'fallback service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'passthrough',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'passthrough service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'change-request',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'change-request service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'navpreload',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'navpreload service worker');
+
+//
+// Form POST tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'no-sw', SameSiteStatus.STRICT);
+}, 'same-origin, form post with no service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'fallback',
+ SameSiteStatus.STRICT);
+}, 'same-origin, form post with fallback');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'passthrough',
+ SameSiteStatus.STRICT);
+}, 'same-origin, form post with passthrough');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'same-origin, form post with change-request');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'no-sw',
+ SameSiteStatus.STRICT);
+}, 'same-site, form post with no service worker');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'fallback',
+ SameSiteStatus.STRICT);
+}, 'same-site, form post with fallback');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'passthrough',
+ SameSiteStatus.STRICT);
+}, 'same-site, form post with passthrough');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'same-site, form post with change-request');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'no-sw',
+ SameSiteStatus.CROSS_SITE);
+}, 'cross-site, form post with no service worker');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'fallback',
+ SameSiteStatus.CROSS_SITE);
+}, 'cross-site, form post with fallback');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'passthrough',
+ SameSiteStatus.CROSS_SITE);
+}, 'cross-site, form post with passthrough');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'cross-site, form post with change-request');
+
+//
+// Form POST redirect tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'no-sw',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with no service worker and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'fallback',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with fallback and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'passthrough',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with passthrough and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with change-request and same-site redirect');
+
+// navpreload is not supported for POST requests
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'no-sw',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with no service worker and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'fallback',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with fallback and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'passthrough',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with passthrough and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with change-request and cross-site redirect');
+
+// navpreload is not supported for POST requests
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'no-sw',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN,
+ self.origin]);
+}, 'same-origin, form post with no service worker, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'fallback',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN,
+ self.origin]);
+}, 'same-origin, form post with fallback, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'passthrough',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN,
+ self.origin]);
+}, 'same-origin, form post with passthrough, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN,
+ self.origin]);
+}, 'same-origin, form post with change-request, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+// navpreload is not supported for POST requests
+
+promise_test(async t => {
+ await unregister_service_worker(self.origin);
+ await unregister_service_worker(SECURE_SUBDOMAIN_ORIGIN);
+ await unregister_service_worker(SECURE_CROSS_SITE_ORIGIN);
+ await unregister_service_worker(self.origin,
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'Cleanup service workers');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html b/testing/web-platform/tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html
new file mode 100644
index 0000000000..ba34e790ff
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html
@@ -0,0 +1,536 @@
+<!DOCTYPE html>
+<title>ServiceWorker FetchEvent for sandboxed iframe.</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var lastCallbackId = 0;
+var callbacks = {};
+function doTest(frame, type) {
+ return new Promise(function(resolve) {
+ var id = ++lastCallbackId;
+ callbacks[id] = resolve;
+ frame.contentWindow.postMessage({id: id, type: type}, '*');
+ });
+}
+
+// Asks the service worker for data about requests and clients seen. The
+// worker posts a message back with |data| where:
+// |data.requests|: the requests the worker received FetchEvents for
+// |data.clients|: the URLs of all the worker's clients
+// The worker clears its data after responding.
+function getResultsFromWorker(worker) {
+ return new Promise(resolve => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = msg => {
+ resolve(msg.data);
+ };
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+}
+
+window.onmessage = function (e) {
+ message = e.data;
+ var id = message['id'];
+ var callback = callbacks[id];
+ delete callbacks[id];
+ callback(message['result']);
+};
+
+const SCOPE = 'resources/sandboxed-iframe-fetch-event-iframe.py';
+const SCRIPT = 'resources/sandboxed-iframe-fetch-event-worker.js';
+const expected_base_url = new URL(SCOPE, location.href);
+// Service worker controlling |SCOPE|.
+let worker;
+// A normal iframe.
+// This should be controlled by a service worker.
+let normal_frame;
+// An iframe created by <iframe sandbox='allow-scripts'>.
+// This should NOT be controlled by a service worker.
+let sandboxed_frame;
+// An iframe created by <iframe sandbox='allow-scripts allow-same-origin'>.
+// This should be controlled by a service worker.
+let sandboxed_same_origin_frame;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts'.
+// This should NOT be controlled by a service worker.
+let sandboxed_frame_by_header;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts allow-same-origin'.
+// This should be controlled by a service worker.
+let sandboxed_same_origin_frame_by_header;
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ add_completion_callback(() => registration.unregister());
+ worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ });
+}, 'Prepare a service worker.');
+
+promise_test(t => {
+ return with_iframe(SCOPE + '?iframe')
+ .then(f => {
+ normal_frame = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1);
+ assert_equals(requests[0], expected_base_url + '?iframe');
+ assert_true(data.clients.includes(expected_base_url + '?iframe'));
+ });
+}, 'Prepare a normal iframe.');
+
+promise_test(t => {
+ return with_sandboxed_iframe(SCOPE + '?sandboxed-iframe', 'allow-scripts')
+ .then(f => {
+ sandboxed_frame = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0);
+ assert_false(data.clients.includes(expected_base_url +
+ '?sandboxed-iframe'));
+ });
+}, 'Prepare an iframe sandboxed by <iframe sandbox="allow-scripts">.');
+
+promise_test(t => {
+ return with_sandboxed_iframe(SCOPE + '?sandboxed-iframe-same-origin',
+ 'allow-scripts allow-same-origin')
+ .then(f => {
+ sandboxed_same_origin_frame = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1);
+ assert_equals(requests[0],
+ expected_base_url + '?sandboxed-iframe-same-origin');
+ assert_true(data.clients.includes(
+ expected_base_url + '?sandboxed-iframe-same-origin'));
+ })
+}, 'Prepare an iframe sandboxed by ' +
+ '<iframe sandbox="allow-scripts allow-same-origin">.');
+
+promise_test(t => {
+ const iframe_full_url = expected_base_url + '?sandbox=allow-scripts&' +
+ 'sandboxed-frame-by-header';
+ return with_iframe(iframe_full_url)
+ .then(f => {
+ sandboxed_frame_by_header = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'Service worker should provide the response');
+ assert_equals(requests[0], iframe_full_url);
+ assert_false(data.clients.includes(iframe_full_url),
+ 'Service worker should NOT control the sandboxed page');
+ });
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts.');
+
+promise_test(t => {
+ const iframe_full_url =
+ expected_base_url + '?sandbox=allow-scripts%20allow-same-origin&' +
+ 'sandboxed-iframe-same-origin-by-header';
+ return with_iframe(iframe_full_url)
+ .then(f => {
+ sandboxed_same_origin_frame_by_header = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1);
+ assert_equals(requests[0], iframe_full_url);
+ assert_true(data.clients.includes(iframe_full_url));
+ })
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts and ' +
+ 'allow-same-origin.');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The fetch request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch');
+ });
+}, 'Fetch request from a normal iframe');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The fetch request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch-from-worker');
+ });
+}, 'Fetch request from a worker in a normal iframe');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=iframe');
+ assert_true(data.clients.includes(frame.src + '&test=iframe'));
+
+ });
+}, 'Request for an iframe in the normal iframe');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the normal ' +
+ 'iframe');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0],
+ frame.src + '&test=sandboxed-iframe-same-origin');
+ assert_true(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the normal iframe');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The fetch request should NOT be handled by SW.');
+ });
+}, 'Fetch request from iframe sandboxed by an attribute with allow-scripts ' +
+ 'flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The fetch request should NOT be handled by SW.');
+ });
+}, 'Fetch request from a worker in iframe sandboxed by an attribute with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(frame.src + '&test=iframe'));
+ });
+}, 'Request for an iframe in the iframe sandboxed by an attribute with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' +
+ 'sandboxed by an attribute with allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the iframe sandboxed by an attribute with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The fetch request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch');
+ });
+}, 'Fetch request from iframe sandboxed by an attribute with allow-scripts ' +
+ 'and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The fetch request should be handled by SW.');
+ assert_equals(requests[0],
+ frame.src + '&test=fetch-from-worker');
+ });
+}, 'Fetch request from a worker in iframe sandboxed by an attribute with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=iframe');
+ assert_true(data.clients.includes(frame.src + '&test=iframe'));
+ });
+}, 'Request for an iframe in the iframe sandboxed by an attribute with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' +
+ 'sandboxed by attribute with allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0],
+ frame.src + '&test=sandboxed-iframe-same-origin');
+ assert_true(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the iframe sandboxed by attribute with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ });
+}, 'Fetch request from iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(frame.src + '&test=iframe'));
+ });
+}, 'Request for an iframe in the iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' +
+ 'sandboxed by CSP HTTP header with allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch');
+ });
+}, 'Fetch request from iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=iframe');
+ assert_true(data.clients.includes(frame.src + '&test=iframe'));
+ });
+}, 'Request for an iframe in the iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(
+ data.clients.includes(frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the ' +
+ 'iframe sandboxed by CSP HTTP header with allow-scripts and ' +
+ 'allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0],
+ frame.src + '&test=sandboxed-iframe-same-origin');
+ assert_true(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts and allow-same-origin flag');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html b/testing/web-platform/tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html
new file mode 100644
index 0000000000..70be6ef9b0
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<title>Accessing navigator.serviceWorker in sandboxed iframe.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var lastCallbackId = 0;
+var callbacks = {};
+function postMessageAndWaitResult(frame) {
+ return new Promise(function(resolve, reject) {
+ var id = ++lastCallbackId;
+ callbacks[id] = resolve;
+ frame.contentWindow.postMessage({id:id}, '*');
+ const timeout = 1000;
+ step_timeout(() => reject("no msg back after " + timeout + "ms"), timeout);
+ });
+}
+
+window.onmessage = function(e) {
+ message = e.data;
+ var id = message['id'];
+ var callback = callbacks[id];
+ delete callbacks[id];
+ callback(message.result);
+};
+
+promise_test(function(t) {
+ var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+ var frame;
+ return with_iframe(url)
+ .then(function(f) {
+ frame = f;
+ add_result_callback(() => { frame.remove(); });
+ return postMessageAndWaitResult(f);
+ })
+ .then(function(result) {
+ assert_equals(result, 'ok');
+ });
+ }, 'Accessing navigator.serviceWorker in normal iframe should not throw.');
+
+promise_test(function(t) {
+ var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+ var frame;
+ return with_sandboxed_iframe(url, 'allow-scripts')
+ .then(function(f) {
+ frame = f;
+ add_result_callback(() => { frame.remove(); });
+ return postMessageAndWaitResult(f);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ 'navigator.serviceWorker failed: SecurityError');
+ });
+ }, 'Accessing navigator.serviceWorker in sandboxed iframe should throw.');
+
+promise_test(function(t) {
+ var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+ var frame;
+ return with_sandboxed_iframe(url, 'allow-scripts allow-same-origin')
+ .then(function(f) {
+ frame = f;
+ add_result_callback(() => { frame.remove(); });
+ return postMessageAndWaitResult(f);
+ })
+ .then(function(result) {
+ assert_equals(result, 'ok');
+ });
+ },
+ 'Accessing navigator.serviceWorker in sandboxed iframe with ' +
+ 'allow-same-origin flag should not throw.');
+
+promise_test(function(t) {
+ var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+ var frame;
+ return new Promise(function(resolve) {
+ frame = document.createElement('iframe');
+ add_result_callback(() => { frame.remove(); });
+ frame.sandbox = '';
+ frame.src = url;
+ frame.onload = resolve;
+ document.body.appendChild(frame);
+ // Switch the sandbox attribute while loading the iframe.
+ frame.sandbox = 'allow-scripts allow-same-origin';
+ })
+ .then(function() {
+ return postMessageAndWaitResult(frame)
+ })
+ .then(function(result) {
+ // The HTML spec seems to say that changing the sandbox attribute
+ // after the iframe is inserted into its parent document does not
+ // affect the sandboxing. If that's true, the frame should still
+ // act as if it still doesn't have
+ // 'allow-scripts allow-same-origin' set and throw a SecurityError.
+ //
+ // 1) From Section 4.8.5 "The iframe element":
+ // "When an iframe element is inserted into a document that has a
+ // browsing context, the user agent must create a new browsing
+ // context..."
+ // 2) "Create a new browsing context" expands to Section 7.1
+ // "Browsing contexts", which includes creating a Document and
+ // "Implement the sandboxing for document."
+ // 3) "Implement the sandboxing" expands to Section 7.6 "Sandboxing",
+ // which includes "populate document's active sandboxing flag set".
+ //
+ // It's not clear whether navigation subsequently creates a new
+ // Document, but I'm assuming it wouldn't.
+ // https://html.spec.whatwg.org/multipage/embedded-content.html#attr-iframe-sandbox
+ assert_true(
+ false,
+ 'should NOT get message back from a sandboxed frame where scripts are not allowed to execute');
+ })
+ .catch(msg => {
+ assert_true(msg.startsWith('no msg back'), 'expecting error message "no msg back"');
+ });
+ }, 'Switching iframe sandbox attribute while loading the iframe');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/secure-context.https.html b/testing/web-platform/tests/service-workers/service-worker/secure-context.https.html
new file mode 100644
index 0000000000..666a5d3787
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/secure-context.https.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Ensure service worker is bypassed in insecure contexts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// This test checks that an HTTPS iframe embedded in an HTTP document is not
+// loaded via a service worker, since it's not a secure context. To that end, we
+// first register a service worker, wait for its activation, and create an
+// iframe that is controlled by said service worker. We use the iframe as a
+// way to receive messages from the service worker.
+// The bulk of the test begins by opening an HTTP window with the noopener
+// option, installing a message event handler, and embedding an HTTPS iframe. If
+// the browser behaves correctly then the iframe will be loaded from the network
+// and will contain a script that posts a message to the parent window,
+// informing it that it was loaded from the network. If, however, the iframe is
+// intercepted, the service worker will return a page with a script that posts a
+// message to the parent window, informing it that it was intercepted.
+// Upon getting either result, the window will report the result to the service
+// worker by navigating to a reporting URL. The service worker will then inform
+// all clients about the result, including the controlled iframe from the
+// beginning of the test. The message event handler will verify that the result
+// is as expected, concluding the test.
+promise_test(t => {
+ const SCRIPT = "resources/secure-context-service-worker.js";
+ const SCOPE = "resources/";
+ const HTTP_IFRAME_URL = get_host_info().HTTP_ORIGIN + base_path() + SCOPE + "secure-context/window.html";
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(registration => {
+ t.add_cleanup(() => {
+ return registration.unregister();
+ });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(SCOPE + "blank.html");
+ })
+ .then(iframe => {
+ t.add_cleanup(() => {
+ iframe.remove();
+ });
+ return new Promise(resolve => {
+ iframe.contentWindow.navigator.serviceWorker.onmessage = t.step_func(event => {
+ assert_equals(event.data, 'network');
+ resolve();
+ });
+ window.open(HTTP_IFRAME_URL, 'MyWindow', 'noopener');
+ });
+ });
+})
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-connect.https.html b/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-connect.https.html
new file mode 100644
index 0000000000..226f4a40e4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-connect.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP connect directive for ServiceWorker script</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+service_worker_test(
+ 'resources/service-worker-csp-worker.py?directive=connect',
+ 'CSP test for connect-src in ServiceWorkerGlobalScope');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-default.https.html b/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-default.https.html
new file mode 100644
index 0000000000..1d4e7624d8
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-default.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP default directive for ServiceWorker script</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+service_worker_test(
+ 'resources/service-worker-csp-worker.py?directive=default',
+ 'CSP test for default-src in ServiceWorkerGlobalScope');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-script.https.html b/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-script.https.html
new file mode 100644
index 0000000000..14c2eb72bd
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/service-worker-csp-script.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP script directive for ServiceWorker script</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+service_worker_test(
+ 'resources/service-worker-csp-worker.py?directive=script',
+ 'CSP test for script-src in ServiceWorkerGlobalScope');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/service-worker-header.https.html b/testing/web-platform/tests/service-workers/service-worker/service-worker-header.https.html
new file mode 100644
index 0000000000..fb902cd1b4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/service-worker-header.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<title>Service Worker: Service-Worker header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(async t => {
+ const script = 'resources/service-worker-header.py'
+ + '?header&import=service-worker-header.py?no-header';
+ const scope = 'resources/service-worker-header';
+ const expected_url = normalizeURL(script);
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ assert_true(registration instanceof ServiceWorkerRegistration);
+
+ await wait_for_state(t, registration.installing, 'activated');
+ await registration.update();
+}, 'A request to fetch service worker main script should have Service-Worker '
+ + 'header and imported scripts should not have one');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/serviceworker-message-event-historical.https.html b/testing/web-platform/tests/service-workers/service-worker/serviceworker-message-event-historical.https.html
new file mode 100644
index 0000000000..fac8f2076f
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/serviceworker-message-event-historical.https.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<title>Service Worker: ServiceWorkerMessageEvent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html';
+ var url = 'resources/postmessage-to-client-worker.js';
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ var w = frame.contentWindow;
+ var worker = w.navigator.serviceWorker.controller;
+ assert_equals(
+ self.ServiceWorkerMessageEvent, undefined,
+ 'ServiceWorkerMessageEvent should not be defined.');
+ return new Promise(function(resolve) {
+ w.navigator.serviceWorker.onmessage = t.step_func(function(e) {
+ assert_true(
+ e instanceof w.MessageEvent,
+ 'message events should use MessageEvent interface.');
+ assert_true(e.source instanceof w.ServiceWorker);
+ assert_equals(e.type, 'message');
+ assert_equals(e.source, worker,
+ 'source should equal to the controller.');
+ assert_equals(e.ports.length, 0);
+ resolve();
+ });
+ worker.postMessage('PING');
+ });
+ });
+ }, 'Test MessageEvent supplants ServiceWorkerMessageEvent.');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html b/testing/web-platform/tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html
new file mode 100644
index 0000000000..6004985a34
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<title>ServiceWorker object: scriptURL property</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+function url_test(name, url) {
+ const scope = 'resources/scope/' + name;
+ const expectedURL = normalizeURL(url);
+
+ promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, url, scope);
+ const worker = registration.installing;
+ assert_equals(worker.scriptURL, expectedURL, 'scriptURL');
+ await registration.unregister();
+ }, 'Verify the scriptURL property: ' + name);
+}
+
+url_test('relative', 'resources/empty-worker.js');
+url_test('with-fragment', 'resources/empty-worker.js#ref');
+url_test('with-query', 'resources/empty-worker.js?ref');
+url_test('absolute', normalizeURL('./resources/empty-worker.js'));
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/skip-waiting-installed.https.html b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-installed.https.html
new file mode 100644
index 0000000000..b604f651b3
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-installed.https.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting installed worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?skip-waiting-installed';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/skip-waiting-installed-worker.js';
+ var frame, frame_sw, service_worker, registration, onmessage, oncontrollerchanged;
+ var saw_message = new Promise(function(resolve) {
+ onmessage = function(e) {
+ resolve(e.data);
+ };
+ })
+ .then(function(message) {
+ assert_equals(
+ message, 'PASS',
+ 'skipWaiting promise should be resolved with undefined');
+ });
+ var saw_controllerchanged = new Promise(function(resolve) {
+ oncontrollerchanged = function() {
+ assert_equals(
+ frame_sw.controller.scriptURL, normalizeURL(url2),
+ 'Controller scriptURL should change to the second one');
+ assert_equals(registration.active.scriptURL, normalizeURL(url2),
+ 'Worker which calls skipWaiting should be active by controllerchange');
+ resolve();
+ };
+ });
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ frame_sw = f.contentWindow.navigator.serviceWorker;
+ assert_equals(
+ frame_sw.controller.scriptURL, normalizeURL(url1),
+ 'Document controller scriptURL should equal to the first one');
+ frame_sw.oncontrollerchange = t.step_func(oncontrollerchanged);
+ return navigator.serviceWorker.register(url2, {scope: scope});
+ })
+ .then(function(r) {
+ registration = r;
+ service_worker = r.installing;
+ return wait_for_state(t, service_worker, 'installed');
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func(onmessage);
+ service_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return Promise.all([saw_message, saw_controllerchanged]);
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Test skipWaiting when a installed worker is waiting');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/skip-waiting-using-registration.https.html b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-using-registration.https.html
new file mode 100644
index 0000000000..412ee2a443
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-using-registration.https.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting using registration</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?skip-waiting-using-registration';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/skip-waiting-worker.js';
+ var frame, frame_sw, sw_registration, oncontrollerchanged;
+ var saw_controllerchanged = new Promise(function(resolve) {
+ oncontrollerchanged = function(e) {
+ resolve(e);
+ };
+ })
+ .then(function(e) {
+ assert_equals(e.type, 'controllerchange',
+ 'Event name should be "controllerchange"');
+ assert_true(
+ e.target instanceof frame.contentWindow.ServiceWorkerContainer,
+ 'Event target should be a ServiceWorkerContainer');
+ assert_equals(e.target.controller.state, 'activating',
+ 'Controller state should be activating');
+ assert_equals(
+ frame_sw.controller.scriptURL, normalizeURL(url2),
+ 'Controller scriptURL should change to the second one');
+ });
+
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(registration) {
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame = f;
+ frame_sw = f.contentWindow.navigator.serviceWorker;
+ assert_equals(
+ frame_sw.controller.scriptURL, normalizeURL(url1),
+ 'Document controller scriptURL should equal to the first one');
+ frame_sw.oncontrollerchange = t.step_func(oncontrollerchanged);
+ return navigator.serviceWorker.register(url2, {scope: scope});
+ })
+ .then(function(registration) {
+ sw_registration = registration;
+ t.add_cleanup(function() {
+ return registration.unregister();
+ });
+ return saw_controllerchanged;
+ })
+ .then(function() {
+ assert_not_equals(sw_registration.active, null,
+ 'Registration active worker should not be null');
+ return fetch_tests_from_worker(sw_registration.active);
+ });
+ }, 'Test skipWaiting while a client is using the registration');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/skip-waiting-without-client.https.html b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-without-client.https.html
new file mode 100644
index 0000000000..62060a8247
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-without-client.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting without client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+service_worker_test(
+ 'resources/skip-waiting-worker.js',
+ 'Test single skipWaiting() when no client attached');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html
new file mode 100644
index 0000000000..ced64e5f67
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting without using registration</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?skip-waiting-without-using-registration';
+ var url = 'resources/skip-waiting-worker.js';
+ var frame_sw, sw_registration;
+
+ return service_worker_unregister(t, scope)
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame_sw = f.contentWindow.navigator.serviceWorker;
+ assert_equals(frame_sw.controller, null,
+ 'Document controller should be null');
+ return navigator.serviceWorker.register(url, {scope: scope});
+ })
+ .then(function(registration) {
+ sw_registration = registration;
+ t.add_cleanup(function() {
+ return registration.unregister();
+ });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(frame_sw.controller, null,
+ 'Document controller should still be null');
+ assert_not_equals(sw_registration.active, null,
+ 'Registration active worker should not be null');
+ return fetch_tests_from_worker(sw_registration.active);
+ });
+ }, 'Test skipWaiting while a client is not being controlled');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/skip-waiting.https.html b/testing/web-platform/tests/service-workers/service-worker/skip-waiting.https.html
new file mode 100644
index 0000000000..f8392fc955
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/skip-waiting.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?skip-waiting';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/empty-worker.js';
+ var url3 = 'resources/skip-waiting-worker.js';
+ var sw_registration, activated_worker, waiting_worker;
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ sw_registration = registration;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ return navigator.serviceWorker.register(url2, {scope: scope});
+ })
+ .then(function(registration) {
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ activated_worker = sw_registration.active;
+ waiting_worker = sw_registration.waiting;
+ assert_equals(activated_worker.scriptURL, normalizeURL(url1),
+ 'Worker with url1 should be activated');
+ assert_equals(waiting_worker.scriptURL, normalizeURL(url2),
+ 'Worker with url2 should be waiting');
+ return navigator.serviceWorker.register(url3, {scope: scope});
+ })
+ .then(function(registration) {
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(activated_worker.state, 'redundant',
+ 'Worker with url1 should be redundant');
+ assert_equals(waiting_worker.state, 'redundant',
+ 'Worker with url2 should be redundant');
+ assert_equals(sw_registration.active.scriptURL, normalizeURL(url3),
+ 'Worker with url3 should be activated');
+ });
+ }, 'Test skipWaiting with both active and waiting workers');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/state.https.html b/testing/web-platform/tests/service-workers/service-worker/state.https.html
new file mode 100644
index 0000000000..7358e58ff1
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/state.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function (t) {
+ var currentState = 'test-is-starting';
+ var scope = 'resources/state/';
+
+ return service_worker_unregister_and_register(
+ t, 'resources/empty-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ var sw = registration.installing;
+
+ assert_equals(sw.state, 'installing',
+ 'the service worker should be in "installing" state.');
+ checkStateTransition(sw.state);
+ return onStateChange(sw);
+ });
+
+ function checkStateTransition(newState) {
+ switch (currentState) {
+ case 'test-is-starting':
+ break; // anything goes
+ case 'installing':
+ assert_in_array(newState, ['installed', 'redundant']);
+ break;
+ case 'installed':
+ assert_in_array(newState, ['activating', 'redundant']);
+ break;
+ case 'activating':
+ assert_in_array(newState, ['activated', 'redundant']);
+ break;
+ case 'activated':
+ assert_equals(newState, 'redundant');
+ break;
+ case 'redundant':
+ assert_unreached('a ServiceWorker should not transition out of ' +
+ 'the "redundant" state');
+ break;
+ default:
+ assert_unreached('should not transition into unknown state "' +
+ newState + '"');
+ break;
+ }
+ currentState = newState;
+ }
+
+ function onStateChange(expectedTarget) {
+ return new Promise(function(resolve) {
+ expectedTarget.addEventListener('statechange', resolve);
+ }).then(function(event) {
+ assert_true(event.target instanceof ServiceWorker,
+ 'the target of the statechange event should be a ' +
+ 'ServiceWorker.');
+ assert_equals(event.target, expectedTarget,
+ 'the target of the statechange event should be ' +
+ 'the installing ServiceWorker');
+ assert_equals(event.type, 'statechange',
+ 'the type of the event should be "statechange".');
+
+ checkStateTransition(event.target.state);
+
+ if (event.target.state != 'activated')
+ return onStateChange(expectedTarget);
+ });
+ }
+}, 'Service Worker state property and "statechange" event');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/svg-target-reftest.https.html b/testing/web-platform/tests/service-workers/service-worker/svg-target-reftest.https.html
new file mode 100644
index 0000000000..3710ee61d8
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/svg-target-reftest.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="utf-8">
+<title>Service worker interception does not break SVG fragment targets</title>
+<meta name="assert" content="SVG with link fragment should render correctly when intercepted by a service worker.">
+<script src="resources/test-helpers.sub.js"></script>
+<link rel="match" href="resources/svg-target-reftest-001.html">
+<p>Pass if you see a green box below.</p>
+<script>
+// We want to use utility functions designed for testharness.js where
+// there is a test object. We don't have a test object in reftests
+// so fake one for now.
+const fake_test = { step_func: f => f };
+
+async function runTest() {
+ const script = './resources/pass-through-worker.js';
+ const scope = './resources/svg-target-reftest-frame.html';
+ let reg = await navigator.serviceWorker.register(script, { scope });
+ await wait_for_state(fake_test, reg.installing, 'activated');
+ let f = await with_iframe(scope);
+ document.documentElement.classList.remove('reftest-wait');
+ await reg.unregister();
+ // Note, we cannot remove the frame explicitly because we can't
+ // tell when the reftest completes.
+}
+runTest();
+</script>
+</html>
diff --git a/testing/web-platform/tests/service-workers/service-worker/synced-state.https.html b/testing/web-platform/tests/service-workers/service-worker/synced-state.https.html
new file mode 100644
index 0000000000..0e9f63a9a2
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/synced-state.https.html
@@ -0,0 +1,93 @@
+<!doctype html>
+<title>ServiceWorker: worker objects have synced state</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests that ServiceWorker objects representing the same Service Worker
+// entity have the same state. JS-level equality is now required according to
+// the spec.
+'use strict';
+
+function nextChange(worker) {
+ return new Promise(function(resolve, reject) {
+ worker.addEventListener('statechange', function handler(event) {
+ try {
+ worker.removeEventListener('statechange', handler);
+ resolve(event.currentTarget.state);
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+}
+
+promise_test(function(t) {
+ var scope = 'resources/synced-state';
+ var script = 'resources/empty-worker.js';
+ var registration, worker;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+ worker = registration.installing;
+
+ t.add_cleanup(function() {
+ return r.unregister();
+ });
+
+ return nextChange(worker);
+ })
+ .then(function(state) {
+ assert_equals(state, 'installed',
+ 'original SW should be installed');
+ assert_equals(registration.installing, null,
+ 'in installed, .installing should be null');
+ assert_equals(registration.waiting, worker,
+ 'in installed, .waiting should be equal to the ' +
+ 'original worker');
+ assert_equals(registration.waiting.state, 'installed',
+ 'in installed, .waiting should be installed');
+ assert_equals(registration.active, null,
+ 'in installed, .active should be null');
+
+ return nextChange(worker);
+ })
+ .then(function(state) {
+ assert_equals(state, 'activating',
+ 'original SW should be activating');
+ assert_equals(registration.installing, null,
+ 'in activating, .installing should be null');
+ assert_equals(registration.waiting, null,
+ 'in activating, .waiting should be null');
+ assert_equals(registration.active, worker,
+ 'in activating, .active should be equal to the ' +
+ 'original worker');
+ assert_equals(
+ registration.active.state, 'activating',
+ 'in activating, .active should be activating');
+
+ return nextChange(worker);
+ })
+ .then(function(state) {
+ assert_equals(state, 'activated',
+ 'original SW should be activated');
+ assert_equals(registration.installing, null,
+ 'in activated, .installing should be null');
+ assert_equals(registration.waiting, null,
+ 'in activated, .waiting should be null');
+ assert_equals(registration.active, worker,
+ 'in activated, .active should be equal to the ' +
+ 'original worker');
+ assert_equals(registration.active.state, 'activated',
+ 'in activated .active should be activated');
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(r) {
+ assert_equals(r, registration, 'getRegistration should return the ' +
+ 'same object');
+ });
+ }, 'worker objects for the same entity have the same state');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/uncontrolled-page.https.html b/testing/web-platform/tests/service-workers/service-worker/uncontrolled-page.https.html
new file mode 100644
index 0000000000..e22ca8f0a9
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/uncontrolled-page.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function fetch_url(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener('load', function(event) {
+ if (request.status == 200)
+ resolve(request.response);
+ else
+ reject(Error(request.statusText));
+ });
+ request.open('GET', url);
+ request.send();
+ });
+}
+var worker = 'resources/fail-on-fetch-worker.js';
+
+promise_test(function(t) {
+ var scope = 'resources/scope/uncontrolled-page/';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ return fetch_url('resources/simple.txt');
+ })
+ .then(function(text) {
+ assert_equals(text, 'a simple text file\n');
+ });
+ }, 'Fetch events should not go through uncontrolled page.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/unregister-controller.https.html b/testing/web-platform/tests/service-workers/service-worker/unregister-controller.https.html
new file mode 100644
index 0000000000..3bf4cff720
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/unregister-controller.https.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var worker_url = 'resources/simple-intercept-worker.js';
+
+async_test(function(t) {
+ var scope =
+ 'resources/unregister-controller-page.html?load-before-unregister';
+ var frame_window;
+ var controller;
+ var registration;
+ var frame;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ frame_window = frame.contentWindow;
+ controller = frame_window.navigator.serviceWorker.controller;
+ assert_true(controller instanceof frame_window.ServiceWorker,
+ 'document should load with a controller');
+ return registration.unregister();
+ })
+ .then(function() {
+ assert_equals(frame_window.navigator.serviceWorker.controller,
+ controller,
+ 'unregistration should not modify controller');
+ return frame_window.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker',
+ 'controller should intercept requests');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Unregister does not affect existing controller');
+
+async_test(function(t) {
+ var scope =
+ 'resources/unregister-controller-page.html?load-after-unregister';
+ var registration;
+ var frame;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return registration.unregister();
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ var frame_window = frame.contentWindow;
+ assert_equals(frame_window.navigator.serviceWorker.controller, null,
+ 'document should not have a controller');
+ return frame_window.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'a simple text file\n',
+ 'requests should not be intercepted');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Unregister prevents control of subsequent navigations');
+
+async_test(function(t) {
+ var scope =
+ 'resources/scope/no-new-controllee-even-if-registration-is-still-used';
+ var registration;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ return registration.unregister();
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ null,
+ 'document should not have a controller');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Unregister prevents new controllee even if registration is still in use');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/unregister-immediately-before-installed.https.html b/testing/web-platform/tests/service-workers/service-worker/unregister-immediately-before-installed.https.html
new file mode 100644
index 0000000000..79cdaf062d
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/unregister-immediately-before-installed.https.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Use Clear-Site-Data to immediately unregister service workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/unregister-immediately-helpers.js"></script>
+<body>
+<script>
+'use strict';
+
+// These tests use the Clear-Site-Data network response header to immediately
+// unregister a service worker registration with a worker whose state is
+// 'installing' or 'parsed'. Clear-Site-Data must delete the registration,
+// abort the installation and then clear the registration by setting the
+// worker's state to 'redundant'.
+
+promise_test(async test => {
+ // This test keeps the the service worker in the 'parsed' state by using a
+ // script with an infinite loop.
+ const script_url = 'resources/onparse-infiniteloop-worker.js';
+ const scope_url =
+ 'resources/scope-for-unregister-immediately-with-parsed-worker';
+
+ await service_worker_unregister(test, /*scope=*/script_url);
+
+ // Clear-Site-Data must cause register() to fail.
+ const register_promise = promise_rejects_dom(test, 'AbortError',
+ navigator.serviceWorker.register(script_url, { scope: scope_url}));;
+
+ await Promise.all([clear_site_data(), register_promise]);
+
+ await assert_no_registrations_exist();
+ }, 'Clear-Site-Data must abort service worker registration.');
+
+promise_test(async test => {
+ // This test keeps the the service worker in the 'installing' state by using a
+ // script with an install event waitUntil() promise that never resolves.
+ const script_url = 'resources/oninstall-waituntil-forever.js';
+ const scope_url =
+ 'resources/scope-for-unregister-immediately-with-installing-worker';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const service_worker = registration.installing;
+
+ // Clear-Site-Data must cause install to fail.
+ await Promise.all([
+ clear_site_data(),
+ wait_for_state(test, service_worker, 'redundant')]);
+
+ await assert_no_registrations_exist();
+ }, 'Clear-Site-Data must unregister a registration with a worker '
+ + 'in the "installing" state.');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html b/testing/web-platform/tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html
new file mode 100644
index 0000000000..6ba87a7ce8
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Use Clear-Site-Data to immediately unregister service workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/unregister-immediately-helpers.js"></script>
+<body>
+<script>
+'use strict';
+
+// These tests use the Clear-Site-Data network response header to immediately
+// unregister a service worker registration with a worker that has pending
+// extendable events. Clear-Site-Data must delete the registration,
+// abort all pending extendable events and then clear the registration by
+// setting the worker's state to 'redundant'
+
+promise_test(async test => {
+ // Use a service worker script that can produce fetch events with pending
+ // respondWith() promises that never resolve.
+ const script_url = 'resources/onfetch-waituntil-forever.js';
+ const scope_url =
+ 'resources/blank.html?unregister-immediately-with-fetch-event';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+
+ await wait_for_state(test, registration.installing, 'activated');
+
+ const frame = await add_controlled_iframe(test, scope_url);
+
+ // Clear-Site-Data must cause the pending fetch promise to reject.
+ const fetch_promise = promise_rejects_js(
+ test, TypeError, frame.contentWindow.fetch('waituntil-forever'));
+
+ const event_watcher = new EventWatcher(
+ test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
+
+ await Promise.all([
+ clear_site_data(),
+ fetch_promise,
+ event_watcher.wait_for('controllerchange'),
+ wait_for_state(test, registration.active, 'redundant'),]);
+
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+ await assert_no_registrations_exist();
+}, 'Clear-Site-Data must fail pending subresource fetch events.');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/unregister-immediately.https.html b/testing/web-platform/tests/service-workers/service-worker/unregister-immediately.https.html
new file mode 100644
index 0000000000..54be40a545
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/unregister-immediately.https.html
@@ -0,0 +1,134 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Use Clear-Site-Data to immediately unregister service workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/unregister-immediately-helpers.js"></script>
+<body>
+<script>
+'use strict';
+
+// These tests use the Clear-Site-Data network response header to immediately
+// unregister a service worker registration with a worker whose state is
+// 'installed', 'waiting', 'activating' or 'activated'. Immediately
+// unregistering runs the "Clear Registration" algorithm without waiting for the
+// active worker's controlled clients to unload.
+
+promise_test(async test => {
+ // This test keeps the the service worker in the 'activating' state by using a
+ // script with an activate event waitUntil() promise that never resolves.
+ const script_url = 'resources/onactivate-waituntil-forever.js';
+ const scope_url =
+ 'resources/scope-for-unregister-immediately-with-waiting-worker';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const service_worker = registration.installing;
+
+ await wait_for_state(test, service_worker, 'activating');
+
+ // Clear-Site-Data must cause activation to fail.
+ await Promise.all([
+ clear_site_data(),
+ wait_for_state(test, service_worker, 'redundant')]);
+
+ await assert_no_registrations_exist();
+ }, 'Clear-Site-Data must unregister a registration with a worker '
+ + 'in the "activating" state.');
+
+promise_test(async test => {
+ // Create an registration with two service workers: one activated and one
+ // installed.
+ const script_url = 'resources/update_shell.py';
+ const scope_url =
+ 'resources/scope-for-unregister-immediately-with-with-update';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const first_service_worker = registration.installing;
+
+ await wait_for_state(test, first_service_worker, 'activated');
+ registration.update();
+
+ const event_watcher = new EventWatcher(test, registration, 'updatefound');
+ await event_watcher.wait_for('updatefound');
+
+ const second_service_worker = registration.installing;
+ await wait_for_state(test, second_service_worker, 'installed');
+
+ // Clear-Site-Data must clear both workers from the registration.
+ await Promise.all([
+ clear_site_data(),
+ wait_for_state(test, first_service_worker, 'redundant'),
+ wait_for_state(test, second_service_worker, 'redundant')]);
+
+ await assert_no_registrations_exist();
+}, 'Clear-Site-Data must unregister an activated registration with '
+ + 'an update waiting.');
+
+promise_test(async test => {
+ const script_url = 'resources/empty.js';
+ const scope_url =
+ 'resources/blank.html?unregister-immediately-with-controlled-client';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const service_worker = registration.installing;
+
+ await wait_for_state(test, service_worker, 'activated');
+ const frame = await add_controlled_iframe(test, scope_url);
+ const frame_registration =
+ await frame.contentWindow.navigator.serviceWorker.ready;
+
+ const event_watcher = new EventWatcher(
+ test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
+
+ // Clear-Site-Data must remove the iframe's controller.
+ await Promise.all([
+ clear_site_data(),
+ event_watcher.wait_for('controllerchange'),
+ wait_for_state(test, service_worker, 'redundant')]);
+
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+ await assert_no_registrations_exist();
+
+ // The ready promise must continue to resolve with the unregistered
+ // registration.
+ assert_equals(frame_registration,
+ await frame.contentWindow.navigator.serviceWorker.ready);
+}, 'Clear-Site-Data must unregister an activated registration with controlled '
+ + 'clients.');
+
+promise_test(async test => {
+ const script_url = 'resources/empty.js';
+ const scope_url =
+ 'resources/blank.html?unregister-immediately-while-waiting-to-clear';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const service_worker = registration.installing;
+
+ await wait_for_state(test, service_worker, 'activated');
+ const frame = await add_controlled_iframe(test, scope_url);
+
+ const event_watcher = new EventWatcher(
+ test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
+
+ // Unregister waits to clear the registration until no controlled clients
+ // exist.
+ await registration.unregister();
+
+ // Clear-Site-Data must clear the unregistered registration immediately.
+ await Promise.all([
+ clear_site_data(),
+ event_watcher.wait_for('controllerchange'),
+ wait_for_state(test, service_worker, 'redundant')]);
+
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+ await assert_no_registrations_exist();
+}, 'Clear-Site-Data must clear an unregistered registration waiting for '
+ + ' controlled clients to unload.');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/unregister-then-register-new-script.https.html b/testing/web-platform/tests/service-workers/service-worker/unregister-then-register-new-script.https.html
new file mode 100644
index 0000000000..d046423e0c
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/unregister-then-register-new-script.https.html
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var worker_url = 'resources/empty-worker.js';
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/unregister-then-register-new-script-that-exists';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ const newWorkerURL = worker_url + '?new';
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ await registration.unregister();
+
+ const newRegistration = await navigator.serviceWorker.register(newWorkerURL, { scope });
+ t.add_cleanup(() => newRegistration.unregister());
+
+ assert_equals(
+ registration.installing,
+ null,
+ 'before activated registration.installing'
+ );
+ assert_equals(
+ registration.waiting,
+ null,
+ 'before activated registration.waiting'
+ );
+ assert_equals(
+ registration.active.scriptURL,
+ normalizeURL(worker_url),
+ 'before activated registration.active'
+ );
+ assert_equals(
+ newRegistration.installing.scriptURL,
+ normalizeURL(newWorkerURL),
+ 'before activated newRegistration.installing'
+ );
+ assert_equals(
+ newRegistration.waiting,
+ null,
+ 'before activated newRegistration.waiting'
+ );
+ assert_equals(
+ newRegistration.active,
+ null,
+ 'before activated newRegistration.active'
+ );
+ iframe.remove();
+
+ await wait_for_state(t, newRegistration.installing, 'activated');
+
+ assert_equals(
+ newRegistration.installing,
+ null,
+ 'after activated newRegistration.installing'
+ );
+ assert_equals(
+ newRegistration.waiting,
+ null,
+ 'after activated newRegistration.waiting'
+ );
+ assert_equals(
+ newRegistration.active.scriptURL,
+ normalizeURL(newWorkerURL),
+ 'after activated newRegistration.active'
+ );
+
+ const newIframe = await with_iframe(scope);
+ t.add_cleanup(() => newIframe.remove());
+
+ assert_equals(
+ newIframe.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(newWorkerURL),
+ 'the new worker should control a new document'
+ );
+}, 'Registering a new script URL while an unregistered registration is in use');
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/unregister-then-register-new-script-that-404s';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ await registration.unregister();
+
+ await promise_rejects_js(
+ t, TypeError,
+ navigator.serviceWorker.register('this-will-404', { scope })
+ );
+
+ assert_equals(registration.installing, null, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active.scriptURL, normalizeURL(worker_url), 'registration.active');
+
+ const newIframe = await with_iframe(scope);
+ t.add_cleanup(() => newIframe.remove());
+
+ assert_equals(newIframe.contentWindow.navigator.serviceWorker.controller, null, 'Document should not be controlled');
+}, 'Registering a new script URL that 404s does not resurrect unregistered registration');
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/unregister-then-register-reject-install-worker';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ await registration.unregister();
+
+ const newRegistration = await navigator.serviceWorker.register(
+ 'resources/reject-install-worker.js', { scope }
+ );
+ t.add_cleanup(() => newRegistration.unregister());
+
+ await wait_for_state(t, newRegistration.installing, 'redundant');
+
+ assert_equals(registration.installing, null, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active.scriptURL, normalizeURL(worker_url),
+ 'registration.active');
+ assert_not_equals(registration, newRegistration, 'New registration is different');
+}, 'Registering a new script URL that fails to install does not resurrect unregistered registration');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/unregister-then-register.https.html b/testing/web-platform/tests/service-workers/service-worker/unregister-then-register.https.html
new file mode 100644
index 0000000000..b61608c841
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/unregister-then-register.https.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var worker_url = 'resources/empty-worker.js';
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/re-register-resolves-to-new-value';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+ await registration.unregister();
+ const newRegistration = await navigator.serviceWorker.register(worker_url, { scope });
+ t.add_cleanup(() => newRegistration.unregister());
+
+ assert_not_equals(
+ registration, newRegistration,
+ 'register should resolve to a new value'
+ );
+ }, 'Unregister then register resolves to a new value');
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/re-register-while-old-registration-in-use';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ await registration.unregister();
+ const newRegistration = await navigator.serviceWorker.register(worker_url, { scope });
+ t.add_cleanup(() => newRegistration.unregister());
+
+ assert_not_equals(
+ registration, newRegistration,
+ 'Unregister and register should always create a new registration'
+ );
+}, 'Unregister then register does not resolve to the original value even if the registration is in use.');
+
+promise_test(function(t) {
+ var scope = 'resources/scope/re-register-does-not-affect-existing-controllee';
+ var iframe;
+ var registration;
+ var controller;
+
+ return service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ iframe = frame;
+ controller = iframe.contentWindow.navigator.serviceWorker.controller;
+ return registration.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(worker_url, { scope: scope });
+ })
+ .then(function(newRegistration) {
+ assert_equals(registration.installing, null,
+ 'installing version is null');
+ assert_equals(registration.waiting, null, 'waiting version is null');
+ assert_equals(
+ iframe.contentWindow.navigator.serviceWorker.controller,
+ controller,
+ 'the worker from the first registration is the controller');
+ iframe.remove();
+ });
+ }, 'Unregister then register does not affect existing controllee');
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/resurrection';
+ const altWorkerURL = worker_url + '?alt';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activating');
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ await registration.unregister();
+ const newRegistration = await navigator.serviceWorker.register(altWorkerURL, { scope });
+ t.add_cleanup(() => newRegistration.unregister());
+
+ assert_equals(newRegistration.active, null, 'Registration is new');
+
+ await wait_for_state(t, newRegistration.installing, 'activating');
+
+ const newIframe = await with_iframe(scope);
+ t.add_cleanup(() => newIframe.remove());
+
+ const iframeController = iframe.contentWindow.navigator.serviceWorker.controller;
+ const newIframeController = newIframe.contentWindow.navigator.serviceWorker.controller;
+
+ assert_not_equals(iframeController, newIframeController, 'iframes have different controllers');
+}, 'Unregister then register does not resurrect the registration');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/unregister.https.html b/testing/web-platform/tests/service-workers/service-worker/unregister.https.html
new file mode 100644
index 0000000000..492aecb21a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/unregister.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+async_test(function(t) {
+ var scope = 'resources/scope/unregister-twice';
+ var registration;
+ navigator.serviceWorker.register('resources/empty-worker.js',
+ {scope: scope})
+ .then(function(r) {
+ registration = r;
+ return registration.unregister();
+ })
+ .then(function() {
+ return registration.unregister();
+ })
+ .then(function(value) {
+ assert_equals(value, false,
+ 'unregistering twice should resolve with false');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Unregister twice');
+
+async_test(function(t) {
+ var scope = 'resources/scope/successful-unregister/';
+ navigator.serviceWorker.register('resources/empty-worker.js',
+ {scope: scope})
+ .then(function(registration) {
+ return registration.unregister();
+ })
+ .then(function(value) {
+ assert_equals(value, true,
+ 'unregistration should resolve with true');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then unregister');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html b/testing/web-platform/tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html
new file mode 100644
index 0000000000..ff51f7f902
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<title>Service Worker: Update should be triggered after a navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+async function cleanup(frame, registration) {
+ if (frame)
+ frame.remove();
+ if (registration)
+ await registration.unregister();
+}
+
+promise_test(async t => {
+ const script = 'resources/update_shell.py?filename=empty.js';
+ const scope = 'resources/scope/update';
+ let registration;
+ let frame;
+
+ async function run() {
+ registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Navigation should trigger update.
+ frame = await with_iframe(scope);
+ await wait_for_update(t, registration);
+ }
+
+ try {
+ await run();
+ } finally {
+ await cleanup(frame, registration);
+ }
+}, 'Update should be triggered after a navigation (no fetch event worker).');
+
+promise_test(async t => {
+ const script = 'resources/update_shell.py?filename=simple-intercept-worker.js';
+ const scope = 'resources/scope/update';
+ let registration;
+ let frame;
+
+ async function run() {
+ registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Navigation should trigger update (network fallback).
+ frame = await with_iframe(scope + '?ignore');
+ await wait_for_update(t, registration);
+
+ // Navigation should trigger update (respondWith called).
+ frame.src = scope + '?string';
+ await wait_for_update(t, registration);
+ }
+
+ try {
+ await run();
+ } finally {
+ await cleanup(frame, registration);
+ }
+}, 'Update should be triggered after a navigation (fetch event worker).');
+
+promise_test(async t => {
+ const script = 'resources/update_shell.py?filename=empty.js';
+ const scope = 'resources/';
+ let registration;
+ let frame;
+
+ async function run() {
+ registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Navigation should trigger update. Don't use with_iframe as it waits for
+ // the onload event.
+ frame = document.createElement('iframe');
+ frame.src = 'resources/malformed-http-response.asis';
+ document.body.appendChild(frame);
+ await wait_for_update(t, registration);
+ }
+
+ try {
+ await run();
+ } finally {
+ await cleanup(frame, registration);
+ }
+}, 'Update should be triggered after a navigation (network error).');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update-after-navigation-redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/update-after-navigation-redirect.https.html
new file mode 100644
index 0000000000..6e821fe479
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update-after-navigation-redirect.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Update should be triggered after redirects during navigation</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(async t => {
+ // This test does a navigation that goes through a redirect chain. Each
+ // request in the chain has a service worker. Each service worker has no
+ // fetch event handler. The redirects are performed by redirect.py.
+ const script = 'resources/update-nocookie-worker.py';
+ const scope1 = 'resources/redirect.py?scope1';
+ const scope2 = 'resources/redirect.py?scope2';
+ const scope3 = 'resources/empty.html';
+ let registration1;
+ let registration2;
+ let registration3;
+ let frame;
+
+ async function cleanup() {
+ if (frame)
+ frame.remove();
+ if (registration1)
+ return registration1.unregister();
+ if (registration2)
+ return registration2.unregister();
+ if (registration3)
+ return registration3.unregister();
+ }
+
+ async function make_active_registration(scope) {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ return registration;
+ }
+
+ async function run() {
+ // Make the registrations.
+ registration1 = await make_active_registration(scope1);
+ registration2 = await make_active_registration(scope2);
+ registration3 = await make_active_registration(scope3);
+
+ // Make the promises that resolve on update.
+ const saw_update1 = wait_for_update(t, registration1);
+ const saw_update2 = wait_for_update(t, registration2);
+ const saw_update3 = wait_for_update(t, registration3);
+
+ // Create a URL for the redirect chain: scope1 -> scope2 -> scope3.
+ // Build the URL in reverse order.
+ let url = `${base_path()}${scope3}`;
+ url = `${base_path()}${scope2}&Redirect=${encodeURIComponent(url)}`
+ url = `${base_path()}${scope1}&Redirect=${encodeURIComponent(url)}`
+
+ // Navigate to the URL.
+ frame = await with_iframe(url);
+
+ // Each registration should update.
+ await saw_update1;
+ await saw_update2;
+ await saw_update3;
+ }
+
+ try {
+ await run();
+ } finally {
+ await cleanup();
+ }
+}, 'service workers are updated on redirects during navigation');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update-after-oneday.https.html b/testing/web-platform/tests/service-workers/service-worker/update-after-oneday.https.html
new file mode 100644
index 0000000000..e7a8aa42d3
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update-after-oneday.https.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!-- This test requires browser to treat all registrations are older than 24 hours.
+ Preference 'dom.serviceWorkers.testUpdateOverOneDay' should be enabled during
+ the execution of the test -->
+<title>Service Worker: Functional events should trigger update if last update time is over 24 hours</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/update-nocookie-worker.py';
+ var scope = 'resources/update/update-after-oneday.https.html';
+ var expected_url = normalizeURL(script);
+ var registration;
+ var frame;
+
+ return service_worker_unregister_and_register(t, expected_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ return wait_for_update(t, registration);
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'new installing should be set after update resolves.');
+ assert_equals(registration.waiting, null,
+ 'waiting should still be null after update resolves.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after update found.');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ // Trigger a non-navigation fetch event
+ frame.contentWindow.load_image(normalizeURL('resources/update/sample'));
+ return wait_for_update(t, registration);
+ })
+ .then(function() {
+ frame.remove();
+ })
+ }, 'Update should be triggered after a functional event when last update time is over 24 hours');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update-bytecheck-cors-import.https.html b/testing/web-platform/tests/service-workers/service-worker/update-bytecheck-cors-import.https.html
new file mode 100644
index 0000000000..121a7378e3
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update-bytecheck-cors-import.https.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+// Tests of updating a service worker. This file contains cors cases only.
+
+/*
+ * @param string main
+ * Decide the content of the main script, where 'default' is for constant
+ * content while 'time' is for time-variant content.
+ * @param string imported
+ * Decide the content of the imported script, where 'default' is for constant
+ * content while 'time' is for time-variant content.
+ */
+const settings = [{main: 'default', imported: 'default'},
+ {main: 'default', imported: 'time' },
+ {main: 'time', imported: 'default'},
+ {main: 'time', imported: 'time' }];
+
+const host_info = get_host_info();
+settings.forEach(({main, imported}) => {
+ promise_test(async (t) => {
+ // Specify a cross origin path to load imported scripts from a cross origin.
+ const path = host_info.HTTPS_REMOTE_ORIGIN +
+ '/service-workers/service-worker/resources/';
+ const script = 'resources/bytecheck-worker.py' +
+ '?main=' + main +
+ '&imported=' + imported +
+ '&path=' + path +
+ '&type=classic';
+ const scope = 'resources/blank.html';
+
+ // Register a service worker.
+ const swr = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => swr.unregister());
+ const sw = await wait_for_update(t, swr);
+ await wait_for_state(t, sw, 'activated');
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+
+ // Update the service worker registration.
+ await swr.update();
+
+ // If there should be a new service worker.
+ if (main === 'time' || imported === 'time') {
+ return wait_for_update(t, swr);
+ }
+ // Otherwise, make sure there is no newly created service worker.
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+ }, `Test(main: ${main}, imported: ${imported})`);
+});
+
+settings.forEach(({main, imported}) => {
+ promise_test(async (t) => {
+ // Specify a cross origin path to load imported scripts from a cross origin.
+ const path = host_info.HTTPS_REMOTE_ORIGIN +
+ '/service-workers/service-worker/resources/';
+ const script = 'resources/bytecheck-worker.py' +
+ '?main=' + main +
+ '&imported=' + imported +
+ '&path=' + path +
+ '&type=module';
+ const scope = 'resources/blank.html';
+
+ // Register a service worker.
+ const swr = await service_worker_unregister_and_register(t, script, scope, {type: 'module'});
+ t.add_cleanup(() => swr.unregister());
+ const sw = await wait_for_update(t, swr);
+ await wait_for_state(t, sw, 'activated');
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+
+ // Update the service worker registration.
+ await swr.update();
+
+ // If there should be a new service worker.
+ if (main === 'time' || imported === 'time') {
+ return wait_for_update(t, swr);
+ }
+ // Otherwise, make sure there is no newly created service worker.
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+ }, `Test module script(main: ${main}, imported: ${imported})`);
+});
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update-bytecheck.https.html b/testing/web-platform/tests/service-workers/service-worker/update-bytecheck.https.html
new file mode 100644
index 0000000000..3e5a28bb67
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update-bytecheck.https.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+// Tests of updating a service worker. This file contains non-cors cases only.
+
+/*
+ * @param string main
+ * Decide the content of the main script, where 'default' is for constant
+ * content while 'time' is for time-variant content.
+ * @param string imported
+ * Decide the content of the imported script, where 'default' is for constant
+ * content while 'time' is for time-variant content.
+ */
+const settings = [{main: 'default', imported: 'default'},
+ {main: 'default', imported: 'time' },
+ {main: 'time', imported: 'default'},
+ {main: 'time', imported: 'time' }];
+
+const host_info = get_host_info();
+settings.forEach(({main, imported}) => {
+ promise_test(async (t) => {
+ // Empty path results in the same origin imported scripts.
+ const path = '';
+ const script = 'resources/bytecheck-worker.py' +
+ '?main=' + main +
+ '&imported=' + imported +
+ '&path=' + path +
+ '&type=classic';
+ const scope = 'resources/blank.html';
+
+ // Register a service worker.
+ const swr = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => swr.unregister());
+ const sw = await wait_for_update(t, swr);
+ await wait_for_state(t, sw, 'activated');
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+
+ // Update the service worker registration.
+ await swr.update();
+
+ // If there should be a new service worker.
+ if (main === 'time' || imported === 'time') {
+ return wait_for_update(t, swr);
+ }
+ // Otherwise, make sure there is no newly created service worker.
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+ }, `Test(main: ${main}, imported: ${imported})`);
+});
+
+settings.forEach(({main, imported}) => {
+ promise_test(async (t) => {
+ // Empty path results in the same origin imported scripts.
+ const path = './';
+ const script = 'resources/bytecheck-worker.py' +
+ '?main=' + main +
+ '&imported=' + imported +
+ '&path=' + path +
+ '&type=module';
+ const scope = 'resources/blank.html';
+
+ // Register a module service worker.
+ const swr = await service_worker_unregister_and_register(t, script, scope,
+ {type: 'module'});
+
+ t.add_cleanup(() => swr.unregister());
+ const sw = await wait_for_update(t, swr);
+ await wait_for_state(t, sw, 'activated');
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+
+ // Update the service worker registration.
+ await swr.update();
+
+ // If there should be a new service worker.
+ if (main === 'time' || imported === 'time') {
+ return wait_for_update(t, swr);
+ }
+ // Otherwise, make sure there is no newly created service worker.
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+ }, `Test module script(main: ${main}, imported: ${imported})`);
+});
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update-import-scripts.https.html b/testing/web-platform/tests/service-workers/service-worker/update-import-scripts.https.html
new file mode 100644
index 0000000000..a2df529e90
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update-import-scripts.https.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: import scripts ignored error</title>
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This file contains tests to check if imported scripts appropriately updated.
+
+const SCOPE = 'resources/simple.txt';
+
+// Create a service worker (update-worker-from-file.py), which is initially
+// |initial_worker| and |updated_worker| later.
+async function prepare_ready_update_worker_from_file(
+ t, initial_worker, updated_worker) {
+ const key = token();
+ const worker_url = `resources/update-worker-from-file.py?` +
+ `First=${initial_worker}&Second=${updated_worker}&Key=${key}`;
+ const expected_url = normalizeURL(worker_url);
+
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ assert_equals(registration.installing, null,
+ 'prepare_ready: installing');
+ assert_equals(registration.waiting, null,
+ 'prepare_ready: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'prepare_ready: active');
+ return [registration, expected_url];
+}
+
+// Create a service worker using the script under resources/.
+async function prepare_ready_normal_worker(t, filename, additional_params='') {
+ const key = token();
+ const worker_url = `resources/${filename}?Key=${key}&${additional_params}`;
+ const expected_url = normalizeURL(worker_url);
+
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ assert_equals(registration.installing, null,
+ 'prepare_ready: installing');
+ assert_equals(registration.waiting, null,
+ 'prepare_ready: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'prepare_ready: active');
+ return [registration, expected_url];
+}
+
+function assert_installing_and_active(registration, expected_url) {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'assert_installing_and_active: installing');
+ assert_equals(registration.waiting, null,
+ 'assert_installing_and_active: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_installing_and_active: active');
+}
+
+function assert_waiting_and_active(registration, expected_url) {
+ assert_equals(registration.installing, null,
+ 'assert_waiting_and_active: installing');
+ assert_equals(registration.waiting.scriptURL, expected_url,
+ 'assert_waiting_and_active: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_waiting_and_active: active');
+}
+
+function assert_active_only(registration, expected_url) {
+ assert_equals(registration.installing, null,
+ 'assert_active_only: installing');
+ assert_equals(registration.waiting, null,
+ 'assert_active_only: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_active_only: active');
+}
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_update_worker_from_file(
+ t, 'empty.js', 'import-scripts-404.js');
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_js(t, TypeError, registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should fail when a new worker imports an unavailable script.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_update_worker_from_file(
+ t, 'import-scripts-404-after-update.js', 'empty.js');
+ t.add_cleanup(() => registration.unregister());
+
+ await Promise.all([registration.update(), wait_for_update(t, registration)]);
+ assert_installing_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.installing, 'installed');
+ assert_waiting_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.waiting, 'activated');
+ assert_active_only(registration, expected_url);
+}, 'update() should succeed when the old imported script no longer exist but ' +
+ "the new worker doesn't import it.");
+
+promise_test(async t => {
+ const [registration, expected_url] = await prepare_ready_normal_worker(
+ t, 'import-scripts-404-after-update.js');
+ t.add_cleanup(() => registration.unregister());
+
+ await registration.update();
+ assert_active_only(registration, expected_url);
+}, 'update() should treat 404 on imported scripts as no change.');
+
+promise_test(async t => {
+ const [registration, expected_url] = await prepare_ready_normal_worker(
+ t, 'import-scripts-404-after-update-plus-update-worker.js',
+ `AdditionalKey=${token()}`);
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_js(t, TypeError, registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should find an update in an imported script but update() should ' +
+ 'result in failure due to missing the other imported script.');
+
+promise_test(async t => {
+ const [registration, expected_url] = await prepare_ready_normal_worker(
+ t, 'import-scripts-cross-origin-worker.sub.js');
+ t.add_cleanup(() => registration.unregister());
+ await registration.update();
+ assert_installing_and_active(registration, expected_url);
+}, 'update() should work with cross-origin importScripts.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update-missing-import-scripts.https.html b/testing/web-platform/tests/service-workers/service-worker/update-missing-import-scripts.https.html
new file mode 100644
index 0000000000..66e8bfac75
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update-missing-import-scripts.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Service Worker: update with missing importScripts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/utils.js"></script>
+<body>
+<script>
+/**
+ * Test ServiceWorkerRegistration.update() when importScripts in a service worker
+ * script is no longer available (but was initially).
+ */
+let registration = null;
+
+promise_test(async (test) => {
+ const script = `resources/update-missing-import-scripts-main-worker.py?key=${token()}`;
+ const scope = 'resources/update-missing-import-scripts';
+
+ registration = await service_worker_unregister_and_register(test, script, scope);
+
+ add_completion_callback(() => { registration.unregister(); });
+
+ await wait_for_state(test, registration.installing, 'activated');
+}, 'Initialize global state');
+
+promise_test(test => {
+ return new Promise(resolve => {
+ registration.addEventListener('updatefound', resolve);
+ registration.update();
+ });
+}, 'Update service worker with new script that\'s missing importScripts()');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update-module-request-mode.https.html b/testing/web-platform/tests/service-workers/service-worker/update-module-request-mode.https.html
new file mode 100644
index 0000000000..b3875d207b
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update-module-request-mode.https.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>Test that mode is set to same-origin for a main module</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests a main module service worker script fetch during an update check.
+// The fetch should have the mode set to 'same-origin'.
+//
+// The test works by registering a main module service worker. It then does an
+// update. The test server responds with an updated worker script that remembers
+// the http request. The updated worker reports back this request to the test
+// page.
+promise_test(async (t) => {
+ const script = "resources/test-request-mode-worker.py";
+ const scope = "resources/";
+
+ // Register the service worker.
+ await service_worker_unregister(t, scope);
+ const registration = await navigator.serviceWorker.register(
+ script, {scope, type: 'module'});
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Do an update.
+ await registration.update();
+
+ // Ask the new worker what the request was.
+ const newWorker = registration.installing;
+ const sawMessage = new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ });
+ newWorker.postMessage('getHeaders');
+ const result = await sawMessage;
+
+ // Test the result.
+ assert_equals(result['sec-fetch-mode'], 'same-origin');
+ assert_equals(result['origin'], undefined);
+
+}, 'headers of a main module script');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update-no-cache-request-headers.https.html b/testing/web-platform/tests/service-workers/service-worker/update-no-cache-request-headers.https.html
new file mode 100644
index 0000000000..6ebad4b7b1
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update-no-cache-request-headers.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that cache is being bypassed/validated in no-cache mode on update</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests a service worker script fetch during an update check which
+// bypasses/validates the browser cache. The fetch should have the
+// 'if-none-match' request header.
+//
+// This tests the Update step:
+// "Set request’s cache mode to "no-cache" if any of the following are true..."
+// https://w3c.github.io/ServiceWorker/#update-algorithm
+//
+// The test works by registering a service worker with |updateViaCache|
+// set to "none". It then does an update. The test server responds with
+// an updated worker script that remembers the http request headers.
+// The updated worker reports back these headers to the test page.
+promise_test(async (t) => {
+ const script = "resources/test-request-headers-worker.py";
+ const scope = "resources/";
+
+ // Register the service worker.
+ await service_worker_unregister(t, scope);
+ const registration = await navigator.serviceWorker.register(
+ script, {scope, updateViaCache: 'none'});
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Do an update.
+ await registration.update();
+
+ // Ask the new worker what the request headers were.
+ const newWorker = registration.installing;
+ const sawMessage = new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ });
+ newWorker.postMessage('getHeaders');
+ const result = await sawMessage;
+
+ // Test the result.
+ assert_equals(result['service-worker'], 'script');
+ assert_equals(result['if-none-match'], 'etag');
+}, 'headers in no-cache mode');
+
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update-not-allowed.https.html b/testing/web-platform/tests/service-workers/service-worker/update-not-allowed.https.html
new file mode 100644
index 0000000000..0a54aa9350
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update-not-allowed.https.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+function send_message_to_worker_and_wait_for_response(worker, message) {
+ return new Promise(resolve => {
+ // Use a dedicated channel for every request to avoid race conditions on
+ // concurrent requests.
+ const channel = new MessageChannel();
+ worker.postMessage(channel.port1, [channel.port1]);
+
+ let messageReceived = false;
+ channel.port2.onmessage = event => {
+ assert_false(messageReceived, 'Already received response for ' + message);
+ messageReceived = true;
+ resolve(event.data);
+ };
+ channel.port2.postMessage(message);
+ });
+}
+
+async function ensure_install_event_fired(worker) {
+ const response = await send_message_to_worker_and_wait_for_response(worker, 'awaitInstallEvent');
+ assert_equals('installEventFired', response);
+ assert_equals('installing', worker.state, 'Expected worker to be installing.');
+}
+
+async function finish_install(worker) {
+ await ensure_install_event_fired(worker);
+ const response = await send_message_to_worker_and_wait_for_response(worker, 'finishInstall');
+ assert_equals('installFinished', response);
+}
+
+async function activate_service_worker(t, worker) {
+ await finish_install(worker);
+ // By waiting for both states at the same time, the test fails
+ // quickly if the installation fails, avoiding a timeout.
+ await Promise.race([wait_for_state(t, worker, 'activated'),
+ wait_for_state(t, worker, 'redundant')]);
+ assert_equals('activated', worker.state, 'Service worker should be activated.');
+}
+
+async function update_within_service_worker(worker) {
+ // This function returns a Promise that resolves when update()
+ // has been called but is not necessarily finished yet.
+ // Call finish() on the returned object to wait for update() settle.
+ const port = await send_message_to_worker_and_wait_for_response(worker, 'callUpdate');
+ let messageReceived = false;
+ return {
+ finish: () => {
+ return new Promise(resolve => {
+ port.onmessage = event => {
+ assert_false(messageReceived, 'Update already finished.');
+ messageReceived = true;
+ resolve(event.data);
+ };
+ });
+ },
+ };
+}
+
+async function update_from_client_and_await_installing_version(test, registration) {
+ const updatefound = wait_for_update(test, registration);
+ registration.update();
+ await updatefound;
+ return registration.installing;
+}
+
+async function spin_up_service_worker(test) {
+ const script = 'resources/update-during-installation-worker.py';
+ const scope = 'resources/blank.html';
+
+ const registration = await service_worker_unregister_and_register(test, script, scope);
+ test.add_cleanup(async () => {
+ if (registration.installing) {
+ // If there is an installing worker, we need to finish installing it.
+ // Otherwise, the tests fails with an timeout because unregister() blocks
+ // until the install-event-handler finishes.
+ const worker = registration.installing;
+ await send_message_to_worker_and_wait_for_response(worker, 'awaitInstallEvent');
+ await send_message_to_worker_and_wait_for_response(worker, 'finishInstall');
+ }
+ return registration.unregister();
+ });
+
+ return registration;
+}
+
+promise_test(async t => {
+ const registration = await spin_up_service_worker(t);
+ const worker = registration.installing;
+ await ensure_install_event_fired(worker);
+
+ const result = registration.update();
+ await activate_service_worker(t, worker);
+ return result;
+}, 'ServiceWorkerRegistration.update() from client succeeds while installing service worker.');
+
+promise_test(async t => {
+ const registration = await spin_up_service_worker(t);
+ const worker = registration.installing;
+ await ensure_install_event_fired(worker);
+
+ // Add event listener to fail the test if update() succeeds.
+ const updatefound = t.step_func(async () => {
+ registration.removeEventListener('updatefound', updatefound);
+ // Activate new worker so non-compliant browsers don't fail with timeout.
+ await activate_service_worker(t, registration.installing);
+ assert_unreached("update() should have failed");
+ });
+ registration.addEventListener('updatefound', updatefound);
+
+ const update = await update_within_service_worker(worker);
+ // Activate worker to ensure update() finishes and the test doesn't timeout
+ // in non-compliant browsers.
+ await activate_service_worker(t, worker);
+
+ const response = await update.finish();
+ assert_false(response.success, 'update() should have failed.');
+ assert_equals('InvalidStateError', response.exception, 'update() should have thrown InvalidStateError.');
+}, 'ServiceWorkerRegistration.update() from installing service worker throws.');
+
+promise_test(async t => {
+ const registration = await spin_up_service_worker(t);
+ const worker1 = registration.installing;
+ await activate_service_worker(t, worker1);
+
+ const worker2 = await update_from_client_and_await_installing_version(t, registration);
+ await ensure_install_event_fired(worker2);
+
+ const update = await update_within_service_worker(worker1);
+ // Activate the new version so that update() finishes and the test doesn't timeout.
+ await activate_service_worker(t, worker2);
+ const response = await update.finish();
+ assert_true(response.success, 'update() from active service worker should have succeeded.');
+}, 'ServiceWorkerRegistration.update() from active service worker succeeds while installing service worker.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update-on-navigation.https.html b/testing/web-platform/tests/service-workers/service-worker/update-on-navigation.https.html
new file mode 100644
index 0000000000..5273420b90
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update-on-navigation.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>Update on navigation</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='resources/test-helpers.sub.js'></script>
+<script>
+promise_test(async (t) => {
+ var script = 'resources/update-fetch-worker.py';
+ var scope = 'resources/trickle.py?ms=1000&count=1';
+
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ if (registration.installing)
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+}, 'The active service worker in charge of a navigation load should not be terminated as part of updating the registration');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update-recovery.https.html b/testing/web-platform/tests/service-workers/service-worker/update-recovery.https.html
new file mode 100644
index 0000000000..17608d2ef7
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update-recovery.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<title>Service Worker: recovery by navigation update</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var scope = 'resources/simple.txt';
+ var worker_url = 'resources/update-recovery-worker.py';
+ var expected_url = normalizeURL(worker_url);
+ var registration;
+
+ function with_bad_iframe(url) {
+ return new Promise(function(resolve, reject) {
+ var frame = document.createElement('iframe');
+
+ // There is no cross-browser event to listen for to detect an
+ // iframe that fails to load due to a bad interception. Unfortunately
+ // we have to use a timeout.
+ var timeout = setTimeout(function() {
+ frame.remove();
+ resolve();
+ }, 5000);
+
+ // If we do get a load event, though, we know something went wrong.
+ frame.addEventListener('load', function() {
+ clearTimeout(timeout);
+ frame.remove();
+ reject('expected bad iframe should not fire a load event!');
+ });
+
+ frame.src = url;
+ document.body.appendChild(frame);
+ });
+ }
+
+ function with_update(t) {
+ return new Promise(function(resolve, reject) {
+ registration.addEventListener('updatefound', function onUpdate() {
+ registration.removeEventListener('updatefound', onUpdate);
+ wait_for_state(t, registration.installing, 'activated').then(function() {
+ resolve();
+ });
+ });
+ });
+ }
+
+ return service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return Promise.all([
+ with_update(t),
+ with_bad_iframe(scope)
+ ]);
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ expected_url);
+ frame.remove();
+ });
+ }, 'Recover from a bad service worker by updating after a failed navigation.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update-registration-with-type.https.html b/testing/web-platform/tests/service-workers/service-worker/update-registration-with-type.https.html
new file mode 100644
index 0000000000..269e61b390
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update-registration-with-type.https.html
@@ -0,0 +1,208 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Update the registration with a different script type.</title>
+<!-- common.js is for guid() -->
+<script src="/common/security-features/resources/common.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// The following two tests check that a registration is updated correctly
+// with different script type. At first Service Worker is registered as
+// classic script type, then it is re-registered as module script type,
+// and vice versa. A main script is also updated at the same time.
+promise_test(async t => {
+ const key = guid();
+ const script = `resources/update-registration-with-type.py?classic_first=1&key=${key}`;
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with classic script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ const firstWorker = firstRegistration.installing;
+ await wait_for_state(t, firstWorker, 'activated');
+ firstWorker.postMessage(' ');
+ let msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+ assert_equals(msgEvent.data, 'A classic script.');
+
+ // Re-register with module script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ const secondWorker = secondRegistration.installing;
+ secondWorker.postMessage(' ');
+ msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+ assert_equals(msgEvent.data, 'A module script.');
+
+ assert_not_equals(firstWorker, secondWorker);
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (classic => module).');
+
+promise_test(async t => {
+ const key = guid();
+ const script = `resources/update-registration-with-type.py?classic_first=0&key=${key}`;
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with module script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ const firstWorker = firstRegistration.installing;
+ await wait_for_state(t, firstWorker, 'activated');
+ firstWorker.postMessage(' ');
+ let msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+ assert_equals(msgEvent.data, 'A module script.');
+
+ // Re-register with classic script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ const secondWorker = secondRegistration.installing;
+ secondWorker.postMessage(' ');
+ msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+ assert_equals(msgEvent.data, 'A classic script.');
+
+ assert_not_equals(firstWorker, secondWorker);
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (module => classic).');
+
+// The following two tests change the script type while keeping
+// the script identical.
+promise_test(async t => {
+ const script = 'resources/empty-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with classic script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ const firstWorker = firstRegistration.installing;
+ await wait_for_state(t, firstWorker, 'activated');
+
+ // Re-register with module script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ const secondWorker = secondRegistration.installing;
+
+ assert_not_equals(firstWorker, secondWorker);
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (classic => module) '
+ + 'and with a same main script.');
+
+promise_test(async t => {
+ const script = 'resources/empty-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with module script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ const firstWorker = firstRegistration.installing;
+ await wait_for_state(t, firstWorker, 'activated');
+
+ // Re-register with classic script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ const secondWorker = secondRegistration.installing;
+
+ assert_not_equals(firstWorker, secondWorker);
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (module => classic) '
+ + 'and with a same main script.');
+
+// This test checks that a registration is not updated with the same script
+// type and the same main script.
+promise_test(async t => {
+ const script = 'resources/empty-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with module script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ await wait_for_state(t, firstRegistration.installing, 'activated');
+
+ // Re-register with module script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ assert_equals(secondRegistration.installing, null);
+
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Does not update the registration with the same script type and '
+ + 'the same main script.');
+
+// In the case (classic => module), a worker script contains importScripts()
+// that is disallowed on module scripts, so the second registration is
+// expected to fail script evaluation.
+promise_test(async t => {
+ const script = 'resources/classic-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with classic script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ assert_not_equals(firstRegistration.installing, null);
+ await wait_for_state(t, firstRegistration.installing, 'activated');
+
+ // Re-register with module script type and expect TypeError.
+ return promise_rejects_js(t, TypeError, navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ }), 'Registering with invalid evaluation should be failed.');
+}, 'Update the registration with a different script type (classic => module) '
+ + 'and with a same main script. Expect evaluation failed.');
+
+// In the case (module => classic), a worker script contains static-import
+// that is disallowed on classic scripts, so the second registration is
+// expected to fail script evaluation.
+promise_test(async t => {
+ const script = 'resources/module-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with module script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ assert_not_equals(firstRegistration.installing, null);
+ await wait_for_state(t, firstRegistration.installing, 'activated');
+
+ // Re-register with classic script type and expect TypeError.
+ return promise_rejects_js(t, TypeError, navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ }), 'Registering with invalid evaluation should be failed.');
+}, 'Update the registration with a different script type (module => classic) '
+ + 'and with a same main script. Expect evaluation failed.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update-result.https.html b/testing/web-platform/tests/service-workers/service-worker/update-result.https.html
new file mode 100644
index 0000000000..d8ed94f776
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update-result.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<title>Service Worker: update() should resolve a ServiceWorkerRegistration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(async function(t) {
+ const script = './resources/empty.js';
+ const scope = './resources/empty.html?update-result';
+
+ let reg = await navigator.serviceWorker.register(script, { scope });
+ t.add_cleanup(async _ => await reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ let result = await reg.update();
+ assert_true(result instanceof ServiceWorkerRegistration,
+ 'update() should resolve a ServiceWorkerRegistration');
+}, 'ServiceWorkerRegistration.update() should resolve a registration object');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/update.https.html b/testing/web-platform/tests/service-workers/service-worker/update.https.html
new file mode 100644
index 0000000000..f9fded3db4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/update.https.html
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration update()</title>
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const SCOPE = 'resources/simple.txt';
+
+// Create a service worker (update-worker.py). The response to update() will be
+// different based on the mode.
+async function prepare_ready_registration_with_mode(t, mode) {
+ const key = token();
+ const worker_url = `resources/update-worker.py?Key=${key}&Mode=${mode}`;
+ const expected_url = normalizeURL(worker_url);
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ assert_equals(registration.installing, null,
+ 'prepare_ready: installing');
+ assert_equals(registration.waiting, null,
+ 'prepare_ready: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'prepare_ready: active');
+ return [registration, expected_url];
+}
+
+// Create a service worker (update-worker-from-file.py), which is initially
+// |initial_worker| and |updated_worker| later.
+async function prepare_ready_registration_with_file(
+ t, initial_worker, updated_worker) {
+ const key = token();
+ const worker_url = `resources/update-worker-from-file.py?` +
+ `First=${initial_worker}&Second=${updated_worker}&Key=${key}`;
+ const expected_url = normalizeURL(worker_url);
+
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ assert_equals(registration.installing, null,
+ 'prepare_ready: installing');
+ assert_equals(registration.waiting, null,
+ 'prepare_ready: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'prepare_ready: active');
+ return [registration, expected_url];
+}
+
+function assert_installing_and_active(registration, expected_url) {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'assert_installing_and_active: installing');
+ assert_equals(registration.waiting, null,
+ 'assert_installing_and_active: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_installing_and_active: active');
+}
+
+function assert_waiting_and_active(registration, expected_url) {
+ assert_equals(registration.installing, null,
+ 'assert_waiting_and_active: installing');
+ assert_equals(registration.waiting.scriptURL, expected_url,
+ 'assert_waiting_and_active: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_waiting_and_active: active');
+}
+
+function assert_active_only(registration, expected_url) {
+ assert_equals(registration.installing, null,
+ 'assert_active_only: installing');
+ assert_equals(registration.waiting, null,
+ 'assert_active_only: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_active_only: active');
+}
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'normal');
+ t.add_cleanup(() => registration.unregister());
+
+ await Promise.all([registration.update(), wait_for_update(t, registration)]);
+ assert_installing_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.installing, 'installed');
+ assert_waiting_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.waiting, 'activated');
+ assert_active_only(registration, expected_url);
+}, 'update() should succeed when new script is available.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'bad_mime_type');
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_dom(t, 'SecurityError', registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should fail when mime type is invalid.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'redirect');
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_js(t, TypeError, registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should fail when a response for the main script is redirect.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'syntax_error');
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_js(t, TypeError, registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should fail when a new script contains a syntax error.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'throw_install');
+ t.add_cleanup(() => registration.unregister());
+
+ await Promise.all([registration.update(), wait_for_update(t, registration)]);
+ assert_installing_and_active(registration, expected_url);
+}, 'update() should resolve when the install event throws.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'normal');
+ t.add_cleanup(() => registration.unregister());
+
+ // We need to hold a client alive so that unregister() below doesn't remove
+ // the registration before update() has had a chance to look at the pending
+ // uninstall flag.
+ const frame = await with_iframe(SCOPE);
+ t.add_cleanup(() => frame.remove());
+
+ await promise_rejects_js(
+ t, TypeError,
+ Promise.all([registration.unregister(), registration.update()]));
+}, 'update() should fail when the pending uninstall flag is set.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_file(
+ t,
+ 'update-smaller-body-before-update-worker.js',
+ 'update-smaller-body-after-update-worker.js');
+ t.add_cleanup(() => registration.unregister());
+
+ await Promise.all([registration.update(), wait_for_update(t, registration)]);
+ assert_installing_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.installing, 'installed');
+ assert_waiting_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.waiting, 'activated');
+ assert_active_only(registration, expected_url);
+}, 'update() should succeed when the script shrinks.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/waiting.https.html b/testing/web-platform/tests/service-workers/service-worker/waiting.https.html
new file mode 100644
index 0000000000..499e581eb3
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/waiting.https.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.waiting</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/empty-worker.js';
+const SCOPE = 'resources/blank.html';
+
+promise_test(async t => {
+
+ t.add_cleanup(async() => {
+ if (frame)
+ frame.remove();
+ if (registration)
+ await registration.unregister();
+ });
+
+ await service_worker_unregister(t, SCOPE);
+ const frame = await with_iframe(SCOPE);
+ const registration =
+ await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ await wait_for_state(t, registration.installing, 'installed');
+ const controller = frame.contentWindow.navigator.serviceWorker.controller;
+ assert_equals(controller, null, 'controller');
+ assert_equals(registration.active, null, 'registration.active');
+ assert_equals(registration.waiting.state, 'installed',
+ 'registration.waiting');
+ assert_equals(registration.installing, null, 'registration.installing');
+}, 'waiting is set after installation');
+
+// Tests that the ServiceWorker objects returned from waiting attribute getter
+// that represent the same service worker are the same objects.
+promise_test(async t => {
+ const registration1 =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ const registration2 = await navigator.serviceWorker.getRegistration(SCOPE);
+ assert_equals(registration1.waiting, registration2.waiting,
+ 'ServiceWorkerRegistration.waiting should return the same ' +
+ 'object');
+ await registration1.unregister();
+}, 'The ServiceWorker objects returned from waiting attribute getter that ' +
+ 'represent the same service worker are the same objects');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/websocket-in-service-worker.https.html b/testing/web-platform/tests/service-workers/service-worker/websocket-in-service-worker.https.html
new file mode 100644
index 0000000000..cda9d6fe67
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/websocket-in-service-worker.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>Service Worker: WebSockets can be created in a Service Worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ const SCRIPT = 'resources/websocket-worker.js?pipe=sub';
+ const SCOPE = 'resources/blank.html';
+ let registration;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(r => {
+ add_completion_callback(() => { r.unregister(); });
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => {
+ return new Promise(resolve => {
+ navigator.serviceWorker.onmessage = t.step_func(msg => {
+ assert_equals(msg.data, 'PASS');
+ resolve();
+ });
+ registration.active.postMessage({});
+ });
+ });
+ }, 'Verify WebSockets can be created in a Service Worker');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/websocket.https.html b/testing/web-platform/tests/service-workers/service-worker/websocket.https.html
new file mode 100644
index 0000000000..cbfed456a9
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/websocket.https.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<title>Service Worker: WebSocket handshake channel is not intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+
+promise_test(function(t) {
+ var path = new URL(".", window.location).pathname
+ var url = 'resources/websocket.js';
+ var scope = 'resources/blank.html?websocket';
+ var host_info = get_host_info();
+ var frameURL = host_info['HTTPS_ORIGIN'] + path + scope;
+ var frame;
+
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(frameURL); })
+ .then(function(f) {
+ frame = f;
+ return websocket(t, frame);
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ frame.contentWindow.navigator.serviceWorker.controller.postMessage({port: channel.port2}, [channel.port2]);
+ });
+ })
+ .then(function(e) {
+ for (var url in e.data.urls) {
+ assert_equals(url.indexOf(get_websocket_url()), -1,
+ "Observed an unexpected FetchEvent for the WebSocket handshake");
+ }
+ frame.remove();
+ });
+ }, 'Verify WebSocket handshake channel does not get intercepted');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/webvtt-cross-origin.https.html b/testing/web-platform/tests/service-workers/service-worker/webvtt-cross-origin.https.html
new file mode 100644
index 0000000000..9394ff75c4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/webvtt-cross-origin.https.html
@@ -0,0 +1,175 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>cross-origin webvtt returned by service worker is detected</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+// This file tests responses for WebVTT text track from a service worker. It
+// creates an iframe with a <track> element, controlled by a service worker.
+// Each test tries to load a text track, the service worker intercepts the
+// requests and responds with opaque or non-opaque responses. As the
+// crossorigin attribute is not set, request's mode is always "same-origin",
+// and as specified in https://fetch.spec.whatwg.org/#http-fetch,
+// a response from a service worker whose type is neither "basic" nor
+// "default" is rejected.
+
+const host_info = get_host_info();
+const kScript = 'resources/fetch-rewrite-worker.js';
+// Add '?ignore' so the service worker falls back for the navigation.
+const kScope = 'resources/vtt-frame.html?ignore';
+let frame;
+
+function load_track(url) {
+ const track = frame.contentDocument.querySelector('track');
+ const result = new Promise((resolve, reject) => {
+ track.onload = (e => {
+ resolve('load event');
+ });
+ track.onerror = (e => {
+ resolve('error event');
+ });
+ });
+
+ track.src = url;
+ // Setting mode to hidden seems needed, or else the text track requests don't
+ // occur.
+ track.track.mode = 'hidden';
+ return result;
+}
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ promise_test(() => {
+ frame.remove();
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ })
+ }, 'initialize global state');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL.
+ url += '?url=' + host_info.HTTPS_ORIGIN + '/media/foo.vtt';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'load event');
+ });
+ }, 'same-origin text track should load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a cross-origin URL.
+ url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'cross-origin text track with no-cors request should not load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a cross-origin URL that
+ // doesn't support CORS.
+ url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN +
+ '/media/foo-no-cors.vtt';
+ // Add '&mode' to tell the service worker to do a CORS request.
+ url += '&mode=cors';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'cross-origin text track with rejected cors request should not load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a cross-origin URL.
+ url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+ // Add '&mode' to tell the service worker to do a CORS request.
+ url += '&mode=cors';
+ // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so
+ // that CORS will succeed if the service approves it.
+ url += '&credentials=same-origin';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'cross-origin text track with approved cors request should not load');
+
+// Redirect tests.
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+ redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+ // ... to a same-origin URL.
+ redirect_target = host_info.HTTPS_ORIGIN + '/media/foo.vtt';
+ url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'load event');
+ });
+ }, 'same-origin text track that redirects same-origin should load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+ redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+ // ... to a cross-origin URL.
+ redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+ url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'same-origin text track that redirects cross-origin should not load');
+
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+ redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+ // ... to a cross-origin URL.
+ redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo-no-cors.vtt';
+ url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+ // Add '&mode' to tell the service worker to do a CORS request.
+ url += '&mode=cors';
+ // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so
+ // that CORS will succeed if the server approves it.
+ url += '&credentials=same-origin';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'same-origin text track that redirects to a cross-origin text track with rejected cors should not load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+ redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+ // ... to a cross-origin URL.
+ redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+ url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+ // Add '&mode' to tell the service worker to do a CORS request.
+ url += '&mode=cors';
+ // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so
+ // that CORS will succeed if the server approves it.
+ url += '&credentials=same-origin';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'same-origin text track that redirects to a cross-origin text track with approved cors should not load');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/windowclient-navigate.https.html b/testing/web-platform/tests/service-workers/service-worker/windowclient-navigate.https.html
new file mode 100644
index 0000000000..ad60f78636
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/windowclient-navigate.https.html
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<title>Service Worker: WindowClient.navigate() tests</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const SCOPE = 'resources/blank.html';
+const SCRIPT_URL = 'resources/windowclient-navigate-worker.js';
+const CROSS_ORIGIN_URL =
+ get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/blank.html';
+
+navigateTest({
+ description: 'normal',
+ destUrl: 'blank.html?navigate',
+ expected: normalizeURL(SCOPE) + '?navigate',
+});
+
+navigateTest({
+ description: 'blank url',
+ destUrl: '',
+ expected: normalizeURL(SCRIPT_URL)
+});
+
+navigateTest({
+ description: 'in scope but not controlled test on installing worker',
+ destUrl: 'blank.html?navigate',
+ expected: 'TypeError',
+ waitState: 'installing',
+});
+
+navigateTest({
+ description: 'in scope but not controlled test on active worker',
+ destUrl: 'blank.html?navigate',
+ expected: 'TypeError',
+ controlled: false,
+});
+
+navigateTest({
+ description: 'out of scope',
+ srcUrl: '/common/blank.html',
+ destUrl: 'blank.html?navigate',
+ expected: 'TypeError',
+});
+
+navigateTest({
+ description: 'cross orgin url',
+ destUrl: CROSS_ORIGIN_URL,
+ expected: null
+});
+
+navigateTest({
+ description: 'invalid url (http://[example.com])',
+ destUrl: 'http://[example].com',
+ expected: 'TypeError'
+});
+
+navigateTest({
+ description: 'invalid url (view-source://example.com)',
+ destUrl: 'view-source://example.com',
+ expected: 'TypeError'
+});
+
+navigateTest({
+ description: 'invalid url (file:///)',
+ destUrl: 'file:///',
+ expected: 'TypeError'
+});
+
+navigateTest({
+ description: 'invalid url (about:blank)',
+ destUrl: 'about:blank',
+ expected: 'TypeError'
+});
+
+navigateTest({
+ description: 'navigate on a top-level window client',
+ destUrl: 'blank.html?navigate',
+ srcUrl: 'resources/loaded.html',
+ scope: 'resources/loaded.html',
+ expected: normalizeURL(SCOPE) + '?navigate',
+ frameType: 'top-level'
+});
+
+async function createFrame(t, parameters) {
+ if (parameters.frameType === 'top-level') {
+ // Wait for window.open is completed.
+ await new Promise(resolve => {
+ const win = window.open(parameters.srcUrl);
+ t.add_cleanup(() => win.close());
+ window.addEventListener('message', (e) => {
+ if (e.data.type === 'LOADED') {
+ resolve();
+ }
+ });
+ });
+ }
+
+ if (parameters.frameType === 'nested') {
+ const frame = await with_iframe(parameters.srcUrl);
+ t.add_cleanup(() => frame.remove());
+ }
+}
+
+function navigateTest(overrideParameters) {
+ // default parameters
+ const parameters = {
+ description: null,
+ srcUrl: SCOPE,
+ destUrl: null,
+ expected: null,
+ waitState: 'activated',
+ scope: SCOPE,
+ controlled: true,
+ // `frameType` can be 'nested' for an iframe WindowClient or 'top-level' for
+ // a main frame WindowClient.
+ frameType: 'nested'
+ };
+
+ for (const key in overrideParameters)
+ parameters[key] = overrideParameters[key];
+
+ promise_test(async function(t) {
+ let pausedLifecyclePort;
+ let scriptUrl = SCRIPT_URL;
+
+ // For in-scope-but-not-controlled test on installing worker,
+ // if the waitState is "installing", then append the query to scriptUrl.
+ if (parameters.waitState === 'installing') {
+ scriptUrl += '?' + parameters.waitState;
+
+ navigator.serviceWorker.addEventListener('message', (event) => {
+ if (event.data.port) {
+ pausedLifecyclePort = event.data.port;
+ }
+ });
+ }
+
+ t.add_cleanup(() => {
+ // Some tests require that the worker remain in a given lifecycle phase.
+ // "Clean up" logic for these tests requires signaling the worker to
+ // release the hold; this allows the worker to be properly discarded
+ // prior to the execution of additional tests.
+ if (pausedLifecyclePort) {
+ // The value of the posted message is inconsequential. A string is
+ // specified here solely to aid in test debugging.
+ pausedLifecyclePort.postMessage('continue lifecycle');
+ }
+ });
+
+ // Create a frame that is not controlled by a service worker.
+ if (!parameters.controlled) {
+ await createFrame(t, parameters);
+ }
+
+ const registration = await service_worker_unregister_and_register(
+ t, scriptUrl, parameters.scope);
+ const serviceWorker = registration.installing;
+ await wait_for_state(t, serviceWorker, parameters.waitState);
+ t.add_cleanup(() => registration.unregister());
+
+ // Create a frame after a service worker is registered so that the frmae is
+ // controlled by an active service worker.
+ if (parameters.controlled) {
+ await createFrame(t, parameters);
+ }
+
+ const response = await new Promise(resolve => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func(resolve);
+ serviceWorker.postMessage({
+ port: channel.port2,
+ url: parameters.destUrl,
+ clientUrl: new URL(parameters.srcUrl, location).toString(),
+ frameType: parameters.frameType,
+ expected: parameters.expected,
+ description: parameters.description,
+ }, [channel.port2]);
+ });
+
+ assert_equals(response.data, null);
+ await fetch_tests_from_worker(serviceWorker);
+ }, parameters.description);
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/worker-client-id.https.html b/testing/web-platform/tests/service-workers/service-worker/worker-client-id.https.html
new file mode 100644
index 0000000000..4e4d31660b
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/worker-client-id.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Workers should have their own unique client Id</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// Get the iframe client ID by calling postMessage() on its controlling
+// worker. This will cause the service worker to post back the
+// MessageEvent.source.id value.
+function getFrameClientId(frame) {
+ return new Promise(resolve => {
+ let mc = new MessageChannel();
+ frame.contentWindow.navigator.serviceWorker.controller.postMessage(
+ 'echo-client-id', [mc.port2]);
+ mc.port1.onmessage = evt => {
+ resolve(evt.data);
+ };
+ });
+}
+
+// Get the worker client ID by creating a worker that performs an intercepted
+// fetch(). The synthetic fetch() response will contain the FetchEvent.clientId
+// value. This is then posted back to here.
+function getWorkerClientId(frame) {
+ return new Promise(resolve => {
+ let w = new frame.contentWindow.Worker('worker-echo-client-id.js');
+ w.onmessage = evt => {
+ resolve(evt.data);
+ };
+ });
+}
+
+promise_test(async function(t) {
+ const script = './resources/worker-client-id-worker.js';
+ const scope = './resources/worker-client-id';
+ const frame = scope + '/frame.html';
+
+ let reg = await navigator.serviceWorker.register(script, { scope });
+ t.add_cleanup(async _ => await reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ let f = await with_iframe(frame);
+ t.add_cleanup(_ => f.remove());
+
+ let frameClientId = await getFrameClientId(f);
+ assert_not_equals(frameClientId, null, 'frame client id should exist');
+
+ let workerClientId = await getWorkerClientId(f);
+ assert_not_equals(workerClientId, null, 'worker client id should exist');
+
+ assert_not_equals(frameClientId, workerClientId,
+ 'frame and worker client ids should be different');
+}, 'Verify workers have a unique client id separate from their owning documents window');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html b/testing/web-platform/tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html
new file mode 100644
index 0000000000..c8480bf1be
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<title>ServiceWorker FetchEvent issued from workers in an iframe sandboxed via CSP HTTP response header.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+let lastCallbackId = 0;
+let callbacks = {};
+function doTest(frame, type) {
+ return new Promise(function(resolve) {
+ var id = ++lastCallbackId;
+ callbacks[id] = resolve;
+ frame.contentWindow.postMessage({id: id, type: type}, '*');
+ });
+}
+
+// Asks the service worker for data about requests and clients seen. The
+// worker posts a message back with |data| where:
+// |data.requests|: the requests the worker received FetchEvents for
+// |data.clients|: the URLs of all the worker's clients
+// The worker clears its data after responding.
+function getResultsFromWorker(worker) {
+ return new Promise(resolve => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = msg => {
+ resolve(msg.data);
+ };
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+}
+
+window.onmessage = function (e) {
+ message = e.data;
+ let id = message['id'];
+ let callback = callbacks[id];
+ delete callbacks[id];
+ callback(message['result']);
+};
+
+const SCOPE = 'resources/sandboxed-iframe-fetch-event-iframe.py';
+const SCRIPT = 'resources/sandboxed-iframe-fetch-event-worker.js';
+const expected_base_url = new URL(SCOPE, location.href);
+// A service worker controlling |SCOPE|.
+let worker;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts'.
+// This should NOT be controlled by a service worker.
+let sandboxed_frame_by_header;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts allow-same-origin'.
+// This should be controlled by a service worker.
+let sandboxed_same_origin_frame_by_header;
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ add_completion_callback(() => registration.unregister());
+ worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ });
+}, 'Prepare a service worker.');
+
+promise_test(t => {
+ const iframe_full_url = expected_base_url + '?sandbox=allow-scripts&' +
+ 'sandboxed-frame-by-header';
+ return with_iframe(iframe_full_url)
+ .then(f => {
+ sandboxed_frame_by_header = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'Service worker should provide the response');
+ assert_equals(requests[0], iframe_full_url);
+ assert_false(data.clients.includes(iframe_full_url),
+ 'Service worker should NOT control the sandboxed page');
+ });
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts.');
+
+promise_test(t => {
+ const iframe_full_url =
+ expected_base_url + '?sandbox=allow-scripts%20allow-same-origin&' +
+ 'sandboxed-iframe-same-origin-by-header';
+ return with_iframe(iframe_full_url)
+ .then(f => {
+ sandboxed_same_origin_frame_by_header = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1);
+ assert_equals(requests[0], iframe_full_url);
+ assert_true(data.clients.includes(iframe_full_url));
+ })
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts and ' +
+ 'allow-same-origin.');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ });
+}, 'Fetch request from a worker in iframe sandboxed by CSP HTTP header ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch-from-worker');
+ });
+}, 'Fetch request from a worker in iframe sandboxed by CSP HTTP header ' +
+ 'with allow-scripts and allow-same-origin flag');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/worker-interception-redirect.https.html b/testing/web-platform/tests/service-workers/service-worker/worker-interception-redirect.https.html
new file mode 100644
index 0000000000..8d566b9c15
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/worker-interception-redirect.https.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<title>Service Worker: controlling Worker/SharedWorker</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This tests service worker interception for worker clients, when the request
+// for the worker script goes through redirects. For example, a request can go
+// through a chain of URLs like A -> B -> C -> D and each URL might fall in the
+// scope of a different service worker, if any.
+// The two key questions are:
+// 1. Upon a redirect from A -> B, should a service worker for scope B
+// intercept the request?
+// 2. After the final response, which service worker controls the resulting
+// client?
+//
+// The standard prescribes the following:
+// 1. The service worker for scope B intercepts the redirect. *However*, once a
+// request falls back to network (i.e., a service worker did not call
+// respondWith()) and a redirect is then received from network, no service
+// worker should intercept that redirect or any subsequent redirects.
+// 2. The final service worker that got a fetch event (or would have, in the
+// case of a non-fetch-event worker) becomes the controller of the client.
+//
+// The standard may change later, see:
+// https://github.com/w3c/ServiceWorker/issues/1289
+//
+// The basic test setup is:
+// 1. Page registers service workers for scope1 and scope2.
+// 2. Page requests a worker from scope1.
+// 3. The request is redirected to scope2 or out-of-scope.
+// 4. The worker posts message to the page describing where the final response
+// was served from (service worker or network).
+// 5. The worker does an importScripts() and fetch(), and posts back the
+// responses, which describe where the responses where served from.
+
+// Globals for easier cleanup.
+const scope1 = 'resources/scope1';
+const scope2 = 'resources/scope2';
+let frame;
+
+function get_message_from_worker(port) {
+ return new Promise(resolve => {
+ port.onmessage = evt => {
+ resolve(evt.data);
+ }
+ });
+}
+
+async function cleanup() {
+ if (frame)
+ frame.remove();
+
+ const reg1 = await navigator.serviceWorker.getRegistration(scope1);
+ if (reg1)
+ await reg1.unregister();
+ const reg2 = await navigator.serviceWorker.getRegistration(scope2);
+ if (reg2)
+ await reg2.unregister();
+}
+
+// Builds the worker script URL, which encodes information about where
+// to redirect to. The URL falls in sw1's scope.
+//
+// - |redirector| is "network" or "serviceworker". If "serviceworker", sw1 will
+// respondWith() a redirect. Otherwise, it falls back to network and the server
+// responds with a redirect.
+// - |redirect_location| is "scope2" or "out-of-scope". If "scope2", the
+// redirect ends up in sw2's scope2. Otherwise it's out of scope.
+function build_worker_url(redirector, redirect_location) {
+ let redirect_path;
+ // Set path to redirect.py, a file on the server that serves
+ // a redirect. When sw1 sees this URL, it falls back to network.
+ if (redirector == 'network')
+ redirector_path = 'redirect.py';
+ // Set path to 'sw-redirect', to tell the service worker
+ // to respond with redirect.
+ else if (redirector == 'serviceworker')
+ redirector_path = 'sw-redirect';
+
+ let redirect_to = base_path() + 'resources/';
+ // Append "scope2/" to redirect_to, so the redirect falls in scope2.
+ // Otherwise no change is needed, as the parent "resources/" directory is
+ // used, and is out-of-scope.
+ if (redirect_location == 'scope2')
+ redirect_to += 'scope2/';
+ // Append the name of the file which serves the worker script.
+ redirect_to += 'worker_interception_redirect_webworker.py';
+
+ return `scope1/${redirector_path}?Redirect=${redirect_to}`
+}
+
+promise_test(async t => {
+ await cleanup();
+ const service_worker = 'resources/worker-interception-redirect-serviceworker.js';
+ const registration1 = await navigator.serviceWorker.register(service_worker, {scope: scope1});
+ await wait_for_state(t, registration1.installing, 'activated');
+ const registration2 = await navigator.serviceWorker.register(service_worker, {scope: scope2});
+ await wait_for_state(t, registration2.installing, 'activated');
+
+ promise_test(t => {
+ return cleanup();
+ }, 'cleanup global state');
+}, 'initialize global state');
+
+async function worker_redirect_test(worker_request_url,
+ worker_expected_url,
+ expected_main_resource_message,
+ expected_import_scripts_message,
+ expected_fetch_message,
+ description) {
+ for (const workerType of ['DedicatedWorker', 'SharedWorker']) {
+ for (const type of ['classic', 'module']) {
+ promise_test(async t => {
+ // Create a frame to load the worker from. This way we can remove the
+ // frame to destroy the worker client when the test is done.
+ frame = await with_iframe('resources/blank.html');
+ t.add_cleanup(() => { frame.remove(); });
+
+ // Start the worker.
+ let w;
+ let port;
+ if (workerType === 'DedicatedWorker') {
+ w = new frame.contentWindow.Worker(worker_request_url, {type});
+ port = w;
+ } else {
+ w = new frame.contentWindow.SharedWorker(worker_request_url, {type});
+ port = w.port;
+ w.port.start();
+ }
+ w.onerror = t.unreached_func('Worker error');
+
+ // Expect a message from the worker indicating which service worker
+ // provided the response for the worker script request, if any.
+ const data = await get_message_from_worker(port);
+
+ // The worker does an importScripts(). Expect a message from the worker
+ // indicating which service worker provided the response for the
+ // importScripts(), if any.
+ const import_scripts_message = await get_message_from_worker(port);
+ test(() => {
+ if (type === 'classic') {
+ assert_equals(import_scripts_message,
+ expected_import_scripts_message);
+ } else {
+ assert_equals(import_scripts_message, 'importScripts failed');
+ }
+ }, `${description} (${type} ${workerType}, importScripts())`);
+
+ // The worker does a fetch(). Expect a message from the worker
+ // indicating which service worker provided the response for the
+ // fetch(), if any.
+ const fetch_message = await get_message_from_worker(port);
+ test(() => {
+ assert_equals(fetch_message, expected_fetch_message);
+ }, `${description} (${type} ${workerType}, fetch())`);
+
+ // Expect a message from the worker indicating |self.location|.
+ const worker_actual_url = await get_message_from_worker(port);
+ test(() => {
+ assert_equals(
+ worker_actual_url,
+ (new URL(worker_expected_url, location.href)).toString(),
+ 'location.href');
+ }, `${description} (${type} ${workerType}, location.href)`);
+
+ assert_equals(data, expected_main_resource_message);
+
+ }, `${description} (${type} ${workerType})`);
+ }
+ }
+}
+
+// request to sw1 scope gets network redirect to sw2 scope
+worker_redirect_test(
+ build_worker_url('network', 'scope2'),
+ 'resources/scope2/worker_interception_redirect_webworker.py',
+ 'the worker script was served from network',
+ 'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/scope2/import-scripts-echo.py',
+ 'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/scope2/simple.txt',
+ 'Case #1: network scope1->scope2');
+
+// request to sw1 scope gets network redirect to out-of-scope
+worker_redirect_test(
+ build_worker_url('network', 'out-scope'),
+ 'resources/worker_interception_redirect_webworker.py',
+ 'the worker script was served from network',
+ 'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/import-scripts-echo.py',
+ 'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/simple.txt',
+ 'Case #2: network scope1->out-scope');
+
+// request to sw1 scope gets service-worker redirect to sw2 scope
+worker_redirect_test(
+ build_worker_url('serviceworker', 'scope2'),
+ 'resources/subdir/worker_interception_redirect_webworker.py?greeting=sw2%20saw%20the%20request%20for%20the%20worker%20script',
+ 'sw2 saw the request for the worker script',
+ 'sw2 saw importScripts from the worker: /service-workers/service-worker/resources/subdir/import-scripts-echo.py',
+ 'fetch(): sw2 saw the fetch from the worker: /service-workers/service-worker/resources/subdir/simple.txt',
+ 'Case #3: sw scope1->scope2');
+
+// request to sw1 scope gets service-worker redirect to out-of-scope
+worker_redirect_test(
+ build_worker_url('serviceworker', 'out-scope'),
+ 'resources/worker_interception_redirect_webworker.py',
+ 'the worker script was served from network',
+ 'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/import-scripts-echo.py',
+ 'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/simple.txt',
+ 'Case #4: sw scope1->out-scope');
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/worker-interception.https.html b/testing/web-platform/tests/service-workers/service-worker/worker-interception.https.html
new file mode 100644
index 0000000000..27983d8352
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/worker-interception.https.html
@@ -0,0 +1,244 @@
+<!DOCTYPE html>
+<title>Service Worker: intercepting Worker script loads</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// ========== Worker main resource interception tests ==========
+
+async function setup_service_worker(t, service_worker_url, scope) {
+ const r = await service_worker_unregister_and_register(
+ t, service_worker_url, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ await wait_for_state(t, r.installing, 'activated');
+ return r.active;
+}
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-synthesized-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ const serviceWorker = await setup_service_worker(t, service_worker_url, scope);
+
+ const channels = new MessageChannel();
+ serviceWorker.postMessage({port: channels.port1}, [channels.port1]);
+
+ const clientId = await new Promise(resolve => channels.port2.onmessage = (e) => resolve(e.data.id));
+
+ const resultPromise = new Promise(resolve => channels.port2.onmessage = (e) => resolve(e.data));
+
+ const w = new Worker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'worker loading intercepted by service worker');
+
+ const results = await resultPromise;
+ assert_equals(results.clientId, clientId);
+ assert_true(!!results.resultingClientId.length);
+
+ channels.port2.postMessage("done");
+}, `Verify a dedicated worker script request gets correct client Ids`);
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-synthesized-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new Worker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'worker loading intercepted by service worker');
+}, `Verify a dedicated worker script request issued from a uncontrolled ` +
+ `document is intercepted by worker's own service worker.`);
+
+promise_test(async t => {
+ const frame_url = 'resources/create-out-of-scope-worker.html';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = frame_url;
+
+ const registration = await service_worker_unregister_and_register(
+ t, service_worker_url, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe(frame_url);
+ t.add_cleanup(_ => frame.remove());
+
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ get_newest_worker(registration).scriptURL,
+ 'the frame should be controlled by a service worker'
+ );
+
+ const result = await frame.contentWindow.getWorkerPromise();
+
+ assert_equals(result,
+ 'worker loading was not intercepted by service worker');
+}, `Verify an out-of-scope dedicated worker script request issued from a ` +
+ `controlled document should not be intercepted by document's service ` +
+ `worker.`);
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-synthesized-worker.js?shared';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new SharedWorker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.port.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'worker loading intercepted by service worker');
+}, `Verify a shared worker script request issued from a uncontrolled ` +
+ `document is intercepted by worker's own service worker.`);
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-same-origin-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new Worker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'dedicated worker script loaded');
+}, 'Verify a same-origin worker script served by a service worker succeeds ' +
+ 'in starting a dedicated worker.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-same-origin-worker.js?shared';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new SharedWorker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.port.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'shared worker script loaded');
+}, 'Verify a same-origin worker script served by a service worker succeeds ' +
+ 'in starting a shared worker.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-cors-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new Worker(worker_url);
+ const watcher = new EventWatcher(t, w, ['message', 'error']);
+ await watcher.wait_for('error');
+}, 'Verify a cors worker script served by a service worker fails dedicated ' +
+ 'worker start.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-cors-worker.js?shared';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new SharedWorker(worker_url);
+ const watcher = new EventWatcher(t, w, ['message', 'error']);
+ await watcher.wait_for('error');
+}, 'Verify a cors worker script served by a service worker fails shared ' +
+ 'worker start.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-no-cors-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new Worker(worker_url);
+ const watcher = new EventWatcher(t, w, ['message', 'error']);
+ await watcher.wait_for('error');
+}, 'Verify a no-cors cross-origin worker script served by a service worker ' +
+ 'fails dedicated worker start.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-no-cors-worker.js?shared';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new SharedWorker(worker_url);
+ const watcher = new EventWatcher(t, w, ['message', 'error']);
+ await watcher.wait_for('error');
+}, 'Verify a no-cors cross-origin worker script served by a service worker ' +
+ 'fails shared worker start.');
+
+// ========== Worker subresource interception tests ==========
+
+const scope_for_subresource_interception = 'resources/load_worker.js';
+
+promise_test(async t => {
+ const service_worker_url = 'resources/worker-load-interceptor.js';
+ const r = await service_worker_unregister_and_register(
+ t, service_worker_url, scope_for_subresource_interception);
+ await wait_for_state(t, r.installing, 'activated');
+}, 'Register a service worker for worker subresource interception tests.');
+
+// Do not call this function multiple times without waiting for the promise
+// resolution because this sets new event handlers on |worker|.
+// TODO(nhiroki): To isolate multiple function calls, use MessagePort instead of
+// worker's onmessage event handler.
+async function request_on_worker(worker, resource_type) {
+ const data = await new Promise((resolve, reject) => {
+ if (worker instanceof Worker) {
+ worker.onmessage = e => resolve(e.data);
+ worker.onerror = e => reject(e);
+ worker.postMessage(resource_type);
+ } else if (worker instanceof SharedWorker) {
+ worker.port.onmessage = e => resolve(e.data);
+ worker.onerror = e => reject(e);
+ worker.port.postMessage(resource_type);
+ } else {
+ reject('Unexpected worker type!');
+ }
+ });
+ assert_equals(data, 'This load was successfully intercepted.');
+}
+
+async function subresource_test(worker) {
+ await request_on_worker(worker, 'xhr');
+ await request_on_worker(worker, 'fetch');
+ await request_on_worker(worker, 'importScripts');
+}
+
+promise_test(async t => {
+ await subresource_test(new Worker('resources/load_worker.js'));
+}, 'Requests on a dedicated worker controlled by a service worker.');
+
+promise_test(async t => {
+ await subresource_test(new SharedWorker('resources/load_worker.js'));
+}, 'Requests on a shared worker controlled by a service worker.');
+
+promise_test(async t => {
+ await subresource_test(new Worker('resources/nested_load_worker.js'));
+}, 'Requests on a dedicated worker nested in a dedicated worker and ' +
+ 'controlled by a service worker');
+
+promise_test(async t => {
+ await subresource_test(new SharedWorker('resources/nested_load_worker.js'));
+}, 'Requests on a dedicated worker nested in a shared worker and controlled ' +
+ 'by a service worker');
+
+promise_test(async t => {
+ await service_worker_unregister(t, scope_for_subresource_interception);
+}, 'Unregister a service worker for subresource interception tests.');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/xhr-content-length.https.window.js b/testing/web-platform/tests/service-workers/service-worker/xhr-content-length.https.window.js
new file mode 100644
index 0000000000..1ae320e9c3
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/xhr-content-length.https.window.js
@@ -0,0 +1,55 @@
+// META: script=resources/test-helpers.sub.js
+
+let frame;
+
+promise_test(async (t) => {
+ const scope = "resources/empty.html";
+ const script = "resources/xhr-content-length-worker.js";
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, "activated");
+ frame = await with_iframe(scope);
+}, "Setup");
+
+promise_test(async t => {
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open("GET", "test?type=no-content-length");
+ xhr.send();
+ const event = await new Promise(resolve => xhr.onload = resolve);
+ assert_equals(xhr.getResponseHeader("content-length"), null);
+ assert_false(event.lengthComputable);
+ assert_equals(event.total, 0);
+ assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response without Content-Length header`);
+
+promise_test(async t => {
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open("GET", "test?type=larger-content-length");
+ xhr.send();
+ const event = await new Promise(resolve => xhr.onload = resolve);
+ assert_equals(xhr.getResponseHeader("content-length"), "10000");
+ assert_true(event.lengthComputable);
+ assert_equals(event.total, 10000);
+ assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response with Content-Length header with value larger than response body length`);
+
+promise_test(async t => {
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open("GET", "test?type=double-content-length");
+ xhr.send();
+ const event = await new Promise(resolve => xhr.onload = resolve);
+ assert_equals(xhr.getResponseHeader("content-length"), "10000, 10000");
+ assert_true(event.lengthComputable);
+ assert_equals(event.total, 10000);
+ assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response with two Content-Length headers value larger than response body length`);
+
+promise_test(async t => {
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open("GET", "test?type=bogus-content-length");
+ xhr.send();
+ const event = await new Promise(resolve => xhr.onload = resolve);
+ assert_equals(xhr.getResponseHeader("content-length"), "test");
+ assert_false(event.lengthComputable);
+ assert_equals(event.total, 0);
+ assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response with bogus Content-Length header`);
diff --git a/testing/web-platform/tests/service-workers/service-worker/xhr-response-url.https.html b/testing/web-platform/tests/service-workers/service-worker/xhr-response-url.https.html
new file mode 100644
index 0000000000..673ca52cc6
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/xhr-response-url.https.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: XHR responseURL uses the response url</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = 'resources/xhr-iframe.html';
+const script = 'resources/xhr-response-url-worker.js';
+let iframe;
+
+function build_url(options) {
+ const url = new URL('test', window.location);
+ const opts = options ? options : {};
+ if (opts.respondWith)
+ url.searchParams.set('respondWith', opts.respondWith);
+ if (opts.url)
+ url.searchParams.set('url', opts.url.href);
+ return url.href;
+}
+
+promise_test(async (t) => {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ iframe = await with_iframe(scope);
+}, 'global setup');
+
+// Test that XMLHttpRequest.responseURL uses the response URL from the service
+// worker.
+promise_test(async (t) => {
+ // Build a URL that tells the service worker to respondWith(fetch(|target|)).
+ const target = new URL('resources/sample.txt', window.location);
+ const url = build_url({
+ respondWith: 'fetch',
+ url: target
+ });
+
+ // Perform the XHR.
+ const xhr = await iframe.contentWindow.xhr(url);
+ assert_equals(xhr.responseURL, target.href, 'responseURL');
+}, 'XHR responseURL should be the response URL');
+
+// Same as above with a generated response.
+promise_test(async (t) => {
+ // Build a URL that tells the service worker to respondWith(new Response()).
+ const url = build_url({respondWith: 'string'});
+
+ // Perform the XHR.
+ const xhr = await iframe.contentWindow.xhr(url);
+ assert_equals(xhr.responseURL, url, 'responseURL');
+}, 'XHR responseURL should be the response URL (generated response)');
+
+// Test that XMLHttpRequest.responseXML is a Document whose URL is the
+// response URL from the service worker.
+promise_test(async (t) => {
+ // Build a URL that tells the service worker to respondWith(fetch(|target|)).
+ const target = new URL('resources/blank.html', window.location);
+ const url = build_url({
+ respondWith: 'fetch',
+ url: target
+ });
+
+ // Perform the XHR.
+ const xhr = await iframe.contentWindow.xhr(url, {responseType: 'document'});
+ assert_equals(xhr.responseURL, target.href, 'responseURL');
+
+ // The document's URL uses the response URL:
+ // "Set |document|’s URL to |response|’s url."
+ // https://xhr.spec.whatwg.org/#document-response
+ assert_equals(xhr.responseXML.URL, target.href, 'responseXML.URL');
+
+ // The document's base URL falls back to the document URL:
+ // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url
+ assert_equals(xhr.responseXML.baseURI, target.href, 'responseXML.baseURI');
+}, 'XHR Document should use the response URL');
+
+// Same as above with a generated response from the service worker.
+promise_test(async (t) => {
+ // Build a URL that tells the service worker to
+ // respondWith(new Response()) with a document response.
+ const url = build_url({respondWith: 'document'});
+
+ // Perform the XHR.
+ const xhr = await iframe.contentWindow.xhr(url, {responseType: 'document'});
+ assert_equals(xhr.responseURL, url, 'responseURL');
+
+ // The document's URL uses the response URL, which is the request URL:
+ // "Set |document|’s URL to |response|’s url."
+ // https://xhr.spec.whatwg.org/#document-response
+ assert_equals(xhr.responseXML.URL, url, 'responseXML.URL');
+
+ // The document's base URL falls back to the document URL:
+ // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url
+ assert_equals(xhr.responseXML.baseURI, url, 'responseXML.baseURI');
+}, 'XHR Document should use the response URL (generated response)');
+
+promise_test(async (t) => {
+ if (iframe)
+ iframe.remove();
+ await service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
diff --git a/testing/web-platform/tests/service-workers/service-worker/xsl-base-url.https.html b/testing/web-platform/tests/service-workers/service-worker/xsl-base-url.https.html
new file mode 100644
index 0000000000..1d3c36408a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/xsl-base-url.https.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: XSL's base URL must be the response URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+// This test loads an XML document which is controlled a service worker. The
+// document loads a stylesheet and a service worker responds with another URL.
+// The stylesheet imports a relative URL to test that the base URL is the
+// response URL from the service worker.
+promise_test(async (t) => {
+ const SCOPE = 'resources/xsl-base-url-iframe.xml';
+ const SCRIPT = 'resources/xsl-base-url-worker.js';
+ let worker;
+ let frame;
+
+ t.add_cleanup(() => {
+ if (frame)
+ frame.remove();
+ service_worker_unregister(t, SCOPE);
+ });
+
+ const registration = await service_worker_unregister_and_register(
+ t, SCRIPT, SCOPE);
+ worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+
+ frame = await with_iframe(SCOPE);
+ assert_equals(frame.contentDocument.body.textContent, 'PASS');
+}, 'base URL when service worker does respondWith(fetch(responseUrl))');
+</script>