summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests')
-rw-r--r--testing/web-platform/tests/.azure-pipelines.yml98
-rw-r--r--testing/web-platform/tests/.github/workflows/documentation.yml2
-rw-r--r--testing/web-platform/tests/.github/workflows/interfaces.yml2
-rw-r--r--testing/web-platform/tests/.github/workflows/manifest.yml2
-rw-r--r--testing/web-platform/tests/.github/workflows/regen_certs.yml4
-rw-r--r--testing/web-platform/tests/FileAPI/blob/Blob-constructor.any.js3
-rw-r--r--testing/web-platform/tests/IndexedDB/idb-binary-key-detached.htm4
-rw-r--r--testing/web-platform/tests/IndexedDB/idb-binary-key-roundtrip.htm1
-rw-r--r--testing/web-platform/tests/IndexedDB/serialize-sharedarraybuffer-throws.https.html2
-rw-r--r--testing/web-platform/tests/IndexedDB/structured-clone.any.js11
-rw-r--r--testing/web-platform/tests/WebCryptoAPI/getRandomValues.any.js7
-rw-r--r--testing/web-platform/tests/accessibility/crashtests/detached-line.html29
-rw-r--r--testing/web-platform/tests/accname/name/comp_name_from_content.html2
-rw-r--r--testing/web-platform/tests/accname/name/comp_text_node.html10
-rw-r--r--testing/web-platform/tests/attribution-reporting/referrer-policy.sub.https.html56
-rw-r--r--testing/web-platform/tests/attribution-reporting/request-format.sub.https.html2
-rw-r--r--testing/web-platform/tests/attribution-reporting/resources/helpers.js12
-rw-r--r--testing/web-platform/tests/clipboard-apis/clipboard-item.https.html41
-rw-r--r--testing/web-platform/tests/close-watcher/abortsignal.html4
-rw-r--r--testing/web-platform/tests/close-watcher/basic.html22
-rw-r--r--testing/web-platform/tests/close-watcher/esc-key/keypress.html2
-rw-r--r--testing/web-platform/tests/close-watcher/esc-key/keyup.html2
-rw-r--r--testing/web-platform/tests/close-watcher/esc-key/not-user-activation.html4
-rw-r--r--testing/web-platform/tests/close-watcher/inside-event-listeners.html24
-rw-r--r--testing/web-platform/tests/close-watcher/resources/helpers.js11
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/README.md25
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/n-activate-preventDefault.html4
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/n-activate.html2
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/n-closerequest-n.html4
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/n-destroy-n.html2
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/n.html2
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/nn-CloseWatcher.html (renamed from testing/web-platform/tests/close-watcher/user-activation/nn.html)6
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/nn-activate-CloseWatcher.html6
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/nn-activate-dialog.html16
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/nn-dialog.html24
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/nnn-CloseWatcher-dialog-popover.html2
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/nnn-CloseWatcher.html25
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/nnn-dialog.html (renamed from testing/web-platform/tests/close-watcher/user-activation/nnn.html)6
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/ny-activate-preventDefault.html6
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/ny.html4
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/nyn.html4
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/nynn-destroy.html4
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/nynn.html4
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/nyyn-CloseWatcher.html34
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/nyyn-dialog.html (renamed from testing/web-platform/tests/close-watcher/user-activation/nyyn.html)10
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/nyyyn-CloseWatcher.html38
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/nyyyn-dialog.html (renamed from testing/web-platform/tests/close-watcher/user-activation/nyyyn.html)12
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/y.html2
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/yn-activate.html4
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/yn.html4
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/ynn-CloseWatcher.html29
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/ynn-dialog.html (renamed from testing/web-platform/tests/close-watcher/user-activation/ynn.html)8
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/yy.html4
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/yyn.html6
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/yyy-CloseWatcher-dialog-popover.html4
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/yyy-activate-CloseWatcher-dialog-popover.html4
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation/yyy.html6
-rw-r--r--testing/web-platform/tests/compression/decompression-buffersource.tentative.any.js12
-rw-r--r--testing/web-platform/tests/compute-pressure/compute_pressure_duplicate_updates.https.any.js4
-rw-r--r--testing/web-platform/tests/compute-pressure/compute_pressure_known_sources.https.any.js (renamed from testing/web-platform/tests/compute-pressure/compute_pressure_supported_sources.https.any.js)10
-rw-r--r--testing/web-platform/tests/compute-pressure/compute_pressure_options.https.any.js43
-rw-r--r--testing/web-platform/tests/compute-pressure/compute_pressure_rate_obfuscation_mitigation_not_triggered.https.window.js4
-rw-r--r--testing/web-platform/tests/compute-pressure/compute_pressure_rate_obfuscation_mitigation_triggered.https.window.js4
-rw-r--r--testing/web-platform/tests/compute-pressure/compute_pressure_timestamp.https.any.js90
-rw-r--r--testing/web-platform/tests/compute-pressure/idlharness.https.any.js2
-rw-r--r--testing/web-platform/tests/compute-pressure/observe_return_type.https.any.js18
-rw-r--r--testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-cross-in-cross-self-block.html2
-rw-r--r--testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-cross-in-same-self-block.html2
-rw-r--r--testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-same-in-cross-self-block.html2
-rw-r--r--testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-same-in-same-self-allow.html2
-rw-r--r--testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-sandbox-same-origin-self.html2
-rw-r--r--testing/web-platform/tests/content-security-policy/generic/case-insensitive-scheme.sub.html51
-rw-r--r--testing/web-platform/tests/content-security-policy/generic/wildcard-host-part.sub.window.js27
-rw-r--r--testing/web-platform/tests/content-security-policy/script-src/script-src-strict_dynamic_hashes.html13
-rw-r--r--testing/web-platform/tests/content-security-policy/script-src/script-src-strict_dynamic_hashes.html.headers2
-rw-r--r--testing/web-platform/tests/credential-management/digital-identity.https.html58
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-context.https.html19
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-disconnect.sub.https.html29
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-endpoint-redirects.https.html5
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-error-basic.https.html6
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/abort-multiple-gets-through-first-idp.https.html35
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/abort-multiple-gets-through-second-idp.https.html35
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-abort.https.html22
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-basic.https.html34
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-context.https.html34
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-mediation-optional.https.html39
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-mediation-silent.https.html41
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-after-onload.https.html47
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-during-onload.https.html42
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-onload-and-during-dom-content-loaded.https.html42
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-after-abort.https.html49
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-after-onload.https.html38
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-before-onload.https.html37
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-during-onload.https.html36
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-after-onload.https.html29
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-before-onload.https.html37
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-during-onload.https.html30
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-register/fedcm-no-registered-idps.https.html25
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-same-site-none/fedcm-same-site-none.https.html2
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-token-returned-with-http-error.https.html12
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm-helper.sub.js20
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/accounts_check_same_site_strict.py2
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/continue_on.py2
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/request-params-check.py11
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/token_check_same_site_strict.py2
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/token_with_account_id.py2
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/token_with_auto_selected_flag.py2
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/token_with_http_error.py2
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/token_with_rp_mode.py2
-rw-r--r--testing/web-platform/tests/credential-management/support/set_cookie.headers5
-rw-r--r--testing/web-platform/tests/css/CSS2/margin-padding-clear/margin-collapse-clear-011-ref.xht13
-rw-r--r--testing/web-platform/tests/css/CSS2/margin-padding-clear/margin-collapse-clear-011.xht21
-rw-r--r--testing/web-platform/tests/css/compositing/background-blending/crashtests/bgblend-root-change.html17
-rw-r--r--testing/web-platform/tests/css/compositing/mix-blend-mode/mix-blend-mode-parent-element-overflow-hidden-and-border-radius-2.html39
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-center-offset-change.html41
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-center-scroll-ref.html15
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-center-scroll.html38
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-fallback-invalidation.html53
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-inherited.html63
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-invalid-fallback.html234
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-001.html (renamed from testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-001.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-002.html (renamed from testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-002.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-003.html (renamed from testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-003.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-004.html (renamed from testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-004.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-fallback.html (renamed from testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-fallback.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-scroll-overflow-hidden-ref.html37
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-scroll-overflow-hidden.html67
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-scroll-position-try-013.html66
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-scroll-position-try-014.html69
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-scroll-scrollable-anchor-ref.html42
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/anchor-scroll-scrollable-anchor.html55
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/at-position-try-cssom.html138
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/base-style-invalidation.html64
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/chrome-336164421-crash.html13
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/chrome-336322507-crash.html15
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/inset-area-function.html71
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/inset-area-in-position-try.html188
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/parsing/position-visibility-computed.tentative.html2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/parsing/position-visibility-parsing.tentative.html2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-try-backdrop.html33
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-try-order-inset-area.html196
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-add-no-overflow.html (renamed from testing/web-platform/tests/css/css-anchor-position/position-visibility-add-no-overflow.tentative.html)4
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-valid.tentative.html2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-in.html (renamed from testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-in.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-out.html (renamed from testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-out.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-both-position-fixed-ref.html5
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-both-position-fixed.tentative.html38
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-001.html73
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-002.html66
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-003.html77
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-004-ref.html34
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-004.html80
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-change-anchor.html (renamed from testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-change-anchor.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-change-css-visibility.html (renamed from testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-change-css-visibility.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-css-visibility.html (renamed from testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-css-visibility.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-non-intervening-container.html (renamed from testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-non-intervening-container.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-stacked-child.html60
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-with-position.html (renamed from testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-with-position.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible.html (renamed from testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow-scroll.html (renamed from testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow-scroll.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow-stacked-child.html (renamed from testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow-stacked-child.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow.html (renamed from testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-remove-anchors-visible.html (renamed from testing/web-platform/tests/css/css-anchor-position/position-visibility-remove-anchors-visible.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/position-visibility-remove-no-overflow.html (renamed from testing/web-platform/tests/css/css-anchor-position/position-visibility-remove-no-overflow.tentative.html)4
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/pseudo-element-anchor-dynamic.html62
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/pseudo-element-anchor.html56
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-001-ref.html (renamed from testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-001.tentative-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-002-ref.html (renamed from testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-002.tentative-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-004-ref.html (renamed from testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-004.tentative-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-fallback-ref.html (renamed from testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-fallback.tentative-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-animations/parsing/animation-delay-end-computed.tentative.html12
-rw-r--r--testing/web-platform/tests/css/css-animations/parsing/animation-delay-end-invalid.tentative.html29
-rw-r--r--testing/web-platform/tests/css/css-animations/parsing/animation-delay-end-valid.tentative.html11
-rw-r--r--testing/web-platform/tests/css/css-animations/parsing/animation-delay-shorthand-computed.html16
-rw-r--r--testing/web-platform/tests/css/css-animations/parsing/animation-delay-shorthand.html49
-rw-r--r--testing/web-platform/tests/css/css-animations/parsing/animation-delay-start-computed.tentative.html12
-rw-r--r--testing/web-platform/tests/css/css-animations/parsing/animation-delay-start-invalid.tentative.html29
-rw-r--r--testing/web-platform/tests/css/css-animations/parsing/animation-delay-start-valid.tentative.html11
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081-print-ref.html33
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081a-print.html33
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081b-print.html33
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081c-print.html33
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081d-print.html33
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082-print-ref.html40
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082a-print.html44
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082b-print.html44
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082c-print.html44
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082d-print.html44
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083a.html42
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083b.html42
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083c.html42
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083d.html42
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068-print-ref.html25
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068a-print.html30
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068b-print.html30
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068c-print.html30
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068d-print.html30
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069-print-ref.html35
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069a-print.html40
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069b-print.html40
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069c-print.html40
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069d-print.html40
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070a.html41
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070b.html41
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070c.html41
-rw-r--r--testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070d.html41
-rw-r--r--testing/web-platform/tests/css/css-break/ruby-002.html10
-rw-r--r--testing/web-platform/tests/css/css-cascade/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-revert-layer.html2
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-pseudo-element-ref.html69
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-pseudo-element.html76
-rw-r--r--testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/color-scheme-iframe-background-about-blank.tentative.html3
-rw-r--r--testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/support/prefers-color-scheme.svg10
-rw-r--r--testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/svg-as-image.html8
-rw-r--r--testing/web-platform/tests/css/css-color/hsl-clamp-negative-saturation-ref.html (renamed from testing/web-platform/tests/css/css-color/t424-hsl-clip-outside-gamut-b-ref.html)17
-rw-r--r--testing/web-platform/tests/css/css-color/hsl-clamp-negative-saturation.html37
-rw-r--r--testing/web-platform/tests/css/css-color/hsla-clamp-negative-saturation-ref.html (renamed from testing/web-platform/tests/css/css-color/t425-hsla-clip-outside-device-gamut-b-ref.html)17
-rw-r--r--testing/web-platform/tests/css/css-color/hsla-clamp-negative-saturation.html38
-rw-r--r--testing/web-platform/tests/css/css-color/parsing/color-valid-color-mix-function.html773
-rw-r--r--testing/web-platform/tests/css/css-color/t424-hsl-clip-outside-gamut-b.xht60
-rw-r--r--testing/web-platform/tests/css/css-color/t425-hsla-clip-outside-device-gamut-b.xht61
-rw-r--r--testing/web-platform/tests/css/css-contain/contain-layout-baseline-005.html1
-rw-r--r--testing/web-platform/tests/css/css-contain/contain-layout-button-001.tentative.html (renamed from testing/web-platform/tests/css/css-contain/contain-layout-button-001.html)2
-rw-r--r--testing/web-platform/tests/css/css-contain/contain-layout-button-002.tentative.html18
-rw-r--r--testing/web-platform/tests/css/css-contain/container-queries/custom-property-style-queries.html35
-rw-r--r--testing/web-platform/tests/css/css-contain/container-queries/registered-color-style-queries.html41
-rw-r--r--testing/web-platform/tests/css/css-contain/content-visibility/content-visibility-background-clip-crash.html23
-rw-r--r--testing/web-platform/tests/css/css-contain/reference/contain-layout-baseline-005-ref.html1
-rw-r--r--testing/web-platform/tests/css/css-contain/reference/contain-layout-button-001-ref.html2
-rw-r--r--testing/web-platform/tests/css/css-contain/reference/contain-layout-button-002-ref.html13
-rw-r--r--testing/web-platform/tests/css/css-content/parsing/content-valid.html4
-rw-r--r--testing/web-platform/tests/css/css-counter-styles/counter-style-at-rule/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/css/css-counter-styles/cssom/WEB_FEATURES.yml26
-rw-r--r--testing/web-platform/tests/css/css-display/accessibility/display-contents-role-and-label.html38
-rw-r--r--testing/web-platform/tests/css/css-easing/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/css/css-easing/linear-timing-functions-output.html (renamed from testing/web-platform/tests/css/css-easing/linear-timing-functions-output.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/css-easing/linear-timing-functions-syntax.html (renamed from testing/web-platform/tests/css/css-easing/linear-timing-functions-syntax.tentative.html)5
-rw-r--r--testing/web-platform/tests/css/css-flexbox/WEB_FEATURES.yml7
-rw-r--r--testing/web-platform/tests/css/css-flexbox/intrinsic-size/col-wrap-020.html34
-rw-r--r--testing/web-platform/tests/css/css-flexbox/min-size-auto-overflow-clip-ref.html14
-rw-r--r--testing/web-platform/tests/css/css-flexbox/min-size-auto-overflow-clip.html18
-rw-r--r--testing/web-platform/tests/css/css-fonts/WEB_FEATURES.yml14
-rw-r--r--testing/web-platform/tests/css/css-fonts/font-size-adjust-reload.html3
-rw-r--r--testing/web-platform/tests/css/css-fonts/matching/font-unicode-PUA-primary-font-notref.html17
-rw-r--r--testing/web-platform/tests/css/css-fonts/matching/font-unicode-PUA-primary-font.html17
-rw-r--r--testing/web-platform/tests/css/css-fonts/parsing/WEB_FEATURES.yml16
-rw-r--r--testing/web-platform/tests/css/css-fonts/parsing/font-palette-values-invalid.html50
-rw-r--r--testing/web-platform/tests/css/css-fonts/parsing/font-palette-values-valid.html13
-rw-r--r--testing/web-platform/tests/css/css-fonts/resources/vs/MPLUS1-Regular_without-cmap14-subset.ttfbin0 -> 1280 bytes
-rw-r--r--testing/web-platform/tests/css/css-fonts/resources/vs/NotoColorEmoji-Regular_subset.ttfbin0 -> 31728 bytes
-rw-r--r--testing/web-platform/tests/css/css-fonts/resources/vs/NotoEmoji-Regular_subset.ttfbin0 -> 1776 bytes
-rw-r--r--testing/web-platform/tests/css/css-fonts/resources/vs/NotoEmoji-Regular_without-cmap14-subset.ttfbin0 -> 1736 bytes
-rw-r--r--testing/web-platform/tests/css/css-fonts/resources/vs/NotoSansJP-Regular_with-cmap14-subset.ttfbin0 -> 1688 bytes
-rw-r--r--testing/web-platform/tests/css/css-fonts/resources/vs/NotoSansMath-Regular_without-cmap14-subset.ttfbin0 -> 5308 bytes
-rw-r--r--testing/web-platform/tests/css/css-fonts/resources/vs/STIXTwoMath-Regular_with-cmap14-subset.ttfbin0 -> 5984 bytes
-rw-r--r--testing/web-platform/tests/css/css-fonts/support/css/variation-sequences.css38
-rw-r--r--testing/web-platform/tests/css/css-fonts/support/js/variation-sequences.js125
-rw-r--r--testing/web-platform/tests/css/css-fonts/variation-sequences-ref.html11
-rw-r--r--testing/web-platform/tests/css/css-fonts/variation-sequences.html18
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-001-ref.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-001-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-001.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-001.html)8
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-002-ref.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-002-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-002.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-002.html)7
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-003-ref.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-003-ref.html)46
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-003.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-003.html)8
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-004-ref.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-004-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-004.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-004.html)7
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-001-ref.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-001-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-001.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-001.html)7
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-002-ref.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-002-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-002.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-002.html)7
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-003-ref.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-003-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-003.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-003.html)7
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-004-ref.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-004-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-004.html (renamed from testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-004.html)7
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-001.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-002.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-003.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-001-ref.html9
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-001.html15
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-002-ref.html62
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-002.html71
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-001-ref.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-001.html3
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-002-ref.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-002.html3
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-001-ref.html143
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-001.html91
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-002-ref.html142
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-002.html93
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-003-ref.html420
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-003.html88
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-004-ref.html349
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-004.html88
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-005-ref.html21
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-005.html25
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-006-ref.html31
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-006.html29
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/support/masonry-intrinsic-sizing-visual.css48
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-001.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-002.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-003.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-004.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-005.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-006.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-007.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-008-ref.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-008.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-columns-item-containing-block-is-grid-content-width.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-grid-template-columns-computed-withcontent.html144
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-not-inhibited-001.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/order/masonry-order-001.html3
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/order/masonry-order-002.html3
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/parsing/masonry-parsing.html3
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/subgrid/masonry-subgrid-001.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/subgrid/masonry-subgrid-002.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize-ref.html10
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize.html18
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-overflow-left-side-ref.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-overflow-right-side-ref.html2
-rw-r--r--testing/web-platform/tests/css/css-grid/min-size-auto-overflow-clip-ref.html18
-rw-r--r--testing/web-platform/tests/css/css-grid/min-size-auto-overflow-clip.html22
-rw-r--r--testing/web-platform/tests/css/css-grid/subgrid/line-names-013.html31
-rw-r--r--testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-004-2.html2
-rw-r--r--testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-020-ref.html26
-rw-r--r--testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-020.html32
-rw-r--r--testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-021-ref.html15
-rw-r--r--testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-021.html29
-rw-r--r--testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-vertical-writing-mode-001-ref.html17
-rw-r--r--testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-vertical-writing-mode-001.html35
-rw-r--r--testing/web-platform/tests/css/css-images/gradient/repeating-gradient-hsl-and-oklch.html2
-rw-r--r--testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001c.tentative.html71
-rw-r--r--testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001e.tentative.html57
-rw-r--r--testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001i.tentative.html58
-rw-r--r--testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001p.tentative.html57
-rw-r--r--testing/web-platform/tests/css/css-images/object-fit-containsize-png-001-ref.tentative.html60
-rw-r--r--testing/web-platform/tests/css/css-images/object-fit-containsize-png-001c.tentative.html71
-rw-r--r--testing/web-platform/tests/css/css-images/object-fit-containsize-png-001e.tentative.html57
-rw-r--r--testing/web-platform/tests/css/css-images/object-fit-containsize-png-001i.tentative.html58
-rw-r--r--testing/web-platform/tests/css/css-images/object-fit-containsize-png-001p.tentative.html57
-rw-r--r--testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-001.html2
-rw-r--r--testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002-ref.html17
-rw-r--r--testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002.html17
-rw-r--r--testing/web-platform/tests/css/css-lists/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error-ref.html1
-rw-r--r--testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error.html3
-rw-r--r--testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position.html2
-rw-r--r--testing/web-platform/tests/css/css-masking/clip-path/animations/two-clip-path-animation-diff-length4.html50
-rw-r--r--testing/web-platform/tests/css/css-masking/mask-image/mask-image-svg-viewport-changed.html18
-rw-r--r--testing/web-platform/tests/css/css-overflow/line-clamp-021.tentative.html56
-rw-r--r--testing/web-platform/tests/css/css-overflow/line-clamp-with-abspos-011.tentative.html52
-rw-r--r--testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-001.tentative.html28
-rw-r--r--testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-002.tentative.html33
-rw-r--r--testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-003.tentative.html28
-rw-r--r--testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-004.tentative.html33
-rw-r--r--testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-005.tentative.html28
-rw-r--r--testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-006.tentative.html33
-rw-r--r--testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-007.tentative.html31
-rw-r--r--testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-008.tentative.html29
-rw-r--r--testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-009.tentative.html29
-rw-r--r--testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-010.tentative.html44
-rw-r--r--testing/web-platform/tests/css/css-overflow/reference/line-clamp-021-ref.html36
-rw-r--r--testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-abspos-011-ref.html39
-rw-r--r--testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-001-ref.html22
-rw-r--r--testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-005-ref.html23
-rw-r--r--testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-006-ref.html28
-rw-r--r--testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-007-ref.html26
-rw-r--r--testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-008-ref.html23
-rw-r--r--testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-010-ref.html37
-rw-r--r--testing/web-platform/tests/css/css-overflow/scroll-with-ancestor-border-radius.html59
-rw-r--r--testing/web-platform/tests/css/css-overflow/scroll-with-border-radius.html51
-rw-r--r--testing/web-platform/tests/css/css-page/fixedpos-009-print-ref.html32
-rw-r--r--testing/web-platform/tests/css/css-page/fixedpos-009-print.html24
-rw-r--r--testing/web-platform/tests/css/css-page/fixedpos-010-print-ref.html28
-rw-r--r--testing/web-platform/tests/css/css-page/fixedpos-010-print.html25
-rw-r--r--testing/web-platform/tests/css/css-page/page-box-000-print-ref.html12
-rw-r--r--testing/web-platform/tests/css/css-page/page-box-000-print.html15
-rw-r--r--testing/web-platform/tests/css/css-page/page-size-012-print-ref.html17
-rw-r--r--testing/web-platform/tests/css/css-page/page-size-012-print.html28
-rw-r--r--testing/web-platform/tests/css/css-position/sticky/position-sticky-left-004.html37
-rw-r--r--testing/web-platform/tests/css/css-position/sticky/position-sticky-left-005.html38
-rw-r--r--testing/web-platform/tests/css/css-position/sticky/position-sticky-left-006.html38
-rw-r--r--testing/web-platform/tests/css/css-position/sticky/position-sticky-margins-002.html38
-rw-r--r--testing/web-platform/tests/css/css-position/sticky/position-sticky-margins-003.html36
-rw-r--r--testing/web-platform/tests/css/css-position/sticky/position-sticky-scrolled-remove-sibling-002.html42
-rw-r--r--testing/web-platform/tests/css/css-position/sticky/position-sticky-top-004.html37
-rw-r--r--testing/web-platform/tests/css/css-position/sticky/position-sticky-top-005.html38
-rw-r--r--testing/web-platform/tests/css/css-position/sticky/position-sticky-top-006.html38
-rw-r--r--testing/web-platform/tests/css/css-properties-values-api/crashtests/computed-property-universal-syntax.html20
-rw-r--r--testing/web-platform/tests/css/css-properties-values-api/registered-property-computation.html8
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade-002-ref.html42
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade-002.html120
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-001-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/cascade-highlight-001-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-001.html (renamed from testing/web-platform/tests/css/css-pseudo/cascade-highlight-001.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-002.html (renamed from testing/web-platform/tests/css/css-pseudo/cascade-highlight-002.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-004-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/cascade-highlight-004-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-004.html (renamed from testing/web-platform/tests/css/css-pseudo/cascade-highlight-004.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-005-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/reference/cascade-highlight-005-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-005.html (renamed from testing/web-platform/tests/css/css-pseudo/cascade-highlight-005.html)4
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-001-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-cascade-001-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-001.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-cascade-001.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-003-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-cascade-003-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-003.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-cascade-003.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-004-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-cascade-004-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-004.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-cascade-004.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-005-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-cascade-005-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-005.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-cascade-005.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-006-ref.xhtml (renamed from testing/web-platform/tests/css/css-pseudo/highlight-cascade-006-ref.xhtml)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-006.xhtml (renamed from testing/web-platform/tests/css/css-pseudo/highlight-cascade-006.xhtml)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-007.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-cascade-007.html)4
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-008-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-cascade-008-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-008.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-cascade-008.html)16
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-009.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-cascade-009.html)20
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed-inheritance.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-computed-inheritance.html)0
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed-visited.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-computed-visited.html)0
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-computed.html)0
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-properties-001-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-properties-001.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-properties-002-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-properties-002.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-001-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-001-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-001.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-001.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-002-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-002.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-001.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-001.html)0
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-002.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-002.html)0
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-001-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-001-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-001.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-001.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-002-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-002-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-002.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-002.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-003-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-003-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-003.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-003.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004-notref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-004-notref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-004.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-005-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-005-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-005.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-005.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-006-ref.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-006-ref.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-006.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-006.html)2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-computed.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-pseudos-computed.html)0
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-inheritance-computed-001.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-pseudos-inheritance-computed-001.html)0
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-visited-computed-001.html (renamed from testing/web-platform/tests/css/css-pseudo/highlight-pseudos-visited-computed-001.html)0
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-custom-properties-dynamic-001-ref.html16
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-custom-properties-dynamic-001.html28
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-horizontal-ref.html73
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-horizontal.html41
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-vertical-ref.html70
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-vertical.html43
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-styling-001.html8
-rw-r--r--testing/web-platform/tests/css/css-pseudo/highlight-styling-002.html8
-rw-r--r--testing/web-platform/tests/css/css-pseudo/selection-over-highlight-001-ref.html12
-rw-r--r--testing/web-platform/tests/css/css-pseudo/selection-over-highlight-001.html24
-rw-r--r--testing/web-platform/tests/css/css-pseudo/target-text-dynamic-004.html2
-rw-r--r--testing/web-platform/tests/css/css-pseudo/target-text-shadow-horizontal-ref.html19
-rw-r--r--testing/web-platform/tests/css/css-pseudo/target-text-shadow-horizontal.html25
-rw-r--r--testing/web-platform/tests/css/css-pseudo/target-text-shadow-vertical-ref.html20
-rw-r--r--testing/web-platform/tests/css/css-pseudo/target-text-shadow-vertical.html26
-rw-r--r--testing/web-platform/tests/css/css-ruby/line-spacing.html11
-rw-r--r--testing/web-platform/tests/css/css-ruby/ruby-dynamic-insertion-005-ref.html9
-rw-r--r--testing/web-platform/tests/css/css-ruby/ruby-dynamic-insertion-005.html9
-rw-r--r--testing/web-platform/tests/css/css-ruby/ruby-dynamic-removal-003-ref.html5
-rw-r--r--testing/web-platform/tests/css/css-ruby/ruby-dynamic-removal-003.html6
-rw-r--r--testing/web-platform/tests/css/css-scoping/font-face-001.html4
-rw-r--r--testing/web-platform/tests/css/css-scoping/font-face-002.html4
-rw-r--r--testing/web-platform/tests/css/css-scoping/font-face-003.html3
-rw-r--r--testing/web-platform/tests/css/css-scoping/font-face-004.html3
-rw-r--r--testing/web-platform/tests/css/css-scoping/font-face-005.html4
-rw-r--r--testing/web-platform/tests/css/css-scoping/font-face-006.html2
-rw-r--r--testing/web-platform/tests/css/css-scoping/font-face-007.html4
-rw-r--r--testing/web-platform/tests/css/css-scoping/font-face-008.html4
-rw-r--r--testing/web-platform/tests/css/css-scoping/font-face-009.html4
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap-2/resources/common.js56
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap-2/resources/programmatic-scroll-common.js26
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap-2/resources/user-scroll-common.js29
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap-2/snap-events-with-pseudo-target.tentative.html86
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-programmatic-root-scroll.tentative.html19
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-programmatic-scroll.tentative.html19
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-user-root-scroll.tentative.html23
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-user-scroll.tentative.html23
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-scrolling-non-snapping-axis.tentative.html77
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-programmatic-root-scroll.tentative.html20
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-programmatic-scroll.tentative.html20
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-user-root-scroll.tentative.html22
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-user-scroll.tentative.html22
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/layout-follows-focused-targeted-block-iframe.html48
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/layout-follows-focused-targeted-block.html124
-rw-r--r--testing/web-platform/tests/css/css-shadow-parts/invalidation-part-pseudo.html1
-rw-r--r--testing/web-platform/tests/css/css-shadow-parts/part-after-combinator-invalidation-ref.html3
-rw-r--r--testing/web-platform/tests/css/css-shadow-parts/part-after-combinator-invalidation.html26
-rw-r--r--testing/web-platform/tests/css/css-sizing/aspect-ratio/support/2x2-green.webmbin555 -> 559 bytes
-rw-r--r--testing/web-platform/tests/css/css-sizing/contain-intrinsic-size/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/css/css-syntax/custom-property-rule-ambiguity.html2
-rw-r--r--testing/web-platform/tests/css/css-tables/collapsed-border-partial-invalidation-003-ref.html17
-rw-r--r--testing/web-platform/tests/css/css-tables/collapsed-border-partial-invalidation-003.html32
-rw-r--r--testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-near-zero-ref.html9
-rw-r--r--testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-near-zero.html14
-rw-r--r--testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-zero-ref.html9
-rw-r--r--testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-zero.html14
-rw-r--r--testing/web-platform/tests/css/css-text/hyphens/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/css/css-text/text-spacing-trim/text-spacing-trim-subset-001-ref.html25
-rw-r--r--testing/web-platform/tests/css/css-text/text-spacing-trim/text-spacing-trim-subset-001.html25
-rw-r--r--testing/web-platform/tests/css/css-transitions/shadow-root-insertion.html43
-rw-r--r--testing/web-platform/tests/css/css-transitions/starting-style-size-container.html53
-rw-r--r--testing/web-platform/tests/css/css-typed-om/the-stylepropertymap/properties/animation-delay-end.tentative.html19
-rw-r--r--testing/web-platform/tests/css/css-typed-om/the-stylepropertymap/properties/animation-delay-start.tentative.html19
-rw-r--r--testing/web-platform/tests/css/css-ui/webkit-appearance-auto-non-html-namespace-001.html19
-rw-r--r--testing/web-platform/tests/css/css-values/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html41
-rw-r--r--testing/web-platform/tests/css/css-values/calc-size/calc-size-parsing.tentative.html12
-rw-r--r--testing/web-platform/tests/css/css-values/container-progress-computed.tentative.html16
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/3d-transform-incoming.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/3d-transform-outgoing.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/block-with-overflowing-text.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/break-inside-avoid-child.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/capture-with-offscreen-child.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/capture-with-opacity-zero-child.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/capture-with-visibility-mixed-descendants.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/clip-path-larger-than-border-box-on-child-of-named-element.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/content-with-transform-new-image.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/content-with-transform-old-image.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/css-tags-paint-order-with-entry.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/dialog-in-rtl-iframe.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/event-pseudo-name.html1
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/far-away-capture.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/fractional-box-with-overflow-children-new.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/fractional-box-with-overflow-children-old.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-new-iframe-ref.html27
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-new-iframe.html68
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-old-iframe-ref.html27
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-old-iframe.html68
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-new-iframe-ref.html27
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-new-iframe.html67
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-old-iframe-ref.html27
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-old-iframe.html67
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-ref.html27
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main.html60
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-with-name-on-iframe-ref.html28
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-with-name-on-iframe.html93
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-new-has-scrollbar.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-old-has-scrollbar.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-transition-destroyed-document-crash.html14
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/iframe-transition.sub.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/inline-with-offset-from-containing-block.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-below-and-on-top-of-viewport-partially-onscreen-new.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-below-and-on-top-of-viewport-partially-onscreen-old.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-offscreen-new.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-offscreen-old.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-partially-onscreen-new.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-partially-onscreen-old.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-offscreen-new.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-offscreen-old.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-partially-onscreen-new.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-partially-onscreen-old.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-offscreen-new.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-offscreen-old.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-partially-onscreen-new.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-partially-onscreen-old.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-right-and-left-of-viewport-partially-onscreen-new.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-right-and-left-of-viewport-partially-onscreen-old.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-offscreen-new.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-offscreen-old.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-partially-onscreen-new.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-partially-onscreen-old.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/multiline-span-with-overflowing-text-and-box-decorations.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/new-and-old-sizes-match.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/new-content-ancestor-clipped-ref.html34
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/new-content-ancestor-clipped.html50
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/new-content-captures-clip-path.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/new-content-captures-different-size.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/new-content-captures-spans.html1
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/new-content-changes-overflow-left-ref.html20
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/new-content-changes-overflow-left.html46
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/new-content-has-scrollbars.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/new-content-is-inline.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/new-content-object-fit-fill.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/new-content-scaling.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/object-view-box-old-image.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/old-content-captures-clip-path.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/old-content-captures-different-size.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/old-content-captures-opacity.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/old-content-captures-root.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/old-content-has-scrollbars.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/old-content-is-inline.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/old-content-object-fit-fill.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/old-content-object-view-box-clip-path-reference.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/old-content-object-view-box-clip-path.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/pseudo-element-overflow-hidden-ref.html33
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/pseudo-element-overflow-hidden.html57
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/pseudo-element-preserve-3d-ref.html20
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/pseudo-element-preserve-3d.html44
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/pseudo-rendering-invalidation.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/pseudo-with-classes-match-wildcard.html (renamed from testing/web-platform/tests/css/css-view-transitions/pseudo-with-classes-match-wildard.html)0
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/root-captured-as-different-tag.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/root-element-display-none-during-transition-crash.html13
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/root-scrollbar-with-fixed-background.html1
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/root-to-shared-animation-incoming-ref.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/rotated-cat-off-top-edge.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/scroller-child-abspos.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/scroller-child.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/scroller.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/set-current-time.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-absolute.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-includes-scrollbar-gutter.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-static.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/span-with-overflowing-text-and-box-decorations.html3
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/span-with-overflowing-text.html3
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/transition-in-empty-iframe.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/web-animations-api-parse-pseudo-argument.html2
-rw-r--r--testing/web-platform/tests/css/css-view-transitions/web-animations-api.html2
-rw-r--r--testing/web-platform/tests/css/css-viewport/zoom/iframe-zoom-nested.html (renamed from testing/web-platform/tests/css/zoom/iframe-zoom-nested.html)0
-rw-r--r--testing/web-platform/tests/css/css-viewport/zoom/iframe-zoom.sub.html (renamed from testing/web-platform/tests/css/zoom/iframe-zoom.sub.html)2
-rw-r--r--testing/web-platform/tests/css/css-viewport/zoom/reference/iframe-zoom-nested-ref.html (renamed from testing/web-platform/tests/css/zoom/reference/iframe-zoom-nested-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-viewport/zoom/reference/iframe-zoom-ref.html (renamed from testing/web-platform/tests/css/zoom/reference/iframe-zoom-ref.html)0
-rw-r--r--testing/web-platform/tests/css/css-viewport/zoom/resources/iframe_content.html (renamed from testing/web-platform/tests/css/zoom/resources/iframe_content.html)0
-rw-r--r--testing/web-platform/tests/css/css-viewport/zoom/resources/nested-iframe-no-zoom.html (renamed from testing/web-platform/tests/css/zoom/resources/nested-iframe-no-zoom.html)0
-rw-r--r--testing/web-platform/tests/css/css-viewport/zoom/resources/nested-iframe-with-zoom.html (renamed from testing/web-platform/tests/css/zoom/resources/nested-iframe-with-zoom.html)0
-rw-r--r--testing/web-platform/tests/css/css-viewport/zoom/scroll-top-test-with-zoom.html33
-rw-r--r--testing/web-platform/tests/css/cssom-view/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/css/cssom-view/offsetTop-offsetLeft-with-zoom.html15
-rw-r--r--testing/web-platform/tests/css/cssom/CSSStyleSheet-constructable-baseURL.html (renamed from testing/web-platform/tests/css/cssom/CSSStyleSheet-constructable-baseURL.tentative.html)2
-rw-r--r--testing/web-platform/tests/css/cssom/WEB_FEATURES.yml7
-rw-r--r--testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-backdrop-filter.html19
-rw-r--r--testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-clip-path-ref.html22
-rw-r--r--testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-clip-path.html19
-rw-r--r--testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-filter-ref.html22
-rw-r--r--testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-filter.html20
-rw-r--r--testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-mix-blend-mode.html19
-rw-r--r--testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-opacity-ref.html23
-rw-r--r--testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-opacity.html20
-rw-r--r--testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-ref.html23
-rw-r--r--testing/web-platform/tests/css/filter-effects/backdrop-filter-isolation-ref.html51
-rw-r--r--testing/web-platform/tests/css/filter-effects/backdrop-filter-isolation.html44
-rw-r--r--testing/web-platform/tests/css/filter-effects/resources/backdrop-filter-backdrop-root.css30
-rw-r--r--testing/web-platform/tests/css/filter-effects/support/hueRotate.svg2
-rw-r--r--testing/web-platform/tests/css/mediaqueries/WEB_FEATURES.yml5
-rw-r--r--testing/web-platform/tests/css/mediaqueries/prefers-color-scheme-svg-as-image-ref.html (renamed from testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/svg-as-image-ref.html)8
-rw-r--r--testing/web-platform/tests/css/mediaqueries/prefers-color-scheme-svg-as-image.html8
-rw-r--r--testing/web-platform/tests/css/motion/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/css/motion/animation/offset-rotate-interpolation.html9
-rw-r--r--testing/web-platform/tests/css/selectors/WEB_FEATURES.yml7
-rw-r--r--testing/web-platform/tests/css/selectors/focus-visible-028.html84
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/css/selectors/user-invalid.html49
-rw-r--r--testing/web-platform/tests/css/selectors/user-valid-user-invalid-multifield-inputs.tentative.html133
-rw-r--r--testing/web-platform/tests/css/selectors/user-valid.html44
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/form-associated-callback.html2
-rw-r--r--testing/web-platform/tests/deprecation-reporting/__dir__.ini1
-rw-r--r--testing/web-platform/tests/device-posture/device-posture-change-event.https.html23
-rw-r--r--testing/web-platform/tests/device-posture/device-posture-clear.https.html29
-rw-r--r--testing/web-platform/tests/device-posture/device-posture-event-listener.https.html25
-rw-r--r--testing/web-platform/tests/device-posture/device-posture-media-queries.https.html27
-rw-r--r--testing/web-platform/tests/docs/admin/pywebsocket3.rst56
-rw-r--r--testing/web-platform/tests/docs/running-tests/from-local-system.md2
-rw-r--r--testing/web-platform/tests/docs/writing-tests/file-names.md12
-rw-r--r--testing/web-platform/tests/docs/writing-tests/testdriver.md6
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-append-meta-referrer-and-script-from-fragment.tentative.html54
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/Node-moveBefore.html297
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/chrome-338071841-crash.html6
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-animation-left.html51
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-animation-transform.html48
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-left-pseudo.html54
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-left.html41
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-transform-pseudo.html54
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-transform.html36
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-animation-commit-styles.html45
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-cross-document.html45
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-cross-shadow.html64
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-to-disconnected-document.html43
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-trigger.html43
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/focus-preserve.html85
-rw-r--r--testing/web-platform/tests/dom/nodes/moveBefore/tentative/fullscreen-preserve.html49
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-every.any.js250
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-filter.any.js12
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-find.any.js85
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-inspect.any.js412
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-some.any.js96
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-isPointInRange-shadowdom.tentative.html75
-rw-r--r--testing/web-platform/tests/dom/xslt/resources/xml2html.xsl2
-rw-r--r--testing/web-platform/tests/dpub-aam/role/roles.html4
-rw-r--r--testing/web-platform/tests/editing/data/delete.js6
-rw-r--r--testing/web-platform/tests/editing/data/forwarddelete.js26
-rw-r--r--testing/web-platform/tests/editing/edit-context/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/editing/include/editor-test-utils.js75
-rw-r--r--testing/web-platform/tests/editing/other/delete-without-unwrapping-first-line-of-child-block.html1020
-rw-r--r--testing/web-platform/tests/encoding/encodeInto.any.js1
-rw-r--r--testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-content-initiated.https.html51
-rw-r--r--testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-nested-urn-iframe.https.html62
-rw-r--r--testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-nested.https.html61
-rw-r--r--testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-no-embedder-opt-in.https.html50
-rw-r--r--testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-no-subframe-opt-in.https.html46
-rw-r--r--testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe-content-initiated.https.html51
-rw-r--r--testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe-no-embedder-opt-in.https.html50
-rw-r--r--testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe-no-subframe-opt-in.https.html46
-rw-r--r--testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe.https.html46
-rw-r--r--testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin.https.html46
-rw-r--r--testing/web-platform/tests/fenced-frame/fence-report-event-sub-fencedframe.https.html46
-rw-r--r--testing/web-platform/tests/fenced-frame/resources/utils.js17
-rw-r--r--testing/web-platform/tests/fenced-frame/setting-null-config-navigates-to-about-blank.https.html15
-rw-r--r--testing/web-platform/tests/fetch/api/basic/request-headers.any.js1
-rw-r--r--testing/web-platform/tests/fetch/api/basic/request-upload.any.js4
-rw-r--r--testing/web-platform/tests/fetch/api/crashtests/huge-fetch.any.js16
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-bad-port.any.js2
-rw-r--r--testing/web-platform/tests/fetch/api/resources/huge-response.py22
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-clone.any.js1
-rw-r--r--testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data-cache.tentative.https.html27
-rw-r--r--testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data-cookies.tentative.https.html27
-rw-r--r--testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data-storage.tentative.https.html27
-rw-r--r--testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data.tentative.https.html54
-rw-r--r--testing/web-platform/tests/fetch/compression-dictionary/dictionary-decompression.tentative.https.html1
-rw-r--r--testing/web-platform/tests/fetch/compression-dictionary/dictionary-fetch-with-link-element.tentative.https.html1
-rw-r--r--testing/web-platform/tests/fetch/compression-dictionary/dictionary-fetch-with-link-header.tentative.https.html1
-rw-r--r--testing/web-platform/tests/fetch/compression-dictionary/dictionary-registration.tentative.https.html1
-rw-r--r--testing/web-platform/tests/fetch/compression-dictionary/resources/compression-dictionary-util.js4
-rw-r--r--testing/web-platform/tests/fetch/metadata/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/fetch/metadata/generated/appcache-manifest.https.sub.html341
-rw-r--r--testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html120
-rw-r--r--testing/web-platform/tests/fetch/metadata/tools/fetch-metadata.conf.yml76
-rw-r--r--testing/web-platform/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html63
-rw-r--r--testing/web-platform/tests/fledge/tentative/TODO8
-rw-r--r--testing/web-platform/tests/fledge/tentative/auction-config-passed-to-worklets.https.window.js12
-rw-r--r--testing/web-platform/tests/fledge/tentative/auction-config.https.window.js115
-rw-r--r--testing/web-platform/tests/fledge/tentative/component-ads.https.window.js59
-rw-r--r--testing/web-platform/tests/fledge/tentative/component-auction.https.window.js124
-rw-r--r--testing/web-platform/tests/fledge/tentative/deprecated-render-url-replacements.https.window.js196
-rw-r--r--testing/web-platform/tests/fledge/tentative/interest-group-update.https.window.js406
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js43
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py20
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/update-url.py6
-rw-r--r--testing/web-platform/tests/fledge/tentative/score-ad-browser-signals.https.window.js57
-rw-r--r--testing/web-platform/tests/fledge/tentative/trusted-bidding-signals.https.window.js45
-rw-r--r--testing/web-platform/tests/focus/cross-origin-ancestor-activeelement-after-child-lose-focus.sub.html48
-rw-r--r--testing/web-platform/tests/focus/support/cross-origin-ancestor-activeelement-after-child-lose-focus-helper.html6
-rw-r--r--testing/web-platform/tests/fonts/math/stretchy.woffbin1428 -> 1464 bytes
-rw-r--r--testing/web-platform/tests/fonts/noto/cjk/NotoSansCJKjp-Regular-subset-halt-min.otfbin0 -> 40336 bytes
-rw-r--r--testing/web-platform/tests/fonts/noto/cjk/README.md20
-rwxr-xr-xtesting/web-platform/tests/fonts/noto/cjk/subset.sh22
-rw-r--r--testing/web-platform/tests/gamepad/gamepad-dual-rumble-effect-manual.https.html94
-rw-r--r--testing/web-platform/tests/gamepad/gamepad-trigger-rumble-effect-manual.https.html95
-rw-r--r--testing/web-platform/tests/geolocation-API/disabled-by-permissions-policy.https.sub.html42
-rw-r--r--testing/web-platform/tests/geolocation-API/enabled-by-permission-policy-attribute-redirect-on-load.https.sub.html60
-rw-r--r--testing/web-platform/tests/geolocation-API/enabled-by-permission-policy-attribute.https.sub.html2
-rw-r--r--testing/web-platform/tests/geolocation-API/enabled-by-permissions-policy.https.sub.html42
-rw-r--r--testing/web-platform/tests/geolocation-API/enabled-on-self-origin-by-permissions-policy.https.sub.html45
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position-inline-nearest.html30
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-1.html29
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-3.html29
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-3.pngbin207 -> 0 bytes
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-4.html29
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-4.pngbin112 -> 0 bytes
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html (renamed from testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-2.html)6
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.png (renamed from testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-2.png)bin207 -> 207 bytes
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-1.html29
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-1.pngbin205 -> 0 bytes
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-3.html29
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-3.pngbin207 -> 0 bytes
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-4.html29
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-4.pngbin112 -> 0 bytes
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-5.pngbin205 -> 0 bytes
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html (renamed from testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-5.html)6
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.png (renamed from testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-1.png)bin205 -> 205 bytes
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html (renamed from testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-6.html)6
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.png (renamed from testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-6.png)bin117 -> 117 bytes
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html (renamed from testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-2.html)6
-rw-r--r--testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.png (renamed from testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-2.png)bin207 -> 207 bytes
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative.html20
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative.html20
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative.html20
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative-expected.html85
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative.html101
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative.html20
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative.html20
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur-expected.html85
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.html106
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.isotropic-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.isotropic.html21
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-x-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-x.html21
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-y-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-y.html21
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.x-only-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.x-only.html21
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.y-only-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.y-only.html21
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.beginLayer-options.html7
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.ctm.getTransform.html7
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.exceptions-are-no-op.html7
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha-expected.html31
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending-expected.html32
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending.html35
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending.shadow-expected.html36
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending.shadow.html39
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite-expected.html32
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite.html35
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite.shadow-expected.html36
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite.shadow.html39
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.html34
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.shadow-expected.html35
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.shadow.html38
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending-expected.html31
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.html34
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.no-transform-expected.html72
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.no-transform.html77
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.rotation-expected.html76
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.rotation.html81
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow-expected.html35
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.html38
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.no-transform-expected.html78
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.no-transform.html83
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.rotation-expected.html82
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.rotation.html87
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite-expected.html31
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.html34
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.no-transform-expected.html72
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.no-transform.html77
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.rotation-expected.html76
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.rotation.html81
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow-expected.html35
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.html38
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.no-transform-expected.html78
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.no-transform.html83
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.rotation-expected.html82
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.rotation.html87
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.no-transform-expected.html72
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.no-transform.html77
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.rotation-expected.html76
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.rotation.html81
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.no-transform-expected.html78
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.no-transform.html83
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.rotation-expected.html82
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.rotation.html87
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha-expected.html50
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending-expected.html51
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending.html39
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending.shadow-expected.html55
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending.shadow.html42
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite-expected.html51
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite.html39
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite.shadow-expected.html55
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite.shadow.html42
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.html38
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.shadow-expected.html54
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.shadow.html42
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending-expected.html50
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.html37
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform-expected.html98
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform.html77
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.rotation-expected.html102
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.rotation.html81
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow-expected.html54
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.html41
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.no-transform-expected.html104
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.no-transform.html83
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.rotation-expected.html108
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.rotation.html87
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite-expected.html50
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.html37
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform-expected.html98
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform.html77
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.rotation-expected.html102
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.rotation.html81
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow-expected.html54
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.html41
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.no-transform-expected.html104
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.no-transform.html83
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.rotation-expected.html108
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.rotation.html87
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform-expected.html98
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform.html77
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.rotation-expected.html102
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.rotation.html81
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.no-transform-expected.html104
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.no-transform.html83
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.rotation-expected.html108
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.rotation.html87
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform-expected.html98
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform.html77
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation-expected.html102
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation.html81
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform-expected.html104
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform.html83
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation-expected.html108
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation.html87
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-global-states-expected.html50
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-global-states.html37
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.shadow-expected.html53
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.shadow.html40
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform-expected.html72
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform.html77
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation-expected.html76
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation.html81
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.no-transform-expected.html78
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.no-transform.html83
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.rotation-expected.html82
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.rotation.html87
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-global-states-expected.html31
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-global-states.html34
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.shadow-expected.html34
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.shadow.html37
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.globalCompositeOperation-expected.html910
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.globalCompositeOperation.html861
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.html7
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-restore.html7
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.html7
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.endLayer.html7
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.save-beginLayer-restore.html7
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.save-endLayer.html7
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations-with-promises.createImageBitmap.html34
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations-with-promises.html40
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations-with-promises.toBlob.html34
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.createPattern.html33
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.drawImage.html35
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.getImageData.html33
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.html87
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.putImageData.html36
-rw-r--r--testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.toDataURL.html33
-rw-r--r--testing/web-platform/tests/html/canvas/element/manual/filters/svg-filter-lh-rlh-expected.html22
-rw-r--r--testing/web-platform/tests/html/canvas/element/manual/filters/svg-filter-lh-rlh.html32
-rw-r--r--testing/web-platform/tests/html/canvas/element/manual/filters/svg-filter.svg10
-rw-r--r--testing/web-platform/tests/html/canvas/element/text/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html30
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.worker.js25
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html30
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.worker.js25
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html30
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.worker.js25
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html30
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.worker.js25
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative.html23
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative.w.html37
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative.html23
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative.w.html37
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative.html23
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative.w.html37
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative-expected.html85
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative.html121
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative.w.html194
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative.html23
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative.w.html37
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative.html23
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative.w.html37
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur-expected.html85
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.html126
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.isotropic-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.isotropic.html24
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.isotropic.w.html38
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-x-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-x.html24
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-x.w.html38
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-y-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-y.html24
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-y.w.html38
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.w.html199
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.x-only-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.x-only.html24
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.x-only.w.html38
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.y-only-expected.html15
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.y-only.html24
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.y-only.w.html38
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.beginLayer-options.html11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.beginLayer-options.worker.js11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.ctm.getTransform.html11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.ctm.getTransform.worker.js11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.exceptions-are-no-op.html11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.exceptions-are-no-op.worker.js11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha-expected.html31
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending-expected.html32
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.html38
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.shadow-expected.html36
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.shadow.html42
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.shadow.w.html56
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.w.html52
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite-expected.html32
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.html38
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.shadow-expected.html36
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.shadow.html42
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.shadow.w.html56
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.w.html52
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.html37
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.shadow-expected.html35
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.shadow.html41
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.shadow.w.html55
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.w.html51
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending-expected.html31
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.html37
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.no-transform-expected.html72
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.no-transform.html85
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.no-transform.w.html116
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.rotation-expected.html76
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.rotation.html89
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.rotation.w.html120
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow-expected.html35
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.html41
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.no-transform-expected.html78
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.no-transform.html91
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.no-transform.w.html122
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.rotation-expected.html82
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.rotation.html95
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.rotation.w.html126
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.w.html55
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.w.html51
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite-expected.html31
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.html37
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.no-transform-expected.html72
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.no-transform.html85
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.no-transform.w.html116
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.rotation-expected.html76
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.rotation.html89
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.rotation.w.html120
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow-expected.html35
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.html41
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.no-transform-expected.html78
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.no-transform.html91
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.no-transform.w.html122
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.rotation-expected.html82
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.rotation.html95
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.rotation.w.html126
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.w.html55
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.w.html51
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.no-transform-expected.html72
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.no-transform.html85
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.no-transform.w.html116
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.rotation-expected.html76
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.rotation.html89
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.rotation.w.html120
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.no-transform-expected.html78
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.no-transform.html91
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.no-transform.w.html122
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.rotation-expected.html82
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.rotation.html95
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.rotation.w.html126
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha-expected.html50
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending-expected.html51
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.html42
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.shadow-expected.html55
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.shadow.html45
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.shadow.w.html59
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.w.html56
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite-expected.html51
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.html42
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.shadow-expected.html55
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.shadow.html45
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.shadow.w.html59
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.w.html56
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.html41
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.shadow-expected.html54
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.shadow.html45
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.shadow.w.html59
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.w.html55
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending-expected.html50
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.html40
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform-expected.html98
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform.html85
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform.w.html116
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.rotation-expected.html102
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.rotation.html89
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.rotation.w.html120
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow-expected.html54
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.html44
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.no-transform-expected.html104
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.no-transform.html91
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.no-transform.w.html122
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.rotation-expected.html108
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.rotation.html95
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.rotation.w.html126
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.w.html58
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.w.html54
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite-expected.html50
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.html40
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform-expected.html98
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform.html85
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform.w.html116
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.rotation-expected.html102
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.rotation.html89
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.rotation.w.html120
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow-expected.html54
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.html44
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.no-transform-expected.html104
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.no-transform.html91
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.no-transform.w.html122
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.rotation-expected.html108
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.rotation.html95
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.rotation.w.html126
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.w.html58
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.w.html54
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform-expected.html98
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform.html85
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform.w.html116
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.rotation-expected.html102
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.rotation.html89
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.rotation.w.html120
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.no-transform-expected.html104
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.no-transform.html91
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.no-transform.w.html122
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.rotation-expected.html108
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.rotation.html95
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.rotation.w.html126
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform-expected.html98
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform.html85
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform.w.html116
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation-expected.html102
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation.html89
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation.w.html120
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform-expected.html104
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform.html91
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform.w.html122
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation-expected.html108
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation.html95
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation.w.html126
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-global-states-expected.html50
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-global-states.html40
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-global-states.w.html54
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.shadow-expected.html53
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.shadow.html43
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.shadow.w.html57
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform-expected.html72
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform.html85
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform.w.html116
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation-expected.html76
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation.html89
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation.w.html120
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.no-transform-expected.html78
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.no-transform.html91
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.no-transform.w.html122
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.rotation-expected.html82
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.rotation.html95
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.rotation.w.html126
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-global-states-expected.html31
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-global-states.html37
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-global-states.w.html51
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.shadow-expected.html34
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.shadow.html40
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.shadow.w.html54
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.globalCompositeOperation-expected.html910
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.globalCompositeOperation.html961
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.globalCompositeOperation.w.html1314
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.html11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.worker.js11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-restore.html11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-restore.worker.js11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.html11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.worker.js11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.endLayer.html11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.endLayer.worker.js11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-beginLayer-restore.html11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-beginLayer-restore.worker.js11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-endLayer.html11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-endLayer.worker.js11
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.convertToBlob.html27
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.convertToBlob.worker.js21
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.createImageBitmap.html27
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.createImageBitmap.worker.js21
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.html38
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.worker.js37
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.createPattern.html34
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.createPattern.worker.js29
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.drawImage.html36
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.drawImage.worker.js31
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.getImageData.html34
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.getImageData.worker.js29
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.html85
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.putImageData.html37
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.putImageData.worker.js32
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.transferToImageBitmap.html34
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.transferToImageBitmap.worker.js29
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.worker.js84
-rw-r--r--testing/web-platform/tests/html/canvas/offscreen/text/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py387
-rw-r--r--testing/web-platform/tests/html/canvas/tools/templates/reftest_element.html2
-rw-r--r--testing/web-platform/tests/html/canvas/tools/templates/reftest_element_grid.html56
-rw-r--r--testing/web-platform/tests/html/canvas/tools/templates/reftest_grid.html30
-rw-r--r--testing/web-platform/tests/html/canvas/tools/templates/reftest_offscreen.html2
-rw-r--r--testing/web-platform/tests/html/canvas/tools/templates/reftest_offscreen_grid.html52
-rw-r--r--testing/web-platform/tests/html/canvas/tools/templates/reftest_worker.html2
-rw-r--r--testing/web-platform/tests/html/canvas/tools/templates/reftest_worker_grid.html62
-rw-r--r--testing/web-platform/tests/html/canvas/tools/templates/testharness_element_grid.html52
-rw-r--r--testing/web-platform/tests/html/canvas/tools/templates/testharness_offscreen_grid.html28
-rw-r--r--testing/web-platform/tests/html/canvas/tools/templates/testharness_worker_grid.js27
-rw-r--r--testing/web-platform/tests/html/canvas/tools/yaml-new/filters.yaml8
-rw-r--r--testing/web-platform/tests/html/canvas/tools/yaml-new/layers.yaml292
-rw-r--r--testing/web-platform/tests/html/canvas/tools/yaml/element/meta.yaml14
-rw-r--r--testing/web-platform/tests/html/canvas/tools/yaml/offscreen/meta.yaml14
-rw-r--r--testing/web-platform/tests/html/cross-origin-embedder-policy/META.yml1
-rw-r--r--testing/web-platform/tests/html/cross-origin-opener-policy/META.yml1
-rw-r--r--testing/web-platform/tests/html/dom/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/html/dom/aria-element-reflection.html4
-rw-r--r--testing/web-platform/tests/html/dom/elements/global-attributes/the-anchor-attribute-003.tentative.html4
-rw-r--r--testing/web-platform/tests/html/dom/elements/global-attributes/the-anchor-attribute-xml.tentative.html25
-rw-r--r--testing/web-platform/tests/html/dom/render-blocking/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/html/dom/usvstring-reflection.https.html2
-rw-r--r--testing/web-platform/tests/html/editing/dnd/events/drag-event-div-manual.html7
-rw-r--r--testing/web-platform/tests/html/editing/dnd/events/drag-event-manual.html7
-rw-r--r--testing/web-platform/tests/html/editing/dnd/events/dragend-event-manual.html7
-rw-r--r--testing/web-platform/tests/html/editing/dnd/events/dragenter-event-manual.html7
-rw-r--r--testing/web-platform/tests/html/editing/dnd/events/dragleave-event-manual.html7
-rw-r--r--testing/web-platform/tests/html/editing/dnd/events/dragover-event-manual.html7
-rw-r--r--testing/web-platform/tests/html/editing/dnd/events/dragstart-event-manual.html7
-rw-r--r--testing/web-platform/tests/html/editing/dnd/events/drop-event-manual.html7
-rw-r--r--testing/web-platform/tests/html/editing/dnd/resources/dragdrop_support.js9
-rw-r--r--testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/effectAllowed-manual.html9
-rw-r--r--testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/files-manual.html13
-rw-r--r--testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/setData-manual.html9
-rw-r--r--testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/types-manual.html9
-rw-r--r--testing/web-platform/tests/html/infrastructure/safe-passing-of-structured-data/shared-array-buffers/window-simple-success.https.html1
-rw-r--r--testing/web-platform/tests/html/interaction/focus/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/html/interaction/focus/sequential-focus-navigation-and-the-tabindex-attribute/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/html/meta/refresh-time.html53
-rw-r--r--testing/web-platform/tests/html/meta/resources/gotRefreshed.html3
-rw-r--r--testing/web-platform/tests/html/meta/resources/refresh.99.html5
-rw-r--r--testing/web-platform/tests/html/meta/resources/refresh1.99.html5
-rw-r--r--testing/web-platform/tests/html/meta/resources/refresh1.html5
-rw-r--r--testing/web-platform/tests/html/meta/resources/refresh1dotdot5dot.html5
-rw-r--r--testing/web-platform/tests/html/rendering/non-replaced-elements/flow-content-0/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/html/rendering/replaced-elements/the-select-element/select-1-block-size-001-ref-2.html2
-rw-r--r--testing/web-platform/tests/html/rendering/replaced-elements/the-select-element/select-1-block-size-001-ref.html2
-rw-r--r--testing/web-platform/tests/html/rendering/the-details-element/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/html/rendering/widgets/input-password-background-suppresses-appearance-ref.html3
-rw-r--r--testing/web-platform/tests/html/rendering/widgets/input-password-background-suppresses-appearance.html7
-rw-r--r--testing/web-platform/tests/html/semantics/embedded-content/media-elements/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/html/semantics/embedded-content/the-object-element/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/html/semantics/forms/attributes-common-to-form-controls/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/html/semantics/forms/constraints/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/html/semantics/forms/constraints/form-validation-validity-textarea-defaultValue.html98
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-button-element/WEB_FEATURES.yml10
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-fieldset-element/WEB_FEATURES.yml8
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-form-element/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-input-element/WEB_FEATURES.yml11
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-input-element/input-stepdown-02.html42
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-output-element/WEB_FEATURES.yml5
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/WEB_FEATURES.yml9
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/native-popup-with-datalist-ref.html17
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/native-popup-with-datalist.tentative.html25
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/nested-options.tenative.html46
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/resources/select-reset-non-interoperable-styles.css5
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/resources/stylable-select-styles.css49
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-custom-button-no-datalist-ref.html (renamed from testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-custom-datalist-ref.html)2
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-custom-button-no-datalist.tentative.html3
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-custom-datalist.tentative.html3
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-no-datalist-ref.html17
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-no-datalist.tentative.html3
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-lr-ref.html23
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-lr.tentative.html29
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-rl-ref.html23
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-rl.tentative.html29
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-child-button-and-datalist-invalidation.tentative.html1
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-child-button-and-datalist.tentative.html1
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-datalist-popover-behavior.tentative.html75
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-keyboard-behavior.tentative.html14
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-selectlist-element/selectlist-option-arbitrary-content-displayed.tentative.html2
-rw-r--r--testing/web-platform/tests/html/semantics/forms/the-textarea-element/WEB_FEATURES.yml6
-rw-r--r--testing/web-platform/tests/html/semantics/interactive-elements/the-details-element/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/html/semantics/invokers/interesttarget-anchor-event-dispatch.tentative.html47
-rw-r--r--testing/web-platform/tests/html/semantics/invokers/interesttarget-area-event-dispatch.tentative.html50
-rw-r--r--testing/web-platform/tests/html/semantics/invokers/interesttarget-button-event-dispatch.tentative.html31
-rw-r--r--testing/web-platform/tests/html/semantics/invokers/interesttarget-svg-a-event-dispatch.tentative.html51
-rw-r--r--testing/web-platform/tests/html/semantics/invokers/invoketarget-on-input-number.tentative.html78
-rw-r--r--testing/web-platform/tests/html/semantics/permission-element/bounded-css-properties-reftest-ref.html (renamed from testing/web-platform/tests/html/semantics/permission-element/bounded-css-properties-reference-expected.html)0
-rw-r--r--testing/web-platform/tests/html/semantics/permission-element/bounded-css-properties-reftest.tentative.html (renamed from testing/web-platform/tests/html/semantics/permission-element/bounded-css-properties-reference.tentative.html)2
-rw-r--r--testing/web-platform/tests/html/semantics/permission-element/bounded-sizes-reftest-ref.html56
-rw-r--r--testing/web-platform/tests/html/semantics/permission-element/bounded-sizes-reftest.tentative.html68
-rw-r--r--testing/web-platform/tests/html/semantics/permission-element/bounded-sizes.tentative.html75
-rw-r--r--testing/web-platform/tests/html/semantics/permission-element/display-css-property-reftest-ref.html (renamed from testing/web-platform/tests/html/semantics/permission-element/display-css-property-reference-expected.html)0
-rw-r--r--testing/web-platform/tests/html/semantics/permission-element/display-css-property-reftest.tentative.html (renamed from testing/web-platform/tests/html/semantics/permission-element/display-css-property-reference.tentative.html)2
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display.tentative.html1
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-none.tentative.html1
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-display.tentative.html1
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display.tentative.html1
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-transition.tentative.tentative.html1
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-focus-2.html2
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-hover-crash-hang.tentative.html34
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-scroll-within.html52
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss.html56
-rw-r--r--testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/dynamic-import/microtasks/basic.any.js4
-rw-r--r--testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/inline-async-inserted-execorder.html70
-rw-r--r--testing/web-platform/tests/html/semantics/scripting-1/the-template-element/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/html/semantics/selectors/pseudo-classes/WEB_FEATURES.yml11
-rw-r--r--testing/web-platform/tests/html/syntax/parsing/template/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/html/user-activation/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/html/webappapis/structured-clone/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/inert/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/inert/inert-and-find-flat-tree.html28
-rw-r--r--testing/web-platform/tests/infrastructure/metadata/infrastructure/testdriver/click_iframe_crossorigin.sub.html.ini1
-rw-r--r--testing/web-platform/tests/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html4
-rw-r--r--testing/web-platform/tests/interfaces/DOM-Parsing.idl16
-rw-r--r--testing/web-platform/tests/interfaces/compute-pressure.idl4
-rw-r--r--testing/web-platform/tests/interfaces/css-anchor-position.idl76
-rw-r--r--testing/web-platform/tests/interfaces/css-nesting.idl9
-rw-r--r--testing/web-platform/tests/interfaces/css-properties-values-api.idl8
-rw-r--r--testing/web-platform/tests/interfaces/css-view-transitions-2.idl7
-rw-r--r--testing/web-platform/tests/interfaces/device-attributes.idl13
-rw-r--r--testing/web-platform/tests/interfaces/digital-identities.idl4
-rw-r--r--testing/web-platform/tests/interfaces/document-picture-in-picture.idl2
-rw-r--r--testing/web-platform/tests/interfaces/dom.idl14
-rw-r--r--testing/web-platform/tests/interfaces/gamepad.idl5
-rw-r--r--testing/web-platform/tests/interfaces/geolocation.idl2
-rw-r--r--testing/web-platform/tests/interfaces/html.idl31
-rw-r--r--testing/web-platform/tests/interfaces/mediasession.idl15
-rw-r--r--testing/web-platform/tests/interfaces/sanitizer-api.idl18
-rw-r--r--testing/web-platform/tests/interfaces/service-workers.idl1
-rw-r--r--testing/web-platform/tests/interfaces/shape-detection-api.idl6
-rw-r--r--testing/web-platform/tests/interfaces/shared-storage.idl51
-rw-r--r--testing/web-platform/tests/interfaces/text-detection-api.idl2
-rw-r--r--testing/web-platform/tests/interfaces/trusted-types.idl6
-rw-r--r--testing/web-platform/tests/interfaces/turtledove.idl1
-rw-r--r--testing/web-platform/tests/interfaces/wasm-js-api.idl2
-rw-r--r--testing/web-platform/tests/interfaces/webcodecs.idl4
-rw-r--r--testing/web-platform/tests/interfaces/webgl1.idl1
-rw-r--r--testing/web-platform/tests/interfaces/webidl.idl2
-rw-r--r--testing/web-platform/tests/interfaces/webnn.idl43
-rw-r--r--testing/web-platform/tests/interfaces/webrtc-encoded-transform.idl10
-rw-r--r--testing/web-platform/tests/interfaces/webrtc.idl7
-rw-r--r--testing/web-platform/tests/interfaces/webxr.idl5
-rw-r--r--testing/web-platform/tests/mathml/presentation-markup/operators/mo-axis-height-1.html169
-rw-r--r--testing/web-platform/tests/mathml/presentation-markup/operators/mo-minsize-maxsize-001.html60
-rw-r--r--testing/web-platform/tests/mathml/presentation-markup/operators/mo-stretch-properties-dynamic-001.html3
-rw-r--r--testing/web-platform/tests/mathml/presentation-markup/operators/size-and-position-of-stretchy-fences-with-default-font-001.html115
-rw-r--r--testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-001-ref.html16
-rw-r--r--testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-001.html23
-rw-r--r--testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-002-ref.html30
-rw-r--r--testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-002.html33
-rw-r--r--testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-003-ref.html31
-rw-r--r--testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-003.html38
-rw-r--r--testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-004-ref.html19
-rw-r--r--testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-004.html22
-rwxr-xr-xtesting/web-platform/tests/mathml/tools/stretchy.py11
-rw-r--r--testing/web-platform/tests/mediacapture-extensions/MediaStreamTrack-audio-stats.https.html209
-rw-r--r--testing/web-platform/tests/mediacapture-extensions/MediaStreamTrack-video-stats.https.html8
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-canvas-media-source.https.html8
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-events-and-exceptions.html8
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-mimetype.html3
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-pause-resume.html4
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-peerconnection.https.html133
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-stop.html43
-rw-r--r--testing/web-platform/tests/mediacapture-streams/BrowserCaptureMediaStreamTrack-restrictTo.https.html245
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/event-constructor.html6
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-cross-origin.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download-userInitiated.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-fragment.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-same-origin-cross-document.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-userInitiated.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-with-target.html7
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-back-forward.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-navigate.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-reload.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-form-get.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-form-reload.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-form-traverse.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-form-userInitiated.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-form-with-target.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-form.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-fragment.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-pushState.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-cross-document.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-history-go-0.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-history-pushState.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-history-replaceState.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-iframe-location.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-location.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-meta-refresh.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-cross-document.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document-in-iframe.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-navigate.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-svg-anchor-fragment.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-to-srcdoc.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open-self.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open.html3
-rw-r--r--testing/web-platform/tests/notifications/tag-different-manual.https.html37
-rw-r--r--testing/web-platform/tests/notifications/tag-same-manual.https.html37
-rw-r--r--testing/web-platform/tests/notifications/tag.https.html47
-rw-r--r--testing/web-platform/tests/orientation-event/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/permissions-policy/payment-allowed-by-permissions-policy-attribute.https.sub.html59
-rw-r--r--testing/web-platform/tests/permissions-policy/payment-allowed-by-permissions-policy.https.sub.html86
-rw-r--r--testing/web-platform/tests/permissions-policy/payment-default-permissions-policy.https.sub.html67
-rw-r--r--testing/web-platform/tests/permissions-policy/payment-disabled-by-permissions-policy.https.sub.html64
-rw-r--r--testing/web-platform/tests/permissions-policy/resources/permissions-policy.js38
-rw-r--r--testing/web-platform/tests/pointerevents/deviceproperties/get-device-properties-uniqueid-from-pointer-event.tentative.html45
-rw-r--r--testing/web-platform/tests/pointerevents/deviceproperties/pointer-event-has-device-properties-uniqueid-from-pointer-event-init.tentative.html92
-rw-r--r--testing/web-platform/tests/pointerevents/deviceproperties/unique-id-is-unique-manual.tentative.html142
-rw-r--r--testing/web-platform/tests/preload/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/resource-timing/initiator-type/workers.html2
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-pressure-service.js26
-rw-r--r--testing/web-platform/tests/resources/idlharness.js1
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html1
-rw-r--r--testing/web-platform/tests/resources/test/tox.ini2
-rw-r--r--testing/web-platform/tests/resources/testdriver.js51
-rw-r--r--testing/web-platform/tests/resources/testharness.js133
-rw-r--r--testing/web-platform/tests/resources/testharnessreport.js25
-rw-r--r--testing/web-platform/tests/scheduler/tentative/yield/yield-priority-posttask.any.js15
-rw-r--r--testing/web-platform/tests/screen-capture/getallscreensmedia-exposure.tentative.https.window.js19
-rw-r--r--testing/web-platform/tests/screen-details/META.yml2
-rw-r--r--testing/web-platform/tests/screen-wake-lock/wakelock-active-document.https.window.js21
-rw-r--r--testing/web-platform/tests/scroll-animations/css/animation-shorthand.html30
-rw-r--r--testing/web-platform/tests/scroll-to-text-fragment/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/scroll-to-text-fragment/percent-encoding.html33
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/WEB_FEATURES.yml5
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/navigation-timing-sizes.https.html80
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/resources/range-request-with-synth-head-worker.js2
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/router-rules.js36
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-invalid-rules.https.html28
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html17
-rw-r--r--testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html14
-rw-r--r--testing/web-platform/tests/shadow-dom/declarative/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/shadow-dom/declarative/gethtml.html31
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-2levels.html1
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots-in-slot.html1
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-with-delegatesFocus.html1
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation.html1
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-on-shadow-host.html43
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-iframe.html33
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-nested-grids.html72
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-popover.html64
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-slots.html164
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order.html89
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/resources/focus-utils.js41
-rw-r--r--testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-include.tentative.https.sub.html1
-rw-r--r--testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-omit.tentative.https.sub.html1
-rw-r--r--testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-same-origin.tentative.https.sub.html1
-rw-r--r--testing/web-platform/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-false-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html32
-rw-r--r--testing/web-platform/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-access-control-allow-credentials.tentative.https.sub.html (renamed from testing/web-platform/tests/shared-storage/cross-origin-create-worklet-failure-missing-access-control-allow-credentials.tentative.https.sub.html)8
-rw-r--r--testing/web-platform/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-access-control-allow-origin.tentative.https.sub.html (renamed from testing/web-platform/tests/shared-storage/cross-origin-create-worklet-failure-missing-access-control-allow-origin.tentative.https.sub.html)8
-rw-r--r--testing/web-platform/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html31
-rw-r--r--testing/web-platform/tests/shared-storage/cross-origin-worklet-select-url-and-verify-data-origin.tentative.https.sub.html46
-rw-r--r--testing/web-platform/tests/shared-storage/resources/credentials-test-helper.py3
-rw-r--r--testing/web-platform/tests/shared-storage/resources/simple-module.js4
-rw-r--r--testing/web-platform/tests/shared-storage/resources/simple-module.js.headers2
-rw-r--r--testing/web-platform/tests/speech-api/WEB_FEATURES.yml7
-rw-r--r--testing/web-platform/tests/storage-access-api/helpers.js7
-rw-r--r--testing/web-platform/tests/storage-access-api/requestStorageAccess-cross-origin-iframe-navigation-relax.tentative.sub.https.window.js65
-rw-r--r--testing/web-platform/tests/storage-access-api/requestStorageAccess-dedicated-worker.tentative.sub.https.window.js6
-rw-r--r--testing/web-platform/tests/storage-access-api/resources/embedded_forwarder.js50
-rw-r--r--testing/web-platform/tests/storage-access-api/resources/embedded_responder.js2
-rw-r--r--testing/web-platform/tests/storage-access-api/resources/script-with-cookie-header.py3
-rw-r--r--testing/web-platform/tests/storage/buckets/detached-iframe.https.html12
-rw-r--r--testing/web-platform/tests/streams/readable-streams/async-iterator.any.js86
-rw-r--r--testing/web-platform/tests/streams/readable-streams/tee-detached-context-crash.html13
-rw-r--r--testing/web-platform/tests/svg/crashtests/chrome-bug-333487749.html14
-rw-r--r--testing/web-platform/tests/svg/layout/svg-embed-intrinsic-size-min-size.html16
-rw-r--r--testing/web-platform/tests/svg/layout/svg-intrinsic-size-min-size-ref.html3
-rw-r--r--testing/web-platform/tests/svg/layout/svg-intrinsic-size-min-size.html16
-rw-r--r--testing/web-platform/tests/svg/painting/reftests/green-100x100.svg3
-rw-r--r--testing/web-platform/tests/svg/painting/reftests/non-scaling-stroke-001.html30
-rw-r--r--testing/web-platform/tests/svg/painting/reftests/paint-context-007-ref.svg29
-rw-r--r--testing/web-platform/tests/svg/painting/reftests/paint-context-007.svg32
-rw-r--r--testing/web-platform/tests/svg/painting/reftests/paint-context-008-ref.svg22
-rw-r--r--testing/web-platform/tests/svg/painting/reftests/paint-context-008.svg33
-rw-r--r--testing/web-platform/tests/svg/painting/scripted/marker-element-added.html29
-rw-r--r--testing/web-platform/tests/svg/path/property/serialization.svg2
-rw-r--r--testing/web-platform/tests/svg/types/scripted/SVGLength-rlh.html31
-rw-r--r--testing/web-platform/tests/tools/ci/azure/install_chrome.yml6
-rw-r--r--testing/web-platform/tests/tools/ci/azure/install_firefox.yml7
-rw-r--r--testing/web-platform/tests/tools/ci/jobs.py7
-rw-r--r--testing/web-platform/tests/tools/ci/requirements_build.txt4
-rw-r--r--testing/web-platform/tests/tools/ci/requirements_macos_color_profile.txt2
-rw-r--r--testing/web-platform/tests/tools/ci/requirements_tc.txt4
-rw-r--r--testing/web-platform/tests/tools/ci/tc/tasks/test.yml59
-rw-r--r--testing/web-platform/tests/tools/ci/tc/tests/test_valid.py16
-rw-r--r--testing/web-platform/tests/tools/ci/update_built.py1
-rw-r--r--testing/web-platform/tests/tools/manifest/item.py2
-rw-r--r--testing/web-platform/tests/tools/manifest/requirements.txt2
-rw-r--r--testing/web-platform/tests/tools/manifest/sourcefile.py20
-rw-r--r--testing/web-platform/tests/tools/mypy.ini6
-rw-r--r--testing/web-platform/tests/tools/pytest.ini7
-rw-r--r--testing/web-platform/tests/tools/requirements_flake8.txt4
-rw-r--r--testing/web-platform/tests/tools/requirements_mypy.txt8
-rw-r--r--testing/web-platform/tests/tools/requirements_pytest.txt2
-rw-r--r--testing/web-platform/tests/tools/requirements_tests.txt6
-rw-r--r--testing/web-platform/tests/tools/serve/serve.py17
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/.gitignore4
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/.travis.yml17
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/CONTRIBUTING30
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/MANIFEST.in2
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/PKG-INFO13
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/README.md28
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py3
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_wsh.py3
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/arraybuffer_benchmark.html134
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.html175
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.js238
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py4
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/console.html317
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py1
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py30
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/handler_map.txt11
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/internal_error_wsh.py3
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.html37
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.js86
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/example/special_headers.cgi26
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/util.js323
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_main.js89
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_worker.js44
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/fast_masking.i98
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/PKG-INFO13
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/SOURCES.txt64
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/dependency_links.txt (renamed from testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/dependency_links.txt)0
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/requires.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/top_level.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/__init__.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/__init__.py)6
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/_stream_exceptions.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/_stream_exceptions.py)0
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/common.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/common.py)4
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/dispatch.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/dispatch.py)14
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/extensions.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/extensions.py)6
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/__init__.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/__init__.py)14
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/base.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/base.py)15
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/hybi.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/hybi.py)17
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/http_header_util.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/http_header_util.py)2
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/memorizingfile.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/memorizingfile.py)3
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/msgutil.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/msgutil.py)14
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/request_handler.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/request_handler.py)14
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/server_util.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/server_util.py)3
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/standalone.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/standalone.py)31
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/stream.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/stream.py)19
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/util.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/util.py)12
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/websocket_server.py (renamed from testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/websocket_server.py)23
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/setup.cfg4
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/setup.py15
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cacert.pem17
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cert.pem61
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/key.pem15
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py28
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py13
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py8
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py5
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py9
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py10
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py7
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py17
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py16
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py3
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py7
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py5
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py26
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py7
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py12
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/README1
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/hello.pl32
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/.github/FUNDING.yml3
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/config.yml1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/issue.md29
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/.github/dependabot.yml9
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/.github/workflows/tests.yml83
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/.github/workflows/wheels.yml88
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/.gitignore16
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/.readthedocs.yml13
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/CODE_OF_CONDUCT.md46
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/LICENSE9
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/Makefile35
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/PKG-INFO174
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/README.rst73
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/SECURITY.md12
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/compliance/README.rst50
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingclient.json11
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingserver.json12
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/compliance/test_client.py48
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/compliance/test_server.py29
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/Makefile23
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.pngbin4069 -> 0 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/conf.py171
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/faq/asyncio.rst69
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/faq/client.rst101
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/faq/common.rst161
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/faq/index.rst21
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/faq/misc.rst49
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/faq/server.rst336
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/autoreload.rst31
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst87
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/django.rst294
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/extensions.rst30
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/fly.rst177
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/haproxy.rst61
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/heroku.rst183
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/index.rst56
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/kubernetes.rst215
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/nginx.rst84
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/patterns.rst110
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/quickstart.rst170
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/render.rst172
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/sansio.rst322
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/supervisor.rst131
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/index.rst75
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/intro/index.rst46
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial1.rst591
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial2.rst565
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial3.rst290
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/make.bat35
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/project/changelog.rst1230
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/project/contributing.rst66
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/project/index.rst12
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/project/license.rst4
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/project/tidelift.rst112
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst64
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst54
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst113
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/datastructures.rst66
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/exceptions.rst6
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/extensions.rst60
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/features.rst187
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/index.rst90
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/client.rst58
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/common.rst64
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/server.rst62
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/client.rst49
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/common.rst41
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/server.rst60
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/types.rst24
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt8
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt85
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.rst348
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.svg63
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/broadcast.rst348
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/compression.rst222
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/data-flow.svg63
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.rst181
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.svg63
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst572
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/index.rst18
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle25
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.svg3
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/logging.rst245
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/memory.rst48
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/performance.rst20
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.graffle37
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.svg3
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/security.rst41
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/timeouts.rst116
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/Procfile1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/app.py36
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/fly.toml16
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/requirements.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/app.py30
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/haproxy.cfg17
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/supervisord.conf7
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/Procfile1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/app.py30
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/requirements.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/Dockerfile7
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/app.py49
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/benchmark.py27
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/deployment.yaml35
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/app.py29
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/nginx.conf25
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/supervisord.conf7
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/render/app.py36
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/render/requirements.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/supervisor/app.py30
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/deployment/supervisor/supervisord.conf7
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/django/authentication.py30
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/django/notifications.py73
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/django/signals.py23
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/echo.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/faq/health_check_server.py22
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/faq/shutdown_client.py19
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/faq/shutdown_server.py20
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/hello.py12
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/legacy/basic_auth_client.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/legacy/basic_auth_server.py21
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/legacy/unix_client.py19
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/legacy/unix_server.py23
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/logging/json_log_formatter.py33
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/quickstart/client.py18
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/quickstart/client_secure.py24
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.css33
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.html18
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.js26
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.py49
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/quickstart/localhost.pem48
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/quickstart/server.py20
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/quickstart/server_secure.py26
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.html9
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.js12
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.py19
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time_2.py28
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.css105
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.js45
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.py62
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/favicon.ico2
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/app.py65
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/index.html10
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/main.js53
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/app.py190
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/index.html15
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/main.js83
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/Procfile1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/app.py198
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/index.html15
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/main.js93
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/requirements.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/app.py226
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie.html15
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie.js23
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.html9
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.js9
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/first_message.html14
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/first_message.js11
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/index.html12
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/query_param.html14
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/query_param.js11
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/script.js51
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/style.css69
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/test.html15
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/test.js6
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/user_info.html14
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/user_info.js11
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/broadcast/clients.py61
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/broadcast/server.py153
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/compression/benchmark.py163
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/compression/client.py59
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/compression/server.py70
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/parse_frames.py101
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/parse_handshake.py102
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/streams.py301
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_http11_request_parser.py42
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_http11_response_parser.py44
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_websocket_parser.py51
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/logo/favicon.ico2
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/logo/github-social-preview.html39
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/logo/horizontal.svg31
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/logo/icon.html25
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/logo/icon.svg15
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/logo/old.svg14
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/logo/vertical.svg31
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/pyproject.toml87
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/setup.cfg42
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/setup.py61
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/PKG-INFO174
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/SOURCES.txt42
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/not-zip-safe1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/top_level.txt3
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py236
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py139
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py4
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py63
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/connection.py705
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/datastructures.py8
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py31
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/frames.py97
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py29
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/http11.py16
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/async_timeout.py265
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/auth.py30
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/client.py102
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py13
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/framing.py42
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py18
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/http.py22
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py396
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/server.py289
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py708
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py175
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/client.py328
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/connection.py773
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/messages.py281
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/server.py530
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/utils.py46
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py7
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py4
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py12
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/__init__.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/extensions/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_base.py4
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_permessage_deflate.py977
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/extensions/utils.py113
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/legacy/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_auth.py184
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_client_server.py1636
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_framing.py206
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_handshake.py184
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_http.py135
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_protocol.py1708
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/legacy/utils.py84
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/maxi_cov.py156
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/protocol.py29
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/sync/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/sync/client.py39
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/sync/connection.py109
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/sync/server.py65
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_client.py274
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_connection.py752
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_messages.py479
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_server.py388
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_utils.py33
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/sync/utils.py26
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_auth.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_client.py614
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_connection.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_datastructures.py236
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_exceptions.py196
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_exports.py30
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_frames.py495
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_headers.py222
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_http.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_http11.py344
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_imports.py64
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.cnf27
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.pem48
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_protocol.py1790
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_server.py686
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_streams.py198
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_typing.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_uri.py96
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_utils.py103
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/utils.py88
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tox.ini39
-rw-r--r--testing/web-platform/tests/tools/third_party_modified/mozlog/mozlog/formatters/html/html.py4
-rw-r--r--testing/web-platform/tests/tools/tox.ini2
-rw-r--r--testing/web-platform/tests/tools/wave/requirements.txt2
-rw-r--r--testing/web-platform/tests/tools/wave/tox.ini2
-rw-r--r--testing/web-platform/tests/tools/webtransport/h3/capsule.py2
-rw-r--r--testing/web-platform/tests/tools/wpt/browser.py20
-rw-r--r--testing/web-platform/tests/tools/wpt/install.py3
-rw-r--r--testing/web-platform/tests/tools/wpt/requirements_android.txt2
-rw-r--r--testing/web-platform/tests/tools/wpt/requirements_install.txt2
-rw-r--r--testing/web-platform/tests/tools/wpt/run.py30
-rw-r--r--testing/web-platform/tests/tools/wpt/tests/test_browser.py16
-rw-r--r--testing/web-platform/tests/tools/wpt/tox.ini2
-rw-r--r--testing/web-platform/tests/tools/wpt/utils.py5
-rw-r--r--testing/web-platform/tests/tools/wptrunner/docs/expectation.rst2
-rw-r--r--testing/web-platform/tests/tools/wptrunner/requirements.txt10
-rw-r--r--testing/web-platform/tests/tools/wptrunner/requirements_firefox.txt8
-rw-r--r--testing/web-platform/tests/tools/wptrunner/requirements_opera.txt2
-rw-r--r--testing/web-platform/tests/tools/wptrunner/requirements_safari.txt2
-rw-r--r--testing/web-platform/tests/tools/wptrunner/requirements_sauce.txt2
-rw-r--r--testing/web-platform/tests/tools/wptrunner/tox.ini2
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/__init__.py5
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/android_weblayer.py105
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome.py20
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/edge.py (renamed from testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/edgechromium.py)20
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py16
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox_android.py6
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/executors/actions.py24
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/executors/base.py2
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executoredge.py24
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py15
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py21
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/executors/protocol.py14
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/testdriver-extra.js8
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/testrunner.py64
-rw-r--r--testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py7
-rw-r--r--testing/web-platform/tests/tools/wptserve/setup.py7
-rw-r--r--testing/web-platform/tests/tools/wptserve/wptserve/response.py4
-rw-r--r--testing/web-platform/tests/tools/wptserve/wptserve/server.py4
-rw-r--r--testing/web-platform/tests/tools/wptserve/wptserve/sslutils/openssl.py4
-rw-r--r--testing/web-platform/tests/tools/wptserve/wptserve/ws_h2_handshake.py10
-rw-r--r--testing/web-platform/tests/trust-tokens/end-to-end/has-trust-token-with-no-top-frame.tentative.https.html8
-rw-r--r--testing/web-platform/tests/trusted-types/Element-setAttribute-respects-Elements-node-documents-globals-CSP.html125
-rw-r--r--testing/web-platform/tests/trusted-types/Element-setAttribute.html1
-rw-r--r--testing/web-platform/tests/trusted-types/HTMLElement-generic.html1
-rw-r--r--testing/web-platform/tests/trusted-types/TrustedType-AttributeNodes.html2
-rw-r--r--testing/web-platform/tests/trusted-types/TrustedTypePolicyFactory-getPropertyType.tentative.html (renamed from testing/web-platform/tests/trusted-types/TrustedTypePolicyFactory-getPropertyType.html)29
-rw-r--r--testing/web-platform/tests/trusted-types/TrustedTypePolicyFactory-metadata.tentative.html (renamed from testing/web-platform/tests/trusted-types/TrustedTypePolicyFactory-metadata.html)16
-rw-r--r--testing/web-platform/tests/trusted-types/block-string-assignment-to-Element-setAttribute.html3
-rw-r--r--testing/web-platform/tests/trusted-types/block-string-assignment-to-Element-setAttributeNS.html3
-rw-r--r--testing/web-platform/tests/trusted-types/block-string-assignment-to-HTMLElement-generic.html3
-rw-r--r--testing/web-platform/tests/trusted-types/block-string-assignment-to-attribute-via-attribute-node.html3
-rw-r--r--testing/web-platform/tests/trusted-types/default-policy-callback-arguments.html2
-rw-r--r--testing/web-platform/tests/trusted-types/modify-attributes-in-callback.html39
-rw-r--r--testing/web-platform/tests/trusted-types/support/resolve-spv.js9
-rw-r--r--testing/web-platform/tests/trusted-types/trusted-types-svg-script-set-href.html107
-rw-r--r--testing/web-platform/tests/trusted-types/trusted-types-svg-script.html102
-rw-r--r--testing/web-platform/tests/url/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/url/resources/setters_tests.json44
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/background-shorthand.html55
-rw-r--r--testing/web-platform/tests/web-locks/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/webauthn/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-passing.https.html4
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-pubkeycredparams.https.html8
-rw-r--r--testing/web-platform/tests/webauthn/getcredential-allowcredentials.https.html77
-rw-r--r--testing/web-platform/tests/webcodecs/audio-encoder-config.https.any.js4
-rw-r--r--testing/web-platform/tests/webcodecs/video-encoder-flush.https.any.js81
-rw-r--r--testing/web-platform/tests/webcodecs/video-encoder-utils.js19
-rw-r--r--testing/web-platform/tests/webcodecs/videoDecoder-codec-specific.https.any.js69
-rw-r--r--testing/web-platform/tests/webcodecs/videoFrame-copyTo-rgb.any.js252
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/capture_screenshot.py28
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/clip.py39
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/invalid.py28
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/locator.py76
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/max_node_count.py123
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/start_nodes.py69
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/device_pixel_ratio.py53
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/viewport.py4
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/invalid.py5
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/set_permission.py20
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key_events.py8
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/wheel.py26
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed.py2
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed_status.py55
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/response_started/response_started.py26
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/__init__.py2
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/add_cookie/add.py4
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_clear/__init__.py5
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_clear/clear.py66
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_clear/disabled.py113
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_clear/user_prompts.py8
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/navigate.py6
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_text/get.py21
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/get.py4
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/__init__.py8
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/enabled.py199
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/user_prompts.py11
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/wheel.py16
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/dom.py29
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/fixtures_bidi.py12
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/html/test_actions_scroll.html16
-rw-r--r--testing/web-platform/tests/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js6
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/arg_min_max.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/batch_normalization.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/buffer.https.any.js17
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/cast.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/clamp.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/compute-arraybufferview-with-bigger-arraybuffer.https.any.js61
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/concat.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/constant.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/conv2d.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/conv_transpose2d.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/elementwise_binary.https.any.js6
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/elementwise_logical.https.any.js27
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/elementwise_unary.https.any.js12
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/elu.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/expand.https.any.js6
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gather.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gemm.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/arg_min_max.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/batch_normalization.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/buffer.https.any.js16
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/cast.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/clamp.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/concat.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/constant.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/conv2d.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/conv_transpose2d.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/elementwise_binary.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/elementwise_logical.https.any.js20
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/elementwise_unary.https.any.js13
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/elu.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/expand.https.any.js11
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/gather.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/gemm.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/hard_sigmoid.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/hard_swish.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/instance_normalization.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/layer_normalization.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/leaky_relu.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/linear.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/matmul.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/pad.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/pooling.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/prelu.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/reduction.https.any.js24
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/relu.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/resample2d.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/reshape.https.any.js11
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/sigmoid.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/slice.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/softmax.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/softplus.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/softsign.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/split.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/tanh.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/transpose.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/triangular.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/gpu/where.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/hard_sigmoid.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/hard_swish.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/instance_normalization.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/layer_normalization.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/leaky_relu.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/linear.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/matmul.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/pad.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/parallel-compute.https.any.js19
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/pooling.https.any.js5
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/prelu.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/reduction.https.any.js31
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/relu.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/resample2d.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/reshape.https.any.js5
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/sigmoid.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/slice.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/softmax.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/softplus.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/softsign.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/split.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/tanh.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/transpose.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/triangular.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/conformance_tests/where.https.any.js4
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/arg_max.json2
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/arg_min.json2
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/matmul.json384
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/prelu.json28
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/reduce_l1.json8
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/reduce_l2.json8
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/reduce_log_sum.json8
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/reduce_log_sum_exp.json8
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/reduce_max.json8
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/reduce_mean.json8
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/reduce_min.json8
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/reduce_product.json8
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/reduce_sum.json8
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/reduce_sum_square.json8
-rw-r--r--testing/web-platform/tests/webnn/resources/test_data/softplus.json152
-rw-r--r--testing/web-platform/tests/webnn/resources/utils.js140
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/clamp.https.any.js53
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/conv2d.https.any.js475
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/convTranspose2d.https.any.js470
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/elu.https.any.js40
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/expand.https.any.js63
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/gelu.https.any.js10
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/gemm.https.any.js140
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/hardSigmoid.https.any.js28
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/instanceNormalization.https.any.js149
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/layerNormalization.https.any.js180
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/leakyRelu.https.any.js28
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/linear.https.any.js28
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/matmul.https.any.js113
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/pad.https.any.js70
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/pooling-and-reduction-keep-dims.https.any.js94
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/reshape.https.any.js65
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/slice.https.any.js66
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/softplus.https.any.js3
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/split.https.any.js80
-rw-r--r--testing/web-platform/tests/webnn/validation_tests/transpose.https.any.js51
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/tentative/RTCEncodedAudioFrame-metadata.https.html4
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/tentative/RTCEncodedVideoFrame-metadata.https.html26
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpReceiver-audio-jitterBufferTarget-stats.https.html (renamed from testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-audio-jitterBufferTarget-stats.https.html)0
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpReceiver-jitterBufferTarget-stats-helper.js (renamed from testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget-stats-helper.js)0
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpReceiver-jitterBufferTarget.html (renamed from testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget.html)0
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpReceiver-video-jitterBufferTarget-stats.html (renamed from testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-video-jitterBufferTarget-stats.html)0
-rw-r--r--testing/web-platform/tests/webrtc/WEB_FEATURES.yml4
-rw-r--r--testing/web-platform/tests/webrtc/back-forward-cache-with-open-webrtc-connection.https.window.js1
-rw-r--r--testing/web-platform/tests/websockets/Send-binary-arraybufferview-float16.any.js40
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/basic_auth_wsh.py2
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/delayed-passive-close_wsh.py2
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/echo-cookie_wsh.py2
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/echo-query_v13_wsh.py2
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/echo-query_wsh.py2
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/echo_raw_wsh.py2
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/echo_wsh.py2
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/empty-message_wsh.py2
-rw-r--r--testing/web-platform/tests/websockets/handlers/msg_channel_wsh.py2
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/origin_wsh.py2
-rw-r--r--testing/web-platform/tests/websockets/handlers/passive-close-abort_wsh.py2
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/protocol_array_wsh.py2
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/protocol_wsh.py2
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/referrer_wsh.py2
-rw-r--r--testing/web-platform/tests/websockets/handlers/remote-close_wsh.py2
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/simple_handshake_wsh.py4
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/sleep_10_v13_wsh.py2
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/stash_responder_blocking_wsh.py2
-rwxr-xr-xtesting/web-platform/tests/websockets/handlers/stash_responder_wsh.py2
-rw-r--r--testing/web-platform/tests/webvtt/parsing/cue-text-parsing/tests/tree-building.html3
-rw-r--r--testing/web-platform/tests/workers/Worker-creation-happens-in-parallel.https.html18
-rw-r--r--testing/web-platform/tests/workers/Worker-creation-happens-in-parallel.https.html.headers2
-rw-r--r--testing/web-platform/tests/workers/Worker-postMessage-happens-in-parallel.https.html20
-rw-r--r--testing/web-platform/tests/workers/Worker-postMessage-happens-in-parallel.https.html.headers2
-rw-r--r--testing/web-platform/tests/workers/modules/WEB_FEATURES.yml7
-rw-r--r--testing/web-platform/tests/workers/semantics/interface-objects/001.worker.js1
-rw-r--r--testing/web-platform/tests/workers/semantics/interface-objects/003.any.js1
-rw-r--r--testing/web-platform/tests/workers/support/Worker-creation-happens-in-parallel.js3
-rw-r--r--testing/web-platform/tests/workers/support/Worker-creation-happens-in-parallel.js.headers1
-rw-r--r--testing/web-platform/tests/workers/support/Worker-postMessage-happens-in-parallel.js7
-rw-r--r--testing/web-platform/tests/workers/support/Worker-postMessage-happens-in-parallel.js.headers1
-rwxr-xr-xtesting/web-platform/tests/wpt4
-rw-r--r--testing/web-platform/tests/x-frame-options/invalid.html6
-rw-r--r--testing/web-platform/tests/xhr/send-data-sharedarraybuffer.any.js2
2088 files changed, 80946 insertions, 18252 deletions
diff --git a/testing/web-platform/tests/.azure-pipelines.yml b/testing/web-platform/tests/.azure-pipelines.yml
index 1a21d2f7a0..36e745a87f 100644
--- a/testing/web-platform/tests/.azure-pipelines.yml
+++ b/testing/web-platform/tests/.azure-pipelines.yml
@@ -39,7 +39,7 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
- template: tools/ci/azure/affected_tests.yml
parameters:
artifactName: 'safari-preview-affected-tests'
@@ -56,7 +56,7 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
- template: tools/ci/azure/affected_tests.yml
parameters:
checkoutCommit: 'HEAD^1'
@@ -77,7 +77,7 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
- template: tools/ci/azure/checkout.yml
- script: |
set -eux -o pipefail
@@ -98,7 +98,7 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/install_fonts.yml
- template: tools/ci/azure/install_certs.yml
@@ -132,8 +132,8 @@ jobs:
- template: tools/ci/azure/publish_logs.yml
- template: tools/ci/azure/sysdiagnose.yml
-- job: tools_unittest_mac_py37
- displayName: 'tools/ unittests: macOS + Python 3.7'
+- job: tools_unittest_mac_py38
+ displayName: 'tools/ unittests: macOS + Python 3.8'
dependsOn: decision
condition: dependencies.decision.outputs['test_jobs.tools_unittest']
pool:
@@ -141,15 +141,15 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.7'
+ versionSpec: '3.8'
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/tox_pytest.yml
parameters:
directory: tools/
- toxenv: py37
+ toxenv: py38
- job: tools_unittest_mac_py311
- displayName: 'tools/ unittests: macOS + Python 3.11'
+ displayName: 'tools/ unittests: macOS + Python 3.12'
dependsOn: decision
condition: dependencies.decision.outputs['test_jobs.tools_unittest']
pool:
@@ -157,15 +157,15 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/tox_pytest.yml
parameters:
directory: tools/
toxenv: py311
-- job: wptrunner_unittest_mac_py37
- displayName: 'tools/wptrunner/ unittests: macOS + Python 3.7'
+- job: wptrunner_unittest_mac_py38
+ displayName: 'tools/wptrunner/ unittests: macOS + Python 3.8'
dependsOn: decision
condition: dependencies.decision.outputs['test_jobs.wptrunner_unittest']
pool:
@@ -173,15 +173,15 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.7'
+ versionSpec: '3.8'
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/tox_pytest.yml
parameters:
directory: tools/wptrunner/
- toxenv: py37
+ toxenv: py38
- job: wptrunner_unittest_mac_py311
- displayName: 'tools/wptrunner/ unittests: macOS + Python 3.11'
+ displayName: 'tools/wptrunner/ unittests: macOS + Python 3.12'
dependsOn: decision
condition: dependencies.decision.outputs['test_jobs.wptrunner_unittest']
pool:
@@ -189,15 +189,15 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/tox_pytest.yml
parameters:
directory: tools/wptrunner/
toxenv: py311
-- job: wpt_integration_mac_py37
- displayName: 'tools/wpt/ tests: macOS + Python 3.7'
+- job: wpt_integration_mac_py38
+ displayName: 'tools/wpt/ tests: macOS + Python 3.8'
dependsOn: decision
condition: dependencies.decision.outputs['test_jobs.wpt_integration']
pool:
@@ -206,7 +206,7 @@ jobs:
# full checkout required
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.7'
+ versionSpec: '3.8'
- template: tools/ci/azure/install_chrome.yml
- template: tools/ci/azure/install_firefox.yml
- template: tools/ci/azure/update_hosts.yml
@@ -214,10 +214,10 @@ jobs:
- template: tools/ci/azure/tox_pytest.yml
parameters:
directory: tools/wpt/
- toxenv: py37
+ toxenv: py38
- job: wpt_integration_mac_py311
- displayName: 'tools/wpt/ tests: macOS + Python 3.11'
+ displayName: 'tools/wpt/ tests: macOS + Python 3.12'
dependsOn: decision
condition: dependencies.decision.outputs['test_jobs.wpt_integration']
pool:
@@ -226,7 +226,7 @@ jobs:
# full checkout required
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
- template: tools/ci/azure/install_chrome.yml
- template: tools/ci/azure/install_firefox.yml
- template: tools/ci/azure/update_hosts.yml
@@ -236,8 +236,8 @@ jobs:
directory: tools/wpt/
toxenv: py311
-- job: tools_unittest_win_py37
- displayName: 'tools/ unittests: Windows + Python 3.7'
+- job: tools_unittest_win_py38
+ displayName: 'tools/ unittests: Windows + Python 3.8'
dependsOn: decision
condition: dependencies.decision.outputs['test_jobs.tools_unittest']
pool:
@@ -247,16 +247,16 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.7'
+ versionSpec: '3.8'
addToPath: false
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/tox_pytest.yml
parameters:
directory: tools/
- toxenv: py37
+ toxenv: py38
- job: tools_unittest_win_py311
- displayName: 'tools/ unittests: Windows + Python 3.11'
+ displayName: 'tools/ unittests: Windows + Python 3.12'
dependsOn: decision
condition: dependencies.decision.outputs['test_jobs.tools_unittest']
pool:
@@ -264,7 +264,7 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
addToPath: false
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/tox_pytest.yml
@@ -272,8 +272,8 @@ jobs:
directory: tools/
toxenv: py311
-- job: wptrunner_unittest_win_py37
- displayName: 'tools/wptrunner/ unittests: Windows + Python 3.7'
+- job: wptrunner_unittest_win_py38
+ displayName: 'tools/wptrunner/ unittests: Windows + Python 3.8'
dependsOn: decision
condition: dependencies.decision.outputs['test_jobs.wptrunner_unittest']
pool:
@@ -281,16 +281,16 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.7'
+ versionSpec: '3.8'
addToPath: false
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/tox_pytest.yml
parameters:
directory: tools/wptrunner/
- toxenv: py37
+ toxenv: py38
- job: wptrunner_unittest_win_py311
- displayName: 'tools/wptrunner/ unittests: Windows + Python 3.11'
+ displayName: 'tools/wptrunner/ unittests: Windows + Python 3.12'
dependsOn: decision
condition: dependencies.decision.outputs['test_jobs.wptrunner_unittest']
pool:
@@ -298,7 +298,7 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
addToPath: false
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/tox_pytest.yml
@@ -306,8 +306,8 @@ jobs:
directory: tools/wptrunner/
toxenv: py311
-- job: wpt_integration_win_py37
- displayName: 'tools/wpt/ tests: Windows + Python 3.7'
+- job: wpt_integration_win_py38
+ displayName: 'tools/wpt/ tests: Windows + Python 3.8'
dependsOn: decision
condition: dependencies.decision.outputs['test_jobs.wpt_integration']
pool:
@@ -316,7 +316,7 @@ jobs:
# full checkout required
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.7'
+ versionSpec: '3.8'
# currently just using the outdated Chrome/Firefox on the VM rather than
# figuring out how to install Chrome Dev channel on Windows
# - template: tools/ci/azure/install_chrome.yml
@@ -326,10 +326,10 @@ jobs:
- template: tools/ci/azure/tox_pytest.yml
parameters:
directory: tools/wpt/
- toxenv: py37
+ toxenv: py38
- job: wpt_integration_win_py311
- displayName: 'tools/wpt/ tests: Windows + Python 3.11'
+ displayName: 'tools/wpt/ tests: Windows + Python 3.12'
dependsOn: decision
condition: dependencies.decision.outputs['test_jobs.wpt_integration']
pool:
@@ -338,7 +338,7 @@ jobs:
# full checkout required
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
# currently just using the outdated Chrome/Firefox on the VM rather than
# figuring out how to install Chrome Dev channel on Windows
# - template: tools/ci/azure/install_chrome.yml
@@ -364,7 +364,7 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
- template: tools/ci/azure/system_info.yml
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/install_certs.yml
@@ -373,7 +373,7 @@ jobs:
channel: stable
- template: tools/ci/azure/update_hosts.yml
- template: tools/ci/azure/update_manifest.yml
- - script: python ./wpt run --yes --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --install-fonts --this-chunk $(System.JobPositionInPhase) --total-chunks $(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel stable edgechromium
+ - script: python ./wpt run --yes --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --install-fonts --this-chunk $(System.JobPositionInPhase) --total-chunks $(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel stable edge
displayName: 'Run tests (Edge Stable)'
- task: PublishBuildArtifacts@1
displayName: 'Publish results'
@@ -400,7 +400,7 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
- template: tools/ci/azure/system_info.yml
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/install_certs.yml
@@ -409,7 +409,7 @@ jobs:
channel: dev
- template: tools/ci/azure/update_hosts.yml
- template: tools/ci/azure/update_manifest.yml
- - script: python ./wpt run --yes --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --install-fonts --this-chunk $(System.JobPositionInPhase) --total-chunks $(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel dev edgechromium
+ - script: python ./wpt run --yes --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --install-fonts --this-chunk $(System.JobPositionInPhase) --total-chunks $(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel dev edge
displayName: 'Run tests (Edge Dev)'
- task: PublishBuildArtifacts@1
displayName: 'Publish results'
@@ -436,7 +436,7 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/install_certs.yml
- template: tools/ci/azure/install_edge.yml
@@ -444,7 +444,7 @@ jobs:
channel: canary
- template: tools/ci/azure/update_hosts.yml
- template: tools/ci/azure/update_manifest.yml
- - script: python ./wpt run --yes --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --install-fonts --this-chunk $(System.JobPositionInPhase) --total-chunks $(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel canary edgechromium
+ - script: python ./wpt run --yes --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --install-fonts --this-chunk $(System.JobPositionInPhase) --total-chunks $(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel canary edge
displayName: 'Run tests (Edge Canary)'
- task: PublishBuildArtifacts@1
displayName: 'Publish results'
@@ -471,7 +471,7 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/install_certs.yml
- template: tools/ci/azure/color_profile.yml
@@ -511,7 +511,7 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/install_certs.yml
- template: tools/ci/azure/color_profile.yml
@@ -548,7 +548,7 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
- versionSpec: '3.11'
+ versionSpec: '3.12'
- template: tools/ci/azure/checkout.yml
- template: tools/ci/azure/install_certs.yml
- template: tools/ci/azure/color_profile.yml
diff --git a/testing/web-platform/tests/.github/workflows/documentation.yml b/testing/web-platform/tests/.github/workflows/documentation.yml
index 0efa736ade..eecf8ea636 100644
--- a/testing/web-platform/tests/.github/workflows/documentation.yml
+++ b/testing/web-platform/tests/.github/workflows/documentation.yml
@@ -19,7 +19,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: '3.11'
+ python-version: '3.12'
- name: Set up Node
uses: actions/setup-node@v4
with:
diff --git a/testing/web-platform/tests/.github/workflows/interfaces.yml b/testing/web-platform/tests/.github/workflows/interfaces.yml
index 04636e0bb3..ff2a679b59 100644
--- a/testing/web-platform/tests/.github/workflows/interfaces.yml
+++ b/testing/web-platform/tests/.github/workflows/interfaces.yml
@@ -15,7 +15,7 @@ jobs:
- name: Create pull request
# Use a conditional step instead of a conditional job to work around #20700.
if: github.repository == 'web-platform-tests/wpt'
- uses: peter-evans/create-pull-request@v5
+ uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
author: wpt-pr-bot <wpt-pr-bot@users.noreply.github.com>
diff --git a/testing/web-platform/tests/.github/workflows/manifest.yml b/testing/web-platform/tests/.github/workflows/manifest.yml
index 597259eb72..aae43615a9 100644
--- a/testing/web-platform/tests/.github/workflows/manifest.yml
+++ b/testing/web-platform/tests/.github/workflows/manifest.yml
@@ -13,7 +13,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: '3.11'
+ python-version: '3.12'
- name: Checkout
uses: actions/checkout@v4
with:
diff --git a/testing/web-platform/tests/.github/workflows/regen_certs.yml b/testing/web-platform/tests/.github/workflows/regen_certs.yml
index d3e1240e79..bb0cea9f39 100644
--- a/testing/web-platform/tests/.github/workflows/regen_certs.yml
+++ b/testing/web-platform/tests/.github/workflows/regen_certs.yml
@@ -12,7 +12,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: '3.11'
+ python-version: '3.12'
- name: Checkout
uses: actions/checkout@v4
- name: Regenerate certs
@@ -24,7 +24,7 @@ jobs:
- name: Commit and create pull request
# Use a conditional step instead of a conditional job to work around #20700.
if: github.repository == 'web-platform-tests/wpt'
- uses: peter-evans/create-pull-request@v5
+ uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
author: wpt-pr-bot <wpt-pr-bot@users.noreply.github.com>
diff --git a/testing/web-platform/tests/FileAPI/blob/Blob-constructor.any.js b/testing/web-platform/tests/FileAPI/blob/Blob-constructor.any.js
index d16f760cae..6dc44e8e15 100644
--- a/testing/web-platform/tests/FileAPI/blob/Blob-constructor.any.js
+++ b/testing/web-platform/tests/FileAPI/blob/Blob-constructor.any.js
@@ -290,10 +290,11 @@ test_blob(function() {
new Int16Array([0x4150, 0x5353]),
new Uint32Array([0x53534150]),
new Int32Array([0x53534150]),
+ new Float16Array([2.65625, 58.59375]),
new Float32Array([0xD341500000])
]);
}, {
- expected: "PASSPASSPASSPASSPASSPASSPASS",
+ expected: "PASSPASSPASSPASSPASSPASSPASSPASS",
type: "",
desc: "Passing typed arrays as elements of the blobParts array should work."
});
diff --git a/testing/web-platform/tests/IndexedDB/idb-binary-key-detached.htm b/testing/web-platform/tests/IndexedDB/idb-binary-key-detached.htm
index ac6fc2ef98..a4ce3fbce0 100644
--- a/testing/web-platform/tests/IndexedDB/idb-binary-key-detached.htm
+++ b/testing/web-platform/tests/IndexedDB/idb-binary-key-detached.htm
@@ -23,7 +23,7 @@ indexeddb_test(
worker.postMessage('', [buffer]);
assert_equals(array.byteLength, 0);
- assert_throws_js(TypeError, () => { store.put('', buffer); });
+ assert_throws_dom("DataError", () => { store.put('', buffer); });
t.done();
},
'Detached ArrayBuffer'
@@ -43,7 +43,7 @@ indexeddb_test(
worker.postMessage('', [array.buffer]);
assert_equals(array.length, 0);
- assert_throws_js(TypeError, () => { store.put('', array); });
+ assert_throws_dom("DataError", () => { store.put('', array); });
t.done();
},
'Detached TypedArray'
diff --git a/testing/web-platform/tests/IndexedDB/idb-binary-key-roundtrip.htm b/testing/web-platform/tests/IndexedDB/idb-binary-key-roundtrip.htm
index de3889a71c..d1bf4016f9 100644
--- a/testing/web-platform/tests/IndexedDB/idb-binary-key-roundtrip.htm
+++ b/testing/web-platform/tests/IndexedDB/idb-binary-key-roundtrip.htm
@@ -83,6 +83,7 @@ function view_type_test(type) {
'Int16Array',
'Uint32Array',
'Int32Array',
+ 'Float16Array',
'Float32Array',
'Float64Array'
].forEach((type) => { view_type_test(type); });
diff --git a/testing/web-platform/tests/IndexedDB/serialize-sharedarraybuffer-throws.https.html b/testing/web-platform/tests/IndexedDB/serialize-sharedarraybuffer-throws.https.html
index 613ddfe99d..bff63fad8d 100644
--- a/testing/web-platform/tests/IndexedDB/serialize-sharedarraybuffer-throws.https.html
+++ b/testing/web-platform/tests/IndexedDB/serialize-sharedarraybuffer-throws.https.html
@@ -13,7 +13,7 @@
let open_rq = createdb(t);
open_rq.onupgradeneeded = function(e) {
let db = e.target.result;
- let objStore = db.createObjectStore("test", { keyPath:"pKey" });
+ let objStore = db.createObjectStore("test");
let sab = new SharedArrayBuffer(256);
diff --git a/testing/web-platform/tests/IndexedDB/structured-clone.any.js b/testing/web-platform/tests/IndexedDB/structured-clone.any.js
index 15ab0359e2..8ab73ac57a 100644
--- a/testing/web-platform/tests/IndexedDB/structured-clone.any.js
+++ b/testing/web-platform/tests/IndexedDB/structured-clone.any.js
@@ -169,7 +169,7 @@ cloneObjectTest(new Uint8Array([0, 1, 254, 255]).buffer, (orig, clone) => {
// TODO SharedArrayBuffer
// Array Buffer Views
-[
+let byteArrays = [
new Uint8Array([]),
new Uint8Array([0, 1, 254, 255]),
new Uint16Array([0x0000, 0x0001, 0xFFFE, 0xFFFF]),
@@ -181,7 +181,14 @@ cloneObjectTest(new Uint8Array([0, 1, 254, 255]).buffer, (orig, clone) => {
new Float32Array([-Infinity, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, Infinity, NaN]),
new Float64Array([-Infinity, -Number.MAX_VALUE, -Number.MIN_VALUE, 0,
Number.MIN_VALUE, Number.MAX_VALUE, Infinity, NaN])
-].forEach(value => cloneObjectTest(value, (orig, clone) => {
+]
+
+if (typeof Float16Array !== 'undefined') {
+ byteArrays.push(
+ new Float16Array([-Infinity, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, Infinity, NaN]));
+}
+
+byteArrays.forEach(value => cloneObjectTest(value, (orig, clone) => {
assert_array_equals(orig, clone);
}));
diff --git a/testing/web-platform/tests/WebCryptoAPI/getRandomValues.any.js b/testing/web-platform/tests/WebCryptoAPI/getRandomValues.any.js
index 1a3370ea13..2b82b9bedb 100644
--- a/testing/web-platform/tests/WebCryptoAPI/getRandomValues.any.js
+++ b/testing/web-platform/tests/WebCryptoAPI/getRandomValues.any.js
@@ -1,6 +1,9 @@
// Step 1.
test(function() {
assert_throws_dom("TypeMismatchError", function() {
+ self.crypto.getRandomValues(new Float16Array(6))
+ }, "Float16Array")
+ assert_throws_dom("TypeMismatchError", function() {
self.crypto.getRandomValues(new Float32Array(6))
}, "Float32Array")
assert_throws_dom("TypeMismatchError", function() {
@@ -8,6 +11,10 @@ test(function() {
}, "Float64Array")
assert_throws_dom("TypeMismatchError", function() {
+ const len = 65536 / Float16Array.BYTES_PER_ELEMENT + 1;
+ self.crypto.getRandomValues(new Float16Array(len));
+ }, "Float16Array (too long)")
+ assert_throws_dom("TypeMismatchError", function() {
const len = 65536 / Float32Array.BYTES_PER_ELEMENT + 1;
self.crypto.getRandomValues(new Float32Array(len));
}, "Float32Array (too long)")
diff --git a/testing/web-platform/tests/accessibility/crashtests/detached-line.html b/testing/web-platform/tests/accessibility/crashtests/detached-line.html
new file mode 100644
index 0000000000..a6a7520c0d
--- /dev/null
+++ b/testing/web-platform/tests/accessibility/crashtests/detached-line.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<style>
+div:before { position: absolute; content: 'x'; }
+</style>
+<!--
+<img usemap="#map1">
+ <map>
+ <caption>
+ <map name=child-map>
+ yz
+ </map>
+ </caption>
+ </map>
+-->
+<script>
+window.addEventListener('load', () => {
+ const caption = document.createElement('caption');
+ const childMap = document.createElement('map');
+ childMap.setAttribute('name', 'child-map');
+ const map2 = document.createElement('map');
+ document.documentElement.appendChild(map2);
+ const img = document.createElement('img');
+ img.setAttribute('usemap', '#child-map');
+ childMap.appendChild(document.createTextNode('yz'));
+ document.documentElement.appendChild(img);
+ caption.appendChild(childMap);
+ map2.appendChild(caption);
+});
+</script>
diff --git a/testing/web-platform/tests/accname/name/comp_name_from_content.html b/testing/web-platform/tests/accname/name/comp_name_from_content.html
index 3504658ea4..2f6b3ab6b4 100644
--- a/testing/web-platform/tests/accname/name/comp_name_from_content.html
+++ b/testing/web-platform/tests/accname/name/comp_name_from_content.html
@@ -239,7 +239,7 @@
<span class="note" id="crossref_link">link</span><!-- this text is skipped the first time around because of aria-labelledby on parent element -->
</a>
<!-- but it's picked up again in inverse order b/c of cross-referencial aria-labelledby edge case -->
- <img id="nested_image_label_3" alt="image" aria-labelledby="crossref_link" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==">
+ <img id="nested_image_label3" alt="image" aria-labelledby="crossref_link" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==">
</h3>
<!-- self-referencial edge case-->
diff --git a/testing/web-platform/tests/accname/name/comp_text_node.html b/testing/web-platform/tests/accname/name/comp_text_node.html
index a31f9e0245..f9cb8f1baf 100644
--- a/testing/web-platform/tests/accname/name/comp_text_node.html
+++ b/testing/web-platform/tests/accname/name/comp_text_node.html
@@ -101,11 +101,11 @@
<br>
<h1>text node, with leading/trailing non-breaking space</h1>
-<span role="button" tabindex="0" class="ex" data-expectedlabel="button&nbsp;label" data-testname="span[role=button] with text node, with leading/trailing non-breaking space">&nbsp;button&nbsp;label&nbsp;</span>
-<div role="heading" class="ex" data-expectedlabel="heading&nbsp;label" data-testname="div[role=heading] with text node, with leading/trailing non-breaking space">&nbsp;heading&nbsp;label&nbsp;</div>
-<button class="ex" data-expectedlabel="button&nbsp;label" data-testname="button with text node, with leading/trailing non-breaking space">&nbsp;button&nbsp;label&nbsp;</button>
-<h3 class="ex" data-expectedlabel="heading&nbsp;label" data-testname="heading with text node, with leading/trailing non-breaking space">&nbsp;heading&nbsp;label&nbsp;</h3>
-<a href="#" class="ex" data-expectedlabel="link&nbsp;label" data-testname="link with text node, with leading/trailing non-breaking space">&nbsp;link&nbsp;label&nbsp;</a>
+<span role="button" tabindex="0" class="ex" data-expectedlabel="&nbsp;button&nbsp;label&nbsp;" data-testname="span[role=button] with text node, with leading/trailing non-breaking space">&nbsp;button&nbsp;label&nbsp;</span>
+<div role="heading" class="ex" data-expectedlabel="&nbsp;heading&nbsp;label&nbsp;" data-testname="div[role=heading] with text node, with leading/trailing non-breaking space">&nbsp;heading&nbsp;label&nbsp;</div>
+<button class="ex" data-expectedlabel="&nbsp;button&nbsp;label&nbsp;" data-testname="button with text node, with leading/trailing non-breaking space">&nbsp;button&nbsp;label&nbsp;</button>
+<h3 class="ex" data-expectedlabel="&nbsp;heading&nbsp;label&nbsp;" data-testname="heading with text node, with leading/trailing non-breaking space">&nbsp;heading&nbsp;label&nbsp;</h3>
+<a href="#" class="ex" data-expectedlabel="&nbsp;link&nbsp;label&nbsp;" data-testname="link with text node, with leading/trailing non-breaking space">&nbsp;link&nbsp;label&nbsp;</a>
<br>
<h1>text node, with mixed space and non-breaking space</h1>
diff --git a/testing/web-platform/tests/attribution-reporting/referrer-policy.sub.https.html b/testing/web-platform/tests/attribution-reporting/referrer-policy.sub.https.html
new file mode 100644
index 0000000000..ee4e0c9a8c
--- /dev/null
+++ b/testing/web-platform/tests/attribution-reporting/referrer-policy.sub.https.html
@@ -0,0 +1,56 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name=timeout content=long>
+<meta name=variant content="?method=a">
+<meta name=variant content="?method=a&noreferrer">
+<meta name=variant content="?method=img">
+<meta name=variant content="?method=img&noreferrer">
+<meta name=variant content="?method=open">
+<meta name=variant content="?method=open&noreferrer">
+<meta name=variant content="?method=script">
+<meta name=variant content="?method=script&noreferrer">
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body>
+<script>
+const waitForRequest = async () => {
+ const url = blankURL();
+ url.searchParams.set('get-requests', 'true');
+
+ for (let i = 0; i < 20; i++) {
+ const resp = await fetch(url);
+ const payload = await resp.json();
+ if (payload !== null && payload.length > 0) {
+ return payload;
+ }
+ await delay(100);
+ }
+ throw new Error('Timeout polling requests');
+};
+
+const searchParams = new URLSearchParams(location.search);
+
+promise_test(async t => {
+ const noreferrer = searchParams.has('noreferrer');
+
+ await registerAttributionSrc({
+ method: 'variant',
+ extraQueryParams: {'store-request': 'true'},
+ referrerPolicy: noreferrer ? 'no-referrer' : '',
+ });
+
+ const requests = await waitForRequest();
+ assert_equals(requests.length, 1);
+
+ if (noreferrer) {
+ assert_not_own_property(requests[0], 'referer');
+ } else {
+ assert_own_property(requests[0], 'referer');
+ assert_equals(requests[0].referer, location.toString());
+ }
+
+}, 'attributionsrc referrer policy is propagated.');
+</script>
diff --git a/testing/web-platform/tests/attribution-reporting/request-format.sub.https.html b/testing/web-platform/tests/attribution-reporting/request-format.sub.https.html
index a9e36dd126..83a2d8f6bd 100644
--- a/testing/web-platform/tests/attribution-reporting/request-format.sub.https.html
+++ b/testing/web-platform/tests/attribution-reporting/request-format.sub.https.html
@@ -53,9 +53,7 @@ promise_test(async t => {
} else {
assert_not_own_property(requests[0], 'attribution-reporting-eligible');
}
- assert_equals(requests[0].referer, location.toString());
- // TODO(apaseltiner): Test various referrer policies.
// TODO(apaseltiner): Test cookie propagation.
const expectedURL = blankURL();
diff --git a/testing/web-platform/tests/attribution-reporting/resources/helpers.js b/testing/web-platform/tests/attribution-reporting/resources/helpers.js
index e5c749931e..054df6b972 100644
--- a/testing/web-platform/tests/attribution-reporting/resources/helpers.js
+++ b/testing/web-platform/tests/attribution-reporting/resources/helpers.js
@@ -171,6 +171,7 @@ const registerAttributionSrc = async ({
extraQueryParams = {},
reportingOrigin,
extraHeaders = [],
+ referrerPolicy = '',
}) => {
const searchParams = new URLSearchParams(location.search);
@@ -201,7 +202,6 @@ const registerAttributionSrc = async ({
headers.push({name, value: cookie});
}
-
let credentials;
if (method === 'fetch') {
const params = getFetchParams(reportingOrigin, cookie);
@@ -219,6 +219,7 @@ const registerAttributionSrc = async ({
switch (method) {
case 'img':
const img = document.createElement('img');
+ img.referrerPolicy = referrerPolicy;
if (eligible === null) {
img.attributionSrc = url;
} else {
@@ -236,6 +237,7 @@ const registerAttributionSrc = async ({
return 'event';
case 'script':
const script = document.createElement('script');
+ script.referrerPolicy = referrerPolicy;
if (eligible === null) {
script.attributionSrc = url;
} else {
@@ -249,6 +251,7 @@ const registerAttributionSrc = async ({
return 'event';
case 'a':
const a = document.createElement('a');
+ a.referrerPolicy = referrerPolicy;
a.target = '_blank';
a.textContent = 'link';
if (eligible === null) {
@@ -263,12 +266,13 @@ const registerAttributionSrc = async ({
return 'navigation';
case 'open':
await test_driver.bless('open window', () => {
+ const feature = referrerPolicy === 'no-referrer' ? 'noreferrer' : '';
if (eligible === null) {
open(
blankURL(), '_blank',
- `attributionsrc=${encodeURIComponent(url)}`);
+ `attributionsrc=${encodeURIComponent(url)} ${feature}`);
} else {
- open(url, '_blank', 'attributionsrc');
+ open(url, '_blank', `attributionsrc ${feature}`);
}
});
return 'navigation';
@@ -277,7 +281,7 @@ const registerAttributionSrc = async ({
if (eligible !== null) {
attributionReporting = JSON.parse(eligible);
}
- await fetch(url, {credentials, attributionReporting});
+ await fetch(url, {credentials, attributionReporting, referrerPolicy});
return 'event';
}
case 'xhr':
diff --git a/testing/web-platform/tests/clipboard-apis/clipboard-item.https.html b/testing/web-platform/tests/clipboard-apis/clipboard-item.https.html
index 7e148703a2..78acd1104a 100644
--- a/testing/web-platform/tests/clipboard-apis/clipboard-item.https.html
+++ b/testing/web-platform/tests/clipboard-apis/clipboard-item.https.html
@@ -96,21 +96,28 @@ promise_test(async () => {
assert_equals(text, 'xxx');
}, "getType(DOMString invalid type) converts DOMString to Blob");
-promise_test(async () => {
- assert_true(ClipboardItem.supports('text/plain'));
- assert_true(ClipboardItem.supports('text/html'));
- assert_true(ClipboardItem.supports('image/png'));
- assert_true(ClipboardItem.supports('image/svg+xml'));
- assert_false(ClipboardItem.supports('web '));
- assert_false(ClipboardItem.supports('web')); // without space.
- assert_false(ClipboardItem.supports('web foo'));
- assert_false(ClipboardItem.supports('foo/bar'));
- assert_true(ClipboardItem.supports('web foo/bar'));
- assert_true(ClipboardItem.supports('web text/html'));
- assert_false(ClipboardItem.supports('weB text/html'));
- assert_false(ClipboardItem.supports(' web text/html'));
- assert_false(ClipboardItem.supports('not a/real type'));
- assert_false(ClipboardItem.supports(''));
- assert_false(ClipboardItem.supports(' '));
-}, "supports(DOMString) returns true for types that are supported, false otherwise");
+[
+ // mandatory data types
+ ['text/plain', true],
+ ['text/html', true],
+ ['image/png', true],
+ // optional data types
+ ['image/svg+xml', true],
+ ['web foo/bar', true],
+ ['web text/html', true],
+ // invalid types
+ ['web ', false],
+ ['web', false],
+ ['web foo', false],
+ ['foo/bar', false],
+ ['weB text/html', false],
+ [' web text/html', false],
+ ['not a/real type', false],
+ ['', false],
+ [' ', false],
+].forEach(([type, result]) => {
+ promise_test(async () => {
+ assert_equals(ClipboardItem.supports(type), result);
+ }, `supports(${type}) returns ${result ? "true" : "false"}`);
+});
</script>
diff --git a/testing/web-platform/tests/close-watcher/abortsignal.html b/testing/web-platform/tests/close-watcher/abortsignal.html
index 9229b37cf6..ec360f483d 100644
--- a/testing/web-platform/tests/close-watcher/abortsignal.html
+++ b/testing/web-platform/tests/close-watcher/abortsignal.html
@@ -49,7 +49,7 @@ test(() => {
watcher.requestClose();
controller.abort();
- assert_equals(oncancel_call_count_, 0);
+ assert_equals(oncancel_call_count_, 1);
assert_equals(onclose_call_count_, 1);
}, "requestClose() then abortController.abort() fires only one close event");
@@ -92,7 +92,7 @@ promise_test(async t => {
await sendCloseRequest();
controller.abort();
- assert_equals(oncancel_call_count_, 0);
+ assert_equals(oncancel_call_count_, 1);
assert_equals(onclose_call_count_, 1);
}, "Esc key then abortController.abort() fires only one close event");
diff --git a/testing/web-platform/tests/close-watcher/basic.html b/testing/web-platform/tests/close-watcher/basic.html
index 9951e54031..79a91e127e 100644
--- a/testing/web-platform/tests/close-watcher/basic.html
+++ b/testing/web-platform/tests/close-watcher/basic.html
@@ -14,8 +14,8 @@ test(t => {
watcher.requestClose();
- assert_array_equals(events, ["close"]);
-}, "requestClose() with no user activation only fires close");
+ assert_array_equals(events, ["cancel[cancelable=false]", "close"]);
+}, "requestClose() with no user activation");
test(t => {
let events = [];
@@ -25,7 +25,7 @@ test(t => {
watcher.requestClose();
assert_array_equals(events, []);
-}, "destroy() then requestClose() fires no events");
+}, "destroy() then requestClose()");
test(t => {
let events = [];
@@ -36,18 +36,18 @@ test(t => {
watcher.requestClose();
assert_array_equals(events, ["close"]);
-}, "close() then requestClose() fires only one close event");
+}, "close() then requestClose()");
test(t => {
let events = [];
let watcher = createRecordingCloseWatcher(t, events);
watcher.requestClose();
- assert_array_equals(events, ["close"]);
+ assert_array_equals(events, ["cancel[cancelable=false]", "close"]);
watcher.destroy();
- assert_array_equals(events, ["close"]);
-}, "requestClose() then destroy() fires only one close event");
+ assert_array_equals(events, ["cancel[cancelable=false]", "close"]);
+}, "requestClose() then destroy()");
test(t => {
let events = [];
@@ -58,7 +58,7 @@ test(t => {
watcher.destroy();
assert_array_equals(events, ["close"]);
-}, "close() then destroy() fires only one close event");
+}, "close() then destroy()");
promise_test(async t => {
let events = [];
@@ -68,7 +68,7 @@ promise_test(async t => {
await sendCloseRequest();
assert_array_equals(events, []);
-}, "destroy() then close request fires no events");
+}, "destroy() then close request");
promise_test(async t => {
let events = [];
@@ -77,6 +77,6 @@ promise_test(async t => {
await sendCloseRequest();
watcher.destroy();
- assert_array_equals(events, ["close"]);
-}, "Close request then destroy() fires only one close event");
+ assert_array_equals(events, ["cancel[cancelable=false]", "close"]);
+}, "Close request then destroy()");
</script>
diff --git a/testing/web-platform/tests/close-watcher/esc-key/keypress.html b/testing/web-platform/tests/close-watcher/esc-key/keypress.html
index 8dd58b064d..c3bfcc0c33 100644
--- a/testing/web-platform/tests/close-watcher/esc-key/keypress.html
+++ b/testing/web-platform/tests/close-watcher/esc-key/keypress.html
@@ -16,6 +16,6 @@ promise_test(async t => {
await sendEscKey();
- assert_array_equals(events, ["close"]);
+ assert_array_equals(events, ["cancel[cancelable=false]", "close"]);
}, "A keypress listener can NOT prevent the Esc keypress from being interpreted as a close request");
</script>
diff --git a/testing/web-platform/tests/close-watcher/esc-key/keyup.html b/testing/web-platform/tests/close-watcher/esc-key/keyup.html
index 341012d6bc..7c75ef7969 100644
--- a/testing/web-platform/tests/close-watcher/esc-key/keyup.html
+++ b/testing/web-platform/tests/close-watcher/esc-key/keyup.html
@@ -16,6 +16,6 @@ promise_test(async t => {
await sendEscKey();
- assert_array_equals(events, ["close"]);
+ assert_array_equals(events, ["cancel[cancelable=false]", "close"]);
}, "A keyup listener can NOT prevent the Esc keypress from being interpreted as a close request");
</script>
diff --git a/testing/web-platform/tests/close-watcher/esc-key/not-user-activation.html b/testing/web-platform/tests/close-watcher/esc-key/not-user-activation.html
index ac29f84f06..a8d5d22fcf 100644
--- a/testing/web-platform/tests/close-watcher/esc-key/not-user-activation.html
+++ b/testing/web-platform/tests/close-watcher/esc-key/not-user-activation.html
@@ -14,6 +14,6 @@ promise_test(async t => {
await sendEscKey();
- assert_array_equals(events, ["close"]);
-}, "Esc key does not count as user activation, so if it is the sole user interaction, that fires close but not cancel");
+ assert_array_equals(events, ["cancel[cancelable=false]", "close"]);
+}, "Esc key does not count as user activation, so if it is the sole user interaction, cancel is cancelable=false");
</script>
diff --git a/testing/web-platform/tests/close-watcher/inside-event-listeners.html b/testing/web-platform/tests/close-watcher/inside-event-listeners.html
index ac037fc147..47f431e250 100644
--- a/testing/web-platform/tests/close-watcher/inside-event-listeners.html
+++ b/testing/web-platform/tests/close-watcher/inside-event-listeners.html
@@ -17,10 +17,10 @@ promise_test(async t => {
await test_driver.bless("give user activation so that cancel will fire", () => {
watcher.requestClose();
});
- assert_array_equals(events, ["cancel"]);
+ assert_array_equals(events, ["cancel[cancelable=true]"]);
watcher.requestClose();
- assert_array_equals(events, ["cancel"], "since it was inactive, no more events fired");
+ assert_array_equals(events, ["cancel[cancelable=true]"], "since it was inactive, no more events fired");
}, "destroy() inside oncancel");
test(t => {
@@ -30,10 +30,10 @@ test(t => {
watcher.onclose = () => { watcher.destroy(); }
watcher.requestClose();
- assert_array_equals(events, ["close"]);
+ assert_array_equals(events, ["cancel[cancelable=false]", "close"]);
watcher.requestClose();
- assert_array_equals(events, ["close"], "since it was inactive, no more events fired");
+ assert_array_equals(events, ["cancel[cancelable=false]", "close"], "since it was inactive, no more events fired");
}, "destroy() inside onclose");
promise_test(async t => {
@@ -45,10 +45,10 @@ promise_test(async t => {
await test_driver.bless("give user activation so that cancel will fire", () => {
watcher.requestClose();
});
- assert_array_equals(events, ["cancel", "close"]);
+ assert_array_equals(events, ["cancel[cancelable=true]", "close"]);
watcher.requestClose();
- assert_array_equals(events, ["cancel", "close"], "since it was inactive, no more events fired");
+ assert_array_equals(events, ["cancel[cancelable=true]", "close"], "since it was inactive, no more events fired");
}, "close() inside oncancel");
test(t => {
@@ -58,10 +58,10 @@ test(t => {
watcher.onclose = () => { watcher.close(); }
watcher.requestClose();
- assert_array_equals(events, ["close"]);
+ assert_array_equals(events, ["cancel[cancelable=false]", "close"]);
watcher.requestClose();
- assert_array_equals(events, ["close"], "since it was inactive, no more events fired");
+ assert_array_equals(events, ["cancel[cancelable=false]", "close"], "since it was inactive, no more events fired");
}, "close() inside onclose");
promise_test(async t => {
@@ -73,10 +73,10 @@ promise_test(async t => {
await test_driver.bless("give user activation so that cancel will fire", () => {
watcher.requestClose();
});
- assert_array_equals(events, ["cancel", "close"]);
+ assert_array_equals(events, ["cancel[cancelable=true]", "close"]);
watcher.requestClose();
- assert_array_equals(events, ["cancel", "close"], "since it was inactive, no more events fired");
+ assert_array_equals(events, ["cancel[cancelable=true]", "close"], "since it was inactive, no more events fired");
}, "requestClose() inside oncancel");
test(t => {
@@ -86,9 +86,9 @@ test(t => {
watcher.onclose = () => { watcher.requestClose(); }
watcher.requestClose();
- assert_array_equals(events, ["close"]);
+ assert_array_equals(events, ["cancel[cancelable=false]", "close"]);
watcher.requestClose();
- assert_array_equals(events, ["close"], "since it was inactive, no more events fired");
+ assert_array_equals(events, ["cancel[cancelable=false]", "close"], "since it was inactive, no more events fired");
}, "requestClose() inside onclose");
</script>
diff --git a/testing/web-platform/tests/close-watcher/resources/helpers.js b/testing/web-platform/tests/close-watcher/resources/helpers.js
index dd9e191c4d..ad80c28847 100644
--- a/testing/web-platform/tests/close-watcher/resources/helpers.js
+++ b/testing/web-platform/tests/close-watcher/resources/helpers.js
@@ -26,9 +26,14 @@ window.createRecordingCloseWatcher = (t, events, name, type, parentWatcher) => {
t.add_cleanup(() => watcher.destroy());
}
- const prefix = name === undefined ? "" : name + " ";
- watcher.addEventListener('cancel', () => events.push(prefix + "cancel"));
- watcher.addEventListener('close', () => events.push(prefix + "close"));
+ const prefix = name === undefined ? '' : name + ' ';
+ watcher.addEventListener('cancel', e => {
+ const cancelable = e.cancelable ? '[cancelable=true]' : '[cancelable=false]';
+ events.push(prefix + 'cancel' + cancelable);
+ });
+ watcher.addEventListener('close', () => {
+ events.push(prefix + 'close');
+ });
return watcher;
};
diff --git a/testing/web-platform/tests/close-watcher/user-activation/README.md b/testing/web-platform/tests/close-watcher/user-activation/README.md
new file mode 100644
index 0000000000..b9aa9a2123
--- /dev/null
+++ b/testing/web-platform/tests/close-watcher/user-activation/README.md
@@ -0,0 +1,25 @@
+# Close watcher user activation tests
+
+These tests are all in separate files (or test variants) because we need to be
+sure we're starting from zero user activation.
+
+## Note on variants vs. `-dialog` and `-CloseWatcher` files
+
+We endeavor to have all the tests in these files cover both `<dialog>` elements
+and the `CloseWatcher` API. (And sometimes the `popover=""` attribute.)
+
+When the test expectations are the same for both `<dialog>` and `CloseWatcher`,
+we use WPT's variants feature.
+
+However, in some cases different expectations are necessary. This is because
+`<dialog>`s queue a task to fire their `close` event, and do not queue a task
+to fire their `cancel` event. Thus, when you have two `<dialog>`s grouped
+together, you get the somewhat-strange behavior of both `cancel`s firing first,
+then both `close`s. Whereas `CloseWatcher`s do not have this issue; both events
+fire synchronously.
+
+(Note that scheduling the `cancel` event for `<dialog>`s is not really possible,
+since it would then fire after the dialog has been closed in the DOM and
+visually. So the only reasonable fix for this would be to stop scheduling the
+`close` event for dialogs. That's risky from a compat standpoint, so for now,
+we test the strange behavior.)
diff --git a/testing/web-platform/tests/close-watcher/user-activation/n-activate-preventDefault.html b/testing/web-platform/tests/close-watcher/user-activation/n-activate-preventDefault.html
index 531ef42599..f413448718 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/n-activate-preventDefault.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/n-activate-preventDefault.html
@@ -22,10 +22,10 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["cancel"]);
+ assert_array_equals(events, ["cancel[cancelable=true]"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["cancel", "close"]);
+ assert_array_equals(events, ["cancel[cancelable=true]", "cancel[cancelable=false]", "close"]);
}, "Create a close watcher without user activation that preventDefault()s cancel; send user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/n-activate.html b/testing/web-platform/tests/close-watcher/user-activation/n-activate.html
index babcf54c3c..d8253ba765 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/n-activate.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/n-activate.html
@@ -22,6 +22,6 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["cancel", "close"]);
+ assert_array_equals(events, ["cancel[cancelable=true]", "close"]);
}, "Create a close watcher without user activation; send user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/n-closerequest-n.html b/testing/web-platform/tests/close-watcher/user-activation/n-closerequest-n.html
index 2424af7820..54ccdd1abe 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/n-closerequest-n.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/n-closerequest-n.html
@@ -19,12 +19,12 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher1 close"]);
+ assert_array_equals(events, ["watcher1 cancel[cancelable=false]", "watcher1 close"]);
createRecordingCloseWatcher(t, events, "watcher2", type);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher1 close", "watcher2 close"]);
+ assert_array_equals(events, ["watcher1 cancel[cancelable=false]", "watcher1 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]);
}, "Create a close watcher without user activation; send a close request; create a close watcher without user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/n-destroy-n.html b/testing/web-platform/tests/close-watcher/user-activation/n-destroy-n.html
index c26f87dd6f..e0a94f490e 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/n-destroy-n.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/n-destroy-n.html
@@ -26,6 +26,6 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 close"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=false]", "watcher2 close"]);
}, "Create a close watcher without user activation; destroy the close watcher; create a close watcher without user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/n.html b/testing/web-platform/tests/close-watcher/user-activation/n.html
index fe04e0dc1b..af8f972ee6 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/n.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/n.html
@@ -20,6 +20,6 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["close"]);
+ assert_array_equals(events, ["cancel[cancelable=false]", "close"]);
}, "Create a close watcher without user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/nn.html b/testing/web-platform/tests/close-watcher/user-activation/nn-CloseWatcher.html
index beb63f1b4f..016745dfbb 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/nn.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/nn-CloseWatcher.html
@@ -1,6 +1,4 @@
<!doctype html>
-<meta name=variant content="?dialog">
-<meta name=variant content="?CloseWatcher">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
@@ -11,7 +9,7 @@
<body>
<script>
-const type = location.search.substring(1);
+const type = "CloseWatcher";
promise_test(async t => {
const events = [];
@@ -21,6 +19,6 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 close", "watcher1 close"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
}, "Create two close watchers without user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/nn-activate-CloseWatcher.html b/testing/web-platform/tests/close-watcher/user-activation/nn-activate-CloseWatcher.html
index 8045f30b48..45718e51a8 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/nn-activate-CloseWatcher.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/nn-activate-CloseWatcher.html
@@ -7,10 +7,6 @@
<script src="/common/top-layer.js"></script>
<script src="../resources/helpers.js"></script>
-<!--
- See note in sibling -dialog.html file.
--->
-
<body>
<script>
const type = "CloseWatcher";
@@ -25,6 +21,6 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 cancel", "watcher2 close", "watcher1 cancel", "watcher1 close"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher2 close", "watcher1 cancel[cancelable=true]", "watcher1 close"]);
}, "Create two CloseWatchers without user activation; send user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/nn-activate-dialog.html b/testing/web-platform/tests/close-watcher/user-activation/nn-activate-dialog.html
index 5cc866044c..eaffb4d9a7 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/nn-activate-dialog.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/nn-activate-dialog.html
@@ -7,20 +7,6 @@
<script src="/common/top-layer.js"></script>
<script src="../resources/helpers.js"></script>
-<!--
- This test has different expectations for dialogs vs. CloseWatchers because
- dialogs queue a task to fire their close event, and do not do so for their
- cancel event. Thus, when you have two dialogs grouped together, you get the
- somewhat-strange behavior of both cancels firing first, then both closes.
- Whereas CloseWatchers do not have this issue; both fire synchronously.
-
- Note that scheduling the cancel event for dialogs is not really possible since
- it would then fire after the dialog has been closed in the DOM and visually.
- So the only reasonable fix for this would be to stop scheduling the close
- event for dialogs. That's risky from a compat standpoint, so for now, test the
- strange behavior.
--->
-
<body>
<script>
const type = "dialog";
@@ -35,6 +21,6 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 cancel", "watcher1 cancel", "watcher2 close", "watcher1 close"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher1 cancel[cancelable=true]", "watcher2 close", "watcher1 close"]);
}, "Create two dialogs without user activation; send user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/nn-dialog.html b/testing/web-platform/tests/close-watcher/user-activation/nn-dialog.html
new file mode 100644
index 0000000000..0d086a525e
--- /dev/null
+++ b/testing/web-platform/tests/close-watcher/user-activation/nn-dialog.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/common/top-layer.js"></script>
+<script src="../resources/helpers.js"></script>
+
+<body>
+<script>
+const type = "dialog";
+
+promise_test(async t => {
+ const events = [];
+
+ createRecordingCloseWatcher(t, events, "watcher1", type);
+ createRecordingCloseWatcher(t, events, "watcher2", type);
+
+ await sendCloseRequest();
+ await waitForPotentialCloseEvent();
+ assert_array_equals(events, ["watcher2 cancel[cancelable=false]", "watcher1 cancel[cancelable=false]", "watcher2 close", "watcher1 close"]);
+}, "Create two close watchers without user activation");
+</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/nnn-CloseWatcher-dialog-popover.html b/testing/web-platform/tests/close-watcher/user-activation/nnn-CloseWatcher-dialog-popover.html
index f8b9061d01..38dd607312 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/nnn-CloseWatcher-dialog-popover.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/nnn-CloseWatcher-dialog-popover.html
@@ -30,6 +30,6 @@ promise_test(async t => {
assert_false(popover.matches(':popover-open'), 'The popover should be closed.');
assert_false(dialog.hasAttribute('open'), 'The dialog should be closed.');
- assert_array_equals(events, ['CloseWatcher close', 'dialog close']);
+ assert_array_equals(events, ['dialog cancel[cancelable=false]', 'CloseWatcher cancel[cancelable=false]', 'CloseWatcher close', 'dialog close']);
}, 'Create a CloseWatcher without user activation; create a dialog without user activation; create a popover without user activation');
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/nnn-CloseWatcher.html b/testing/web-platform/tests/close-watcher/user-activation/nnn-CloseWatcher.html
new file mode 100644
index 0000000000..5d2f07e617
--- /dev/null
+++ b/testing/web-platform/tests/close-watcher/user-activation/nnn-CloseWatcher.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/common/top-layer.js"></script>
+<script src="../resources/helpers.js"></script>
+
+<body>
+<script>
+const type = "CloseWatcher";
+
+promise_test(async t => {
+ const events = [];
+
+ createRecordingCloseWatcher(t, events, "watcher1", type);
+ createRecordingCloseWatcher(t, events, "watcher2", type);
+ createRecordingCloseWatcher(t, events, "watcher3", type);
+
+ await sendCloseRequest();
+ await waitForPotentialCloseEvent();
+ assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
+}, "Create three close watchers without user activation");
+</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/nnn.html b/testing/web-platform/tests/close-watcher/user-activation/nnn-dialog.html
index 9b604e91db..f1c071dbb3 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/nnn.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/nnn-dialog.html
@@ -1,6 +1,4 @@
<!doctype html>
-<meta name=variant content="?dialog">
-<meta name=variant content="?CloseWatcher">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
@@ -11,7 +9,7 @@
<body>
<script>
-const type = location.search.substring(1);
+const type = "dialog";
promise_test(async t => {
const events = [];
@@ -22,6 +20,6 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher3 close", "watcher2 close", "watcher1 close"]);
+ assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher2 cancel[cancelable=false]", "watcher1 cancel[cancelable=false]", "watcher3 close", "watcher2 close", "watcher1 close"]);
}, "Create three close watchers without user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/ny-activate-preventDefault.html b/testing/web-platform/tests/close-watcher/user-activation/ny-activate-preventDefault.html
index 5ffb64b113..7cd1c2e508 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/ny-activate-preventDefault.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/ny-activate-preventDefault.html
@@ -24,14 +24,14 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 cancel"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=true]"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 cancel", "watcher2 close"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher2 cancel[cancelable=false]", "watcher2 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 cancel", "watcher2 close", "watcher1 close"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
}, "Create a close watcher without user activation; create a close watcher with user activation that preventDefault()s cancel; send user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/ny.html b/testing/web-platform/tests/close-watcher/user-activation/ny.html
index 226912233e..49f50a123e 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/ny.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/ny.html
@@ -21,10 +21,10 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 close"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=false]", "watcher2 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 close", "watcher1 close"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
}, "Create a close watcher without user activation; create a close watcher with user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/nyn.html b/testing/web-platform/tests/close-watcher/user-activation/nyn.html
index ec5153c767..b227d566d4 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/nyn.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/nyn.html
@@ -21,10 +21,10 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher3 close", "watcher2 close"]);
+ assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher3 close", "watcher2 close", "watcher1 close"]);
+ assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
}, "Create a close watcher without user activation; create a close watcher with user activation; create a close watcher without user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/nynn-destroy.html b/testing/web-platform/tests/close-watcher/user-activation/nynn-destroy.html
index 8519c8a2a9..fb04109994 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/nynn-destroy.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/nynn-destroy.html
@@ -24,10 +24,10 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher4 close", "watcher3 close"]);
+ assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher4 close", "watcher3 close", "watcher1 close"]);
+ assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
}, "Create a close watcher without user activation; create a close watcher with user activation; create two close watchers without user activation; remove the second close watcher");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/nynn.html b/testing/web-platform/tests/close-watcher/user-activation/nynn.html
index f6e74a0ba1..ed9203db66 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/nynn.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/nynn.html
@@ -22,10 +22,10 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher4 close", "watcher3 close", "watcher2 close"]);
+ assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher4 close", "watcher3 close", "watcher2 close", "watcher1 close"]);
+ assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
}, "Create a close watcher without user activation; create a close watcher with user activation; create two close watchers without user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/nyyn-CloseWatcher.html b/testing/web-platform/tests/close-watcher/user-activation/nyyn-CloseWatcher.html
new file mode 100644
index 0000000000..4f60ef3c4b
--- /dev/null
+++ b/testing/web-platform/tests/close-watcher/user-activation/nyyn-CloseWatcher.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/common/top-layer.js"></script>
+<script src="../resources/helpers.js"></script>
+
+<body>
+<script>
+const type = "CloseWatcher";
+
+promise_test(async t => {
+ const events = [];
+
+ const watcher1 = createRecordingCloseWatcher(t, events, "watcher1", type);
+ const watcher2 = await createBlessedRecordingCloseWatcher(t, events, "watcher2", type, watcher1);
+ const watcher3 = await createBlessedRecordingCloseWatcher(t, events, "watcher3", type, watcher2);
+ createRecordingCloseWatcher(t, events, "watcher4", type);
+
+ await sendCloseRequest();
+ await waitForPotentialCloseEvent();
+ assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close"]);
+
+ await sendCloseRequest();
+ await waitForPotentialCloseEvent();
+ assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]);
+
+ await sendCloseRequest();
+ await waitForPotentialCloseEvent();
+ assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
+}, "Create a close watcher without user activation; create two close watchers with user activation; create a close watcher without user activation");
+</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/nyyn.html b/testing/web-platform/tests/close-watcher/user-activation/nyyn-dialog.html
index f3987c1a21..44926fd5c3 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/nyyn.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/nyyn-dialog.html
@@ -1,6 +1,4 @@
<!doctype html>
-<meta name=variant content="?dialog">
-<meta name=variant content="?CloseWatcher">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
@@ -11,7 +9,7 @@
<body>
<script>
-const type = location.search.substring(1);
+const type = "dialog";
promise_test(async t => {
const events = [];
@@ -23,14 +21,14 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher4 close", "watcher3 close"]);
+ assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher3 cancel[cancelable=false]", "watcher4 close", "watcher3 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher4 close", "watcher3 close", "watcher2 close"]);
+ assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher3 cancel[cancelable=false]", "watcher4 close", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher4 close", "watcher3 close", "watcher2 close", "watcher1 close"]);
+ assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher3 cancel[cancelable=false]", "watcher4 close", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
}, "Create a close watcher without user activation; create two close watchers with user activation; create a close watcher without user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/nyyyn-CloseWatcher.html b/testing/web-platform/tests/close-watcher/user-activation/nyyyn-CloseWatcher.html
new file mode 100644
index 0000000000..e2565a82a3
--- /dev/null
+++ b/testing/web-platform/tests/close-watcher/user-activation/nyyyn-CloseWatcher.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/common/top-layer.js"></script>
+<script src="../resources/helpers.js"></script>
+
+<body>
+<script>
+const type = "CloseWatcher";
+
+promise_test(async t => {
+ const events = [];
+ const watcher1 = createRecordingCloseWatcher(t, events, "watcher1", type);
+ const watcher2 = await createBlessedRecordingCloseWatcher(t, events, "watcher2", type, watcher1);
+ const watcher3 = await createBlessedRecordingCloseWatcher(t, events, "watcher3", type, watcher2);
+ await createBlessedRecordingCloseWatcher(t, events, "watcher4", type, watcher3);
+ createRecordingCloseWatcher(t, events, "watcher5", type);
+
+ await sendCloseRequest();
+ await waitForPotentialCloseEvent();
+ assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher5 close", "watcher4 cancel[cancelable=false]", "watcher4 close"]);
+
+ await sendCloseRequest();
+ await waitForPotentialCloseEvent();
+ assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher5 close", "watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close"]);
+
+ await sendCloseRequest();
+ await waitForPotentialCloseEvent();
+ assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher5 close", "watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]);
+
+ await sendCloseRequest();
+ await waitForPotentialCloseEvent();
+ assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher5 close", "watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
+}, "Create a close watcher without user activation; create three close watchers with user activation; create a close watcher without user activation");
+</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/nyyyn.html b/testing/web-platform/tests/close-watcher/user-activation/nyyyn-dialog.html
index 6cb8f3a445..86361124d3 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/nyyyn.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/nyyyn-dialog.html
@@ -1,6 +1,4 @@
<!doctype html>
-<meta name=variant content="?dialog">
-<meta name=variant content="?CloseWatcher">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
@@ -11,7 +9,7 @@
<body>
<script>
-const type = location.search.substring(1);
+const type = "dialog";
promise_test(async t => {
const events = [];
@@ -23,18 +21,18 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher5 close", "watcher4 close"]);
+ assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher4 cancel[cancelable=false]", "watcher5 close", "watcher4 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher5 close", "watcher4 close", "watcher3 close"]);
+ assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher4 cancel[cancelable=false]", "watcher5 close", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher5 close", "watcher4 close", "watcher3 close", "watcher2 close"]);
+ assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher4 cancel[cancelable=false]", "watcher5 close", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher5 close", "watcher4 close", "watcher3 close", "watcher2 close", "watcher1 close"]);
+ assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher4 cancel[cancelable=false]", "watcher5 close", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
}, "Create a close watcher without user activation; create three close watchers with user activation; create a close watcher without user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/y.html b/testing/web-platform/tests/close-watcher/user-activation/y.html
index ee58a92293..78c432de38 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/y.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/y.html
@@ -20,6 +20,6 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["cancel", "close"]);
+ assert_array_equals(events, ["cancel[cancelable=true]", "close"]);
}, "Create a close watcher with user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/yn-activate.html b/testing/web-platform/tests/close-watcher/user-activation/yn-activate.html
index af7289aa28..d62b4df425 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/yn-activate.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/yn-activate.html
@@ -23,10 +23,10 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 cancel", "watcher2 close"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher2 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 cancel", "watcher2 close", "watcher1 cancel", "watcher1 close"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher2 close", "watcher1 cancel[cancelable=true]", "watcher1 close"]);
}, "Create a close watcher with user activation; create a close watcher without user activation; send user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/yn.html b/testing/web-platform/tests/close-watcher/user-activation/yn.html
index 8f7e90e2d8..578f43de25 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/yn.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/yn.html
@@ -21,10 +21,10 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 close"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=false]", "watcher2 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 close", "watcher1 close"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
}, "Create a close watcher with user activation; create a close watcher without user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/ynn-CloseWatcher.html b/testing/web-platform/tests/close-watcher/user-activation/ynn-CloseWatcher.html
new file mode 100644
index 0000000000..50b5a8131d
--- /dev/null
+++ b/testing/web-platform/tests/close-watcher/user-activation/ynn-CloseWatcher.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/common/top-layer.js"></script>
+<script src="../resources/helpers.js"></script>
+
+<body>
+<script>
+const type = "CloseWatcher";
+
+promise_test(async t => {
+ const events = [];
+
+ await createBlessedRecordingCloseWatcher(t, events, "watcher1", type);
+ createRecordingCloseWatcher(t, events, "watcher2", type);
+ createRecordingCloseWatcher(t, events, "watcher3", type);
+
+ await sendCloseRequest();
+ await waitForPotentialCloseEvent();
+ assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]);
+
+ await sendCloseRequest();
+ await waitForPotentialCloseEvent();
+ assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
+}, "Create a close watcher with user activation; create two close watchers without user activation");
+</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/ynn.html b/testing/web-platform/tests/close-watcher/user-activation/ynn-dialog.html
index 8cc7f5bfb6..c10e94dc73 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/ynn.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/ynn-dialog.html
@@ -1,6 +1,4 @@
<!doctype html>
-<meta name=variant content="?dialog">
-<meta name=variant content="?CloseWatcher">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
@@ -11,7 +9,7 @@
<body>
<script>
-const type = location.search.substring(1);
+const type = "dialog";
promise_test(async t => {
const events = [];
@@ -22,10 +20,10 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher3 close", "watcher2 close"]);
+ assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher2 cancel[cancelable=false]", "watcher3 close", "watcher2 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher3 close", "watcher2 close", "watcher1 close"]);
+ assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher2 cancel[cancelable=false]", "watcher3 close", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
}, "Create a close watcher with user activation; create two close watchers without user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/yy.html b/testing/web-platform/tests/close-watcher/user-activation/yy.html
index 0aa03cdd05..9c0f21be22 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/yy.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/yy.html
@@ -21,10 +21,10 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 cancel", "watcher2 close"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher2 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher2 cancel", "watcher2 close", "watcher1 cancel", "watcher1 close"]);
+ assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher2 close", "watcher1 cancel[cancelable=true]", "watcher1 close"]);
}, "Create two close watchers with user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/yyn.html b/testing/web-platform/tests/close-watcher/user-activation/yyn.html
index b87cf7a7e3..2f75377444 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/yyn.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/yyn.html
@@ -22,14 +22,14 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher3 close"]);
+ assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher3 close", "watcher2 close"]);
+ assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher3 close", "watcher2 close", "watcher1 close"]);
+ assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]);
}, "Create two close watchers with user activation; create a close watcher without user activation");
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/yyy-CloseWatcher-dialog-popover.html b/testing/web-platform/tests/close-watcher/user-activation/yyy-CloseWatcher-dialog-popover.html
index f0a1cb06d1..8650fb3b7c 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/yyy-CloseWatcher-dialog-popover.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/yyy-CloseWatcher-dialog-popover.html
@@ -35,12 +35,12 @@ promise_test(async t => {
await waitForPotentialCloseEvent();
assert_false(popover.matches(':popover-open'), 'Second close request: The popover should be closed.');
assert_false(dialog.hasAttribute('open'), 'Second close request: The dialog should be closed.');
- assert_array_equals(events, ['dialog cancel', 'dialog close']);
+ assert_array_equals(events, ['dialog cancel[cancelable=true]', 'dialog close']);
await sendCloseRequest();
await waitForPotentialCloseEvent();
assert_false(popover.matches(':popover-open'), 'Third close request: The popover should be closed.');
assert_false(dialog.hasAttribute('open'), 'Third close request: The dialog should be closed.');
- assert_array_equals(events, ['dialog cancel', 'dialog close', 'CloseWatcher cancel', 'CloseWatcher close']);
+ assert_array_equals(events, ['dialog cancel[cancelable=true]', 'dialog close', 'CloseWatcher cancel[cancelable=true]', 'CloseWatcher close']);
}, 'Create a CloseWatcher with user activation; create a dialog with user activation; create a popover with user activation');
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/yyy-activate-CloseWatcher-dialog-popover.html b/testing/web-platform/tests/close-watcher/user-activation/yyy-activate-CloseWatcher-dialog-popover.html
index ed41d1bc32..a58dd0751b 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/yyy-activate-CloseWatcher-dialog-popover.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/yyy-activate-CloseWatcher-dialog-popover.html
@@ -37,13 +37,13 @@ promise_test(async t => {
await waitForPotentialCloseEvent();
assert_false(popover.matches(':popover-open'), 'Second close request: The popover should be closed.');
assert_false(dialog.hasAttribute('open'), 'Second close request: The dialog should be closed.');
- assert_array_equals(events, ['dialog cancel', 'dialog close']);
+ assert_array_equals(events, ['dialog cancel[cancelable=true]', 'dialog close']);
await test_driver.bless();
await sendCloseRequest();
await waitForPotentialCloseEvent();
assert_false(popover.matches(':popover-open'), 'Third close request: The popover should be closed.');
assert_false(dialog.hasAttribute('open'), 'Third close request: The dialog should be closed.');
- assert_array_equals(events, ['dialog cancel', 'dialog close', 'CloseWatcher cancel', 'CloseWatcher close']);
+ assert_array_equals(events, ['dialog cancel[cancelable=true]', 'dialog close', 'CloseWatcher cancel[cancelable=true]', 'CloseWatcher close']);
}, 'Create a CloseWatcher with user activation; create a dialog with user activation; create a popover with user activation; sending user activation before each close request');
</script>
diff --git a/testing/web-platform/tests/close-watcher/user-activation/yyy.html b/testing/web-platform/tests/close-watcher/user-activation/yyy.html
index f16767a86b..eaf8944bce 100644
--- a/testing/web-platform/tests/close-watcher/user-activation/yyy.html
+++ b/testing/web-platform/tests/close-watcher/user-activation/yyy.html
@@ -22,14 +22,14 @@ promise_test(async t => {
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher3 cancel", "watcher3 close"]);
+ assert_array_equals(events, ["watcher3 cancel[cancelable=true]", "watcher3 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher3 cancel", "watcher3 close", "watcher2 cancel", "watcher2 close"]);
+ assert_array_equals(events, ["watcher3 cancel[cancelable=true]", "watcher3 close", "watcher2 cancel[cancelable=true]", "watcher2 close"]);
await sendCloseRequest();
await waitForPotentialCloseEvent();
- assert_array_equals(events, ["watcher3 cancel", "watcher3 close", "watcher2 cancel", "watcher2 close", "watcher1 cancel", "watcher1 close"]);
+ assert_array_equals(events, ["watcher3 cancel[cancelable=true]", "watcher3 close", "watcher2 cancel[cancelable=true]", "watcher2 close", "watcher1 cancel[cancelable=true]", "watcher1 close"]);
}, "Create three close watchers with user activation");
</script>
diff --git a/testing/web-platform/tests/compression/decompression-buffersource.tentative.any.js b/testing/web-platform/tests/compression/decompression-buffersource.tentative.any.js
index e81fc56677..abb51751c8 100644
--- a/testing/web-platform/tests/compression/decompression-buffersource.tentative.any.js
+++ b/testing/web-platform/tests/compression/decompression-buffersource.tentative.any.js
@@ -48,6 +48,10 @@ const bufferSourceChunksForDeflate = [
value: new Uint32Array(new Uint8Array(compressedBytesWithDeflate).buffer)
},
{
+ name: 'Float16Array',
+ value: new Float16Array(new Uint8Array(compressedBytesWithDeflate).buffer)
+ },
+ {
name: 'Float32Array',
value: new Float32Array(new Uint8Array(compressedBytesWithDeflate).buffer)
},
@@ -95,6 +99,10 @@ const bufferSourceChunksForGzip = [
value: new Uint32Array(new Uint8Array(compressedBytesWithGzip).buffer)
},
{
+ name: 'Float16Array',
+ value: new Float16Array(new Uint8Array(compressedBytesWithGzip).buffer)
+ },
+ {
name: 'Float32Array',
value: new Float32Array(new Uint8Array(compressedBytesWithGzip).buffer)
},
@@ -142,6 +150,10 @@ const bufferSourceChunksForDeflateRaw = [
value: new Uint32Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer)
},
{
+ name: 'Float16Array',
+ value: new Float16Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer)
+ },
+ {
name: 'Float32Array',
value: new Float32Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer)
},
diff --git a/testing/web-platform/tests/compute-pressure/compute_pressure_duplicate_updates.https.any.js b/testing/web-platform/tests/compute-pressure/compute_pressure_duplicate_updates.https.any.js
index 04c5df5e57..609fb5ad70 100644
--- a/testing/web-platform/tests/compute-pressure/compute_pressure_duplicate_updates.https.any.js
+++ b/testing/web-platform/tests/compute-pressure/compute_pressure_duplicate_updates.https.any.js
@@ -12,8 +12,8 @@ pressure_test(async (t, mockPressureService) => {
observer_changes.push(changes);
if (++n === 2)
resolve(observer_changes);
- }, {sampleInterval: 200});
- observer.observe('cpu');
+ });
+ observer.observe('cpu', {sampleInterval: 200});
const updatesDelivered = mockPressureService.updatesDelivered();
mockPressureService.setPressureUpdate('cpu', 'critical');
mockPressureService.startPlatformCollector(/*sampleInterval*/ 200);
diff --git a/testing/web-platform/tests/compute-pressure/compute_pressure_supported_sources.https.any.js b/testing/web-platform/tests/compute-pressure/compute_pressure_known_sources.https.any.js
index 63f2666cca..5db3053ce9 100644
--- a/testing/web-platform/tests/compute-pressure/compute_pressure_supported_sources.https.any.js
+++ b/testing/web-platform/tests/compute-pressure/compute_pressure_known_sources.https.any.js
@@ -4,18 +4,18 @@
test(() => {
// Compute Pressure should support at least "cpu"
- const sources = PressureObserver.supportedSources;
+ const sources = PressureObserver.knownSources;
assert_in_array('cpu', sources);
}, 'PressureObserver should support at least "cpu"');
test(() => {
// Compute Pressure should be frozen array
- const sources = PressureObserver.supportedSources;
- assert_equals(sources, PressureObserver.supportedSources);
+ const sources = PressureObserver.knownSources;
+ assert_equals(sources, PressureObserver.knownSources);
}, 'PressureObserver must return always the same array');
test(() => {
// Compute Pressure should be frozen array
- let sources = PressureObserver.supportedSources;
- assert_equals(Object.isFrozen(), true);
+ let sources = PressureObserver.knownSources;
+ assert_equals(Object.isFrozen(sources), true);
}, 'PressureObserver must return a frozen array');
diff --git a/testing/web-platform/tests/compute-pressure/compute_pressure_options.https.any.js b/testing/web-platform/tests/compute-pressure/compute_pressure_options.https.any.js
index d0760ef622..ecf3c29dbf 100644
--- a/testing/web-platform/tests/compute-pressure/compute_pressure_options.https.any.js
+++ b/testing/web-platform/tests/compute-pressure/compute_pressure_options.https.any.js
@@ -1,26 +1,31 @@
+// META: script=/resources/test-only-api.js
+// META: script=resources/pressure-helpers.js
// META: global=window,dedicatedworker,sharedworker
'use strict';
-test(t => {
- const observer = new PressureObserver(() => {}, {sampleInterval: 0});
- assert_equals(typeof observer, 'object');
-}, 'PressureObserver constructor doesnt throw error for sampleInterval value 0');
-
-
-test(t => {
- assert_throws_js(TypeError, () => {
- new PressureObserver(() => {}, {sampleInterval: -2});
+pressure_test(async (t, mockPressureService) => {
+ await new Promise(resolve => {
+ const observer = new PressureObserver(resolve);
+ t.add_cleanup(() => observer.disconnect());
+ observer.observe('cpu', {sampleInterval: 0});
+ mockPressureService.setPressureUpdate('cpu', 'critical');
+ mockPressureService.startPlatformCollector(/*sampleInterval=*/ 200);
});
-}, 'PressureObserver constructor requires a positive sampleInterval');
+}, 'PressureObserver observe method doesnt throw error for sampleInterval value 0');
-test(t => {
- assert_throws_js(TypeError, () => {
- new PressureObserver(() => {}, {sampleInterval: 2 ** 32});
- });
-}, 'PressureObserver constructor requires a sampleInterval in unsigned long range');
+promise_test(async t => {
+ const observer =
+ new PressureObserver(t.unreached_func('oops should not end up here'));
+ t.add_cleanup(() => observer.disconnect());
+ await promise_rejects_js(
+ t, TypeError, observer.observe('cpu', {sampleInterval: -2}));
+}, 'PressureObserver observe method requires a positive sampleInterval');
-test(t => {
- const observer = new PressureObserver(() => {}, {});
- assert_equals(typeof observer, 'object');
-}, 'PressureObserver constructor succeeds on empty sampleInterval');
+promise_test(async t => {
+ const observer =
+ new PressureObserver(t.unreached_func('oops should not end up here'));
+ t.add_cleanup(() => observer.disconnect());
+ await promise_rejects_js(
+ t, TypeError, observer.observe('cpu', {sampleInterval: 2 ** 32}));
+}, 'PressureObserver observe method requires a sampleInterval in unsigned long range');
diff --git a/testing/web-platform/tests/compute-pressure/compute_pressure_rate_obfuscation_mitigation_not_triggered.https.window.js b/testing/web-platform/tests/compute-pressure/compute_pressure_rate_obfuscation_mitigation_not_triggered.https.window.js
index e348a8ea08..f3e966de24 100644
--- a/testing/web-platform/tests/compute-pressure/compute_pressure_rate_obfuscation_mitigation_not_triggered.https.window.js
+++ b/testing/web-platform/tests/compute-pressure/compute_pressure_rate_obfuscation_mitigation_not_triggered.https.window.js
@@ -17,9 +17,9 @@ pressure_test(async (t, mockPressureService) => {
const observerChanges = [];
const observer = new PressureObserver(changes => {
observerChanges.push(changes);
- }, {sampleInterval: sampleIntervalInMs});
+ });
- observer.observe('cpu');
+ observer.observe('cpu', {sampleInterval: sampleIntervalInMs});
mockPressureService.startPlatformCollector(sampleIntervalInMs);
let i = 0;
// mockPressureService.updatesDelivered() does not necessarily match
diff --git a/testing/web-platform/tests/compute-pressure/compute_pressure_rate_obfuscation_mitigation_triggered.https.window.js b/testing/web-platform/tests/compute-pressure/compute_pressure_rate_obfuscation_mitigation_triggered.https.window.js
index ebe33bc8bf..b481cf6c87 100644
--- a/testing/web-platform/tests/compute-pressure/compute_pressure_rate_obfuscation_mitigation_triggered.https.window.js
+++ b/testing/web-platform/tests/compute-pressure/compute_pressure_rate_obfuscation_mitigation_triggered.https.window.js
@@ -31,9 +31,9 @@ pressure_test(async (t, mockPressureService) => {
}
}
observerChanges.push(changes);
- }, {sampleInterval: sampleIntervalInMs});
+ });
- observer.observe('cpu');
+ observer.observe('cpu', {sampleInterval: sampleIntervalInMs});
mockPressureService.startPlatformCollector(sampleIntervalInMs);
let i = 0;
// mockPressureService.updatesDelivered() does not necessarily match
diff --git a/testing/web-platform/tests/compute-pressure/compute_pressure_timestamp.https.any.js b/testing/web-platform/tests/compute-pressure/compute_pressure_timestamp.https.any.js
index 09caeb3478..18db1dac46 100644
--- a/testing/web-platform/tests/compute-pressure/compute_pressure_timestamp.https.any.js
+++ b/testing/web-platform/tests/compute-pressure/compute_pressure_timestamp.https.any.js
@@ -5,32 +5,73 @@
'use strict';
pressure_test(async (t, mockPressureService) => {
+ const [change, timeOrigin] = await new Promise(resolve => {
+ const observer = new PressureObserver(change => {
+ resolve([change, performance.timeOrigin]);
+ });
+ t.add_cleanup(() => observer.disconnect());
+ observer.observe('cpu');
+ mockPressureService.setPressureUpdate('cpu', 'critical');
+ mockPressureService.startPlatformCollector(/*sampleInterval=*/ 200);
+ });
+ assert_greater_than(change[0].time, timeOrigin);
+}, 'Timestamp from update should be greater than timeOrigin');
+
+pressure_test(async (t, mockPressureService) => {
const readings = ['nominal', 'fair', 'serious', 'critical'];
const sampleInterval = 250;
- const pressureChanges = await new Promise(async resolve => {
- const observerChanges = [];
- const observer = new PressureObserver(changes => {
- observerChanges.push(changes);
- }, {sampleInterval});
- observer.observe('cpu');
+ const pressureChanges = [];
+ const observer = new PressureObserver(changes => {
+ pressureChanges.push(changes);
+ });
+ observer.observe('cpu', {sampleInterval});
- mockPressureService.startPlatformCollector(sampleInterval / 2);
- let i = 0;
- // mockPressureService.updatesDelivered() does not necessarily match
- // pressureChanges.length, as system load and browser optimizations can
- // cause the actual timer used by mockPressureService to deliver readings
- // to be a bit slower or faster than requested.
- while (observerChanges.length < 4) {
- mockPressureService.setPressureUpdate(
- 'cpu', readings[i++ % readings.length]);
- await t.step_wait(
- () => mockPressureService.updatesDelivered() >= i,
- `At least ${i} readings have been delivered`);
- }
- observer.disconnect();
- resolve(observerChanges);
+ mockPressureService.startPlatformCollector(sampleInterval / 2);
+ let i = 0;
+ // mockPressureService.updatesDelivered() does not necessarily match
+ // pressureChanges.length, as system load and browser optimizations can
+ // cause the actual timer used by mockPressureService to deliver readings
+ // to be a bit slower or faster than requested.
+ while (pressureChanges.length < 4) {
+ mockPressureService.setPressureUpdate(
+ 'cpu', readings[i++ % readings.length]);
+ await t.step_wait(
+ () => mockPressureService.updatesDelivered() >= i,
+ `At least ${i} readings have been delivered`);
+ }
+ observer.disconnect();
+
+ assert_equals(pressureChanges.length, 4);
+ assert_greater_than(pressureChanges[1][0].time, pressureChanges[0][0].time);
+ assert_greater_than(pressureChanges[2][0].time, pressureChanges[1][0].time);
+ assert_greater_than(pressureChanges[3][0].time, pressureChanges[2][0].time);
+}, 'Timestamp difference between two changes should be continuously increasing');
+
+pressure_test(async (t, mockPressureService) => {
+ const readings = ['nominal', 'fair', 'serious', 'critical'];
+
+ const sampleInterval = 250;
+ const pressureChanges = [];
+ const observer = new PressureObserver(change => {
+ pressureChanges.push(change);
});
+ observer.observe('cpu', {sampleInterval});
+
+ mockPressureService.startPlatformCollector(sampleInterval / 2);
+ let i = 0;
+ // mockPressureService.updatesDelivered() does not necessarily match
+ // pressureChanges.length, as system load and browser optimizations can
+ // cause the actual timer used by mockPressureService to deliver readings
+ // to be a bit slower or faster than requested.
+ while (pressureChanges.length < 4) {
+ mockPressureService.setPressureUpdate(
+ 'cpu', readings[i++ % readings.length]);
+ await t.step_wait(
+ () => mockPressureService.updatesDelivered() >= i,
+ `At least ${i} readings have been delivered`);
+ }
+ observer.disconnect();
assert_equals(pressureChanges.length, 4);
assert_greater_than_equal(
@@ -46,10 +87,10 @@ pressure_test(async (t, mockPressureService) => {
const sampleInterval = 1000;
const observer = new PressureObserver(changes => {
pressureChanges.push(changes);
- }, {sampleInterval});
+ });
await new Promise(async resolve => {
- observer.observe('cpu');
+ observer.observe('cpu', {sampleInterval});
mockPressureService.setPressureUpdate('cpu', 'critical');
mockPressureService.startPlatformCollector(sampleInterval);
await t.step_wait(() => pressureChanges.length == 1);
@@ -71,5 +112,6 @@ pressure_test(async (t, mockPressureService) => {
// should be deleted. So the second PressureRecord is not discarded even
// though the time interval does not meet the requirement.
assert_less_than(
- pressureChanges[1][0].time - pressureChanges[0][0].time, sampleInterval);
+ (pressureChanges[1][0].time - pressureChanges[0][0].time),
+ sampleInterval);
}, 'disconnect() should update [[LastRecordMap]]');
diff --git a/testing/web-platform/tests/compute-pressure/idlharness.https.any.js b/testing/web-platform/tests/compute-pressure/idlharness.https.any.js
index 48ab5615b0..6cd7e87b5b 100644
--- a/testing/web-platform/tests/compute-pressure/idlharness.https.any.js
+++ b/testing/web-platform/tests/compute-pressure/idlharness.https.any.js
@@ -11,5 +11,5 @@ idl_test(['compute-pressure'], ['dom', 'html'], async idl_array => {
PressureObserver: ['observer'],
});
- self.observer = new PressureObserver(() => {}, {sampleInterval: 1000});
+ self.observer = new PressureObserver(() => {});
});
diff --git a/testing/web-platform/tests/compute-pressure/observe_return_type.https.any.js b/testing/web-platform/tests/compute-pressure/observe_return_type.https.any.js
new file mode 100644
index 0000000000..b24878ab39
--- /dev/null
+++ b/testing/web-platform/tests/compute-pressure/observe_return_type.https.any.js
@@ -0,0 +1,18 @@
+// META: script=/resources/test-only-api.js
+// META: script=resources/pressure-helpers.js
+// META: global=window,dedicatedworker,sharedworker
+
+'use strict';
+
+// Regression test for https://issues.chromium.org/issues/333957909
+// Make sure that observe() always returns a Promise.
+pressure_test(async (t, mockPressureService) => {
+ const observer = new PressureObserver(() => {});
+ t.add_cleanup(() => observer.disconnect());
+
+ for (let i = 0; i < 2; i++) {
+ const promise = observer.observe('cpu');
+ assert_class_string(promise, 'Promise');
+ await promise;
+ }
+}, 'PressureObserver.observe() is idempotent');
diff --git a/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-cross-in-cross-self-block.html b/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-cross-in-cross-self-block.html
index 85b7f0efdc..eb7cbef866 100644
--- a/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-cross-in-cross-self-block.html
+++ b/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-cross-in-cross-self-block.html
@@ -7,7 +7,7 @@
</head>
<body>
<script>
- test = async_test("A 'frame-ancestors' CSP directive with a value 'same' should block render in same-origin nested frames.");
+ test = async_test("A 'frame-ancestors' CSP directive with a value 'self' should block render in same-origin nested frames.");
testNestedIFrame("'self'", CROSS_ORIGIN, CROSS_ORIGIN, EXPECT_BLOCK);
</script>
diff --git a/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-cross-in-same-self-block.html b/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-cross-in-same-self-block.html
index da97339711..8f9d94e7d6 100644
--- a/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-cross-in-same-self-block.html
+++ b/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-cross-in-same-self-block.html
@@ -7,7 +7,7 @@
</head>
<body>
<script>
- test = async_test("A 'frame-ancestors' CSP directive with a value 'same' should block render in same-origin nested frames.");
+ test = async_test("A 'frame-ancestors' CSP directive with a value 'self' should block render in same-origin nested frames.");
testNestedIFrame("'self'", SAME_ORIGIN, CROSS_ORIGIN, EXPECT_BLOCK);
</script>
diff --git a/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-same-in-cross-self-block.html b/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-same-in-cross-self-block.html
index bae5992e86..f9d32eb3ed 100644
--- a/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-same-in-cross-self-block.html
+++ b/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-same-in-cross-self-block.html
@@ -7,7 +7,7 @@
</head>
<body>
<script>
- test = async_test("A 'frame-ancestors' CSP directive with a value 'same' should block render in same-origin nested frames.");
+ test = async_test("A 'frame-ancestors' CSP directive with a value 'self' should block render in same-origin nested frames.");
testNestedIFrame("'self'", CROSS_ORIGIN, SAME_ORIGIN, EXPECT_BLOCK);
</script>
diff --git a/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-same-in-same-self-allow.html b/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-same-in-same-self-allow.html
index 747c563696..a4271dfd92 100644
--- a/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-same-in-same-self-allow.html
+++ b/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-nested-same-in-same-self-allow.html
@@ -7,7 +7,7 @@
</head>
<body>
<script>
- test = async_test("A 'frame-ancestors' CSP directive with a value 'same' should block render in same-origin nested frames.");
+ test = async_test("A 'frame-ancestors' CSP directive with a value 'self' should block render in same-origin nested frames.");
testNestedIFrame("'self'", SAME_ORIGIN, SAME_ORIGIN, EXPECT_LOAD);
</script>
diff --git a/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-sandbox-same-origin-self.html b/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-sandbox-same-origin-self.html
index 4a2a19698d..825f9a8ae3 100644
--- a/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-sandbox-same-origin-self.html
+++ b/testing/web-platform/tests/content-security-policy/frame-ancestors/frame-ancestors-sandbox-same-origin-self.html
@@ -11,7 +11,7 @@
"should compare the child URL (self) against each parent's origin's URL" +
" rather then URL. When the ancestors are sandboxed, they never match.");
- testNestedSandboxedIFrame('self', SAME_ORIGIN, SAME_ORIGIN, EXPECT_BLOCK);
+ testNestedSandboxedIFrame("'self'", SAME_ORIGIN, SAME_ORIGIN, EXPECT_BLOCK);
</script>
</body>
</html>
diff --git a/testing/web-platform/tests/content-security-policy/generic/case-insensitive-scheme.sub.html b/testing/web-platform/tests/content-security-policy/generic/case-insensitive-scheme.sub.html
new file mode 100644
index 0000000000..7225cd359f
--- /dev/null
+++ b/testing/web-platform/tests/content-security-policy/generic/case-insensitive-scheme.sub.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+</head>
+<body>
+ <script>
+ let tests = [
+ {
+ "csp": "img-src http://{{host}}:{{ports[http][0]}}/",
+ "name": "Lowercase `http` should allow the image to load.",
+ },
+ {
+ "csp": "img-src HtTp://{{host}}:{{ports[http][0]}}/",
+ "name": "Mixed-case `http` should allow the image to load.",
+ },
+ {
+ "csp": "img-src HTTP://{{host}}:{{ports[http][0]}}/",
+ "name": "Uppercase `http` should allow the image to load.",
+ },
+ ];
+
+ tests.forEach(test => {
+ async_test(t => {
+ let url = "support/load_img_and_post_result_meta.sub.html?csp="
+ + encodeURIComponent(test.csp);
+ test_image_loads_as_expected(test, t, url);
+ }, test.name + " - meta tag");
+
+ async_test(t => {
+ let url = "support/load_img_and_post_result_header.html?csp="
+ + encodeURIComponent(test.csp);
+ test_image_loads_as_expected(test, t, url);
+ }, test.name + " - HTTP header");
+ });
+
+ function test_image_loads_as_expected(test, t, url) {
+ let i = document.createElement('iframe');
+ i.src = url;
+ window.addEventListener('message', t.step_func(function(e) {
+ if (e.source != i.contentWindow) return;
+ assert_equals(e.data, "img loaded");
+ t.done();
+ }));
+ document.body.appendChild(i);
+ }
+ </script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/content-security-policy/generic/wildcard-host-part.sub.window.js b/testing/web-platform/tests/content-security-policy/generic/wildcard-host-part.sub.window.js
new file mode 100644
index 0000000000..d210cc6670
--- /dev/null
+++ b/testing/web-platform/tests/content-security-policy/generic/wildcard-host-part.sub.window.js
@@ -0,0 +1,27 @@
+setup(_ => {
+ const meta = document.createElement("meta");
+ meta.httpEquiv = "content-security-policy";
+ meta.content = "img-src http://*:{{ports[http][0]}}";
+ document.head.appendChild(meta);
+});
+
+async_test((t) => {
+ const img = document.createElement("img");
+ img.onerror = t.step_func_done();
+ img.onload = t.unreached_func("`data:` image should have been blocked.");
+ img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+}, "Host wildcard doesn't affect scheme matching.");
+
+async_test((t) => {
+ const img = document.createElement("img");
+ img.onload = t.step_func_done();
+ img.onerror = t.unreached_func("Image from www2 host should have loaded.");
+ img.src = "http://{{domains[www1]}}:{{ports[http][0]}}/content-security-policy/support/pass.png";
+}, "Host wildcard allows arbitrary hosts (www1).");
+
+async_test((t) => {
+ const img = document.createElement("img");
+ img.onload = t.step_func_done();
+ img.onerror = t.unreached_func("Image from www2 host should have loaded.");
+ img.src = "http://{{domains[www2]}}:{{ports[http][0]}}/content-security-policy/support/pass.png";
+}, "Host wildcard allows arbitrary hosts (www2).");
diff --git a/testing/web-platform/tests/content-security-policy/script-src/script-src-strict_dynamic_hashes.html b/testing/web-platform/tests/content-security-policy/script-src/script-src-strict_dynamic_hashes.html
index e4ce1e5944..02c5c9642b 100644
--- a/testing/web-platform/tests/content-security-policy/script-src/script-src-strict_dynamic_hashes.html
+++ b/testing/web-platform/tests/content-security-policy/script-src/script-src-strict_dynamic_hashes.html
@@ -6,7 +6,7 @@
<script src='/resources/testharness.js' nonce='dummy'></script>
<script src='/resources/testharnessreport.js' nonce='dummy'></script>
- <!-- CSP served: script-src 'strict-dynamic' 'nonce-dummy' 'sha256-yU6Q7nD1TCBB9JvY06iIJ8ONLOPU4g8ml5JCDgXkv+M=' 'sha256-EEoi70frWHkGFhK51NVIJkXpq72aPxSCNZEow37ZmRA=' -->
+ <!-- CSP served: script-src 'strict-dynamic' 'nonce-dummy' 'sha256-yU6Q7nD1TCBB9JvY06iIJ8ONLOPU4g8ml5JCDgXkv+M=' 'sha256-EEoi70frWHkGFhK51NVIJkXpq72aPxSCNZEow37ZmRA=' 'sha256-wIc3KtqOuTFEu6t17sIBuOswgkV406VJvhSk79Gw6U0=' -->
</head>
<body>
@@ -47,6 +47,17 @@
document.body.appendChild(e);
}, 'Script injected via `appendChild` from a script matching SHA256 hash is allowed with `strict-dynamic`.');
</script>
+
+ <script nonce='dummy'>
+ var externalRan = false;
+ </script>
+ <script src='./externalScript.js'
+ integrity="sha256-wIc3KtqOuTFEu6t17sIBuOswgkV406VJvhSk79Gw6U0="></script>
+ <script nonce='dummy'>
+ test(function(t) {
+ assert_true(externalRan);
+ }, "External script in a script tag with matching SRI hash is allowed with `strict-dynamic`.");
+ </script>
</body>
</html>
diff --git a/testing/web-platform/tests/content-security-policy/script-src/script-src-strict_dynamic_hashes.html.headers b/testing/web-platform/tests/content-security-policy/script-src/script-src-strict_dynamic_hashes.html.headers
index 0d824d8b0e..4d3d904c68 100644
--- a/testing/web-platform/tests/content-security-policy/script-src/script-src-strict_dynamic_hashes.html.headers
+++ b/testing/web-platform/tests/content-security-policy/script-src/script-src-strict_dynamic_hashes.html.headers
@@ -2,4 +2,4 @@ Expires: Mon, 26 Jul 1997 05:00:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Cache-Control: post-check=0, pre-check=0, false
Pragma: no-cache
-Content-Security-Policy: script-src 'strict-dynamic' 'nonce-dummy' 'sha256-yU6Q7nD1TCBB9JvY06iIJ8ONLOPU4g8ml5JCDgXkv+M=' 'sha256-EEoi70frWHkGFhK51NVIJkXpq72aPxSCNZEow37ZmRA='
+Content-Security-Policy: script-src 'strict-dynamic' 'nonce-dummy' 'sha256-yU6Q7nD1TCBB9JvY06iIJ8ONLOPU4g8ml5JCDgXkv+M=' 'sha256-EEoi70frWHkGFhK51NVIJkXpq72aPxSCNZEow37ZmRA=' 'sha256-wIc3KtqOuTFEu6t17sIBuOswgkV406VJvhSk79Gw6U0='
diff --git a/testing/web-platform/tests/credential-management/digital-identity.https.html b/testing/web-platform/tests/credential-management/digital-identity.https.html
index b2f36d21ee..8ae9caa002 100644
--- a/testing/web-platform/tests/credential-management/digital-identity.https.html
+++ b/testing/web-platform/tests/credential-management/digital-identity.https.html
@@ -17,31 +17,6 @@ const host = get_host_info();
const basePath = window.location.pathname.replace(/\/[^\/]*$/, '/');
const remoteBaseURL = host.HTTPS_REMOTE_ORIGIN + basePath;
-// Builds valid digital identity request for navigator.credentials.get() API.
-function buildValidNavigatorCredentialsRequest() {
- return {
- identity: {
- providers: [{
- holder: {
- selector: {
- format: ['mdoc'],
- doctype: 'org.iso.18013.5.1.mDL',
- fields: [
- 'org.iso.18013.5.1.family_name',
- 'org.iso.18013.5.1.portrait',
- ]
- },
- params: {
- nonce: '1234',
- readerPublicKey: 'test_reader_public_key',
- extraParamAsNeededByDigitalCredentials: true,
- },
- },
- }],
- },
- };
-}
-
async function createIframeAndWaitForMessage(test, iframeUrl) {
const messageWatcher = new EventWatcher(test, window, "message");
var iframe = document.createElement("iframe");
@@ -54,39 +29,6 @@ async function createIframeAndWaitForMessage(test, iframeUrl) {
// Requires browser to have mode where OS-presented digital-identity-prompt is
// bypassed in favour of returning "fake_test_token" directly.
promise_test(async t => {
- const {token} = await navigator.credentials.get(buildValidNavigatorCredentialsRequest());
- assert_equals("fake_test_token", token);
-}, "navigator.credentials.get() API works in toplevel frame.");
-
-promise_test(async t => {
- let request = buildValidNavigatorCredentialsRequest();
- request.identity.providers = undefined;
-
- await promise_rejects_js(t, TypeError, navigator.credentials.get(request));
-}, "navigator.credentials.get() API fails if IdentityCredentialRequestOptions::providers is not specified.");
-
-promise_test(async t => {
- let request = buildValidNavigatorCredentialsRequest();
- request.identity.providers = [];
-
- await promise_rejects_js(t, TypeError, navigator.credentials.get(request));
-}, "navigator.credentials.get() API fails if there are no providers.");
-
-promise_test(async t => {
- let request = buildValidNavigatorCredentialsRequest();
- let providerCopy = structuredClone(request.identity.providers[0]);
- request.identity.providers.push(providerCopy);
- await promise_rejects_js(t, TypeError, navigator.credentials.get(request));
-}, "navigator.credentials.get() API fails if there is more than one provider.");
-
-promise_test(async t => {
- let request = buildValidNavigatorCredentialsRequest();
- request.identity.providers[0].holder = undefined;
-
- await promise_rejects_js(t, TypeError, navigator.credentials.get(request));
-}, "navigator.credentials.get() API fails if IdentityProviderConfig::holder is not specified.");
-
-promise_test(async t => {
let request = buildValidNavigatorIdentityRequest();
let credential = await navigator.identity.get(request);
assert_equals("urn:openid.net:oid4vp", credential.protocol);
diff --git a/testing/web-platform/tests/credential-management/fedcm-context.https.html b/testing/web-platform/tests/credential-management/fedcm-context.https.html
index 7b3e1032af..f235437b78 100644
--- a/testing/web-platform/tests/credential-management/fedcm-context.https.html
+++ b/testing/web-platform/tests/credential-management/fedcm-context.https.html
@@ -12,37 +12,38 @@
import {request_options_with_mediation_required,
request_options_with_context,
fedcm_get_title_promise,
- fedcm_test} from './support/fedcm-helper.sub.js';
+ fedcm_test,
+ fedcm_select_account_promise} from './support/fedcm-helper.sub.js';
fedcm_test(async t => {
- let p = navigator.credentials.get(request_options_with_mediation_required());
+ const p = navigator.credentials.get(request_options_with_mediation_required());
const result = await fedcm_get_title_promise(t);
assert_true(result.title.toLowerCase().includes('sign in'));
- window.test_driver.select_fedcm_account(0);
+ fedcm_select_account_promise(t, 0);
return p;
}, "FedCM call defaults to 'signin' context.");
fedcm_test(async t => {
- let p = navigator.credentials.get(request_options_with_context("manifest.py", "signup"));
+ const p = navigator.credentials.get(request_options_with_context("manifest.py", "signup"));
const result = await fedcm_get_title_promise(t);
assert_true(result.title.toLowerCase().includes('sign up'));
- window.test_driver.select_fedcm_account(0);
+ fedcm_select_account_promise(t, 0);
return p;
}, "FedCM with 'signup' context.");
fedcm_test(async t => {
- let p = navigator.credentials.get(request_options_with_context("manifest.py", "use"));
+ const p = navigator.credentials.get(request_options_with_context("manifest.py", "use"));
const result = await fedcm_get_title_promise(t);
assert_true(result.title.toLowerCase().includes('use'));
- window.test_driver.select_fedcm_account(0);
+ fedcm_select_account_promise(t, 0);
return p;
}, "FedCM with 'use' context.");
fedcm_test(async t => {
- let p = navigator.credentials.get(request_options_with_context("manifest.py", "continue"));
+ const p = navigator.credentials.get(request_options_with_context("manifest.py", "continue"));
const result = await fedcm_get_title_promise(t);
assert_true(result.title.toLowerCase().includes('continue'));
- window.test_driver.select_fedcm_account(0);
+ fedcm_select_account_promise(t, 0);
return p;
}, "FedCM with 'continue' context.");
</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-disconnect.sub.https.html b/testing/web-platform/tests/credential-management/fedcm-disconnect.sub.https.html
index 300144fa72..2ea2d4a259 100644
--- a/testing/web-platform/tests/credential-management/fedcm-disconnect.sub.https.html
+++ b/testing/web-platform/tests/credential-management/fedcm-disconnect.sub.https.html
@@ -11,7 +11,6 @@
<script type="module">
import {fedcm_test,
mark_signed_in,
- set_fedcm_cookie,
disconnect_options,
fedcm_get_and_select_first_account,
request_options_with_mediation_required,
@@ -21,10 +20,9 @@ import {fedcm_test,
set_alt_fedcm_cookie} from './support/fedcm-helper.sub.js';
fedcm_test(async t => {
- await mark_signed_in();
- await set_fedcm_cookie();
+ await mark_signed_in(alt_manifest_origin);
// Get at least one connected account that can be disconnected.
- const cred = await fedcm_get_and_select_first_account(t, request_options_with_mediation_required());
+ const cred = await fedcm_get_and_select_first_account(t, alt_request_options_with_mediation_required());
// The IDP implementation will accept any account hint, so this is really testing that the user
// agent eventually stops sending the requests to the IDP.
// This test clears the connection just created above, but it also clears any previously existing
@@ -32,7 +30,7 @@ fedcm_test(async t => {
return new Promise(async resolve => {
while (true) {
try {
- await IdentityCredential.disconnect(disconnect_options("1234"));
+ await IdentityCredential.disconnect(alt_disconnect_options("1234"));
} catch(e) {
resolve();
break;
@@ -43,34 +41,37 @@ fedcm_test(async t => {
fedcm_test(async t => {
const disconnect = IdentityCredential.disconnect(
- disconnect_options("nonExistent"));
+ alt_disconnect_options("nonExistent"));
return promise_rejects_dom(t, 'NetworkError', disconnect);
}, 'Test that disconnect fails when there is no account to disconnect');
fedcm_test(async t => {
- const cred = await fedcm_get_and_select_first_account(t, request_options_with_mediation_required());
+ await mark_signed_in(alt_manifest_origin);
+ const cred = await fedcm_get_and_select_first_account(t, alt_request_options_with_mediation_required());
- return IdentityCredential.disconnect(disconnect_options("1234"));
+ return IdentityCredential.disconnect(alt_disconnect_options("1234"));
}, 'Test that disconnect succeeds when there is an account to disconnect');
fedcm_test(async t => {
- const cred = await fedcm_get_and_select_first_account(t, request_options_with_mediation_required());
+ await mark_signed_in(alt_manifest_origin);
+ const cred = await fedcm_get_and_select_first_account(t, alt_request_options_with_mediation_required());
- await IdentityCredential.disconnect(disconnect_options("1234"));
+ await IdentityCredential.disconnect(alt_disconnect_options("1234"));
- const disconnect = IdentityCredential.disconnect(disconnect_options("1234"));
+ const disconnect = IdentityCredential.disconnect(alt_disconnect_options("1234"));
return promise_rejects_dom(t, 'NetworkError', disconnect);
}, 'Test that disconnecting the same account twice results in failure.');
fedcm_test(async t => {
- const cred = await fedcm_get_and_select_first_account(t, request_options_with_mediation_required());
+ await mark_signed_in(alt_manifest_origin);
+ const cred = await fedcm_get_and_select_first_account(t, alt_request_options_with_mediation_required());
// A connected account is guaranteed by the above, and IDP accepts any account hint, so this tests
// that the user agent allows the request to go through to the IDP.
- return IdentityCredential.disconnect(disconnect_options("noMatch"));
+ return IdentityCredential.disconnect(alt_disconnect_options("noMatch"));
}, 'Disconnect passing an incorrect ID can still succeed');
fedcm_test(async t => {
- await set_alt_fedcm_cookie();
+ await mark_signed_in();
await mark_signed_in(alt_manifest_origin);
await fedcm_get_and_select_first_account(t, alt_request_options_with_mediation_required());
await fedcm_get_and_select_first_account(t,
diff --git a/testing/web-platform/tests/credential-management/fedcm-endpoint-redirects.https.html b/testing/web-platform/tests/credential-management/fedcm-endpoint-redirects.https.html
index 36a4de7900..71dbce0326 100644
--- a/testing/web-platform/tests/credential-management/fedcm-endpoint-redirects.https.html
+++ b/testing/web-platform/tests/credential-management/fedcm-endpoint-redirects.https.html
@@ -11,6 +11,7 @@ import {request_options_with_mediation_required,
fedcm_test,
select_manifest,
mark_signed_in,
+ fedcm_error_dialog_dismiss,
fedcm_get_dialog_type_promise,
fedcm_get_and_select_first_account} from './support/fedcm-helper.sub.js';
@@ -35,7 +36,9 @@ fedcm_test(async t => {
await select_manifest(t, test_options);
try {
- const cred = await fedcm_get_and_select_first_account(t, test_options);
+ const cred = fedcm_get_and_select_first_account(t, test_options);
+ fedcm_error_dialog_dismiss(t);
+ await cred;
assert_unreached("An IdentityCredentialError exception should be thrown.");
} catch (e) {
assert_true(e instanceof DOMException);
diff --git a/testing/web-platform/tests/credential-management/fedcm-error-basic.https.html b/testing/web-platform/tests/credential-management/fedcm-error-basic.https.html
index 49d6ea50df..fd902bcf90 100644
--- a/testing/web-platform/tests/credential-management/fedcm-error-basic.https.html
+++ b/testing/web-platform/tests/credential-management/fedcm-error-basic.https.html
@@ -23,7 +23,7 @@ fedcm_test(async t => {
await select_manifest(t, test_options);
try {
- const cred = await fedcm_get_and_select_first_account(t, test_options);
+ const cred = fedcm_get_and_select_first_account(t, test_options);
fedcm_error_dialog_dismiss(t);
await cred;
assert_unreached("An IdentityCredentialError exception should be thrown.");
@@ -41,7 +41,7 @@ fedcm_test(async t => {
await select_manifest(t, test_options);
try {
- const cred = await fedcm_get_and_select_first_account(t, test_options);
+ const cred = fedcm_get_and_select_first_account(t, test_options);
fedcm_error_dialog_click_button(t, "ErrorGotIt");
await cred;
assert_unreached("An IdentityCredentialError exception should be thrown.");
@@ -59,7 +59,7 @@ fedcm_test(async t => {
await select_manifest(t, test_options);
try {
- const cred = await fedcm_get_and_select_first_account(t, test_options);
+ const cred = fedcm_get_and_select_first_account(t, test_options);
fedcm_error_dialog_click_button(t, "ErrorMoreDetails");
await cred;
assert_unreached("An IdentityCredentialError exception should be thrown.");
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/abort-multiple-gets-through-first-idp.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/abort-multiple-gets-through-first-idp.https.html
deleted file mode 100644
index ed7c1300bd..0000000000
--- a/testing/web-platform/tests/credential-management/fedcm-multi-idp/abort-multiple-gets-through-first-idp.https.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<title>Federated Credential Management API multi IDP abort first IDP test.</title>
-<link rel="help" href="https://fedidcg.github.io/FedCM">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-
-<script type="module">
- import {
- set_fedcm_cookie,
- set_alt_fedcm_cookie,
- request_options_with_mediation_required,
- alt_request_options_with_mediation_required
- } from '../support/fedcm-helper.sub.js';
-
- let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
-
- promise_test(async t => {
- let first_controller = new AbortController();
- let first_test_options = request_options_with_mediation_required();
- first_test_options.signal = first_controller.signal;
- const first_cred = navigator.credentials.get(first_test_options);
-
- let second_controller = new AbortController();
- let second_test_options = alt_request_options_with_mediation_required();
- second_test_options.signal = second_controller.signal;
- const second_cred = navigator.credentials.get(second_test_options);
-
- await cookies_promise;
- first_controller.abort();
- return Promise.all([
- promise_rejects_dom(t, 'AbortError', first_cred),
- promise_rejects_dom(t, 'AbortError', second_cred)
- ]);
- }, "Test abort signal for a multi IDP request by aborting the first IDP");
-</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/abort-multiple-gets-through-second-idp.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/abort-multiple-gets-through-second-idp.https.html
deleted file mode 100644
index dfe8969932..0000000000
--- a/testing/web-platform/tests/credential-management/fedcm-multi-idp/abort-multiple-gets-through-second-idp.https.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<title>Federated Credential Management API multi IDP abort second IDP test.</title>
-<link rel="help" href="https://fedidcg.github.io/FedCM">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-
-<script type="module">
- import {
- set_fedcm_cookie,
- set_alt_fedcm_cookie,
- request_options_with_mediation_required,
- alt_request_options_with_mediation_required
- } from '../support/fedcm-helper.sub.js';
-
- let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
-
- promise_test(async t => {
- let first_controller = new AbortController();
- let first_test_options = request_options_with_mediation_required();
- first_test_options.signal = first_controller.signal;
- const first_cred = navigator.credentials.get(first_test_options);
-
- let second_controller = new AbortController();
- let second_test_options = alt_request_options_with_mediation_required();
- second_test_options.signal = second_controller.signal;
- const second_cred = navigator.credentials.get(second_test_options);
-
- await cookies_promise;
- second_controller.abort();
- return Promise.all([
- promise_rejects_dom(t, 'AbortError', first_cred),
- promise_rejects_dom(t, 'AbortError', second_cred)
- ]);
- }, "Test abort signal for a multi IDP request by aborting the second IDP");
-</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-abort.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-abort.https.html
new file mode 100644
index 0000000000..712a7b6a34
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-abort.https.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API multi IDP abort.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<script type="module">
+import {request_options_with_two_idps,
+ fedcm_test,
+ fedcm_get_and_select_first_account} from '../support/fedcm-helper.sub.js';
+
+fedcm_test(async t => {
+ let controller = new AbortController();
+ let test_options = request_options_with_two_idps();
+ test_options.signal = controller.signal;
+ const cred = fedcm_get_and_select_first_account(t, test_options);
+ controller.abort();
+ return promise_rejects_dom(t, 'AbortError', cred);
+}, "Test that the abort signal works when multiple IDPs are used.");
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-basic.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-basic.https.html
new file mode 100644
index 0000000000..d855e0ad8d
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-basic.https.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API multi IDP basic success tests.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<body>
+
+<script type="module">
+import {fedcm_test,
+ fedcm_get_and_select_first_account,
+ request_options_with_two_idps,
+ manifest_origin,
+ default_manifest_path,
+ fedcm_select_account_promise,
+ alt_manifest_origin} from '../support/fedcm-helper.sub.js';
+
+fedcm_test(async t => {
+ const cred = await fedcm_get_and_select_first_account(t, request_options_with_two_idps());
+ assert_equals(cred.token, "token");
+ assert_equals(cred.configURL, manifest_origin + default_manifest_path);
+}, "Multi IDP FedCM call succeeds when picking the first account.");
+
+fedcm_test(async t => {
+ const promise = navigator.credentials.get(request_options_with_two_idps());
+ // Each IDP has one account, so select the second one.
+ fedcm_select_account_promise(t, 1);
+ const cred = await promise;
+ assert_equals(cred.token, "token");
+ assert_equals(cred.configURL, alt_manifest_origin + default_manifest_path);
+}, "Multi IDP FedCM call succeeds when picking account from the second IDP.");
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-context.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-context.https.html
new file mode 100644
index 0000000000..1bc3eb1f56
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-context.https.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API multi IDP context tests.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<body>
+
+<script type="module">
+import {request_options_with_two_idps,
+ fedcm_get_title_promise,
+ fedcm_test,
+ fedcm_select_account_promise} from '../support/fedcm-helper.sub.js';
+
+fedcm_test(async t => {
+ const p = navigator.credentials.get(request_options_with_two_idps());
+ const result = await fedcm_get_title_promise(t);
+ assert_true(result.title.toLowerCase().includes('sign in'));
+ fedcm_select_account_promise(t, 0);
+ return p;
+}, "FedCM multi IDP call defaults to 'signin' context.");
+
+fedcm_test(async t => {
+ const options = request_options_with_two_idps();
+ options.identity.context = "signup";
+ const p = navigator.credentials.get(options);
+ const result = await fedcm_get_title_promise(t);
+ assert_true(result.title.toLowerCase().includes('sign up'));
+ fedcm_select_account_promise(t, 0);
+ return p;
+}, "FedCM multi IDP with non-default context.");
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-mediation-optional.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-mediation-optional.https.html
new file mode 100644
index 0000000000..1a819efb31
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-mediation-optional.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API multi IDP optional mediation tests.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<script type="module">
+import {request_options_with_two_idps,
+ fedcm_test,
+ fedcm_get_and_select_first_account,
+ fedcm_select_account_promise} from '../support/fedcm-helper.sub.js';
+
+fedcm_test(async t => {
+ return fedcm_get_and_select_first_account(t, request_options_with_two_idps('optional'));
+}, "Mediation optional can succeed without a returning account.");
+
+fedcm_test(async t => {
+ // Sign in to the first account.
+ await fedcm_get_and_select_first_account(t, request_options_with_two_idps());
+
+ // Now use mediation:optional and it should work.
+ return navigator.credentials.get(request_options_with_two_idps('optional'));
+}, "Mediation optional automatically succeeds when there is one returning account.");
+
+fedcm_test(async t => {
+ // Sign in to the first account.
+ await fedcm_get_and_select_first_account(t, request_options_with_two_idps());
+
+ // Sign in to the second account as well.
+ let cred = navigator.credentials.get(request_options_with_two_idps());
+ fedcm_select_account_promise(t, 1);
+ await cred;
+
+ // Now use mediation:optional.
+ return fedcm_get_and_select_first_account(t, request_options_with_two_idps('optional'));
+}, "Mediation optional can succeed when there is more than one returning account.");
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-mediation-silent.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-mediation-silent.https.html
new file mode 100644
index 0000000000..d47d4898c7
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-mediation-silent.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API multi IDP silent mediation tests.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<script type="module">
+import {request_options_with_two_idps,
+ fedcm_test,
+ fedcm_get_and_select_first_account,
+ fedcm_select_account_promise} from '../support/fedcm-helper.sub.js';
+
+fedcm_test(async t => {
+ const cred = navigator.credentials.get(request_options_with_two_idps('silent'));
+ return promise_rejects_dom(t, 'NetworkError', cred);
+}, "Mediation silent fails if there is no returning account.");
+
+fedcm_test(async t => {
+ // Sign in to the first account.
+ await fedcm_get_and_select_first_account(t, request_options_with_two_idps());
+
+ // Now use mediation:silent and it should work.
+ return navigator.credentials.get(request_options_with_two_idps('silent'));
+}, "Mediation silent succeeds when there is one returning account.");
+
+fedcm_test(async t => {
+ // Sign in to the first account.
+ await fedcm_get_and_select_first_account(t, request_options_with_two_idps());
+
+ // Sign in to the second account as well.
+ let cred = navigator.credentials.get(request_options_with_two_idps());
+ fedcm_select_account_promise(t, 1);
+ await cred;
+
+ // Now use mediation:silent and it should fail.
+ cred = navigator.credentials.get(request_options_with_two_idps('silent'));
+ return promise_rejects_dom(t, 'NetworkError', cred);
+}, "Mediation silent fails when there is more than one returning account.");
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-after-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-after-onload.https.html
deleted file mode 100644
index 12e0eb4d81..0000000000
--- a/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-after-onload.https.html
+++ /dev/null
@@ -1,47 +0,0 @@
-<!DOCTYPE html>
-<title>Federated Credential Management API multi IDP get before and after onload test.</title>
-<link rel="help" href="https://fedidcg.github.io/FedCM">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/resources/testdriver.js"></script>
-<script src="/resources/testdriver-vendor.js"></script>
-
-<body>
-
-<script type="module">
-import {set_fedcm_cookie, set_alt_fedcm_cookie,
- request_options_with_mediation_required,
- alt_request_options_with_mediation_required,
- fedcm_select_account_promise} from '../support/fedcm-helper.sub.js';
-
-let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
-let has_window_loaded = false;
-const window_loaded = new Promise(resolve => {
- window.addEventListener('load', () => {
- has_window_loaded = true;
- resolve();
- });
-});
-
-promise_test(async t => {
- let first_cred_resolved = false;
- assert_false(has_window_loaded);
- // First navigator.credentials.get() is called prior to window.onload
- const first_cred = navigator.credentials.get(request_options_with_mediation_required()).finally(() => { first_cred_resolved = true; });
- await Promise.all([cookies_promise, window_loaded]);
- assert_true(has_window_loaded);
- assert_false(first_cred_resolved);
-
- // Second navigator.credentials.get() is called after window.onload but before first navigator.credentials.get()
- // resolves. Should be rejected because it occurs after onload, and the first get() call is pending.
- const second_cred = navigator.credentials.get(alt_request_options_with_mediation_required());
- const rejection = promise_rejects_dom(t, 'NotAllowedError', second_cred);
-
- // Select first account from the first get() call.
- await fedcm_select_account_promise(t, 0);
- const first = await first_cred;
- assert_equals(first.token, "token");
- return rejection;
-}, "When there's a `get` call before onload, a `get` call which occurs after onload but before the first `get` call resolves, should be rejected.");
-
-</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-during-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-during-onload.https.html
deleted file mode 100644
index 3e2f134f20..0000000000
--- a/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-during-onload.https.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE html>
-<title>Federated Credential Management API multi IDP get before and during onload test.</title>
-<link rel="help" href="https://fedidcg.github.io/FedCM">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/resources/testdriver.js"></script>
-<script src="/resources/testdriver-vendor.js"></script>
-
-<body>
-
-<script type="module">
-import {set_fedcm_cookie, set_alt_fedcm_cookie,
- request_options_with_mediation_required,
- alt_request_options_with_mediation_required,
- fedcm_select_account_promise} from '../support/fedcm-helper.sub.js';
-
-let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
-
-promise_test(async t => {
- let has_window_loaded = false;
- let rejection;
- const window_loaded = new Promise(resolve => {
- window.addEventListener('load', async () => {
- const second_cred = navigator.credentials.get(alt_request_options_with_mediation_required());
- rejection = promise_rejects_dom(t, 'NetworkError', second_cred);
- has_window_loaded = true;
- resolve();
- });
- });
- assert_false(has_window_loaded);
- const first_cred = navigator.credentials.get(request_options_with_mediation_required());
- await Promise.all([cookies_promise, window_loaded]);
-
- // Select first account from the first get() call.
- await fedcm_select_account_promise(t, 0);
- assert_true(has_window_loaded);
- const first = await first_cred;
- assert_equals(first.token, "token");
- return rejection;
-}, "A `get` call before onload and a `get` call during onload should be combined.");
-
-</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-onload-and-during-dom-content-loaded.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-onload-and-during-dom-content-loaded.https.html
deleted file mode 100644
index 95495948b7..0000000000
--- a/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-onload-and-during-dom-content-loaded.https.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE html>
-<title>Federated Credential Management API multi IDP get before onload and during DOMContentLoaded test.</title>
-<link rel="help" href="https://fedidcg.github.io/FedCM">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/resources/testdriver.js"></script>
-<script src="/resources/testdriver-vendor.js"></script>
-
-<body>
-
-<script type="module">
-import {set_fedcm_cookie,
- set_alt_fedcm_cookie,
- request_options_with_mediation_required,
- alt_request_options_with_mediation_required,
- fedcm_select_account_promise} from '../support/fedcm-helper.sub.js';
-
-let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
-
-promise_test(async t => {
- let has_dom_content_loaded = false;
- let rejection;
- const dom_content_loaded = new Promise(resolve => {
- document.addEventListener('DOMContentLoaded', async () => {
- const second_cred = navigator.credentials.get(alt_request_options_with_mediation_required());
- rejection = promise_rejects_dom(t, 'NetworkError', second_cred);
- has_dom_content_loaded = true;
- resolve();
- });
- });
- assert_false(has_dom_content_loaded);
- const first_cred = navigator.credentials.get(request_options_with_mediation_required());
- await Promise.all([cookies_promise, dom_content_loaded]);
- assert_true(has_dom_content_loaded);
-
- await fedcm_select_account_promise(t, 0);
- const first = await first_cred;
- assert_equals(first.token, "token");
- return rejection;
-}, "A `get` call before onload and a `get` call during DOMContentLoaded event should combine despite being called from different tasks.");
-
-</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-after-abort.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-after-abort.https.html
deleted file mode 100644
index 899302fb22..0000000000
--- a/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-after-abort.https.html
+++ /dev/null
@@ -1,49 +0,0 @@
-<!DOCTYPE html>
-<title>Federated Credential Management API multi IDP get after abort test.</title>
-<link rel="help" href="https://fedidcg.github.io/FedCM">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/resources/testdriver.js"></script>
-<script src="/resources/testdriver-vendor.js"></script>
-
-<script type="module">
- import {
- set_fedcm_cookie,
- set_alt_fedcm_cookie,
- request_options_with_mediation_required,
- alt_request_options_with_mediation_required,
- fedcm_select_account_promise
- } from '../support/fedcm-helper.sub.js';
-
- let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
-
- promise_test(async t => {
- let first_controller = new AbortController();
- let first_test_options = request_options_with_mediation_required();
- first_test_options.signal = first_controller.signal;
- const first_cred = navigator.credentials.get(first_test_options);
-
- let second_controller = new AbortController();
- let second_test_options = alt_request_options_with_mediation_required();
- second_test_options.signal = second_controller.signal;
- const second_cred = navigator.credentials.get(second_test_options);
-
- await cookies_promise;
- second_controller.abort();
- await Promise.all([
- promise_rejects_dom(t, 'AbortError', first_cred),
- promise_rejects_dom(t, 'AbortError', second_cred)
- ]);
-
- const third_cred = navigator.credentials.get(request_options_with_mediation_required());
- const fourth_cred = navigator.credentials.get(alt_request_options_with_mediation_required());
-
- // Select first account, i.e. from the `third_cred`.
- await fedcm_select_account_promise(t, 0);
-
- // NetworkError is returned when another IDP is selected.
- await promise_rejects_dom(t, 'NetworkError', fourth_cred);
- const cred = await third_cred;
- assert_equals(cred.token, "token");
- }, "Multiple gets after aborting a multi IDP request should work");
-</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-after-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-after-onload.https.html
deleted file mode 100644
index 1b5d744e8f..0000000000
--- a/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-after-onload.https.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<title>Federated Credential Management API multi IDP multiple gets after onload test.</title>
-<link rel="help" href="https://fedidcg.github.io/FedCM">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/resources/testdriver.js"></script>
-<script src="/resources/testdriver-vendor.js"></script>
-
-<body>
-
-<script type="module">
-import {set_fedcm_cookie,
- set_alt_fedcm_cookie,
- request_options_with_mediation_required,
- alt_request_options_with_mediation_required,
- fedcm_select_account_promise} from '../support/fedcm-helper.sub.js';
-
-let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
-const window_loaded = new Promise(resolve => {
- window.addEventListener('load', () => {
- resolve();
- });
-});
-
-promise_test(async t => {
- await Promise.all([cookies_promise, window_loaded]);
- const first_cred = navigator.credentials.get(request_options_with_mediation_required());
- const second_cred = navigator.credentials.get(alt_request_options_with_mediation_required());
-
- // Select first account from the first get() call.
- await fedcm_select_account_promise(t, 0);
- // NetworkError is returned when another IDP is selected.
- await promise_rejects_dom(t, 'NetworkError', second_cred);
- const first = await first_cred;
- assert_equals(first.token, "token");
-}, "No `get` calls before or during onload, multiple `get` calls after onload in the same task are allowed.");
-
-</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-before-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-before-onload.https.html
deleted file mode 100644
index 8c98bf53b0..0000000000
--- a/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-before-onload.https.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<title>Federated Credential Management API multi IDP multiple gets before onload test.</title>
-<link rel="help" href="https://fedidcg.github.io/FedCM">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/resources/testdriver.js"></script>
-<script src="/resources/testdriver-vendor.js"></script>
-
-<body>
-
-<script type="module">
-import {set_fedcm_cookie,
- set_alt_fedcm_cookie,
- request_options_with_mediation_required,
- alt_request_options_with_mediation_required,
- fedcm_select_account_promise} from '../support/fedcm-helper.sub.js';
-
-let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
-let has_window_loaded = false;
-window.addEventListener('load', () => {
- has_window_loaded = true;
-});
-
-promise_test(async t => {
- assert_false(has_window_loaded);
- const first_cred = navigator.credentials.get(alt_request_options_with_mediation_required());
- const second_cred = navigator.credentials.get(request_options_with_mediation_required());
- await cookies_promise;
-
- // Select second account, i.e. from the second get() call.
- await fedcm_select_account_promise(t, 1);
- await promise_rejects_dom(t, 'NetworkError', first_cred);
- const cred = await second_cred;
- assert_equals(cred.token, "token");
-}, "Multiple get calls before window onload are allowed.");
-
-</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-during-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-during-onload.https.html
deleted file mode 100644
index bcf70a31c7..0000000000
--- a/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-during-onload.https.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<title>Federated Credential Management API multi IDP multiple gets during onload test.</title>
-<link rel="help" href="https://fedidcg.github.io/FedCM">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/resources/testdriver.js"></script>
-<script src="/resources/testdriver-vendor.js"></script>
-
-<body>
-
-<script type="module">
-import {set_fedcm_cookie,
- set_alt_fedcm_cookie,
- request_options_with_mediation_required,
- alt_request_options_with_mediation_required,
- fedcm_select_account_promise} from '../support/fedcm-helper.sub.js';
-
-let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
-
-promise_test(async t => {
- const window_loaded = new Promise(resolve => {
- window.addEventListener('load', async () => {
- const first_cred = navigator.credentials.get(request_options_with_mediation_required());
- const second_cred = navigator.credentials.get(alt_request_options_with_mediation_required());
- await cookies_promise;
- await fedcm_select_account_promise(t, 0);
- await promise_rejects_dom(t, 'NetworkError', second_cred);
- const first = await first_cred;
- assert_equals(first.token, "token");
- resolve();
- });
- });
- await window_loaded;
-}, "No `get` calls before onload, multiple `get` calls during onload are allowed.");
-
-</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-after-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-after-onload.https.html
deleted file mode 100644
index de6a7c5371..0000000000
--- a/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-after-onload.https.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<title>Federated Credential Management API multi IDP single get after onload test.</title>
-<link rel="help" href="https://fedidcg.github.io/FedCM">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/resources/testdriver.js"></script>
-<script src="/resources/testdriver-vendor.js"></script>
-
-<body>
-
-<script type="module">
-import {set_fedcm_cookie,
- request_options_with_mediation_required,
- fedcm_get_and_select_first_account} from '../support/fedcm-helper.sub.js';
-
-const window_loaded = new Promise(resolve => {
- window.addEventListener('load', () => {
- resolve();
- });
-});
-
-promise_test(async t => {
- await set_fedcm_cookie();
- await window_loaded;
- const cred = await fedcm_get_and_select_first_account(t, request_options_with_mediation_required());
- assert_equals(cred.token, "token");
-}, "Single `get` call after onload is allowed.");
-
-</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-before-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-before-onload.https.html
deleted file mode 100644
index 0ac9b0e920..0000000000
--- a/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-before-onload.https.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<title>Federated Credential Management API multi IDP single get before onload test.</title>
-<link rel="help" href="https://fedidcg.github.io/FedCM">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/resources/testdriver.js"></script>
-<script src="/resources/testdriver-vendor.js"></script>
-
-<body>
-
-<script type="module">
-import {set_fedcm_cookie,
- request_options_with_mediation_required,
- fedcm_select_account_promise} from '../support/fedcm-helper.sub.js';
-
-let has_window_loaded = false;
-const window_loaded = new Promise(resolve => {
- window.addEventListener('load', () => {
- has_window_loaded = true;
- resolve();
- });
-});
-
-promise_test(async t => {
- const first_cred = navigator.credentials.get(request_options_with_mediation_required());
- assert_false(has_window_loaded);
- await set_fedcm_cookie();
- await window_loaded;
- assert_true(has_window_loaded);
-
- // Select first account after onload.
- await fedcm_select_account_promise(t, 0);
- const first = await first_cred;
- assert_equals(first.token, "token");
-}, "Single `get` call before onload is allowed even if account is selected after.");
-
-</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-during-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-during-onload.https.html
deleted file mode 100644
index 832565744d..0000000000
--- a/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-during-onload.https.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<!DOCTYPE html>
-<title>Federated Credential Management API multi IDP single get during onload test.</title>
-<link rel="help" href="https://fedidcg.github.io/FedCM">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/resources/testdriver.js"></script>
-<script src="/resources/testdriver-vendor.js"></script>
-
-<body>
-
-<script type="module">
-import {request_options_with_mediation_required,
- set_fedcm_cookie,
- fedcm_get_and_select_first_account} from '../support/fedcm-helper.sub.js';
-
-promise_test(async t => {
- const window_loaded = new Promise(resolve => {
- window.addEventListener('load', async () => {
- await set_fedcm_cookie();
- const first_cred = fedcm_get_and_select_first_account(t,
- request_options_with_mediation_required());
- const cred = await first_cred;
- assert_equals(cred.token, "token");
- resolve();
- });
- });
- await window_loaded;
-}, "Single `get` call during onload is allowed.");
-
-</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-register/fedcm-no-registered-idps.https.html b/testing/web-platform/tests/credential-management/fedcm-register/fedcm-no-registered-idps.https.html
new file mode 100644
index 0000000000..7be2d397e6
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-register/fedcm-no-registered-idps.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API network request tests.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<body>
+
+<script type="module">
+promise_test(async t => {
+ const cred = navigator.credentials.get({
+ identity: {
+ providers: [{
+ configURL: "any",
+ clientId: "na",
+ nonce: "1"
+ }]
+ }
+ });
+ return promise_rejects_dom(t, 'NetworkError', cred);
+}, "When no providers are registered an error should be returned.");
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-same-site-none/fedcm-same-site-none.https.html b/testing/web-platform/tests/credential-management/fedcm-same-site-none/fedcm-same-site-none.https.html
index 77ecdaff9f..d3d20ea9df 100644
--- a/testing/web-platform/tests/credential-management/fedcm-same-site-none/fedcm-same-site-none.https.html
+++ b/testing/web-platform/tests/credential-management/fedcm-same-site-none/fedcm-same-site-none.https.html
@@ -20,6 +20,6 @@ fedcm_test(async t => {
const cred = await fedcm_get_and_select_first_account(t, options);
assert_equals(cred.token, "token");
assert_equals(cred.isAutoSelected, false);
-}, "FedCM requests should be considered cross-origin and therefore not send SameSite=Strict cookies.");
+}, "FedCM requests should be considered cross-origin and therefore not send SameSite=Strict or Lax cookies.");
</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-token-returned-with-http-error.https.html b/testing/web-platform/tests/credential-management/fedcm-token-returned-with-http-error.https.html
index 2337829add..7c7687f00f 100644
--- a/testing/web-platform/tests/credential-management/fedcm-token-returned-with-http-error.https.html
+++ b/testing/web-platform/tests/credential-management/fedcm-token-returned-with-http-error.https.html
@@ -8,6 +8,7 @@
<script type="module">
import {request_options_with_mediation_required,
+ fedcm_error_dialog_click_button,
fedcm_test,
select_manifest,
fedcm_get_and_select_first_account} from './support/fedcm-helper.sub.js';
@@ -18,6 +19,15 @@ fedcm_test(async t => {
await select_manifest(t, test_options);
const cred = fedcm_get_and_select_first_account(t, test_options);
- return promise_rejects_dom(t, 'NetworkError', cred);
+ fedcm_error_dialog_click_button(t, "ErrorGotIt");
+ try {
+ await cred;
+ assert_unreached("An IdentityCredentialError exception should be thrown.");
+ } catch (e) {
+ assert_true(e instanceof DOMException);
+ assert_equals(e.name, "IdentityCredentialError");
+ assert_equals(e.code, "");
+ assert_equals(e.url, "");
+ }
}, 'Test that the promise will be rejected if the response has http error');
</script>
diff --git a/testing/web-platform/tests/credential-management/support/fedcm-helper.sub.js b/testing/web-platform/tests/credential-management/support/fedcm-helper.sub.js
index f0031fa531..308950e1e2 100644
--- a/testing/web-platform/tests/credential-management/support/fedcm-helper.sub.js
+++ b/testing/web-platform/tests/credential-management/support/fedcm-helper.sub.js
@@ -1,6 +1,7 @@
export const manifest_origin = "https://{{host}}:{{ports[https][0]}}";
export const alt_manifest_origin = 'https://{{hosts[alt][]}}:{{ports[https][0]}}';
export const same_site_manifest_origin = 'https://{{hosts[][www1]}}:{{ports[https][0]}}';
+export const default_manifest_path = '/credential-management/support/fedcm/manifest.py';
export function open_and_wait_for_popup(origin, path) {
return new Promise(resolve => {
@@ -100,6 +101,25 @@ credential-management/support/fedcm/${manifest_filename}`;
};
}
+export function request_options_with_two_idps(mediation = 'required') {
+ const first_config = `${manifest_origin}${default_manifest_path}`;
+ const second_config = `${alt_manifest_origin}${default_manifest_path}`;
+ return {
+ identity: {
+ providers: [{
+ configURL: first_config,
+ clientId: '123',
+ nonce: 'N1'
+ },
+ {
+ configURL: second_config,
+ clientId: '456',
+ nonce: 'N2'
+ }],
+ },
+ mediation: mediation
+ };
+}
// Test wrapper which does FedCM-specific setup.
export function fedcm_test(test_func, test_name) {
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/accounts_check_same_site_strict.py b/testing/web-platform/tests/credential-management/support/fedcm/accounts_check_same_site_strict.py
index a6f385feac..796ac003cb 100644
--- a/testing/web-platform/tests/credential-management/support/fedcm/accounts_check_same_site_strict.py
+++ b/testing/web-platform/tests/credential-management/support/fedcm/accounts_check_same_site_strict.py
@@ -7,6 +7,8 @@ def main(request, response):
return request_error
if request.cookies.get(b"same_site_strict") == b"1":
return (546, [], "Should not send SameSite=Strict cookies")
+ if request.cookies.get(b"same_site_lax") == b"1":
+ return (547, [], "Should not send SameSite=Lax cookies")
if request.headers.get(b"Sec-Fetch-Site") != b"cross-site":
return (538, [], "Wrong Sec-Fetch-Site header")
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/continue_on.py b/testing/web-platform/tests/credential-management/support/fedcm/continue_on.py
index 1b4831b51d..2a580e0f3f 100644
--- a/testing/web-platform/tests/credential-management/support/fedcm/continue_on.py
+++ b/testing/web-platform/tests/credential-management/support/fedcm/continue_on.py
@@ -7,6 +7,8 @@ def main(request, response):
return request_error
response.headers.set(b"Content-Type", b"application/json")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"Origin"))
+ response.headers.set(b"Access-Control-Allow-Credentials", "true")
account = request.POST.get(b"account_id").decode("utf-8")
nonce = request.POST.get(b"nonce").decode("utf-8")
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/request-params-check.py b/testing/web-platform/tests/credential-management/support/fedcm/request-params-check.py
index 6c610e6e20..08c28e32b7 100644
--- a/testing/web-platform/tests/credential-management/support/fedcm/request-params-check.py
+++ b/testing/web-platform/tests/credential-management/support/fedcm/request-params-check.py
@@ -63,12 +63,16 @@ def accountsCheck(request):
return (539, [], "Should not have Origin")
def tokenCheck(request):
- common_error = commonCheck(request)
+ common_error = commonCheck(request, b"cors")
if (common_error):
return common_error
common_credentialed_error = commonCredentialedRequestCheck(request)
if (common_credentialed_error):
return common_credentialed_error
+ # The value of the Sec-Fetch-Site header can vary depending on the IdP origin
+ # but it should not be 'none'.
+ if request.headers.get(b"Sec-Fetch-Site") == b"none":
+ return (538, [], "Wrong Sec-Fetch-Site header")
post_error = commonPostCheck(request)
if (post_error):
@@ -86,8 +90,9 @@ def revokeCheck(request):
if (common_error):
return common_error
- if request.cookies.get(b"cookie") != b"1":
- return (537, [], "Missing cookie")
+ common_credentialed_error = commonCredentialedRequestCheck(request)
+ if (common_credentialed_error):
+ return common_credentialed_error
# The value of the Sec-Fetch-Site header can vary depending on the IdP origin
# but it should not be 'none'.
if request.headers.get(b"Sec-Fetch-Site") == b"none":
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/token_check_same_site_strict.py b/testing/web-platform/tests/credential-management/support/fedcm/token_check_same_site_strict.py
index 8a4b3a234b..4e55bf27f6 100644
--- a/testing/web-platform/tests/credential-management/support/fedcm/token_check_same_site_strict.py
+++ b/testing/web-platform/tests/credential-management/support/fedcm/token_check_same_site_strict.py
@@ -7,6 +7,8 @@ def main(request, response):
return request_error
if request.cookies.get(b"same_site_strict") == b"1":
return (546, [], "Should not send SameSite=Strict cookies")
+ if request.cookies.get(b"same_site_lax") == b"1":
+ return (547, [], "Should not send SameSite=Lax cookies")
response.headers.set(b"Content-Type", b"application/json")
response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"Origin"))
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/token_with_account_id.py b/testing/web-platform/tests/credential-management/support/fedcm/token_with_account_id.py
index 52fb20184b..04e7b5b56b 100644
--- a/testing/web-platform/tests/credential-management/support/fedcm/token_with_account_id.py
+++ b/testing/web-platform/tests/credential-management/support/fedcm/token_with_account_id.py
@@ -7,6 +7,8 @@ def main(request, response):
return request_error
response.headers.set(b"Content-Type", b"application/json")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"Origin"))
+ response.headers.set(b"Access-Control-Allow-Credentials", "true")
account_id = request.POST.get(b"account_id")
return "{\"token\": \"account_id=" + account_id.decode("utf-8") + "\"}"
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/token_with_auto_selected_flag.py b/testing/web-platform/tests/credential-management/support/fedcm/token_with_auto_selected_flag.py
index 93ccf3ee7e..3e011ce788 100644
--- a/testing/web-platform/tests/credential-management/support/fedcm/token_with_auto_selected_flag.py
+++ b/testing/web-platform/tests/credential-management/support/fedcm/token_with_auto_selected_flag.py
@@ -7,6 +7,8 @@ def main(request, response):
return request_error
response.headers.set(b"Content-Type", b"application/json")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"Origin"))
+ response.headers.set(b"Access-Control-Allow-Credentials", "true")
is_auto_selected = request.POST.get(b"is_auto_selected")
return "{\"token\": \"is_auto_selected=" + is_auto_selected.decode("utf-8") + "\"}"
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/token_with_http_error.py b/testing/web-platform/tests/credential-management/support/fedcm/token_with_http_error.py
index c8d95ab63d..05b9945ba8 100644
--- a/testing/web-platform/tests/credential-management/support/fedcm/token_with_http_error.py
+++ b/testing/web-platform/tests/credential-management/support/fedcm/token_with_http_error.py
@@ -7,6 +7,8 @@ def main(request, response):
return request_error
response.headers.set(b"Content-Type", b"application/json")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"Origin"))
+ response.headers.set(b"Access-Control-Allow-Credentials", "true")
response.status = (403, b"Forbidden")
return "{\"token\": \"token\"}"
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/token_with_rp_mode.py b/testing/web-platform/tests/credential-management/support/fedcm/token_with_rp_mode.py
index 515736416f..add634c99b 100644
--- a/testing/web-platform/tests/credential-management/support/fedcm/token_with_rp_mode.py
+++ b/testing/web-platform/tests/credential-management/support/fedcm/token_with_rp_mode.py
@@ -7,6 +7,8 @@ def main(request, response):
return request_error
response.headers.set(b"Content-Type", b"application/json")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"Origin"))
+ response.headers.set(b"Access-Control-Allow-Credentials", "true")
rp_mode = request.POST.get(b"mode")
return "{\"token\": \"mode=" + rp_mode.decode("utf-8") + "\"}"
diff --git a/testing/web-platform/tests/credential-management/support/set_cookie.headers b/testing/web-platform/tests/credential-management/support/set_cookie.headers
index 4226ff4c99..df223115a7 100644
--- a/testing/web-platform/tests/credential-management/support/set_cookie.headers
+++ b/testing/web-platform/tests/credential-management/support/set_cookie.headers
@@ -1,3 +1,4 @@
Content-Type: text/html
-Set-Cookie: cookie=1; SameSite=None; Secure
-Set-Cookie: same_site_strict=1; SameSite=Strict; Secure
+Set-Cookie: cookie=1; SameSite=None; Secure; Path=/
+Set-Cookie: same_site_strict=1; SameSite=Strict; Secure; Path=/
+Set-Cookie: same_site_lax=1; SameSite=Lax; Secure; Path=/
diff --git a/testing/web-platform/tests/css/CSS2/margin-padding-clear/margin-collapse-clear-011-ref.xht b/testing/web-platform/tests/css/CSS2/margin-padding-clear/margin-collapse-clear-011-ref.xht
new file mode 100644
index 0000000000..2611de9f4a
--- /dev/null
+++ b/testing/web-platform/tests/css/CSS2/margin-padding-clear/margin-collapse-clear-011-ref.xht
@@ -0,0 +1,13 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>CSS Reftest Reference</title>
+ <link rel="author" title="Oriol Brufau" href="obrufau@igalia.com"/>
+ <link rel="stylesheet" type="text/css" href="/fonts/ahem.css"/>
+ </head>
+ <body style="font: 25px/1 Ahem">
+ <div style="margin-bottom: 1em">X</div>
+ <div style="color: cyan;">X</div>
+ <div style="color: magenta">X</div>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/CSS2/margin-padding-clear/margin-collapse-clear-011.xht b/testing/web-platform/tests/css/CSS2/margin-padding-clear/margin-collapse-clear-011.xht
new file mode 100644
index 0000000000..fed7bef25c
--- /dev/null
+++ b/testing/web-platform/tests/css/CSS2/margin-padding-clear/margin-collapse-clear-011.xht
@@ -0,0 +1,21 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>CSS Test: Margin Collapsing with Clearance</title>
+ <link rel="author" title="Oriol Brufau" href="obrufau@igalia.com"/>
+ <link rel="help" href="http://www.w3.org/TR/CSS21/visuren.html#flow-control"/>
+ <link rel="help" href="http://www.w3.org/TR/CSS21/box.html#collapsing-margins"/>
+ <link rel="help" href="http://www.w3.org/TR/CSS21/visuren.html#floats"/>
+ <link rel="match" href="margin-collapse-clear-011-ref.xht"/>
+ <meta name="assert" content="The magenta X appears below the cyan X due to clearance"/>
+ <link rel="stylesheet" type="text/css" href="/fonts/ahem.css"/>
+ </head>
+ <body style="font: 25px/1 Ahem">
+ <div style="margin-bottom: 1em">X</div>
+ <div style="float: left; color: cyan;">X</div>
+ <div style="margin-bottom: 1em; height: 0px;">
+ <div style="margin-bottom: -1em;"></div>
+ </div>
+ <div style="clear: both; color: magenta">X</div>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/compositing/background-blending/crashtests/bgblend-root-change.html b/testing/web-platform/tests/css/compositing/background-blending/crashtests/bgblend-root-change.html
new file mode 100644
index 0000000000..06db053574
--- /dev/null
+++ b/testing/web-platform/tests/css/compositing/background-blending/crashtests/bgblend-root-change.html
@@ -0,0 +1,17 @@
+<style>
+* {
+ position: sticky;
+ border-left: double 488200679.54Q hsla(-39 5% 68% / 7%) !important;
+ box-shadow: 172vmax 60991vmax 32in 106cm hsl(-57532411.87deg, 70%, 54%);
+ background-blend-mode: overlay;
+ background: url(data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=) local content-box space space 0em / 15438983.37cm auto;
+}
+</style>
+<script>
+window.addEventListener("load", () => {
+ let a = document.createElementNS("http://www.w3.org/1998/Math/MathML", "math")
+ a.setAttribute("href", "x")
+ a.autofocus = true
+ document.documentElement.appendChild(a)
+})
+</script>
diff --git a/testing/web-platform/tests/css/compositing/mix-blend-mode/mix-blend-mode-parent-element-overflow-hidden-and-border-radius-2.html b/testing/web-platform/tests/css/compositing/mix-blend-mode/mix-blend-mode-parent-element-overflow-hidden-and-border-radius-2.html
new file mode 100644
index 0000000000..0b12301c4d
--- /dev/null
+++ b/testing/web-platform/tests/css/compositing/mix-blend-mode/mix-blend-mode-parent-element-overflow-hidden-and-border-radius-2.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CSS Test: blending between an element having overflow:hidden and border-radius and its child, with will-change:opacity</title>
+ <link rel="author" title="Mirela Budăeș" href="mailto:mbudaes@adobe.com">
+ <link rel="author" title="Ion Roșca" href="mailto:rosca@adobe.com">
+ <link rel="author" title="Xianzhu Wang" href="mailto:wangxianzhu@chromium.org">
+ <link rel="help" href="https://drafts.fxtf.org/compositing-1/#mix-blend-mode">
+ <link rel="help" href="https://crbug.com/328339028">
+ <meta name="assert" content="Test checks that an element having mix-blend-mode and will-change:opacity blends with the parent element having overflow:hidden and border-radius">
+ <meta name="fuzzy" content="0-128;0-400">
+ <link rel="match" href="reference/mix-blend-mode-parent-element-overflow-hidden-and-border-radius-ref.html">
+ <style type="text/css">
+ .parent {
+ background: red;
+ width: 140px;
+ height: 140px;
+ position: relative;
+ z-index: 1;
+ overflow: hidden;
+ border-radius: 1em 5em;
+ }
+ .blended {
+ background: yellow;
+ width: 200px;
+ height: 200px;
+ mix-blend-mode: difference;
+ will-change: opacity;
+ }
+ </style>
+ </head>
+ <body>
+ <p> Test passes if you see a lime square with rounded corners.</p>
+ <div class="parent">
+ <div class="blended"></div>
+ </div>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-center-offset-change.html b/testing/web-platform/tests/css/css-anchor-position/anchor-center-offset-change.html
new file mode 100644
index 0000000000..fa383154da
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-center-offset-change.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>CSS Anchor Positioning Test: Dynamically change the anchor-center position</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#anchor-center">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #cb {
+ position: relative;
+ width: 200px;
+ height: 200px;
+ }
+ #anchor {
+ width: 100px;
+ height: 100px;
+ anchor-name: --anchor;
+ }
+ #anchored {
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ position-anchor: --anchor;
+ align-self: anchor-center;
+ left: anchor(--unknown right, 0px);
+ }
+</style>
+<div id="cb">
+ <div id="anchor"></div>
+ <div id="anchored"></div>
+</div>
+<script>
+ test(() => {
+ assert_equals(anchored.offsetLeft, 0);
+ assert_equals(anchored.offsetTop, 0);
+ }, "Anchored initially have the same width as the anchor");
+
+ test(() => {
+ anchor.style.height = "200px";
+ assert_equals(anchored.offsetLeft, 0);
+ assert_equals(anchored.offsetTop, 50);
+ }, "Increase the height of the anchor to move the anchor-center offset");
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-center-scroll-ref.html b/testing/web-platform/tests/css/css-anchor-position/anchor-center-scroll-ref.html
new file mode 100644
index 0000000000..d2638491ce
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-center-scroll-ref.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<title>CSS Test Reference</title>
+<body style="margin:0">
+ <div id="scroller" style="width:400px;height:400px;overflow:auto;background:orange">
+ <div style="height:100px"></div>
+ <div style="width:100px;height:100px;background:pink">
+ <div style="height:25px"></div>
+ <div style="width:50px;height:50px;background:purple"></div>
+ </div>
+ <div style="height:1000px"></div>
+ </div>
+</body>
+<script>
+ scroller.scrollTop = 100;
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-center-scroll.html b/testing/web-platform/tests/css/css-anchor-position/anchor-center-scroll.html
new file mode 100644
index 0000000000..29b5e06e2a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-center-scroll.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<title>CSS Anchor Positioning Test: scroll adjusted anchor-center</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#anchor-center">
+<link rel="match" href="anchor-center-scroll-ref.html">
+<style>
+ body { margin: 0; }
+ #scroller {
+ width: 400px;
+ height: 400px;
+ overflow: auto;
+ background: orange;
+ }
+ #anchor {
+ margin-top: 100px;
+ width: 100px;
+ height: 100px;
+ background: pink;
+ anchor-name: --anchor;
+ }
+ #anchored {
+ position: absolute;
+ position-anchor: --anchor;
+ align-self: anchor-center;
+ width: 50px;
+ height: 50px;
+ background: purple;
+ }
+ #filler { height: 1000px; }
+</style>
+<div id="scroller">
+ <div id="anchor"></div>
+ <div id="anchored"></div>
+ <div id="filler"></div>
+</div>
+<script>
+ scroller.offsetTop;
+ scroller.scrollTop = 100;
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-fallback-invalidation.html b/testing/web-platform/tests/css/css-anchor-position/anchor-fallback-invalidation.html
new file mode 100644
index 0000000000..a066c2da55
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-fallback-invalidation.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<title>CSS Anchor Positioning: Invalidation when the anchor*() fallback matches old style</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/">
+<link rel="help" href="https://issues.chromium.org/issues/333858786">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #cb {
+ position: relative;
+ width: 200px;
+ height: 200px;
+ border: 1px solid black;
+ }
+
+ #anchor {
+ anchor-name: --a;
+ position: absolute;
+ width: 40px;
+ height: 30px;
+ left: 75px;
+ top: 75px;
+ background: coral;
+ }
+
+ #anchored {
+ position: absolute;
+ background: seagreen;
+ width: 50px;
+ height: 50px;
+ }
+
+ #anchored.change {
+ /* The fallbacks match what the unchanged style says, but we shouldn't
+ take the fallbacks here. */
+ width: anchor-size(--a width, 50px);
+ height: anchor-size(--a height, 50px);
+ }
+</style>
+<div id=cb>
+ <div id=anchor></div>
+ <div id=anchored>X</div>
+</div>
+<script>
+ test(() => {
+ assert_equals(anchored.offsetWidth, 50);
+ assert_equals(anchored.offsetHeight, 50);
+
+ anchored.classList.toggle('change');
+
+ assert_equals(anchored.offsetWidth, 40);
+ assert_equals(anchored.offsetHeight, 30);
+ }, 'Correct invalidation when fallbacks match the old style');
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-inherited.html b/testing/web-platform/tests/css/css-anchor-position/anchor-inherited.html
new file mode 100644
index 0000000000..0f84311d6d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-inherited.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<title>Tests that anchor functions inherit as pixels</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/check-layout-th.js"></script>
+<script src="/css/css-anchor-position/support/test-common.js"></script>
+
+<style>
+.cb {
+ width: 400px;
+ height: 400px;
+ position: relative;
+ border: 1px solid black;
+}
+
+.anchor {
+ width: 100px;
+ height: 100px;
+ top: 10px;
+ left: 20px;
+ position: absolute;
+ background: red;
+ anchor-name: --a;
+}
+
+.anchored {
+ position-anchor: --a;
+ position: absolute;
+ /* Anchored directly on top */
+ top: anchor(top);
+ left: anchor(left);
+ width: anchor-size(width);
+ height: anchor-size(height);
+ background: coral;
+}
+
+/* The child should have the same size as the anchored element,
+ and inset by top:10px,left:20px vs. that element. */
+.child {
+ position-anchor: --unknown; /* Should have no effect. */
+ position: relative;
+ background: skyblue;
+ top: inherit;
+ left: inherit;
+ width: inherit;
+ height: inherit;
+}
+
+</style>
+
+<body onload="checkLayoutForAnchorPos('.child')">
+
+<div class=cb>
+ <div class=anchor></div>
+ <div class=anchored>
+ <div class=child
+ data-offset-x=20 data-offset-y=10
+ data-expected-width=100 data-expected-height=100></div>
+ </div>
+</div>
+
+</body>
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-invalid-fallback.html b/testing/web-platform/tests/css/css-anchor-position/anchor-invalid-fallback.html
new file mode 100644
index 0000000000..4768beac62
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-invalid-fallback.html
@@ -0,0 +1,234 @@
+<!DOCTYPE html>
+<title>CSS Anchor Position Test: invalid at computed-value time</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#anchor-valid">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#anchor-size-valid">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ :root {
+ --top: top;
+ }
+ #cb {
+ position: relative;
+ width: 200px;
+ height: 200px;
+ border: 1px solid black;
+ }
+
+ #anchor {
+ anchor-name: --a;
+ position: absolute;
+ width: 50px;
+ height: 40px;
+ left: 75px;
+ top: 75px;
+ background: coral;
+ }
+
+ #main > div, #ref {
+ position: absolute;
+ background: seagreen;
+ }
+
+ #ref {
+ inset: unset;
+ width: unset;
+ height: unset;
+ min-width: unset;
+ min-height: unset;
+ max-width: unset;
+ max-height: unset;
+ }
+
+</style>
+<div id=cb>
+ <div id=anchor></div>
+ <div id=main></div>
+ <div id=ref>X</div>
+</div>
+<script>
+
+// Append <div>X</div> to `container`, and remove it again once the test (`t`)
+// is finished.
+function createTarget(t, container) {
+ t.add_cleanup(() => { container.replaceChildren(); });
+ let target = document.createElement('div');
+ target.textContent = 'X';
+ container.append(target);
+ return target;
+}
+
+// First, some sanity checks to verify that the anchor etc is set up correctly,
+// and that anchor() queries can produce results if done correctly.
+
+test((t) => {
+ let target = createTarget(t, main);
+ target.style = `
+ position-anchor: --a;
+ left:anchor(right);
+ top:anchor(top);
+ width:anchor-size(width);
+ height:anchor-size(height);
+ `;
+ let cs = getComputedStyle(target);
+ assert_equals(cs.left, '125px');
+ assert_equals(cs.top, '75px');
+ assert_equals(cs.width, '50px');
+ assert_equals(cs.height, '40px');
+}, 'Element can be anchor positioned');
+
+test((t) => {
+ let target = createTarget(t, main);
+ target.style = `
+ /* No position-anchor here */
+ left:anchor(right, 17px);
+ top:anchor(top, 18px);
+ width:anchor-size(width, 42px);
+ height:anchor-size(height, 43px);
+ `;
+ let cs = getComputedStyle(target);
+ assert_equals(cs.left, '17px');
+ assert_equals(cs.top, '18px');
+ assert_equals(cs.width, '42px');
+ assert_equals(cs.height, '43px');
+}, 'Element can use <length> fallback if present');
+
+// Now test that any invalid anchor*() behaves as invalid at computed-value
+// time if there's no fallback specified.
+
+// Check that an anchored element with the specified style has the same
+// computed insets and sizing as the reference element (#ref), i.e. all
+// insets and sizing properties behave as 'unset'.
+function test_ref(style, description) {
+ test((t) => {
+ let target = createTarget(t, main);
+ target.style = style;
+ let cs = getComputedStyle(target);
+ let ref_cs = getComputedStyle(ref);
+ assert_equals(cs.top, ref_cs.top, 'top');
+ assert_equals(cs.left, ref_cs.left, 'left');
+ assert_equals(cs.right, ref_cs.right, 'right');
+ assert_equals(cs.bottom, ref_cs.bottom, 'bottom');
+ assert_equals(cs.width, ref_cs.width, 'width');
+ assert_equals(cs.height, ref_cs.height, 'height');
+ assert_equals(cs.minWidth, ref_cs.minWidth, 'minWidth');
+ assert_equals(cs.minHeight, ref_cs.minHeight, 'minHeight');
+ assert_equals(cs.maxWidth, ref_cs.maxWidth, 'maxWidth');
+ assert_equals(cs.maxHeight, ref_cs.maxHeight, 'maxHeight');
+ }, `Invalid anchor function, ${description}`);
+}
+
+// No default anchor (position-anchor):
+test_ref('left:anchor(left)', 'left');
+test_ref('right:anchor(right)', 'right');
+test_ref('bottom:anchor(bottom)', 'bottom');
+test_ref('top:anchor(top)', 'top');
+test_ref('width:anchor-size(width)', 'width');
+test_ref('height:anchor-size(height)', 'height');
+test_ref('min-width:anchor-size(width)', 'min-width');
+test_ref('min-height:anchor-size(height)', 'min-height');
+test_ref('max-width:anchor-size(width)', 'max-width');
+test_ref('max-height:anchor-size(height)', 'max-height');
+
+// Unknown anchor reference:
+test_ref('left:anchor(--unknown left)', '--unknown left');
+test_ref('width:anchor-size(--unknown width)', '--unknown width');
+
+// Wrong axis;
+test_ref('left:anchor(--a top)', 'cross-axis query (vertical)');
+test_ref('top:anchor(--a left)', ' cross-axis query (horizontal)');
+
+// Wrong query for the given property:
+test_ref('top:anchor-size(--a width)', 'anchor-size() in inset');
+test_ref('width:anchor(--a left)', 'anchor() in sizing property');
+
+// Invalid anchor*() deeper within calc():
+test_ref('left:calc(anchor(left) + 10px)', 'nested left');
+test_ref('right:calc(anchor(right) + 10px)', 'nested right');
+test_ref('bottom:calc(anchor(bottom) + 10px)', 'nested bottom');
+test_ref('top:calc(anchor(top) + 10px)', 'nested top');
+test_ref('min-width:calc(anchor-size(width) + 10px)', 'nested min-width');
+test_ref('min-height:calc(anchor-size(height) + 10px)', 'nested min-height');
+test_ref('max-width:calc(anchor-size(width) + 10px)', 'nested max-width');
+test_ref('max-height:calc(anchor-size(height) + 10px)', 'nested max-height');
+
+// Invalid anchor*() within fallback:
+test_ref('top:anchor(top, anchor(--unknown top))', 'invalid anchor() in fallback');
+test_ref('width:anchor-size(width, anchor-size(--unknown width))', 'invalid anchor-size() in fallback');
+
+// Non-calc() functions:
+test_ref('top:min(10px, anchor(top))', 'min()');
+test_ref('top:max(10px, anchor(top))', 'max()');
+test_ref('top:abs(anchor(top) - 100px)', 'abs()');
+test_ref('top:calc(sign(anchor(top) - 100px) * 20px)', 'sign()');
+
+// var():
+test_ref('top:anchor(var(--top))', 'anchor(var())');
+test_ref('top:anchor(var(--unknown, top))', 'anchor(unknown var()) (fallback)');
+test_ref('top:anchor(var(--unknown))', 'anchor(unknown var()) (no fallback)');
+
+// Reverting to an invalid anchor():
+test((t) => {
+ let target = createTarget(t, main);
+ target.setAttribute('id', 'target');
+
+ let css = document.createElement('style');
+ css.textContent = `
+ @layer base {
+ #target {
+ top: anchor(top); /* Invalid */
+ color: green;
+ }
+ }
+ #target {
+ top: revert-layer; /* Reverts to 'base'. */
+ }
+ `;
+
+ t.add_cleanup(() => { css.remove(); })
+ cb.append(css);
+
+ let cs = getComputedStyle(target);
+ let ref_cs = getComputedStyle(ref);
+ // The color check verifies that the rule is applied at all.
+ assert_equals(cs.color, 'rgb(0, 128, 0)');
+ assert_equals(cs.top, ref_cs.top);
+}, 'Revert to invalid anchor()');
+
+// Using <try-tactic> to flip to an invalid anchor():
+test((t) => {
+ let target = createTarget(t, main);
+ target.setAttribute('id', 'target');
+
+ let css = document.createElement('style');
+ css.textContent = `
+ @position-try --pt {
+ /* Undo force overflow, and also use this value to check that
+ the rule is applied at all. */
+ left: 10px;
+
+ /* Invalid. Becomes bottom:anchor(bottom) (also invalid)
+ after flip-block. */
+ top: anchor(top);
+ }
+
+ #target {
+ left: 9999px; /* Force overflow. */
+ position-try-options: --pt flip-block;
+ }
+ `;
+
+ t.add_cleanup(() => { css.remove(); })
+ cb.append(css);
+
+ let cs = getComputedStyle(target);
+ let ref_cs = getComputedStyle(ref);
+ assert_equals(cs.left, '10px', 'left');
+ // 'right' is not important in this test.
+
+ assert_equals(cs.top, ref_cs.top, 'top');
+ assert_equals(cs.bottom, ref_cs.bottom, 'bottom');
+}, 'Flip to invalid anchor()');
+
+</script>
+
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-001.tentative.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-001.html
index 1235f8fad4..3a86da5f70 100644
--- a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-001.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-001.html
@@ -2,7 +2,7 @@
<title>Tests scroll adjustments of element anchored to another anchored element</title>
<link rel="author" href="mailto:wangxianzhu@chromium.org">
<link rel="help" href="https://drafts.csswg.org/css-anchor-1/">
-<link rel="match" href="reference/anchor-scroll-chained-001.tentative-ref.html">
+<link rel="match" href="reference/anchor-scroll-chained-001-ref.html">
<style>
body {
margin: 0;
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-002.tentative.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-002.html
index 9c60799e0b..38dadba5c8 100644
--- a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-002.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-002.html
@@ -2,7 +2,7 @@
<title>Tests scroll adjustments of element anchored to another anchored element</title>
<link rel="author" href="mailto:wangxianzhu@chromium.org">
<link rel="help" href="https://drafts.csswg.org/css-anchor-1/">
-<link rel="match" href="reference/anchor-scroll-chained-002.tentative-ref.html">
+<link rel="match" href="reference/anchor-scroll-chained-002-ref.html">
<style>
body {
margin: 0;
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-003.tentative.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-003.html
index b441c92bf1..6352ebbfb6 100644
--- a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-003.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-003.html
@@ -2,7 +2,7 @@
<title>Tests scroll adjustments of element anchored to another anchored element</title>
<link rel="author" href="mailto:wangxianzhu@chromium.org">
<link rel="help" href="https://drafts.csswg.org/css-anchor-1/">
-<link rel="match" href="reference/anchor-scroll-chained-002.tentative-ref.html">
+<link rel="match" href="reference/anchor-scroll-chained-002-ref.html">
<style>
body {
margin: 0;
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-004.tentative.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-004.html
index f1765a9870..b31519f580 100644
--- a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-004.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-004.html
@@ -2,7 +2,7 @@
<title>Tests scroll adjustments of element anchored to another anchored element</title>
<link rel="author" href="mailto:wangxianzhu@chromium.org">
<link rel="help" href="https://drafts.csswg.org/css-anchor-1/">
-<link rel="match" href="reference/anchor-scroll-chained-004.tentative-ref.html">
+<link rel="match" href="reference/anchor-scroll-chained-004-ref.html">
<style>
body {
margin: 0;
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-fallback.tentative.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-fallback.html
index d2300da818..221df77b06 100644
--- a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-fallback.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-chained-fallback.html
@@ -2,7 +2,7 @@
<title>Tests scroll adjustments of element anchored to another anchored element with fallback</title>
<link rel="author" href="mailto:wangxianzhu@chromium.org">
<link rel="help" href="https://drafts.csswg.org/css-anchor-1/">
-<link rel="match" href="reference/anchor-scroll-chained-fallback.tentative-ref.html">
+<link rel="match" href="reference/anchor-scroll-chained-fallback-ref.html">
<style>
body {
margin: 0;
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-overflow-hidden-ref.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-overflow-hidden-ref.html
new file mode 100644
index 0000000000..b674998a5d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-overflow-hidden-ref.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<link rel="stylesheet" href="/fonts/ahem.css">
+<style>
+body {
+ font: 20px/1 Ahem;
+ margin: 0;
+}
+
+#placefiller-above-anchor {
+ height: 200px;
+}
+
+#placefiller-before-anchor {
+ display: inline-block;
+ width: 50px;
+}
+
+#inner-anchored {
+ color: green;
+ position: fixed;
+ left: 70px;
+ top: 180px;
+}
+
+#outer-anchored {
+ color: yellow;
+ position: fixed;
+ left: 70px;
+ top: 220px;
+}
+</style>
+
+<div id="placefiller-above-anchor"></div>
+<div id="placefiller-before-anchor"></div>
+<span id="anchor">anchor</span>
+<div id="inner-anchored">inner-anchored</div>
+<div id="outer-anchored">outer-anchored</div>
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-overflow-hidden.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-overflow-hidden.html
new file mode 100644
index 0000000000..b57e39956b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-overflow-hidden.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<title>Basic of anchor positioned scrolling: anchored elements should be "pinned" to the anchor when anchor is scrolled in overflow:hidden</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-1/">
+<link rel="match" href="anchor-scroll-overflow-hidden-ref.html">
+<link rel="stylesheet" href="/fonts/ahem.css">
+<style>
+body {
+ font: 20px/1 Ahem;
+ margin: 0;
+}
+
+#scroll-container {
+ width: 400px;
+ height: 400px;
+ overflow: hidden;
+}
+
+#scroll-contents {
+ width: 1000px;
+ height: 1000px;
+}
+
+#placefiller-above-anchor {
+ height: 500px;
+}
+
+#placefiller-before-anchor {
+ display: inline-block;
+ width: 500px;
+}
+
+#anchor {
+ anchor-name: --anchor;
+}
+
+#inner-anchored {
+ color: green;
+ position: absolute;
+ left: anchor(--anchor left);
+ bottom: anchor(--anchor top);
+ position-anchor: --anchor;
+}
+
+#outer-anchored {
+ color: yellow;
+ position: absolute;
+ left: anchor(--anchor left);
+ top: anchor(--anchor bottom);
+ position-anchor: --anchor;
+}
+</style>
+
+<div id="scroll-container">
+ <div id="scroll-contents">
+ <div id="placefiller-above-anchor"></div>
+ <div id="placefiller-before-anchor"></div>
+ <span id="anchor">anchor</span>
+ <div id="inner-anchored">inner-anchored</div>
+ </div>
+</div>
+<div id="outer-anchored">outer-anchored</div>
+
+<script>
+const scroller = document.getElementById('scroll-container');
+scroller.scrollTop = 300;
+scroller.scrollLeft = 450;
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-position-try-013.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-position-try-013.html
new file mode 100644
index 0000000000..3f1330d744
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-position-try-013.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-1/#scroll">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-1/#fallback-apply">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="support/test-common.js"></script>
+<style>
+#cb {
+ position: relative;
+ width: 200px;
+ height: 200px;
+ border: solid 1px;
+}
+
+#scroller {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#abspos {
+ position: absolute;
+ background: hotpink;
+ width: 50px;
+ height: 50px;
+
+ inset-area: bottom;
+ position-try-options: flip-block;
+ position-anchor: --a;
+}
+
+#anchor {
+ anchor-name: --a;
+ width: 50px;
+ height: 50px;
+ margin: 150px 0 150px 50px;
+ background: dodgerblue;
+}
+
+</style>
+<div id="cb">
+ <div id="scroller">
+ <div id="anchor">
+ </div>
+ </div>
+ <div id="abspos"></div>
+</div>
+
+<script>
+promise_test(async () => {
+ await waitUntilNextAnimationFrame();
+ assert_fallback_position(abspos, anchor, 'top');
+});
+
+promise_test(async () => {
+ scroller.scrollTop = 50;
+ await waitUntilNextAnimationFrame();
+ assert_fallback_position(abspos, anchor, 'bottom');
+});
+
+promise_test(async () => {
+ scroller.scrollTop = 40;
+ await waitUntilNextAnimationFrame();
+ assert_fallback_position(abspos, anchor, 'top');
+});
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-position-try-014.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-position-try-014.html
new file mode 100644
index 0000000000..f19f41c088
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-position-try-014.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-1/#scroll">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-1/#fallback-apply">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="support/test-common.js"></script>
+<style>
+#cb {
+ position: relative;
+ width: 200px;
+ height: 200px;
+ border: solid 1px;
+}
+
+#scroller {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ display: flex;
+ flex-direction: column-reverse;
+}
+
+#abspos {
+ position: absolute;
+ background: hotpink;
+ width: 50px;
+ height: 50px;
+
+ inset-area: top;
+ position-try-options: flip-block;
+ position-anchor: --a;
+}
+
+#anchor {
+ anchor-name: --a;
+ width: 50px;
+ min-height: 50px;
+ flex: 0;
+ margin: 150px 0 150px 50px;
+ background: dodgerblue;
+}
+
+</style>
+<div id="cb">
+ <div id="scroller">
+ <div id="anchor">
+ </div>
+ </div>
+ <div id="abspos"></div>
+</div>
+
+<script>
+promise_test(async () => {
+ await waitUntilNextAnimationFrame();
+ assert_fallback_position(abspos, anchor, 'bottom');
+});
+
+promise_test(async () => {
+ scroller.scrollTop = -100;
+ await waitUntilNextAnimationFrame();
+ assert_fallback_position(abspos, anchor, 'top');
+});
+
+promise_test(async () => {
+ scroller.scrollTop = -50;
+ await waitUntilNextAnimationFrame();
+ assert_fallback_position(abspos, anchor, 'bottom');
+});
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-scrollable-anchor-ref.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-scrollable-anchor-ref.html
new file mode 100644
index 0000000000..3528b565fc
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-scrollable-anchor-ref.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<style>
+#scroll-container {
+ width: 400px;
+ height: 400px;
+ overflow: scroll;
+}
+
+#scroll-contents {
+ width: 1000px;
+ height: 1000px;
+ position: relative;
+}
+
+#anchor {
+ anchor-name: --anchor;
+ height: 100px;
+ width: 100px;
+ overflow: scroll;
+}
+
+#anchored {
+ background: green;
+ width: 100px;
+ height: 100px;
+}
+</style>
+
+<div id="scroll-container">
+ <div id="scroll-contents">
+ <div style="height: 400px"></div>
+ <div id="anchored"></div>
+ <div id="anchor">
+ <div style="height: 500px"></div>
+ </div>
+ </div>
+</div>
+
+<script>
+document.getElementById('scroll-container').scrollTop = 300;
+document.getElementById('anchor').scrollTop = 300;
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-scrollable-anchor.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-scrollable-anchor.html
new file mode 100644
index 0000000000..c2a256877d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-scrollable-anchor.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<title>Basic of anchor positioned scrolling: scroll of a scrollable anchor should not affect anchor positioing</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-1/">
+<link rel="match" href="anchor-scroll-scrollable-anchor-ref.html">
+<style>
+#scroll-container {
+ width: 400px;
+ height: 400px;
+ overflow: scroll;
+ will-change: scroll-position;
+}
+
+#scroll-contents {
+ width: 1000px;
+ height: 1000px;
+ position: relative;
+}
+
+.placefiller {
+ height: 500px;
+}
+
+#anchor {
+ anchor-name: --anchor;
+ height: 100px;
+ width: 100px;
+ overflow: scroll;
+ will-change: scroll-position;
+}
+
+#anchored {
+ background: green;
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ left: anchor(left);
+ bottom: anchor(top);
+ position-anchor: --anchor;
+}
+</style>
+
+<div id="scroll-container">
+ <div id="scroll-contents">
+ <div class="placefiller"></div>
+ <div id="anchor">
+ <div class="placefiller"></div>
+ </div>
+ </div>
+</div>
+<div id="anchored"></div>
+
+<script>
+document.getElementById('scroll-container').scrollTop = 300;
+document.getElementById('anchor').scrollTop = 300;
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/at-position-try-cssom.html b/testing/web-platform/tests/css/css-anchor-position/at-position-try-cssom.html
index 91172c5185..42f82d9d4b 100644
--- a/testing/web-platform/tests/css/css-anchor-position/at-position-try-cssom.html
+++ b/testing/web-platform/tests/css/css-anchor-position/at-position-try-cssom.html
@@ -24,7 +24,7 @@ test(t => {
const positionTryRule = style.sheet.cssRules[0];
assert_true(positionTryRule instanceof CSSPositionTryRule);
assert_equals(positionTryRule.name, '--pf');
- assert_true(positionTryRule.style instanceof CSSStyleDeclaration);
+ assert_true(positionTryRule.style instanceof CSSPositionTryDescriptors);
assert_equals(positionTryRule.style.length, 1);
assert_equals(positionTryRule.style.left, 'anchor(right)');
}, 'CSSPositionTryRule attribute values');
@@ -63,4 +63,140 @@ test(t => {
}, 'CSSPositionTryRule.style.setProperty setting allowed and disallowed properties');
+test(t => {
+ const style = createStyle(t, `
+ @position-try --pf {
+ top: 10px;
+ left: 20px;
+ --x: 200px;
+ color: red;
+ }
+ `);
+ let declarations = style.sheet.cssRules[0].style;
+ assert_equals(declarations.length, 2);
+ assert_equals(declarations.item(0), 'top');
+ assert_equals(declarations.item(1), 'left');
+}, 'CSSPositionTryDescriptors.item');
+
+test(t => {
+ const style = createStyle(t, '@position-try --pf {}');
+ let declarations = style.sheet.cssRules[0].style;
+ assert_equals(declarations.length, 0);
+ declarations.cssText = `color:red;top:10px;`;
+ assert_equals(declarations.length, 1);
+}, 'CSSPositionTryDescriptors.cssText');
+
+let supported_properties = [
+ 'margin',
+ 'margin-top',
+ 'margin-right',
+ 'margin-bottom',
+ 'margin-left',
+ 'margin-block',
+ 'margin-block-start',
+ 'margin-block-end',
+ 'margin-inline',
+ 'margin-inline-start',
+ 'margin-inline-end',
+ 'inset',
+ 'top',
+ 'left',
+ 'right',
+ 'bottom',
+ 'inset-block',
+ 'inset-block-start',
+ 'inset-block-end',
+ 'inset-inline',
+ 'inset-inline-start',
+ 'inset-inline-end',
+ 'width',
+ 'height',
+ 'min-width',
+ 'max-width',
+ 'min-height',
+ 'max-height',
+ 'block-size',
+ 'min-block-size',
+ 'max-block-size',
+ 'inline-size',
+ 'min-inline-size',
+ 'max-inline-size',
+ 'place-self',
+ 'align-self',
+ 'justify-self',
+ 'position-anchor',
+ 'inset-area',
+];
+
+// A selection of unsupported properties.
+let unsupported_properties = [
+ 'color',
+ 'align-items',
+ 'align-content',
+ 'background',
+ 'display',
+ 'position',
+ 'writing-mode',
+ 'direction',
+ 'syntax', // @property
+];
+
+let upperFirst = (x) => x[0].toUpperCase() + x.slice(1);
+let lowerFirst = (x) => x[0].toLowerCase() + x.slice(1);
+let toLowerCamelCase = (x) => lowerFirst(x.split('-').map(upperFirst).join(''));
+
+// Test getting/setting the specified property on a CSSPositionTryDescriptors
+// object. The property can either be supported or not supported,
+// which determines the expected results.
+function test_property(prop, supported) {
+ test(t => {
+ let decls = supported_properties.map(x => `${x}:unset;`).join('');
+ let style = createStyle(t, `@position-try --pf { ${decls} }`);
+ let declarations = style.sheet.cssRules[0].style;
+ assert_equals(declarations.getPropertyValue(prop), supported ? 'unset' : '');
+ }, `CSSPositionTryDescriptors.getPropertyValue(${prop})`);
+
+ test(t => {
+ let style = createStyle(t, '@position-try --pf {}');
+ let declarations = style.sheet.cssRules[0].style;
+ declarations.setProperty(prop, 'unset');
+ assert_equals(declarations.getPropertyValue(prop), supported ? 'unset' : '');
+ }, `CSSPositionTryDescriptors.setProperty(${prop})`);
+
+ test(t => {
+ let decls = supported_properties.map(x => `${x}:unset;`).join('');
+ let style = createStyle(t, `@position-try --pf { ${decls} }`);
+ let declarations = style.sheet.cssRules[0].style;
+ assert_equals(declarations[prop], supported ? 'unset' : undefined);
+ }, `CSSPositionTryDescriptors[${prop}] (set)`);
+
+ test(t => {
+ let style = createStyle(t, '@position-try --pf {}');
+ let declarations = style.sheet.cssRules[0].style;
+ declarations[prop] = 'unset';
+ assert_equals(declarations.getPropertyValue(prop), supported ? 'unset' : '');
+ }, `CSSPositionTryDescriptors[${prop}] (get)`);
+
+ let camelCaseAttr = toLowerCamelCase(prop);
+ if (camelCaseAttr != prop) {
+ // Also test the camelCase version of the attribute.
+ test(t => {
+ let decls = supported_properties.map(x => `${x}:unset;`).join('');
+ let style = createStyle(t, `@position-try --pf { ${decls} }`);
+ let declarations = style.sheet.cssRules[0].style;
+ assert_equals(declarations[camelCaseAttr], supported ? 'unset' : undefined);
+ }, `CSSPositionTryDescriptors[${camelCaseAttr}] (get)`);
+
+ test(t => {
+ let style = createStyle(t, '@position-try --pf {}');
+ let declarations = style.sheet.cssRules[0].style;
+ declarations[camelCaseAttr] = 'unset';
+ assert_equals(declarations.getPropertyValue(prop), supported ? 'unset' : '');
+ }, `CSSPositionTryDescriptors[${camelCaseAttr}] (set)`);
+ }
+}
+
+supported_properties.forEach(x => { test_property(x, /* supported */ true); });
+unsupported_properties.forEach(x => { test_property(x, /* supported */ false); });
+
</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/base-style-invalidation.html b/testing/web-platform/tests/css/css-anchor-position/base-style-invalidation.html
new file mode 100644
index 0000000000..8e8e69f9b7
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/base-style-invalidation.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>CSS Anchor Positioning: Invalidation from changing the base style</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/">
+<link rel="help" href="https://issues.chromium.org/issues/333608683">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ @position-try --pt {
+ width: 50px; /* Undo force overflow */
+ }
+ #cb {
+ position: relative;
+ width: 200px;
+ height: 200px;
+ border: 1px solid black;
+ }
+ #anchor {
+ position: absolute;
+ left: 75px;
+ top: 75px;
+ width: 50px;
+ height: 50px;
+ background: coral;
+ anchor-name: --a;
+ }
+ #anchored {
+ position: absolute;
+ position-anchor: --a;
+ position-try-options: --pt flip-start;
+ inset: 0;
+ top: anchor(top);
+ bottom: anchor(bottom);
+ right: calc(anchor(left) + 5px);
+ width: 50px;
+ height: 50px;
+ background: skyblue;
+ justify-self: end;
+ }
+ #anchored.flip {
+ background: seagreen;
+ width: 300px; /* Force overflow */
+ }
+</style>
+<div id=cb>
+ <div id=anchor></div>
+ <div id=anchored></div>
+</div>
+<script>
+ test(() => {
+ // Initially to the left.
+ assert_equals(anchored.offsetLeft, 20);
+ assert_equals(anchored.offsetTop, 75);
+
+ // Flips to the right.
+ anchored.classList.toggle('flip');
+ assert_equals(anchored.offsetLeft, 75);
+ assert_equals(anchored.offsetTop, 20);
+
+ // Flips to the original position.
+ anchored.classList.toggle('flip');
+ assert_equals(anchored.offsetLeft, 20);
+ assert_equals(anchored.offsetTop, 75);
+ }, 'The chosen position options changes when the base style differs');
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/chrome-336164421-crash.html b/testing/web-platform/tests/css/css-anchor-position/chrome-336164421-crash.html
new file mode 100644
index 0000000000..c45b69059b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/chrome-336164421-crash.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<link rel="help" href="https://crbug.com/336164421">
+<style>
+ #inner {
+ position: absolute;
+ left: anchor(left);
+ }
+ #inner::before {
+ display: none;
+ content: "";
+ }
+</style>
+<div id="inner"></div>
diff --git a/testing/web-platform/tests/css/css-anchor-position/chrome-336322507-crash.html b/testing/web-platform/tests/css/css-anchor-position/chrome-336322507-crash.html
new file mode 100644
index 0000000000..922c53b977
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/chrome-336322507-crash.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<link rel="help" href="https://crbug.com/336322507">
+<style>
+ #crash {
+ --grad: linear-gradient(black, white);
+ background: var(--grad);
+ position: absolute;
+ top: anchor(center);
+ }
+</style>
+<div id="crash"></div>
+<script>
+ document.body.offsetTop;
+ crash.remove();
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/inset-area-function.html b/testing/web-platform/tests/css/css-anchor-position/inset-area-function.html
new file mode 100644
index 0000000000..9d68bf9ddf
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/inset-area-function.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<title>CSS Anchor Positioning: inset-area()</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-try-options">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #cb {
+ position: relative;
+ width: 200px;
+ height: 200px;
+ border: 1px solid black;
+ }
+ #anchor {
+ position: absolute;
+ left: 100px;
+ top: 100px;
+ width: 80px;
+ height: 80px;
+ background-color: tomato;
+ anchor-name: --a;
+ }
+ #target, #ref {
+ position: absolute;
+ width: 40px;
+ height: 40px;
+ background-color: skyblue;
+ inset-area: bottom right;
+ position-anchor: --a;
+ }
+ #ref {
+ background-color: seagreen;
+ }
+</style>
+<style id=style>
+</style>
+<div id=cb>
+ <div id=anchor></div>
+ <div id=target></div>
+ <div id=ref></div>
+</div>
+<script>
+
+// Test that a given inset-area() produces the same result as a reference
+// element that is styled with an inset-area declaration directly.
+function test_inset_area_fn(inset_area_function, inset_area_expected) {
+ test((t) => {
+ t.add_cleanup(() => {
+ style.textContent = '';
+ });
+ style.textContent = `
+ #target {
+ position-try-options: ${inset_area_function};
+ }
+ #ref {
+ inset-area: ${inset_area_expected};
+ }
+ `;
+ assert_equals(target.offsetLeft, ref.offsetLeft, 'offsetLeft');
+ assert_equals(target.offsetTop, ref.offsetTop, 'offsetTop');
+ }, `${inset_area_function}, ${inset_area_expected}`);
+}
+
+test_inset_area_fn('inset-area(top left)', 'top left');
+test_inset_area_fn('inset-area(span-top left)', 'span-top left');
+test_inset_area_fn('inset-area(top span-left)', 'top span-left');
+test_inset_area_fn('inset-area(top center)', 'top center');
+test_inset_area_fn('inset-area(left center)', 'left center');
+test_inset_area_fn('inset-area(start center)', 'start center');
+test_inset_area_fn('inset-area(center start)', 'center start');
+
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/inset-area-in-position-try.html b/testing/web-platform/tests/css/css-anchor-position/inset-area-in-position-try.html
new file mode 100644
index 0000000000..7339c4f41d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/inset-area-in-position-try.html
@@ -0,0 +1,188 @@
+<!DOCTYPE html>
+<title>CSS Anchor Positioning: inset-area in @position-try</title>
+<link rel="help" href='https://drafts.csswg.org/css-anchor-position-1/#position-try-options'>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<style>
+ #cb {
+ position: relative;
+ width: 200px;
+ height: 200px;
+ border: 1px solid black;
+ }
+ #anchor {
+ position: absolute;
+ left: 100px;
+ top: 100px;
+ width: 50px;
+ height: 50px;
+ background-color: tomato;
+ anchor-name: --a;
+ }
+ #target {
+ left: 200px; /* force fallback */
+ }
+ #target, #ref {
+ position: absolute;
+ width: 40px;
+ height: 40px;
+ background-color: skyblue;
+ inset-area: bottom right;
+ position-anchor: --a;
+ }
+ #ref {
+ background-color: seagreen;
+ }
+</style>
+<style id=style>
+</style>
+<div id=cb>
+ <div id=anchor></div>
+ <div id=target></div>
+ <div id=ref></div>
+</div>
+<script>
+
+// Test that inside-area, when used inside @position-try, works the same
+// as when it's specified in the base style.
+function test_inset_area_ref(inset_area) {
+ test((t) => {
+ t.add_cleanup(() => {
+ style.textContent = '';
+ });
+ style.textContent = `
+ @position-try --pt {
+ inset: unset; /* Undo force fallback */
+ inset-area: ${inset_area};
+ }
+ #target {
+ position-try-options: --pt;
+ }
+ #ref {
+ inset-area: ${inset_area};
+ }
+ `;
+ assert_true(CSS.supports('inset-area', 'top left'));
+ assert_true(CSS.supports('position-try-options', '--x'));
+ assert_equals(target.offsetLeft, ref.offsetLeft, 'offsetLeft');
+ assert_equals(target.offsetTop, ref.offsetTop, 'offsetTop');
+ }, `${inset_area}`);
+}
+
+test_inset_area_ref('none');
+test_inset_area_ref('span-all');
+test_inset_area_ref('span-all span-all');
+test_inset_area_ref('top left');
+test_inset_area_ref('top center');
+test_inset_area_ref('top right');
+test_inset_area_ref('center left');
+test_inset_area_ref('center center');
+test_inset_area_ref('center right');
+test_inset_area_ref('bottom left');
+test_inset_area_ref('bottom center');
+test_inset_area_ref('bottom right');
+test_inset_area_ref('start start');
+test_inset_area_ref('start center');
+test_inset_area_ref('start end');
+test_inset_area_ref('center start');
+test_inset_area_ref('center end');
+test_inset_area_ref('end start');
+test_inset_area_ref('end center');
+test_inset_area_ref('end end');
+test_inset_area_ref('self-start self-start');
+test_inset_area_ref('self-start center');
+test_inset_area_ref('self-start self-end');
+test_inset_area_ref('center self-start');
+test_inset_area_ref('center self-end');
+test_inset_area_ref('self-end self-start');
+test_inset_area_ref('self-end center');
+test_inset_area_ref('self-end self-end');
+test_inset_area_ref('y-start x-start');
+test_inset_area_ref('y-start center');
+test_inset_area_ref('y-start x-end');
+test_inset_area_ref('center x-start');
+test_inset_area_ref('center x-end');
+test_inset_area_ref('y-end x-start');
+test_inset_area_ref('y-end center');
+test_inset_area_ref('y-end x-end');
+test_inset_area_ref('y-self-start x-self-start');
+test_inset_area_ref('y-self-start center');
+test_inset_area_ref('y-self-start x-self-end');
+test_inset_area_ref('center x-self-start');
+test_inset_area_ref('center x-self-end');
+test_inset_area_ref('y-self-end x-self-start');
+test_inset_area_ref('y-self-end center');
+test_inset_area_ref('y-self-end x-self-end');
+test_inset_area_ref('span-y-self-start span-x-self-end');
+test_inset_area_ref('span-bottom span-all');
+
+</script>
+
+<style>
+ @position-try --top {
+ inset: unset; /* Undo force fallback */
+ inset-area: top;
+ }
+ @position-try --right {
+ inset: unset; /* Undo force fallback */
+ inset-area: right;
+ }
+ @position-try --bottom {
+ inset: unset; /* Undo force fallback */
+ inset-area: bottom;
+ }
+ @position-try --left {
+ inset: unset; /* Undo force fallback */
+ inset-area: left;
+ }
+</style>
+<script>
+
+// Test that an element with the specified position-try-options is placed
+// at the same position as a reference element with inset-area:`expected`.
+// This test uses #target/#ref size of 60x60px, which causes overflow if
+// if we attempt the --right and --bottom positions.
+function test_inset_area_placement(position_try_options, expected) {
+ test((t) => {
+ style.textContent = `
+ #target, #ref {
+ width: 60px;
+ height: 60px;
+ }
+ #target {
+ position-try-options: ${position_try_options};
+ }
+ #ref {
+ inset-area: ${expected};
+ }
+ `;
+ assert_true(CSS.supports('inset-area', 'top left'));
+ assert_true(CSS.supports('position-try-options', '--x'));
+ assert_equals(target.offsetLeft, ref.offsetLeft, 'offsetLeft');
+ assert_equals(target.offsetTop, ref.offsetTop, 'offsetTop');
+ }, `Placement: ${position_try_options}`);
+}
+
+test_inset_area_placement('--top', 'top');
+test_inset_area_placement('--left', 'left');
+
+// No space to to the right/bottom:
+test_inset_area_placement('--right, --top', 'top');
+test_inset_area_placement('--bottom, --top', 'top');
+test_inset_area_placement('--bottom, --right, --top', 'top');
+test_inset_area_placement('--bottom, --right, --left, --top', 'left');
+test_inset_area_placement('--bottom, --left, --top, --right', 'left');
+
+// Flipping:
+test_inset_area_placement('--right flip-inline', 'left');
+test_inset_area_placement('--bottom flip-block', 'top');
+test_inset_area_placement('--left flip-start', 'top');
+
+// left + flip-inline => right (no space).
+test_inset_area_placement('--left flip-inline, --top', 'top');
+// top + flip-block => bottom (no space).
+test_inset_area_placement('--top flip-block, --left', 'left');
+// left + flip-start flip-block => bottom (no space).
+test_inset_area_placement('--left flip-start flip-block, --left', 'left');
+
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/parsing/position-visibility-computed.tentative.html b/testing/web-platform/tests/css/css-anchor-position/parsing/position-visibility-computed.tentative.html
index ff4ceb73df..8a8ba88706 100644
--- a/testing/web-platform/tests/css/css-anchor-position/parsing/position-visibility-computed.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/parsing/position-visibility-computed.tentative.html
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Anchor Positioning Test: Computed position-visibility</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/css/support/computed-testcommon.js"></script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/parsing/position-visibility-parsing.tentative.html b/testing/web-platform/tests/css/css-anchor-position/parsing/position-visibility-parsing.tentative.html
index 18dd27eadb..942ec71754 100644
--- a/testing/web-platform/tests/css/css-anchor-position/parsing/position-visibility-parsing.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/parsing/position-visibility-parsing.tentative.html
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Anchor Positioning Test: Parsing of position-visibility</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/css/support/parsing-testcommon.js"></script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-try-backdrop.html b/testing/web-platform/tests/css/css-anchor-position/position-try-backdrop.html
new file mode 100644
index 0000000000..6bf2262c69
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/position-try-backdrop.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>CSS Anchor Positioning: position-try-options on ::backdrop</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#fallback-apply">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ @position-try --pt {
+ left: 300px;
+ }
+ #anchor {
+ width: 100px;
+ height: 100px;
+ margin-left: 150px;
+ margin-top: 50px;
+ background: coral;
+ }
+ dialog::backdrop {
+ background: seagreen;
+ width:100px;
+ height:100px;
+ left: 9999px; /* Force overflow */
+ position-try-options: --pt;
+ }
+</style>
+<div id=anchor>Anchor</div>
+<dialog id=dialog>Dialog</dialog>
+<script>
+ test((t) => {
+ dialog.showModal();
+ let style = getComputedStyle(dialog, '::backdrop');
+ assert_equals(style.left, '300px');
+ }, '::backdrop can use position-try-options');
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-try-order-inset-area.html b/testing/web-platform/tests/css/css-anchor-position/position-try-order-inset-area.html
new file mode 100644
index 0000000000..98e606a7d8
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/position-try-order-inset-area.html
@@ -0,0 +1,196 @@
+<!DOCTYPE html>
+<title>CSS Anchor Positioning: position-try-order behavior with inset-area</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-try-order-property">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#inset-area">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!--
+ Note: This test is the inset-area version of position-try-order-basic.html.
+-->
+<style>
+ #cb {
+ position: absolute;
+ width: 400px;
+ height: 400px;
+ border: 1px solid black;
+ }
+ #anchor {
+ position: absolute;
+ left: 150px;
+ top: 200px;
+ width: 150px;
+ height: 150px;
+ background-color: coral;
+ anchor-name: --a;
+ }
+ #target, #ref {
+ position: absolute;
+ left: 450px; /* force fallback */
+ width: 40px;
+ height: 40px;
+ background-color: skyblue;
+ position-anchor: --a;
+ /* Note: align/justify is for cosmetic/debugging reasons only,
+ it should not have any effect on the result. */
+ align-self: start;
+ justify-self: start;
+ }
+ #ref {
+ background-color: seagreen;
+ }
+
+/*
+
+The IMCB for --right is the whole area to the right of the anchor, and similarly
+for --left, etc.
+
+ ┌──────────────┐
+ │ xxxx│
+ │ xxxx│
+ │ ┌────┐xxxx│
+ │ │ │xxxx│
+ │ └────┘xxxx│
+ │ xxxx│
+ │ xxxx│
+ └──────────────┘
+
+**/
+
+ @position-try --right {
+ inset: unset;
+ inset-area: right;
+ }
+ @position-try --left {
+ inset: unset;
+ inset-area: left;
+ }
+ @position-try --top {
+ inset: unset;
+ inset-area: top;
+ }
+ @position-try --bottom {
+ inset: unset;
+ inset-area: bottom;
+ }
+
+/*
+
+The IMCB for --right-sweep is the area that would be "swept" by the anchor if it
+moved right, and similarly for --left-sweep, etc.
+
+ ┌──────────────┐
+ │ │
+ │ │
+ │ ┌────┐xxxx│
+ │ │ │xxxx│
+ │ └────┘xxxx│
+ │ │
+ │ │
+ └──────────────┘
+
+*/
+
+ @position-try --right-sweep {
+ inset: unset;
+ inset-area: right center;
+ }
+
+ @position-try --left-sweep {
+ inset: unset;
+ inset-area: left center;
+ }
+
+ @position-try --bottom-sweep {
+ inset: unset;
+ inset-area: bottom center;
+ }
+
+ @position-try --top-sweep {
+ inset: unset;
+ inset-area: top center;
+ }
+
+</style>
+<style id=style>
+</style>
+<div id=cb>
+ <div id=anchor></div>
+ <div id=target></div>
+ <div id=ref></div>
+</div>
+<script>
+
+// Test that an element with the specified `position_try` gets the same
+// position as a reference element with `position_try_expected`.
+function test_inset_area_order(position_try, position_try_expected) {
+ test((t) => {
+ style.textContent = `
+ #target {
+ position-try: ${position_try};
+ }
+ #ref {
+ position-try: ${position_try_expected};
+ }
+ `;
+ assert_true(CSS.supports('position-try', 'normal --x'));
+ assert_equals(target.offsetLeft, ref.offsetLeft, 'offsetLeft');
+ assert_equals(target.offsetTop, ref.offsetTop, 'offsetTop');
+ }, `${position_try} | ${position_try_expected}`);
+}
+
+// Note: --right, --left, --top, and --bottom all fit, but have different
+// inset-modifed containing blocks.
+
+test_inset_area_order('--right', '--right');
+test_inset_area_order('--left', '--left');
+test_inset_area_order('--top', '--top');
+test_inset_area_order('--bottom', '--bottom');
+
+// position-try-order:normal just picks the first option.
+test_inset_area_order('--right, --left, --bottom, --top', '--right');
+test_inset_area_order('normal --right, --left, --bottom, --top', '--right');
+test_inset_area_order('normal --top, --left, --bottom, --right', '--top');
+
+// --right and --left have the same IMCB block-size.
+test_inset_area_order('most-block-size --right, --left', '--right');
+test_inset_area_order('most-height --right, --left', '--right');
+// --left has more inline-size than --right.
+test_inset_area_order('most-inline-size --right, --left', '--left');
+test_inset_area_order('most-width --right, --left', '--left');
+
+// --bottom and --top have the same IMCB inline-size.
+test_inset_area_order('most-inline-size --bottom, --top', '--bottom');
+test_inset_area_order('most-width --bottom, --top', '--bottom');
+// --top has more block-size than --bottom.
+test_inset_area_order('most-block-size --bottom, --top', '--top');
+test_inset_area_order('most-height --bottom, --top', '--top');
+
+// --bottom/--top has more IMBC inline-size than --right/--left.
+test_inset_area_order('most-inline-size --right, --left, --bottom, --top', '--bottom');
+test_inset_area_order('most-inline-size --right, --left, --top, --bottom', '--top');
+
+// --right/--left has more IMBC block-size than --bottom/--top.
+test_inset_area_order('most-block-size --bottom, --top, --right, --left', '--right');
+test_inset_area_order('most-block-size --bottom, --top, --left, --right', '--left');
+
+// --left-sweep and --bottom-sweep has the same IMBC inline-size ...
+test_inset_area_order('most-inline-size --left-sweep, --bottom-sweep', '--left-sweep');
+test_inset_area_order('most-inline-size --bottom-sweep, --left-sweep', '--bottom-sweep');
+// ... but not the same block-size.
+test_inset_area_order('most-block-size --left-sweep, --bottom-sweep', '--left-sweep');
+test_inset_area_order('most-block-size --bottom-sweep, --left-sweep', '--left-sweep');
+
+test_inset_area_order('most-inline-size --right-sweep, --left-sweep, --bottom-sweep, --top-sweep', '--left-sweep');
+test_inset_area_order('most-block-size --right-sweep, --left-sweep, --bottom-sweep, --top-sweep', '--top-sweep');
+
+test_inset_area_order(`most-inline-size
+ --right-sweep, --left-sweep, --bottom-sweep, --top-sweep,
+ --right, --left, --bottom, --top
+ `, '--left-sweep');
+
+test_inset_area_order(`most-block-size
+ --right-sweep, --left-sweep, --bottom-sweep, --top-sweep,
+ --right, --left, --bottom, --top
+ `, '--right');
+
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-add-no-overflow.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-add-no-overflow.html
index 9d87f82b9f..de0647f88b 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-add-no-overflow.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-add-no-overflow.html
@@ -2,7 +2,7 @@
<html class=reftest-wait>
<meta charset="utf-8">
<title>CSS Anchor Positioning Test: position-visibility: no-overflow</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-no-overflow-ref.html">
<style>
#scroll-container {
@@ -44,4 +44,4 @@ requestAnimationFrame(() => {
});
});
</script>
-</html> \ No newline at end of file
+</html>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-valid.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-valid.tentative.html
index bf67921639..4b069c253b 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-valid.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-valid.tentative.html
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Anchor Positioning Test: position-visibility: anchors-valid</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-anchors-valid-ref.html">
<style>
.anchor {
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-in.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-in.html
index cea439c55f..f13c500915 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-in.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-in.html
@@ -3,7 +3,7 @@
<meta charset="utf-8">
<meta name="assert" content="Scrolling an anchor in to view should cause a position-visibility: anchors-visible element to appear." />
<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-anchors-visible-after-scroll-in-ref.html">
<script src="/common/reftest-wait.js"></script>
<script src="/common/rendering-utils.js"></script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-out.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-out.html
index b2e3643b07..4294091b89 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-out.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-out.html
@@ -3,7 +3,7 @@
<meta charset="utf-8">
<meta name="assert" content="Scrolling an anchor out of view should cause a position-visibility: anchors-visible element to disappear." />
<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-anchors-visible-after-scroll-out-ref.html">
<script src="/common/reftest-wait.js"></script>
<script src="/common/rendering-utils.js"></script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-both-position-fixed-ref.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-both-position-fixed-ref.html
new file mode 100644
index 0000000000..7a9d69a9e3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-both-position-fixed-ref.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+<!-- This test passes if both the anchor and anchored elements are hidden. -->
+<div style="height: 200vh;"></div>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-both-position-fixed.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-both-position-fixed.tentative.html
new file mode 100644
index 0000000000..3ea26c896e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-both-position-fixed.tentative.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="assert" content="position-visibility: anchors-visible should work with a fixed-position anchored element." />
+<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="match" href="position-visibility-anchors-visible-both-position-fixed-ref.html">
+<style>
+ #anchor {
+ anchor-name: --a1;
+ position: fixed;
+ top: -100px;
+ left: 0;
+ width: 100px;
+ height: 100px;
+ background: orange;
+ }
+
+ #target {
+ position-anchor: --a1;
+ position-visibility: anchors-visible;
+ inset-area: bottom right;
+ width: 100px;
+ height: 100px;
+ background: red;
+ position: fixed;
+ top: 0;
+ left: 0;
+ }
+
+ #spacer {
+ height: 200vh;
+ }
+</style>
+
+<!-- Test passes if #target is not visible, due to #anchor being off-screen. -->
+<div id="anchor">anchor</div>
+<div id="target">target</div>
+<div id="spacer"></div>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-001.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-001.html
new file mode 100644
index 0000000000..3f515a5686
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-001.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<meta charset="utf-8">
+<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="match" href="position-visibility-anchors-visible-after-scroll-out-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="/common/rendering-utils.js"></script>
+<style>
+ #scroll-container {
+ overflow: hidden scroll;
+ width: 300px;
+ height: 100px;
+ }
+
+ #anchor1 {
+ anchor-name: --a1;
+ width: 100px;
+ height: 100px;
+ background: orange;
+ }
+
+ #anchor2 {
+ anchor-name: --a2;
+ width: 100px;
+ height: 50px;
+ background: yellow;
+ }
+
+ .spacer {
+ height: 100px;
+ }
+
+ .anchored {
+ position-visibility: anchors-visible;
+ inset-area: bottom;
+ width: 100px;
+ height: 50px;
+ background: red;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+
+ #chained {
+ position-anchor: --a1;
+ }
+
+ #target {
+ position-anchor: --a2;
+ }
+</style>
+
+<div id="scroll-container">
+ <div id="anchor1">anchor1</div>
+ <div class="spacer"></div>
+ <div id="chained" class="anchored">
+ <div id="anchor2">chained</div>
+ </div>
+ <div id="target" class="anchored">target</div>
+</div>
+
+<script>
+ // #target should be initially visible because it is anchored to #anchor2
+ // which is chained to #anchor1 and both anchors are visible.
+ waitForAtLeastOneFrame().then(() => {
+ // Scroll #anchor1 so that it is out of view.
+ document.getElementById('scroll-container').scrollTop = 100;
+
+ // #target should now be invisible.
+ takeScreenshot();
+ });
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-002.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-002.html
new file mode 100644
index 0000000000..aa1dcd0eb9
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-002.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<meta charset="utf-8">
+<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="match" href="position-visibility-anchors-visible-after-scroll-out-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="/common/rendering-utils.js"></script>
+<style>
+ #scroll-container {
+ overflow: hidden scroll;
+ width: 300px;
+ height: 100px;
+ }
+
+ #anchor1 {
+ anchor-name: --a1;
+ width: 100px;
+ height: 100px;
+ background: orange;
+ }
+
+ #chained {
+ position-anchor: --a1;
+ anchor-name: --a2;
+ background: yellow;
+ }
+
+ .spacer {
+ height: 100px;
+ }
+
+ .anchored {
+ position-visibility: anchors-visible;
+ inset-area: bottom;
+ width: 100px;
+ height: 50px;
+ background: red;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+
+ #target {
+ position-anchor: --a2;
+ }
+</style>
+
+<div id="scroll-container">
+ <div id="anchor1">anchor1</div>
+ <div class="spacer"></div>
+ <div id="chained" class="anchored"></div>
+ <div id="target" class="anchored">target</div>
+</div>
+
+<script>
+ // #target should be initially visible because it is anchored to #anchor2
+ // which is chained to #anchor1 and both anchors are visible.
+ waitForAtLeastOneFrame().then(() => {
+ // Scroll #anchor1 so that it is out of view.
+ document.getElementById('scroll-container').scrollTop = 100;
+
+ // #target should now be invisible.
+ takeScreenshot();
+ });
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-003.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-003.html
new file mode 100644
index 0000000000..9a1e077de0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-003.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<meta charset="utf-8">
+<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="match" href="position-visibility-anchors-visible-after-scroll-out-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="/common/rendering-utils.js"></script>
+<style>
+ #scroll-container {
+ overflow: hidden scroll;
+ width: 300px;
+ height: 100px;
+ }
+
+ #anchor1 {
+ anchor-name: --a1;
+ width: 100px;
+ height: 100px;
+ background: orange;
+ }
+
+ .spacer {
+ height: 100px;
+ }
+
+ #anchor2 {
+ anchor-name: --a2;
+ position-anchor: --a1;
+ }
+
+ #anchor3 {
+ anchor-name: --a3;
+ position-anchor: --a2;
+ }
+
+ #anchor4 {
+ anchor-name: --a4;
+ position-anchor: --a3;
+ }
+
+ .anchored {
+ position-visibility: anchors-visible;
+ inset-area: bottom;
+ width: 100px;
+ height: 50px;
+ background: red;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+
+ #target {
+ position-anchor: --a4;
+ }
+</style>
+
+<div id="scroll-container">
+ <div id="anchor1">anchor1</div>
+ <div class="spacer"></div>
+ <div id="anchor2" class="anchored">anchor2</div>
+ <div id="anchor3" class="anchored">anchor3</div>
+ <div id="anchor4" class="anchored">anchor4</div>
+ <div id="target" class="anchored">target</div>
+</div>
+
+<script>
+ // #target should be initially visible because it is anchored to #anchor2
+ // which is chained to #anchor1 and both anchors are visible.
+ waitForAtLeastOneFrame().then(() => {
+ // Scroll #anchor1 so that it is out of view.
+ document.getElementById('scroll-container').scrollTop = 100;
+
+ // #target should now be invisible.
+ takeScreenshot();
+ });
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-004-ref.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-004-ref.html
new file mode 100644
index 0000000000..409247a695
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-004-ref.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+ #scroll-container {
+ overflow: hidden scroll;
+ width: 300px;
+ height: 100px;
+ }
+
+ #anchor {
+ width: 100px;
+ height: 100px;
+ background: orange;
+ margin-bottom: 100px;
+ }
+
+ #chained {
+ width: 100px;
+ height: 50px;
+ background: blue;
+ }
+
+ #target {
+ width: 100px;
+ height: 50px;
+ background: green;
+ }
+</style>
+
+<div id="scroll-container">
+ <div id="anchor">anchor1</div>
+</div>
+<div id="chained">chained</div>
+<div id="target">target</div>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-004.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-004.html
new file mode 100644
index 0000000000..62ab7578a3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-chained-004.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<meta charset="utf-8">
+<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="match" href="position-visibility-anchors-visible-chained-004-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="/common/rendering-utils.js"></script>
+<style>
+ #scroll-container {
+ overflow: hidden scroll;
+ width: 300px;
+ height: 100px;
+ }
+
+ #anchor1 {
+ anchor-name: --a1;
+ width: 100px;
+ height: 100px;
+ background: orange;
+ }
+
+ #anchor2 {
+ anchor-name: --a2;
+ width: 100px;
+ height: 50px;
+ background: blue;
+ }
+
+ .spacer {
+ height: 100px;
+ }
+
+ .anchored {
+ position-visibility: anchors-visible;
+ inset-area: bottom;
+ width: 100px;
+ height: 50px;
+ background: green;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+
+ #chained {
+ position-anchor: --a1;
+ }
+
+ #target {
+ position-anchor: --a2;
+ }
+</style>
+
+<div id="scroll-container">
+ <div id="anchor1">anchor1</div>
+ <div class="spacer"></div>
+ <div id="chained" class="anchored">
+ <div id="anchor2">chained</div>
+ </div>
+ <div id="target" class="anchored">target</div>
+</div>
+
+<script>
+ // #target should be initially visible because it is anchored to #anchor2
+ // which is chained to #anchor1 and both anchors are visible.
+ waitForAtLeastOneFrame().then(() => {
+ // Scroll #anchor out of view.
+ const scroller = document.getElementById('scroll-container');
+ scroller.scrollTop = 100;
+ // #target1 should now be invisible.
+
+ waitForAtLeastOneFrame().then(() => {
+ // Scroll #anchor back into view.
+ scroller.scrollTop = 0;
+
+ // #target should now be visible again.
+ takeScreenshot();
+ });
+ });
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-change-anchor.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-change-anchor.html
index f8b1cc6d10..117628e7dc 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-change-anchor.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-change-anchor.html
@@ -3,7 +3,7 @@
<meta charset="utf-8">
<meta name="assert" content="Position-visibility should not be affected by the visibility of a previous anchor." />
<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-anchors-visible-change-anchor-ref.html">
<script src="/common/reftest-wait.js"></script>
<script src="/common/rendering-utils.js"></script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-change-css-visibility.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-change-css-visibility.html
index 22a30658c8..f9c598374c 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-change-css-visibility.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-change-css-visibility.html
@@ -3,7 +3,7 @@
<meta charset="utf-8">
<meta name="assert" content="Position-visibility: anchors-visible should show an element after an anchor changes from visibility: hidden to visibility: visible." />
<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-anchors-visible-change-css-visibility-ref.html">
<script src="/common/reftest-wait.js"></script>
<script src="/common/rendering-utils.js"></script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-css-visibility.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-css-visibility.html
index 31be797798..a699025325 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-css-visibility.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-css-visibility.html
@@ -2,7 +2,7 @@
<meta charset="utf-8">
<meta name="assert" content="Position-visibility: anchors-visible should hide an element with an anchor that has visibility: hidden." />
<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-anchors-visible-css-visibility-ref.html">
<style>
#container {
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-non-intervening-container.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-non-intervening-container.html
index 7b84976fd3..9c4d085b7e 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-non-intervening-container.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-non-intervening-container.html
@@ -2,7 +2,7 @@
<meta charset="utf-8">
<meta name="assert" content="position-visibility: anchors-visible should consider the visibility of the anchor relative the containing scroller, ignoring visibility in other scrollers." />
<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-anchors-visible-non-intervening-container-ref.html">
<style>
#non-intervening-scroll-container {
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-stacked-child.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-stacked-child.html
new file mode 100644
index 0000000000..e563fecfed
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-stacked-child.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="assert" content="Position-visibility: anchors-visible should hide an element and stacked children with an out-of-view anchor." />
+<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
+<link rel="match" href="position-visibility-anchors-visible-ref.html">
+<style>
+ #scroll-container {
+ overflow: hidden scroll;
+ width: 300px;
+ height: 100px;
+ }
+
+ #anchor {
+ anchor-name: --a1;
+ width: 100px;
+ height: 100px;
+ background: orange;
+ }
+
+ #spacer {
+ height: 100px;
+ }
+
+ #target {
+ position-anchor: --a1;
+ position-visibility: anchors-visible;
+ inset-area: bottom right;
+ width: 100px;
+ height: 100px;
+ background: red;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+ #stacking-child {
+ /* stacking context */
+ z-index: 1;
+ width: 100px;
+ height: 100px;
+ background: maroon;
+ position: absolute;
+ top: 25px;
+ left: 25px;
+ }
+</style>
+
+<div id="scroll-container">
+ <div id="anchor">anchor</div>
+ <div id="spacer"></div>
+ <div id="target">target
+ <div id="stacking-child"></div>
+ </div>
+</div>
+
+<script>
+ const scroller = document.getElementById('scroll-container');
+ scroller.scrollTop = 100;
+ // #target should not be visible because #anchor is scrolled out of view.
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-with-position.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-with-position.html
index 82eed0beb9..43dd2cc782 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-with-position.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible-with-position.html
@@ -2,7 +2,7 @@
<meta charset="utf-8">
<meta name="assert" content="Position-visibility: anchors-visible should hide an element with an out-of-view anchor and a relpos scroller." />
<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-anchors-visible-ref.html">
<style>
#scroll-container {
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible.html
index 85b8d897db..78daffb11b 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-anchors-visible.html
@@ -2,7 +2,7 @@
<meta charset="utf-8">
<meta name="assert" content="Position-visibility: anchors-visible should hide an element with an out-of-view anchor." />
<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-anchors-visible-ref.html">
<style>
#scroll-container {
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow-scroll.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow-scroll.html
index 4751faeb0d..f646f819cd 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow-scroll.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow-scroll.html
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html class="reftest-wait">
<title>CSS Anchor Positioning Test: position-visibility: no-overflow</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-no-overflow-scroll-ref.html">
<style>
#scroll-container {
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow-stacked-child.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow-stacked-child.html
index f748fda33e..1ea5ff9a1e 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow-stacked-child.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow-stacked-child.html
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Anchor Positioning Test: position-visibility: no-overflow</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-no-overflow-ref.html">
<style>
#scroll-container {
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow.html
index 39fb55b120..ea3b2d0802 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-no-overflow.html
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Anchor Positioning Test: position-visibility: no-overflow</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-no-overflow-ref.html">
<style>
#scroll-container {
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-remove-anchors-visible.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-remove-anchors-visible.html
index c6649e5f93..95be15ca55 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-remove-anchors-visible.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-remove-anchors-visible.html
@@ -3,7 +3,7 @@
<meta charset="utf-8">
<meta name="assert" content="Removing position-visibility: anchors-visible from an invisible anchored element should cause it to become visible." />
<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-remove-anchors-visible-ref.html">
<script src="/common/reftest-wait.js"></script>
<script src="/common/rendering-utils.js"></script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-visibility-remove-no-overflow.tentative.html b/testing/web-platform/tests/css/css-anchor-position/position-visibility-remove-no-overflow.html
index a043917da6..2cd2ed9fa3 100644
--- a/testing/web-platform/tests/css/css-anchor-position/position-visibility-remove-no-overflow.tentative.html
+++ b/testing/web-platform/tests/css/css-anchor-position/position-visibility-remove-no-overflow.html
@@ -2,7 +2,7 @@
<html class=reftest-wait>
<meta charset="utf-8">
<title>CSS Anchor Positioning Test: position-visibility: no-overflow</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-visibility">
<link rel="match" href="position-visibility-no-overflow-ref.html">
<style>
#scroll-container {
@@ -45,4 +45,4 @@ requestAnimationFrame(() => {
});
});
</script>
-</html> \ No newline at end of file
+</html>
diff --git a/testing/web-platform/tests/css/css-anchor-position/pseudo-element-anchor-dynamic.html b/testing/web-platform/tests/css/css-anchor-position/pseudo-element-anchor-dynamic.html
new file mode 100644
index 0000000000..9e4fa9828e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/pseudo-element-anchor-dynamic.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<title>CSS Anchor Positioning: Pseudo elements as anchors</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-1/#position-anchor">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ .cb {
+ position: relative;
+ width: 200px;
+ height: 200px;
+ }
+ #anchor1.enabled::before {
+ display: block;
+ width: 100px;
+ height: 100px;
+ content: "";
+ anchor-name: --a1;
+ background: blue;
+ }
+ #anchor2.enabled::after {
+ display: block;
+ width: 100px;
+ height: 100px;
+ content: "";
+ anchor-name: --a2;
+ background: pink;
+ }
+ .anchored {
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ left: anchor(right);
+ top: anchor(bottom);
+ background: orange;
+ }
+ #anchored1 { position-anchor: --a1; }
+ #anchored2 { position-anchor: --a2; }
+</style>
+<div class="cb">
+ <div id="anchor1"></div>
+ <div id="anchored1" class="anchored"></div>
+</div>
+<div class="cb">
+ <div id="anchor2"></div>
+ <div id="anchored2" class="anchored"></div>
+</div>
+<script>
+ test(() => {
+ assert_equals(anchored1.offsetLeft, 0);
+ assert_equals(anchored1.offsetTop, 0);
+ anchor1.className = "enabled";
+ assert_equals(anchored1.offsetLeft, 100);
+ assert_equals(anchored1.offsetTop, 100);
+ }, "::before as anchor dynamically generated");
+ test(() => {
+ assert_equals(anchored2.offsetLeft, 0);
+ assert_equals(anchored2.offsetTop, 0);
+ anchor2.className = "enabled";
+ assert_equals(anchored2.offsetLeft, 100);
+ assert_equals(anchored2.offsetTop, 100);
+ }, "::after as anchor dynamically generated");
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/pseudo-element-anchor.html b/testing/web-platform/tests/css/css-anchor-position/pseudo-element-anchor.html
new file mode 100644
index 0000000000..a107a0ec17
--- /dev/null
+++ b/testing/web-platform/tests/css/css-anchor-position/pseudo-element-anchor.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<title>CSS Anchor Positioning: Pseudo elements as anchors</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-1/#position-anchor">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ .cb {
+ position: relative;
+ width: 200px;
+ height: 200px;
+ }
+ #anchor1::before {
+ display: block;
+ width: 100px;
+ height: 100px;
+ content: "";
+ anchor-name: --a1;
+ background: blue;
+ }
+ #anchor2::after {
+ display: block;
+ width: 100px;
+ height: 100px;
+ content: "";
+ anchor-name: --a2;
+ background: pink;
+ }
+ .anchored {
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ left: anchor(right);
+ top: anchor(bottom);
+ background: orange;
+ }
+ #anchored1 { position-anchor: --a1; }
+ #anchored2 { position-anchor: --a2; }
+</style>
+<div class="cb">
+ <div id="anchor1"></div>
+ <div id="anchored1" class="anchored"></div>
+</div>
+<div class="cb">
+ <div id="anchor2"></div>
+ <div id="anchored2" class="anchored"></div>
+</div>
+<script>
+ test(() => {
+ assert_equals(anchored1.offsetLeft, 100);
+ assert_equals(anchored1.offsetTop, 100);
+ }, "::before as anchor");
+ test(() => {
+ assert_equals(anchored2.offsetLeft, 100);
+ assert_equals(anchored2.offsetTop, 100);
+ }, "::after as anchor");
+</script>
diff --git a/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-001.tentative-ref.html b/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-001-ref.html
index fb858cc105..fb858cc105 100644
--- a/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-001.tentative-ref.html
+++ b/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-001-ref.html
diff --git a/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-002.tentative-ref.html b/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-002-ref.html
index 9dde5d00f0..9dde5d00f0 100644
--- a/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-002.tentative-ref.html
+++ b/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-002-ref.html
diff --git a/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-004.tentative-ref.html b/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-004-ref.html
index 22c2270dde..22c2270dde 100644
--- a/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-004.tentative-ref.html
+++ b/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-004-ref.html
diff --git a/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-fallback.tentative-ref.html b/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-fallback-ref.html
index 0933430fc3..0933430fc3 100644
--- a/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-fallback.tentative-ref.html
+++ b/testing/web-platform/tests/css/css-anchor-position/reference/anchor-scroll-chained-fallback-ref.html
diff --git a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-end-computed.tentative.html b/testing/web-platform/tests/css/css-animations/parsing/animation-delay-end-computed.tentative.html
deleted file mode 100644
index 77f9670638..0000000000
--- a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-end-computed.tentative.html
+++ /dev/null
@@ -1,12 +0,0 @@
-<!DOCTYPE html>
-<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay-end">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/css/support/computed-testcommon.js"></script>
-<div id="target"></div>
-<script>
-test_computed_value("animation-delay-end", "initial", "0s");
-test_computed_value("animation-delay-end", "-500ms", "-0.5s");
-test_computed_value("animation-delay-end", "calc(2 * 3s)", "6s");
-test_computed_value("animation-delay-end", "20s, 10s");
-</script>
diff --git a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-end-invalid.tentative.html b/testing/web-platform/tests/css/css-animations/parsing/animation-delay-end-invalid.tentative.html
deleted file mode 100644
index 7cabd4e8e5..0000000000
--- a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-end-invalid.tentative.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay-end">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/css/support/parsing-testcommon.js"></script>
-<script>
-test_invalid_value("animation-delay-end", "infinite");
-test_invalid_value("animation-delay-end", "0");
-test_invalid_value("animation-delay-end", "1s 2s");
-test_invalid_value("animation-delay-end", "1s / 2s");
-test_invalid_value("animation-delay-end", "100px");
-test_invalid_value("animation-delay-end", "100%");
-
-test_invalid_value("animation-delay-end", "peek 50%");
-test_invalid_value("animation-delay-end", "50% contain");
-test_invalid_value("animation-delay-end", "50% cover");
-test_invalid_value("animation-delay-end", "50% entry");
-test_invalid_value("animation-delay-end", "50% enter");
-test_invalid_value("animation-delay-end", "50% exit");
-test_invalid_value("animation-delay-end", "contain contain");
-test_invalid_value("animation-delay-end", "auto");
-test_invalid_value("animation-delay-end", "none");
-test_invalid_value("animation-delay-end", "cover 50% enter 50%");
-test_invalid_value("animation-delay-end", "cover 100px");
-test_invalid_value("animation-delay-end", "cover");
-test_invalid_value("animation-delay-end", "contain");
-test_invalid_value("animation-delay-end", "enter");
-test_invalid_value("animation-delay-end", "exit");
-</script>
diff --git a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-end-valid.tentative.html b/testing/web-platform/tests/css/css-animations/parsing/animation-delay-end-valid.tentative.html
deleted file mode 100644
index 162c781bb0..0000000000
--- a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-end-valid.tentative.html
+++ /dev/null
@@ -1,11 +0,0 @@
-<!DOCTYPE html>
-<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay-end">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/css/support/parsing-testcommon.js"></script>
-<script>
-test_valid_value("animation-delay-end", "-5ms");
-test_valid_value("animation-delay-end", "0s");
-test_valid_value("animation-delay-end", "10s");
-test_valid_value("animation-delay-end", "20s, 10s");
-</script>
diff --git a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-shorthand-computed.html b/testing/web-platform/tests/css/css-animations/parsing/animation-delay-shorthand-computed.html
deleted file mode 100644
index 0a1eb96041..0000000000
--- a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-shorthand-computed.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<!DOCTYPE html>
-<title>animation-delay shorthand (computed values)</title>
-<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/css/support/computed-testcommon.js"></script>
-<div id="target"></div>
-<script>
-test_computed_value("animation-delay", "1s");
-test_computed_value("animation-delay", "-1s");
-test_computed_value("animation-delay", "1s 2s");
-test_computed_value("animation-delay", "1s, 2s");
-test_computed_value("animation-delay", "1s 2s, 3s");
-test_computed_value("animation-delay", "1s, 2s 3s");
-test_computed_value("animation-delay", "1s, 2s, 3s");
-</script>
diff --git a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-shorthand.html b/testing/web-platform/tests/css/css-animations/parsing/animation-delay-shorthand.html
deleted file mode 100644
index 5c74a4d8e4..0000000000
--- a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-shorthand.html
+++ /dev/null
@@ -1,49 +0,0 @@
-<!DOCTYPE html>
-<title>animation-delay shorthand</title>
-<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/css/support/shorthand-testcommon.js"></script>
-<script src="/css/support/parsing-testcommon.js"></script>
-<script>
-test_valid_value("animation-delay", "1s");
-test_valid_value("animation-delay", "-1s");
-test_valid_value("animation-delay", "1s 2s");
-test_valid_value("animation-delay", "1s, 2s");
-test_valid_value("animation-delay", "1s 2s, 3s");
-test_valid_value("animation-delay", "1s, 2s 3s");
-test_valid_value("animation-delay", "1s, 2s, 3s");
-
-test_invalid_value("animation-delay", "1s 2s 3s");
-test_invalid_value("animation-delay", "0s, 1s 2s 3s");
-test_invalid_value("animation-delay", "1s / 2s");
-test_invalid_value("animation-delay", "1s, 2px");
-test_invalid_value("animation-delay", "#ff0000");
-test_invalid_value("animation-delay", "red");
-test_invalid_value("animation-delay", "thing");
-test_invalid_value("animation-delay", "thing 0%");
-test_invalid_value("animation-delay", "thing 42%");
-test_invalid_value("animation-delay", "thing 100%");
-test_invalid_value("animation-delay", "thing 100px");
-test_invalid_value("animation-delay", "100% thing");
-
-test_shorthand_value('animation-delay', '1s 2s', {
- 'animation-delay-start': '1s',
- 'animation-delay-end': '2s',
-});
-
-test_shorthand_value('animation-delay', '1s', {
- 'animation-delay-start': '1s',
- 'animation-delay-end': '0s',
-});
-
-test_shorthand_value('animation-delay', '1s 2s, 3s 4s', {
- 'animation-delay-start': '1s, 3s',
- 'animation-delay-end': '2s, 4s',
-});
-
-test_shorthand_value('animation-delay', '1s 2s, 3s, 4s 5s', {
- 'animation-delay-start': '1s, 3s, 4s',
- 'animation-delay-end': '2s, 0s, 5s',
-});
-</script>
diff --git a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-start-computed.tentative.html b/testing/web-platform/tests/css/css-animations/parsing/animation-delay-start-computed.tentative.html
deleted file mode 100644
index bfb89d0267..0000000000
--- a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-start-computed.tentative.html
+++ /dev/null
@@ -1,12 +0,0 @@
-<!DOCTYPE html>
-<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay-start">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/css/support/computed-testcommon.js"></script>
-<div id="target"></div>
-<script>
-test_computed_value("animation-delay-start", "initial", "0s");
-test_computed_value("animation-delay-start", "-500ms", "-0.5s");
-test_computed_value("animation-delay-start", "calc(2 * 3s)", "6s");
-test_computed_value("animation-delay-start", "20s, 10s");
-</script>
diff --git a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-start-invalid.tentative.html b/testing/web-platform/tests/css/css-animations/parsing/animation-delay-start-invalid.tentative.html
deleted file mode 100644
index bff31f3789..0000000000
--- a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-start-invalid.tentative.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay-start">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/css/support/parsing-testcommon.js"></script>
-<script>
-test_invalid_value("animation-delay-start", "infinite");
-test_invalid_value("animation-delay-start", "0");
-test_invalid_value("animation-delay-start", "1s 2s");
-test_invalid_value("animation-delay-start", "1s / 2s");
-test_invalid_value("animation-delay-start", "100px");
-test_invalid_value("animation-delay-start", "100%");
-
-test_invalid_value("animation-delay-start", "peek 50%");
-test_invalid_value("animation-delay-start", "50% contain");
-test_invalid_value("animation-delay-start", "50% cover");
-test_invalid_value("animation-delay-start", "50% entry");
-test_invalid_value("animation-delay-start", "50% enter");
-test_invalid_value("animation-delay-start", "50% exit");
-test_invalid_value("animation-delay-start", "contain contain");
-test_invalid_value("animation-delay-start", "auto");
-test_invalid_value("animation-delay-start", "none");
-test_invalid_value("animation-delay-start", "cover 50% enter 50%");
-test_invalid_value("animation-delay-start", "cover 100px");
-test_invalid_value("animation-delay-start", "cover");
-test_invalid_value("animation-delay-start", "contain");
-test_invalid_value("animation-delay-start", "enter");
-test_invalid_value("animation-delay-start", "exit");
-</script>
diff --git a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-start-valid.tentative.html b/testing/web-platform/tests/css/css-animations/parsing/animation-delay-start-valid.tentative.html
deleted file mode 100644
index f52286444e..0000000000
--- a/testing/web-platform/tests/css/css-animations/parsing/animation-delay-start-valid.tentative.html
+++ /dev/null
@@ -1,11 +0,0 @@
-<!DOCTYPE html>
-<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay-start">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/css/support/parsing-testcommon.js"></script>
-<script>
-test_valid_value("animation-delay-start", "-5ms");
-test_valid_value("animation-delay-start", "0s");
-test_valid_value("animation-delay-start", "10s");
-test_valid_value("animation-delay-start", "20s, 10s");
-</script>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081-print-ref.html b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081-print-ref.html
new file mode 100644
index 0000000000..5eb4a31173
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081-print-ref.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: block;
+ border: 0.25in solid black;
+}
+.flexbox > div > div {
+ display: inline-block;
+ box-sizing: border-box;
+ width: 50%;
+ border: 4px solid purple;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div>
+ <div>1</div><div>2</div>
+ </div>
+ <div style="break-before: page">
+ <div>3</div><div>4</div>
+ </div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081a-print.html b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081a-print.html
new file mode 100644
index 0000000000..d773768bf0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081a-print.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a row flex container, the break-before values on flex items are propagated to the flex line</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="multi-line-row-flex-fragmentation-081-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-wrap: wrap;
+ border: 0.25in solid black;
+}
+.flexbox > div {
+ box-sizing: border-box;
+ width: 50%;
+ border: 4px solid purple;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div>1</div><div>2</div>
+ <div style="break-before: page">3</div><div>4</div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081b-print.html b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081b-print.html
new file mode 100644
index 0000000000..7c4eeb2083
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081b-print.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a row flex container, the break-before values on flex items are propagated to the flex line</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="multi-line-row-flex-fragmentation-081-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-wrap: wrap;
+ border: 0.25in solid black;
+}
+.flexbox > div {
+ box-sizing: border-box;
+ width: 50%;
+ border: 4px solid purple;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div>1</div><div>2</div>
+ <div>3</div><div style="break-before: page">4</div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081c-print.html b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081c-print.html
new file mode 100644
index 0000000000..2181bc56b0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081c-print.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a row flex container, the break-after values on flex items are propagated to the flex line</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="multi-line-row-flex-fragmentation-081-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-wrap: wrap;
+ border: 0.25in solid black;
+}
+.flexbox > div {
+ box-sizing: border-box;
+ width: 50%;
+ border: 4px solid purple;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div style="break-after: page">1</div><div>2</div>
+ <div>3</div><div>4</div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081d-print.html b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081d-print.html
new file mode 100644
index 0000000000..b4d9f06142
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-081d-print.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a row flex container, the break-after values on flex items are propagated to the flex line</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="multi-line-row-flex-fragmentation-081-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-wrap: wrap;
+ border: 0.25in solid black;
+}
+.flexbox > div {
+ box-sizing: border-box;
+ width: 50%;
+ border: 4px solid purple;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div>1</div><div style="break-after: page">2</div>
+ <div>3</div><div>4</div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082-print-ref.html b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082-print-ref.html
new file mode 100644
index 0000000000..e7314be63b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082-print-ref.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: block;
+ border: 0.25in solid black;
+}
+.flexbox > div > div {
+ display: inline-block;
+ box-sizing: border-box;
+ width: 50%;
+ border: 4px solid purple;
+}
+.flexbox > .nested {
+ box-sizing: border-box;
+ width: 100%;
+ border: 4px solid gold;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div class="flexbox nested">
+ <div>1</div><div>2</div>
+ <div>3</div><div>4</div>
+ </div>
+ <div class="flexbox nested" style="break-before: page">
+ <div>5</div><div>6</div>
+ <div>7</div><div>8</div>
+ </div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082a-print.html b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082a-print.html
new file mode 100644
index 0000000000..18c732d242
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082a-print.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a row flex container, the break-before values on the flex item in the flex line are propagated to the flex container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="multi-line-row-flex-fragmentation-082-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-wrap: wrap;
+ border: 0.25in solid black;
+}
+.flexbox > div {
+ box-sizing: border-box;
+ width: 50%;
+ border: 4px solid purple;
+}
+.flexbox > .nested {
+ box-sizing: border-box;
+ width: 100%;
+ border: 4px solid gold;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div class="flexbox nested">
+ <div>1</div><div>2</div>
+ <div>3</div><div>4</div>
+ </div>
+ <div class="flexbox nested">
+ <div style="break-before: page">5</div><div>6</div>
+ <div>7</div><div>8</div>
+ </div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082b-print.html b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082b-print.html
new file mode 100644
index 0000000000..6f88af6cf6
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082b-print.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a row flex container, the break-before values on the flex item in the flex line are propagated to the flex container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="multi-line-row-flex-fragmentation-082-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-wrap: wrap;
+ border: 0.25in solid black;
+}
+.flexbox > div {
+ box-sizing: border-box;
+ width: 50%;
+ border: 4px solid purple;
+}
+.flexbox > .nested {
+ box-sizing: border-box;
+ width: 100%;
+ border: 4px solid gold;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div class="flexbox nested">
+ <div>1</div><div>2</div>
+ <div>3</div><div>4</div>
+ </div>
+ <div class="flexbox nested">
+ <div>5</div><div style="break-before: page">6</div>
+ <div>7</div><div>8</div>
+ </div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082c-print.html b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082c-print.html
new file mode 100644
index 0000000000..116e8db2b3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082c-print.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a row flex container, the break-after values on the flex item in the flex line are propagated to the flex container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="multi-line-row-flex-fragmentation-082-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-wrap: wrap;
+ border: 0.25in solid black;
+}
+.flexbox > div {
+ box-sizing: border-box;
+ width: 50%;
+ border: 4px solid purple;
+}
+.flexbox > .nested {
+ box-sizing: border-box;
+ width: 100%;
+ border: 4px solid gold;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div class="flexbox nested">
+ <div>1</div><div>2</div>
+ <div style="break-after: page">3</div><div>4</div>
+ </div>
+ <div class="flexbox nested">
+ <div>5</div><div>6</div>
+ <div>7</div><div>8</div>
+ </div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082d-print.html b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082d-print.html
new file mode 100644
index 0000000000..e649304e7b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-082d-print.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a row flex container, the break-after values on the flex item in the flex line are propagated to the flex container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="multi-line-row-flex-fragmentation-082-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-wrap: wrap;
+ border: 0.25in solid black;
+}
+.flexbox > div {
+ box-sizing: border-box;
+ width: 50%;
+ border: 4px solid purple;
+}
+.flexbox > .nested {
+ box-sizing: border-box;
+ width: 100%;
+ border: 4px solid gold;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div class="flexbox nested">
+ <div>1</div><div>2</div>
+ <div>3</div><div style="break-after: page">4</div>
+ </div>
+ <div class="flexbox nested">
+ <div>5</div><div>6</div>
+ <div>7</div><div>8</div>
+ </div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083a.html b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083a.html
new file mode 100644
index 0000000000..efc64d5f4a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083a.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a row flex container, the break-before values on flex items are propagated to the flex line</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="../../reference/ref-filled-green-100px-square-only.html">
+
+<style>
+.multicol {
+ columns: 2;
+ column-fill: auto;
+ column-gap: 0;
+ width: 100px;
+ height: 300px;
+}
+.flexbox {
+ display: flex;
+ flex-wrap: wrap;
+}
+.item {
+ background: green;
+ width: 25px;
+ height: 100px;
+}
+</style>
+
+<p>Test passes if there is a filled green square.</p>
+
+<div class="multicol">
+ <div class="flexbox">
+ <div class="flexbox">
+ <div class="item"></div>
+ <div class="item"></div>
+ </div>
+ <div class="flexbox" style="break-before: always; break-before: column">
+ <div class="item"></div>
+ <div class="item"></div>
+ </div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083b.html b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083b.html
new file mode 100644
index 0000000000..9181fd5f01
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083b.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a row flex container, the break-before values on the flex item in the flex line are propagated to the flex container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="../../reference/ref-filled-green-100px-square-only.html">
+
+<style>
+.multicol {
+ columns: 2;
+ column-fill: auto;
+ column-gap: 0;
+ width: 100px;
+ height: 300px;
+}
+.flexbox {
+ display: flex;
+ flex-wrap: wrap;
+}
+.item {
+ background: green;
+ width: 25px;
+ height: 100px;
+}
+</style>
+
+<p>Test passes if there is a filled green square.</p>
+
+<div class="multicol">
+ <div class="flexbox">
+ <div class="flexbox">
+ <div class="item"></div>
+ <div class="item"></div>
+ </div>
+ <div class="flexbox">
+ <div class="item" style="break-before: always; break-before: column"></div>
+ <div class="item"></div>
+ </div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083c.html b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083c.html
new file mode 100644
index 0000000000..b54571898d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083c.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a row flex container, the break-after values on flex items are propagated to the flex line</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="../../reference/ref-filled-green-100px-square-only.html">
+
+<style>
+.multicol {
+ columns: 2;
+ column-fill: auto;
+ column-gap: 0;
+ width: 100px;
+ height: 300px;
+}
+.flexbox {
+ display: flex;
+ flex-wrap: wrap;
+}
+.item {
+ background: green;
+ width: 25px;
+ height: 100px;
+}
+</style>
+
+<p>Test passes if there is a filled green square.</p>
+
+<div class="multicol">
+ <div class="flexbox">
+ <div class="flexbox" style="break-after: always; break-after: column">
+ <div class="item"></div>
+ <div class="item"></div>
+ </div>
+ <div class="flexbox">
+ <div class="item"></div>
+ <div class="item"></div>
+ </div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083d.html b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083d.html
new file mode 100644
index 0000000000..025fd67436
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/multi-line-row-flex-fragmentation-083d.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a row flex container, the break-after values on the flex item in the flex line are propagated to the flex container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="../../reference/ref-filled-green-100px-square-only.html">
+
+<style>
+.multicol {
+ columns: 2;
+ column-fill: auto;
+ column-gap: 0;
+ width: 100px;
+ height: 300px;
+}
+.flexbox {
+ display: flex;
+ flex-wrap: wrap;
+}
+.item {
+ background: green;
+ width: 25px;
+ height: 100px;
+}
+</style>
+
+<p>Test passes if there is a filled green square.</p>
+
+<div class="multicol">
+ <div class="flexbox">
+ <div class="flexbox">
+ <div class="item"></div>
+ <div class="item" style="break-after: always; break-after: column"></div>
+ </div>
+ <div class="flexbox">
+ <div class="item"></div>
+ <div class="item"></div>
+ </div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068-print-ref.html b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068-print-ref.html
new file mode 100644
index 0000000000..feff86edf9
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068-print-ref.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: block;
+ border: 0.25in solid black;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div>1</div>
+ <div>2</div>
+ <div style="break-before: page">3</div>
+ <div>4</div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068a-print.html b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068a-print.html
new file mode 100644
index 0000000000..5eebf66432
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068a-print.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a column flex container, the break-before values on flex items are honored</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="single-line-column-flex-fragmentation-068-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-direction: column;
+ border: 0.25in solid black;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div>1</div>
+ <div>2</div>
+ <div style="break-before: page">3</div>
+ <div>4</div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068b-print.html b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068b-print.html
new file mode 100644
index 0000000000..64a495e01c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068b-print.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a column flex container, the break-after values on flex items are honored</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="single-line-column-flex-fragmentation-068-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-direction: column;
+ border: 0.25in solid black;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div>1</div>
+ <div style="break-after: page">2</div>
+ <div>3</div>
+ <div>4</div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068c-print.html b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068c-print.html
new file mode 100644
index 0000000000..6c004f3d19
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068c-print.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a column-reverse flex container, the break-before values on flex items are honored</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="single-line-column-flex-fragmentation-068-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-direction: column-reverse;
+ border: 0.25in solid black;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div>4</div>
+ <div style="break-before: page">3</div>
+ <div>2</div>
+ <div>1</div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068d-print.html b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068d-print.html
new file mode 100644
index 0000000000..7ccf5e9f3e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-068d-print.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a column-reverse flex container, the break-after values on flex items are honored</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="single-line-column-flex-fragmentation-068-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-direction: column-reverse;
+ border: 0.25in solid black;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div>4</div>
+ <div>3</div>
+ <div style="break-after: page">2</div>
+ <div>1</div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069-print-ref.html b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069-print-ref.html
new file mode 100644
index 0000000000..7f37d32454
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069-print-ref.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: block;
+ border: 0.25in solid black;
+}
+.flexbox > div {
+ border: 4px solid purple;
+}
+.flexbox > .nested {
+ border: 4px solid gold;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div class="flexbox nested">
+ <div>1</div>
+ <div>2</div>
+ </div>
+ <div class="flexbox nested" style="break-before: page">
+ <div>3</div>
+ <div>4</div>
+ </div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069a-print.html b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069a-print.html
new file mode 100644
index 0000000000..1d18987036
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069a-print.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a column flex container, the break-before values on flex items are honored</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="single-line-column-flex-fragmentation-069-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-direction: column;
+ border: 0.25in solid black;
+}
+.flexbox > div {
+ border: 4px solid purple;
+}
+.flexbox > .nested {
+ border: 4px solid gold;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div class="flexbox nested">
+ <div>1</div>
+ <div>2</div>
+ </div>
+ <div class="flexbox nested" style="break-before: page">
+ <div>3</div>
+ <div>4</div>
+ </div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069b-print.html b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069b-print.html
new file mode 100644
index 0000000000..b5312837a7
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069b-print.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a column flex container, the break-before values on the first item are propagated to the flex container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="single-line-column-flex-fragmentation-069-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-direction: column;
+ border: 0.25in solid black;
+}
+.flexbox > div {
+ border: 4px solid purple;
+}
+.flexbox > .nested {
+ border: 4px solid gold;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div class="flexbox nested">
+ <div>1</div>
+ <div>2</div>
+ </div>
+ <div class="flexbox nested">
+ <div style="break-before: page">3</div>
+ <div>4</div>
+ </div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069c-print.html b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069c-print.html
new file mode 100644
index 0000000000..76686cb481
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069c-print.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a column flex container, the break-after values on flex items are honored</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="single-line-column-flex-fragmentation-069-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-direction: column;
+ border: 0.25in solid black;
+}
+.flexbox > div {
+ border: 4px solid purple;
+}
+.flexbox > .nested {
+ border: 4px solid gold;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div class="flexbox nested" style="break-after: page">
+ <div>1</div>
+ <div>2</div>
+ </div>
+ <div class="flexbox nested">
+ <div>3</div>
+ <div>4</div>
+ </div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069d-print.html b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069d-print.html
new file mode 100644
index 0000000000..7a740c35f5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-069d-print.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a column flex container, the break-after values on the first item are propagated to the flex container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="single-line-column-flex-fragmentation-069-print-ref.html">
+
+<style>
+@page { size: 5in 3in; margin: 0.5in; }
+body {
+ margin: 0;
+ font-size: 0.25in;
+}
+.flexbox {
+ display: flex;
+ flex-direction: column;
+ border: 0.25in solid black;
+}
+.flexbox > div {
+ border: 4px solid purple;
+}
+.flexbox > .nested {
+ border: 4px solid gold;
+}
+</style>
+
+<div>Before Flexbox</div>
+<div class="flexbox">
+ <div class="flexbox nested">
+ <div>1</div>
+ <div style="break-after: page">2</div>
+ </div>
+ <div class="flexbox nested">
+ <div>3</div>
+ <div>4</div>
+ </div>
+</div>
+<div>After Flexbox</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070a.html b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070a.html
new file mode 100644
index 0000000000..aa9af6a290
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070a.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a column flex container, the break-before values on flex items are honored</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="../../reference/ref-filled-green-100px-square-only.html">
+
+<style>
+.multicol {
+ columns: 2;
+ column-fill: auto;
+ column-gap: 0;
+ width: 100px;
+ height: 300px;
+}
+.flexbox {
+ display: flex;
+ flex-direction: column;
+}
+.item {
+ background: green;
+ height: 50px;
+}
+</style>
+
+<p>Test passes if there is a filled green square.</p>
+
+<div class="multicol">
+ <div class="flexbox">
+ <div class="flexbox">
+ <div class="item"></div>
+ <div class="item"></div>
+ </div>
+ <div class="flexbox" style="break-before: always; break-before: column">
+ <div class="item"></div>
+ <div class="item"></div>
+ </div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070b.html b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070b.html
new file mode 100644
index 0000000000..45a91d96cf
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070b.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a column flex container, the break-before values on the first item are propagated to the flex container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="../../reference/ref-filled-green-100px-square-only.html">
+
+<style>
+.multicol {
+ columns: 2;
+ column-fill: auto;
+ column-gap: 0;
+ width: 100px;
+ height: 300px;
+}
+.flexbox {
+ display: flex;
+ flex-direction: column;
+}
+.item {
+ background: green;
+ height: 50px;
+}
+</style>
+
+<p>Test passes if there is a filled green square.</p>
+
+<div class="multicol">
+ <div class="flexbox">
+ <div class="flexbox">
+ <div class="item"></div>
+ <div class="item"></div>
+ </div>
+ <div class="flexbox">
+ <div class="item" style="break-before: always; break-before: column"></div>
+ <div class="item"></div>
+ </div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070c.html b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070c.html
new file mode 100644
index 0000000000..149ff6667c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070c.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a column flex container, the break-after values on flex items are honored</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="../../reference/ref-filled-green-100px-square-only.html">
+
+<style>
+.multicol {
+ columns: 2;
+ column-fill: auto;
+ column-gap: 0;
+ width: 100px;
+ height: 300px;
+}
+.flexbox {
+ display: flex;
+ flex-direction: column;
+}
+.item {
+ background: green;
+ height: 50px;
+}
+</style>
+
+<p>Test passes if there is a filled green square.</p>
+
+<div class="multicol">
+ <div class="flexbox">
+ <div class="flexbox" style="break-after: always; break-after: column">
+ <div class="item"></div>
+ <div class="item"></div>
+ </div>
+ <div class="flexbox">
+ <div class="item"></div>
+ <div class="item"></div>
+ </div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070d.html b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070d.html
new file mode 100644
index 0000000000..2265e71df6
--- /dev/null
+++ b/testing/web-platform/tests/css/css-break/flexbox/single-line-column-flex-fragmentation-070d.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that in a column flex container, the break-after values on the first item are propagated to the flex container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#pagination">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1890238">
+<link rel="match" href="../../reference/ref-filled-green-100px-square-only.html">
+
+<style>
+.multicol {
+ columns: 2;
+ column-fill: auto;
+ column-gap: 0;
+ width: 100px;
+ height: 300px;
+}
+.flexbox {
+ display: flex;
+ flex-direction: column;
+}
+.item {
+ background: green;
+ height: 50px;
+}
+</style>
+
+<p>Test passes if there is a filled green square.</p>
+
+<div class="multicol">
+ <div class="flexbox">
+ <div class="flexbox">
+ <div class="item"></div>
+ <div class="item" style="break-after: always; break-after: column"></div>
+ </div>
+ <div class="flexbox">
+ <div class="item"></div>
+ <div class="item"></div>
+ </div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-break/ruby-002.html b/testing/web-platform/tests/css/css-break/ruby-002.html
index d17cc563f3..2c4f6aae5b 100644
--- a/testing/web-platform/tests/css/css-break/ruby-002.html
+++ b/testing/web-platform/tests/css/css-break/ruby-002.html
@@ -30,15 +30,13 @@
<div style="position:relative; width:100px; height:100px; background:red;">
<div style="columns:2; column-fill:auto; column-gap:0; height:175px; orphans:1; widows:1;">
<ruby>
- <div class="main"></div><rt><div class="annotation"></div></rt>
- </ruby><ruby class="under">
- <div class="main"></div><rt><div class="annotation"></div></rt>
+ <div class="main"></div><rt><div class="annotation"></div></rt></ruby
+ ><ruby class="under"><div class="main"></div><rt><div class="annotation"></div></rt>
</ruby>
<br>
<ruby>
- <div class="main"></div><rt><div class="annotation"></div></rt>
- </ruby><ruby class="under">
- <div class="main"></div><rt><div class="annotation"></div></rt>
+ <div class="main"></div><rt><div class="annotation"></div></rt></ruby
+ ><ruby class="under"><div class="main"></div><rt><div class="annotation"></div></rt>
</ruby>
<br>
</div>
diff --git a/testing/web-platform/tests/css/css-cascade/WEB_FEATURES.yml b/testing/web-platform/tests/css/css-cascade/WEB_FEATURES.yml
index 38aaee3021..5d0f726ab5 100644
--- a/testing/web-platform/tests/css/css-cascade/WEB_FEATURES.yml
+++ b/testing/web-platform/tests/css/css-cascade/WEB_FEATURES.yml
@@ -2,3 +2,7 @@ features:
- name: cascade-layers
files:
- layer-*
+- name: scope
+ files:
+ - at-scope-*
+ - scope-*
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-revert-layer.html b/testing/web-platform/tests/css/css-cascade/all-prop-revert-layer.html
index 868267b285..3a1d621d17 100644
--- a/testing/web-platform/tests/css/css-cascade/all-prop-revert-layer.html
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-revert-layer.html
@@ -15,8 +15,6 @@
alt: "a";
animation-composition: add;
animation-delay: 123s;
- animation-delay-start: 123s;
- animation-delay-end: 456s;
animation-direction: reverse;
animation-duration: 123s;
animation-fill-mode: both;
diff --git a/testing/web-platform/tests/css/css-cascade/scope-pseudo-element-ref.html b/testing/web-platform/tests/css/css-cascade/scope-pseudo-element-ref.html
new file mode 100644
index 0000000000..2ad6a0995a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-pseudo-element-ref.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<title>@scope - pseudo-elements (ref)</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scoped-styles">
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#link">
+
+<!-- Cosmetic -->
+<style>
+ body > div {
+ display: inline-block;
+ width: 100px;
+ height: 100px;
+ border: 1px solid black;
+ vertical-align:top;
+ }
+</style>
+
+<!-- ::before -->
+<style>
+ #before_test > main {
+ background-color: skyblue;
+ }
+ #before_test > main::before {
+ content: "B";
+ width: 20px;
+ height: 20px;
+ display: inline-block;
+ background-color: tomato;
+ }
+</style>
+<div id=before_test>
+ <main>
+ Foo
+ </main>
+</div>
+
+<!-- ::after -->
+<style>
+ #after_test > main {
+ background-color: skyblue;
+ }
+ #after_test > main::after {
+ content: "A";
+ width: 20px;
+ height: 20px;
+ display: inline-block;
+ background-color: tomato;
+ }
+</style>
+<div id=after_test>
+ <main>
+ Foo
+ </main>
+</div>
+
+<!-- ::marker -->
+<style>
+ #marker_test li {
+ background-color: skyblue;
+ }
+ #marker_test li::marker {
+ content: "M";
+ }
+</style>
+<div id=marker_test>
+ <ul>
+ <li>One</li>
+ <li>Two</li>
+ </ul>
+</div>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-pseudo-element.html b/testing/web-platform/tests/css/css-cascade/scope-pseudo-element.html
new file mode 100644
index 0000000000..29c4469060
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-pseudo-element.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<title>@scope - pseudo-elements</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scoped-styles">
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#link">
+<link rel="match" href="scope-pseudo-element-ref.html">
+
+<!-- Cosmetic -->
+<style>
+ body > div {
+ display: inline-block;
+ width: 100px;
+ height: 100px;
+ border: 1px solid black;
+ vertical-align:top;
+ }
+</style>
+
+<!-- ::before -->
+<style>
+ @scope (#before_test > main) {
+ :scope {
+ background-color: skyblue;
+ }
+ :scope::before {
+ content: "B";
+ width: 20px;
+ height: 20px;
+ display: inline-block;
+ background-color: tomato;
+ }
+ }
+</style>
+<div id=before_test>
+ <main>
+ Foo
+ </main>
+</div>
+
+<!-- ::after -->
+<style>
+ @scope (#after_test > main) {
+ :scope {
+ background-color: skyblue;
+ }
+ :scope::after {
+ content: "A";
+ width: 20px;
+ height: 20px;
+ display: inline-block;
+ background-color: tomato;
+ }
+ }
+</style>
+<div id=after_test>
+ <main>
+ Foo
+ </main>
+</div>
+
+<!-- ::marker -->
+<style>
+ @scope (#marker_test li) {
+ :scope {
+ background-color: skyblue;
+ }
+ :scope::marker {
+ content: "M";
+ }
+ }
+</style>
+<div id=marker_test>
+ <ul>
+ <li>One</li>
+ <li>Two</li>
+ </ul>
+</div>
diff --git a/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/WEB_FEATURES.yml b/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/WEB_FEATURES.yml
new file mode 100644
index 0000000000..48bbe25901
--- /dev/null
+++ b/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: color-scheme
+ files: "**"
diff --git a/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/color-scheme-iframe-background-about-blank.tentative.html b/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/color-scheme-iframe-background-about-blank.tentative.html
index a5a9843ac8..f9ab24fdc1 100644
--- a/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/color-scheme-iframe-background-about-blank.tentative.html
+++ b/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/color-scheme-iframe-background-about-blank.tentative.html
@@ -8,6 +8,3 @@
</style>
<p>Should not see a white frame below</p>
<iframe width="600" height="400"></iframe>
-<script>
- document.querySelector("iframe").contentWindow.stop();
-</script>
diff --git a/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/support/prefers-color-scheme.svg b/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/support/prefers-color-scheme.svg
deleted file mode 100644
index 3afcac70fe..0000000000
--- a/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/support/prefers-color-scheme.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width='100px' height='100px'>
- <style>
- @media (prefers-color-scheme: dark) {
- rect {
- fill: green;
- }
- }
- </style>
- <rect fill='blue' width='100px' height='100px' />
-</svg>
diff --git a/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/svg-as-image.html b/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/svg-as-image.html
deleted file mode 100644
index 6fc33f56ce..0000000000
--- a/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/svg-as-image.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<!doctype html>
-<head>
- <title>prefers-color-scheme inside an SVG image</title>
- <link rel="help" href="https://www.w3.org/TR/mediaqueries-5/#descdef-media-prefers-color-scheme">
- <link rel="match" href="svg-as-image-ref.html">
-</head>
-<p>There should be green square below when the preferred color-scheme is dark, and blue otherwise.</p>
-<img src='support/prefers-color-scheme.svg'>
diff --git a/testing/web-platform/tests/css/css-color/t424-hsl-clip-outside-gamut-b-ref.html b/testing/web-platform/tests/css/css-color/hsl-clamp-negative-saturation-ref.html
index 7d1f1bf278..33a82517ae 100644
--- a/testing/web-platform/tests/css/css-color/t424-hsl-clip-outside-gamut-b-ref.html
+++ b/testing/web-platform/tests/css/css-color/hsl-clamp-negative-saturation-ref.html
@@ -7,8 +7,6 @@
td { border: none; padding: 0; height: 1.2em; }
</style>
<body>
- <p><strong>WARNING: This test assumes that the device gamut is sRGB
- (as it will be for many CRT monitors).</strong></p>
<p>Every row in this table should have both columns the same color:</p>
<table>
<tr>
@@ -16,21 +14,6 @@
<th style="background:black; color: white">Column 2</th>
</tr>
<tr>
- <td colspan='2' style="background: black">&nbsp;</td>
- </tr>
- <tr>
- <td colspan='2' style="background: black">&nbsp;</td>
- </tr>
- <tr>
- <td colspan='2' style="background: white">&nbsp;</td>
- </tr>
- <tr>
- <td colspan='2' style="background: rgb(0, 0, 255)">&nbsp;</td>
- </tr>
- <tr>
- <td colspan='2' style="background: rgb(102, 0, 255)">&nbsp;</td>
- </tr>
- <tr>
<td colspan='2' style="background: rgb(102, 102, 102)">&nbsp;</td>
</tr>
<tr>
diff --git a/testing/web-platform/tests/css/css-color/hsl-clamp-negative-saturation.html b/testing/web-platform/tests/css/css-color/hsl-clamp-negative-saturation.html
new file mode 100644
index 0000000000..25b54c2e2c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-color/hsl-clamp-negative-saturation.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>CSS Test: hsl() clamp negative saturation values</title>
+ <link rel="author" title="L. David Baron" href="https://dbaron.org/" />
+ <link rel="author" title="Mozilla Corporation" href="http://mozilla.com/" />
+ <link rel="help" href="https://www.w3.org/TR/css-color-4/#the-hsl-notation" />
+ <link rel="match" href="hsl-clamp-negative-saturation-ref.html" />
+ <meta name="assert" content="Test clamping of negative saturation values in hsl() functions." />
+ <style type="text/css">
+
+ table { border-spacing: 0 2px; padding: 0; border: none; }
+ td { border: none; padding: 0; height: 1.2em; }
+
+ </style>
+ </head>
+ <body>
+
+ <p>Every row in this table should have both columns the same color:</p>
+
+ <table>
+ <tr>
+ <th style="background:white; color: black">Column 1</th>
+ <th style="background:black; color: white">Column 2</th>
+ </tr>
+ <tr>
+ <td style="background: hsl(0, -50%, 40%)">&nbsp;</td>
+ <td style="background: rgb(102, 102, 102)">&nbsp;</td>
+ </tr>
+ <tr>
+ <td style="background: hsl(30, -50%, 60%)">&nbsp;</td>
+ <td style="background: rgb(153, 153, 153)">&nbsp;</td>
+ </tr>
+ </table>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-color/t425-hsla-clip-outside-device-gamut-b-ref.html b/testing/web-platform/tests/css/css-color/hsla-clamp-negative-saturation-ref.html
index d35a608fb5..bbf5c3b037 100644
--- a/testing/web-platform/tests/css/css-color/t425-hsla-clip-outside-device-gamut-b-ref.html
+++ b/testing/web-platform/tests/css/css-color/hsla-clamp-negative-saturation-ref.html
@@ -8,8 +8,6 @@
td { border: none; padding: 0; height: 1.2em; }
</style>
<body>
- <p><strong>WARNING: This test assumes that the device gamut is sRGB
- (as it will be for many CRT monitors).</strong></p>
<p>Every row in this table should have both columns the same color:</p>
<table>
<tr>
@@ -17,21 +15,6 @@
<th style="background:black; color: white">Column 2</th>
</tr>
<tr>
- <td colspan='2' style="background: rgb(102, 102, 102)">&nbsp;</td>
- </tr>
- <tr>
- <td colspan='2' style="background: rgb(153, 153, 153)">&nbsp;</td>
- </tr>
- <tr>
- <td colspan='2' style="background: white">&nbsp;</td>
- </tr>
- <tr>
- <td colspan='2' style="background: rgb(153, 153, 255)">&nbsp;</td>
- </tr>
- <tr>
- <td colspan='2' style="background: rgb(163, 102, 255)">&nbsp;</td>
- </tr>
- <tr>
<td colspan='2' style="background: rgb(194, 194, 194)">&nbsp;</td>
</tr>
<tr>
diff --git a/testing/web-platform/tests/css/css-color/hsla-clamp-negative-saturation.html b/testing/web-platform/tests/css/css-color/hsla-clamp-negative-saturation.html
new file mode 100644
index 0000000000..239151efe3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-color/hsla-clamp-negative-saturation.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>CSS Test: hsla() clamp negative saturation values</title>
+ <link rel="author" title="L. David Baron" href="https://dbaron.org/" />
+ <link rel="author" title="Mozilla Corporation" href="http://mozilla.com/" />
+ <link rel="help" href="https://www.w3.org/TR/css-color-4/#the-hsl-notation" />
+ <link rel="match" href="hsla-clamp-negative-saturation-ref.html" />
+ <meta name="assert" content="Test clamping of negative saturation values in hsla() functions." />
+ <style type="text/css">
+
+ body { background: white; color: black; }
+ table { border-spacing: 0 2px; padding: 0; border: none; }
+ td { border: none; padding: 0; height: 1.2em; }
+
+ </style>
+ </head>
+ <body>
+
+ <p>Every row in this table should have both columns the same color:</p>
+
+ <table>
+ <tr>
+ <th style="background:white; color: black">Column 1</th>
+ <th style="background:black; color: white">Column 2</th>
+ </tr>
+ <tr>
+ <td style="background: hsla(0, -50%, 40%, 0.4)">&nbsp;</td>
+ <td style="background: rgb(194, 194, 194)">&nbsp;</td>
+ </tr>
+ <tr>
+ <td style="background: hsla(30, -50%, 60%, 0.6)">&nbsp;</td>
+ <td style="background: rgb(194, 194, 194)">&nbsp;</td>
+ </tr>
+ </table>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-color/parsing/color-valid-color-mix-function.html b/testing/web-platform/tests/css/css-color/parsing/color-valid-color-mix-function.html
index cd1f381a1a..baa6512053 100644
--- a/testing/web-platform/tests/css/css-color/parsing/color-valid-color-mix-function.html
+++ b/testing/web-platform/tests/css/css-color/parsing/color-valid-color-mix-function.html
@@ -11,409 +11,410 @@
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/css/support/parsing-testcommon.js"></script>
+<script src="/css/support/color-testcommon.js"></script>
</head>
<body>
<div id="target"></div>
<script>
// https://github.com/w3c/csswg-drafts/issues/7302: Specified values shouldn't resolve keyword colors or calc values
- test_valid_value(`color`, `color-mix(in srgb, red, blue)`, `color-mix(in srgb, red, blue)`);
- test_valid_value(`color`, `color-mix(in srgb, 70% red, 50% blue)`, `color-mix(in srgb, red 70%, blue 50%)`);
- test_valid_value(`color`, `color-mix(in hsl, red, blue)`, `color-mix(in hsl, red, blue)`);
- test_valid_value(`color`, `color-mix(in hsl, red calc(20%), blue)`, `color-mix(in hsl, red calc(20%), blue)`);
- test_valid_value(`color`, `color-mix(in hsl, red calc(var(--v)*1%), blue)`, `color-mix(in hsl, red calc(var(--v)*1%), blue)`);
- test_valid_value(`color`, `color-mix(in hsl, currentcolor, blue)`, `color-mix(in hsl, currentcolor, blue)`);
- test_valid_value(`color`, `color-mix(in hsl, red 60%, blue 40%)`, `color-mix(in hsl, red 60%, blue)`);
- test_valid_value(`color`, `color-mix(in hsl, red 50%, blue)`, `color-mix(in hsl, red, blue)`);
- test_valid_value(`color`, `color-mix(in hsl, red, blue 50%)`, `color-mix(in hsl, red, blue)`);
- test_valid_value(`color`, `color-mix(in lch decreasing hue, red, hsl(120, 100%, 50%))`, `color-mix(in lch decreasing hue, red, rgb(0, 255, 0))`);
-
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20%), hsl(30deg 30% 40%))`, `color-mix(in hsl, rgb(46, 56, 46), rgb(133, 102, 71))`);
- test_valid_value(`color`, `color-mix(in hsl, 50% hsl(120deg 10% 20%), hsl(30deg 30% 40%))`, `color-mix(in hsl, rgb(46, 56, 46), rgb(133, 102, 71))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20%), 50% hsl(30deg 30% 40%))`, `color-mix(in hsl, rgb(46, 56, 46), rgb(133, 102, 71))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20%) 25%, hsl(30deg 30% 40%))`, `color-mix(in hsl, rgb(46, 56, 46) 25%, rgb(133, 102, 71))`);
- test_valid_value(`color`, `color-mix(in hsl, 25% hsl(120deg 10% 20%), hsl(30deg 30% 40%))`, `color-mix(in hsl, rgb(46, 56, 46) 25%, rgb(133, 102, 71))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20%), 25% hsl(30deg 30% 40%))`, `color-mix(in hsl, rgb(46, 56, 46) 75%, rgb(133, 102, 71))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20%), hsl(30deg 30% 40%) 25%)`, `color-mix(in hsl, rgb(46, 56, 46) 75%, rgb(133, 102, 71))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20%) 25%, hsl(30deg 30% 40%) 75%)`, `color-mix(in hsl, rgb(46, 56, 46) 25%, rgb(133, 102, 71))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20%) 30%, hsl(30deg 30% 40%) 90%)`, `color-mix(in hsl, rgb(46, 56, 46) 30%, rgb(133, 102, 71) 90%)`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20%) 12.5%, hsl(30deg 30% 40%) 37.5%)`, `color-mix(in hsl, rgb(46, 56, 46) 12.5%, rgb(133, 102, 71) 37.5%)`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20%) 0%, hsl(30deg 30% 40%))`, `color-mix(in hsl, rgb(46, 56, 46) 0%, rgb(133, 102, 71))`);
-
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20% / .4), hsl(30deg 30% 40% / .8))`, `color-mix(in hsl, rgba(46, 56, 46, 0.4), rgba(133, 102, 71, 0.8))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20%) 25%, hsl(30deg 30% 40% / .8))`, `color-mix(in hsl, rgb(46, 56, 46) 25%, rgba(133, 102, 71, 0.8))`);
- test_valid_value(`color`, `color-mix(in hsl, 25% hsl(120deg 10% 20% / .4), hsl(30deg 30% 40% / .8))`, `color-mix(in hsl, rgba(46, 56, 46, 0.4) 25%, rgba(133, 102, 71, 0.8))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20% / .4), 25% hsl(30deg 30% 40% / .8))`, `color-mix(in hsl, rgba(46, 56, 46, 0.4) 75%, rgba(133, 102, 71, 0.8))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20% / .4), hsl(30deg 30% 40% / .8) 25%)`, `color-mix(in hsl, rgba(46, 56, 46, 0.4) 75%, rgba(133, 102, 71, 0.8))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20% / .4) 25%, hsl(30deg 30% 40% / .8) 75%)`, `color-mix(in hsl, rgba(46, 56, 46, 0.4) 25%, rgba(133, 102, 71, 0.8))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20% / .4) 30%, hsl(30deg 30% 40% / .8) 90%)`, `color-mix(in hsl, rgba(46, 56, 46, 0.4) 30%, rgba(133, 102, 71, 0.8) 90%)`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20% / .4) 12.5%, hsl(30deg 30% 40% / .8) 37.5%)`, `color-mix(in hsl, rgba(46, 56, 46, 0.4) 12.5%, rgba(133, 102, 71, 0.8) 37.5%)`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 10% 20% / .4) 0%, hsl(30deg 30% 40% / .8))`, `color-mix(in hsl, rgba(46, 56, 46, 0.4) 0%, rgba(133, 102, 71, 0.8))`);
-
- test_valid_value(`color`, `color-mix(in hsl, hsl(40deg 50% 50%), hsl(60deg 50% 50%))`, `color-mix(in hsl, rgb(191, 149, 64), rgb(191, 191, 64))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(60deg 50% 50%), hsl(40deg 50% 50%))`, `color-mix(in hsl, rgb(191, 191, 64), rgb(191, 149, 64))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(50deg 50% 50%), hsl(330deg 50% 50%))`, `color-mix(in hsl, rgb(191, 170, 64), rgb(191, 64, 128))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(330deg 50% 50%), hsl(50deg 50% 50%))`, `color-mix(in hsl, rgb(191, 64, 128), rgb(191, 170, 64))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(20deg 50% 50%), hsl(320deg 50% 50%))`, `color-mix(in hsl, rgb(191, 106, 64), rgb(191, 64, 149))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(320deg 50% 50%), hsl(20deg 50% 50%))`, `color-mix(in hsl, rgb(191, 64, 149), rgb(191, 106, 64))`);
-
- test_valid_value(`color`, `color-mix(in hsl shorter hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%))`, `color-mix(in hsl, rgb(191, 149, 64), rgb(191, 191, 64))`);
- test_valid_value(`color`, `color-mix(in hsl shorter hue, hsl(60deg 50% 50%), hsl(40deg 50% 50%))`, `color-mix(in hsl, rgb(191, 191, 64), rgb(191, 149, 64))`);
- test_valid_value(`color`, `color-mix(in hsl shorter hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%))`, `color-mix(in hsl, rgb(191, 170, 64), rgb(191, 64, 128))`);
- test_valid_value(`color`, `color-mix(in hsl shorter hue, hsl(330deg 50% 50%), hsl(50deg 50% 50%))`, `color-mix(in hsl, rgb(191, 64, 128), rgb(191, 170, 64))`);
- test_valid_value(`color`, `color-mix(in hsl shorter hue, hsl(20deg 50% 50%), hsl(320deg 50% 50%))`, `color-mix(in hsl, rgb(191, 106, 64), rgb(191, 64, 149))`);
- test_valid_value(`color`, `color-mix(in hsl shorter hue, hsl(320deg 50% 50%), hsl(20deg 50% 50%))`, `color-mix(in hsl, rgb(191, 64, 149), rgb(191, 106, 64))`);
-
- test_valid_value(`color`, `color-mix(in hsl longer hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%))`, `color-mix(in hsl longer hue, rgb(191, 149, 64), rgb(191, 191, 64))`);
- test_valid_value(`color`, `color-mix(in hsl longer hue, hsl(60deg 50% 50%), hsl(40deg 50% 50%))`, `color-mix(in hsl longer hue, rgb(191, 191, 64), rgb(191, 149, 64))`);
- test_valid_value(`color`, `color-mix(in hsl longer hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%))`, `color-mix(in hsl longer hue, rgb(191, 170, 64), rgb(191, 64, 128))`);
- test_valid_value(`color`, `color-mix(in hsl longer hue, hsl(330deg 50% 50%), hsl(50deg 50% 50%))`, `color-mix(in hsl longer hue, rgb(191, 64, 128), rgb(191, 170, 64))`);
- test_valid_value(`color`, `color-mix(in hsl longer hue, hsl(20deg 50% 50%), hsl(320deg 50% 50%))`, `color-mix(in hsl longer hue, rgb(191, 106, 64), rgb(191, 64, 149))`);
- test_valid_value(`color`, `color-mix(in hsl longer hue, hsl(320deg 50% 50%), hsl(20deg 50% 50%))`, `color-mix(in hsl longer hue, rgb(191, 64, 149), rgb(191, 106, 64))`);
-
- test_valid_value(`color`, `color-mix(in hsl increasing hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%))`, `color-mix(in hsl increasing hue, rgb(191, 149, 64), rgb(191, 191, 64))`);
- test_valid_value(`color`, `color-mix(in hsl increasing hue, hsl(60deg 50% 50%), hsl(40deg 50% 50%))`, `color-mix(in hsl increasing hue, rgb(191, 191, 64), rgb(191, 149, 64))`);
- test_valid_value(`color`, `color-mix(in hsl increasing hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%))`, `color-mix(in hsl increasing hue, rgb(191, 170, 64), rgb(191, 64, 128))`);
- test_valid_value(`color`, `color-mix(in hsl increasing hue, hsl(330deg 50% 50%), hsl(50deg 50% 50%))`, `color-mix(in hsl increasing hue, rgb(191, 64, 128), rgb(191, 170, 64))`);
- test_valid_value(`color`, `color-mix(in hsl increasing hue, hsl(20deg 50% 50%), hsl(320deg 50% 50%))`, `color-mix(in hsl increasing hue, rgb(191, 106, 64), rgb(191, 64, 149))`);
- test_valid_value(`color`, `color-mix(in hsl increasing hue, hsl(320deg 50% 50%), hsl(20deg 50% 50%))`, `color-mix(in hsl increasing hue, rgb(191, 64, 149), rgb(191, 106, 64))`);
-
- test_valid_value(`color`, `color-mix(in hsl decreasing hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%))`, `color-mix(in hsl decreasing hue, rgb(191, 149, 64), rgb(191, 191, 64))`);
- test_valid_value(`color`, `color-mix(in hsl decreasing hue, hsl(60deg 50% 50%), hsl(40deg 50% 50%))`, `color-mix(in hsl decreasing hue, rgb(191, 191, 64), rgb(191, 149, 64))`);
- test_valid_value(`color`, `color-mix(in hsl decreasing hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%))`, `color-mix(in hsl decreasing hue, rgb(191, 170, 64), rgb(191, 64, 128))`);
- test_valid_value(`color`, `color-mix(in hsl decreasing hue, hsl(330deg 50% 50%), hsl(50deg 50% 50%))`, `color-mix(in hsl decreasing hue, rgb(191, 64, 128), rgb(191, 170, 64))`);
- test_valid_value(`color`, `color-mix(in hsl decreasing hue, hsl(20deg 50% 50%), hsl(320deg 50% 50%))`, `color-mix(in hsl decreasing hue, rgb(191, 106, 64), rgb(191, 64, 149))`);
- test_valid_value(`color`, `color-mix(in hsl decreasing hue, hsl(320deg 50% 50%), hsl(20deg 50% 50%))`, `color-mix(in hsl decreasing hue, rgb(191, 64, 149), rgb(191, 106, 64))`);
-
- test_valid_value(`color`, `color-mix(in hsl, hsl(none none none), hsl(none none none))`, `color-mix(in hsl, rgb(0, 0, 0), rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(none none none), hsl(30deg 40% 80%))`, `color-mix(in hsl, rgb(0, 0, 0), rgb(224, 204, 184))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 20% 40%), hsl(none none none))`, `color-mix(in hsl, rgb(82, 122, 82), rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 20% none), hsl(30deg 40% 60%))`, `color-mix(in hsl, rgb(0, 0, 0), rgb(194, 153, 112))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 20% 40%), hsl(30deg 20% none))`, `color-mix(in hsl, rgb(82, 122, 82), rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(none 20% 40%), hsl(30deg none 80%))`, `color-mix(in hsl, rgb(122, 82, 82), rgb(204, 204, 204))`);
-
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40%))`, `color-mix(in hsl, rgba(61, 143, 61, 0), rgb(143, 61, 61))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40% / 0.5))`, `color-mix(in hsl, rgba(61, 143, 61, 0), rgba(143, 61, 61, 0.5))`);
- test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40% / none))`, `color-mix(in hsl, rgba(61, 143, 61, 0), rgba(143, 61, 61, 0))`);
-
- test_valid_value(`color`, `color-mix(in hsl, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, color(display-p3 0 1 0) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hsl, lab(100 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, lab(100 104.3 -50.9) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hsl, lab(0 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, lab(0 104.3 -50.9) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hsl, lch(100 116 334) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, lch(100 116 334) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hsl, lch(0 116 334) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, lch(0 116 334) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hsl, oklab(100 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, oklab(1 0.365 -0.16) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hsl, oklab(0 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, oklab(0 0.365 -0.16) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hsl, oklch(100 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, oklch(1 0.399 336.3) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hsl, oklab(1 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, oklab(1 0.365 -0.16) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hsl, oklch(1 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, oklch(1 0.399 336.3) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hsl, oklch(0 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, oklch(0 0.399 336.3) 100%, rgb(0, 0, 0))`);
-
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%), hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 204, 26), rgb(153, 115, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, 50% hwb(120deg 10% 20%), hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 204, 26), rgb(153, 115, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%), 50% hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 204, 26), rgb(153, 115, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%) 25%, hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 204, 26) 25%, rgb(153, 115, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, 25% hwb(120deg 10% 20%), hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 204, 26) 25%, rgb(153, 115, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%), 25% hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 204, 26) 75%, rgb(153, 115, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%), hwb(30deg 30% 40%) 25%)`, `color-mix(in hwb, rgb(26, 204, 26) 75%, rgb(153, 115, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%) 25%, hwb(30deg 30% 40%) 75%)`, `color-mix(in hwb, rgb(26, 204, 26) 25%, rgb(153, 115, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%) 30%, hwb(30deg 30% 40%) 90%)`, `color-mix(in hwb, rgb(26, 204, 26) 30%, rgb(153, 115, 77) 90%)`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%) 12.5%, hwb(30deg 30% 40%) 37.5%)`, `color-mix(in hwb, rgb(26, 204, 26) 12.5%, rgb(153, 115, 77) 37.5%)`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%) 0%, hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 204, 26) 0%, rgb(153, 115, 77))`);
-
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20% / .4), hwb(30deg 30% 40% / .8))`, `color-mix(in hwb, rgba(26, 204, 26, 0.4), rgba(153, 115, 77, 0.8))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20% / .4) 25%, hwb(30deg 30% 40% / .8))`, `color-mix(in hwb, rgba(26, 204, 26, 0.4) 25%, rgba(153, 115, 77, 0.8))`);
- test_valid_value(`color`, `color-mix(in hwb, 25% hwb(120deg 10% 20% / .4), hwb(30deg 30% 40% / .8))`, `color-mix(in hwb, rgba(26, 204, 26, 0.4) 25%, rgba(153, 115, 77, 0.8))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%), 25% hwb(30deg 30% 40% / .8))`, `color-mix(in hwb, rgb(26, 204, 26) 75%, rgba(153, 115, 77, 0.8))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20% / .4), hwb(30deg 30% 40% / .8) 25%)`, `color-mix(in hwb, rgba(26, 204, 26, 0.4) 75%, rgba(153, 115, 77, 0.8))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20% / .4) 25%, hwb(30deg 30% 40% / .8) 75%)`, `color-mix(in hwb, rgba(26, 204, 26, 0.4) 25%, rgba(153, 115, 77, 0.8))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20% / .4) 30%, hwb(30deg 30% 40% / .8) 90%)`, `color-mix(in hwb, rgba(26, 204, 26, 0.4) 30%, rgba(153, 115, 77, 0.8) 90%)`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20% / .4) 12.5%, hwb(30deg 30% 40% / .8) 37.5%)`, `color-mix(in hwb, rgba(26, 204, 26, 0.4) 12.5%, rgba(153, 115, 77, 0.8) 37.5%)`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20% / .4) 0%, hwb(30deg 30% 40% / .8))`, `color-mix(in hwb, rgba(26, 204, 26, 0.4) 0%, rgba(153, 115, 77, 0.8))`);
-
- test_valid_value(`color`, `color-mix(in hwb, hwb(40deg 30% 40%), hwb(60deg 30% 40%))`, `color-mix(in hwb, rgb(153, 128, 77), rgb(153, 153, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(60deg 30% 40%), hwb(40deg 30% 40%))`, `color-mix(in hwb, rgb(153, 153, 77), rgb(153, 128, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(50deg 30% 40%), hwb(330deg 30% 40%))`, `color-mix(in hwb, rgb(153, 140, 77), rgb(153, 77, 115))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(330deg 30% 40%), hwb(50deg 30% 40%))`, `color-mix(in hwb, rgb(153, 77, 115), rgb(153, 140, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(20deg 30% 40%), hwb(320deg 30% 40%))`, `color-mix(in hwb, rgb(153, 102, 77), rgb(153, 77, 128))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(320deg 30% 40%), hwb(20deg 30% 40%))`, `color-mix(in hwb, rgb(153, 77, 128), rgb(153, 102, 77))`);
-
- test_valid_value(`color`, `color-mix(in hwb shorter hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%))`, `color-mix(in hwb, rgb(153, 128, 77), rgb(153, 153, 77))`);
- test_valid_value(`color`, `color-mix(in hwb shorter hue, hwb(60deg 30% 40%), hwb(40deg 30% 40%))`, `color-mix(in hwb, rgb(153, 153, 77), rgb(153, 128, 77))`);
- test_valid_value(`color`, `color-mix(in hwb shorter hue, hwb(50deg 30% 40%), hwb(330deg 30% 40%))`, `color-mix(in hwb, rgb(153, 140, 77), rgb(153, 77, 115))`);
- test_valid_value(`color`, `color-mix(in hwb shorter hue, hwb(330deg 30% 40%), hwb(50deg 30% 40%))`, `color-mix(in hwb, rgb(153, 77, 115), rgb(153, 140, 77))`);
- test_valid_value(`color`, `color-mix(in hwb shorter hue, hwb(20deg 30% 40%), hwb(320deg 30% 40%))`, `color-mix(in hwb, rgb(153, 102, 77), rgb(153, 77, 128))`);
- test_valid_value(`color`, `color-mix(in hwb shorter hue, hwb(320deg 30% 40%), hwb(20deg 30% 40%))`, `color-mix(in hwb, rgb(153, 77, 128), rgb(153, 102, 77))`);
-
- test_valid_value(`color`, `color-mix(in hwb longer hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%))`, `color-mix(in hwb longer hue, rgb(153, 128, 77), rgb(153, 153, 77))`);
- test_valid_value(`color`, `color-mix(in hwb longer hue, hwb(60deg 30% 40%), hwb(40deg 30% 40%))`, `color-mix(in hwb longer hue, rgb(153, 153, 77), rgb(153, 128, 77))`);
- test_valid_value(`color`, `color-mix(in hwb longer hue, hwb(50deg 30% 40%), hwb(330deg 30% 40%))`, `color-mix(in hwb longer hue, rgb(153, 140, 77), rgb(153, 77, 115))`);
- test_valid_value(`color`, `color-mix(in hwb longer hue, hwb(330deg 30% 40%), hwb(50deg 30% 40%))`, `color-mix(in hwb longer hue, rgb(153, 77, 115), rgb(153, 140, 77))`);
- test_valid_value(`color`, `color-mix(in hwb longer hue, hwb(20deg 30% 40%), hwb(320deg 30% 40%))`, `color-mix(in hwb longer hue, rgb(153, 102, 77), rgb(153, 77, 128))`);
- test_valid_value(`color`, `color-mix(in hwb longer hue, hwb(320deg 30% 40%), hwb(20deg 30% 40%))`, `color-mix(in hwb longer hue, rgb(153, 77, 128), rgb(153, 102, 77))`);
-
- test_valid_value(`color`, `color-mix(in hwb increasing hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%))`, `color-mix(in hwb increasing hue, rgb(153, 128, 77), rgb(153, 153, 77))`);
- test_valid_value(`color`, `color-mix(in hwb increasing hue, hwb(60deg 30% 40%), hwb(40deg 30% 40%))`, `color-mix(in hwb increasing hue, rgb(153, 153, 77), rgb(153, 128, 77))`);
- test_valid_value(`color`, `color-mix(in hwb increasing hue, hwb(50deg 30% 40%), hwb(330deg 30% 40%))`, `color-mix(in hwb increasing hue, rgb(153, 140, 77), rgb(153, 77, 115))`);
- test_valid_value(`color`, `color-mix(in hwb increasing hue, hwb(330deg 30% 40%), hwb(50deg 30% 40%))`, `color-mix(in hwb increasing hue, rgb(153, 77, 115), rgb(153, 140, 77))`);
- test_valid_value(`color`, `color-mix(in hwb increasing hue, hwb(20deg 30% 40%), hwb(320deg 30% 40%))`, `color-mix(in hwb increasing hue, rgb(153, 102, 77), rgb(153, 77, 128))`);
- test_valid_value(`color`, `color-mix(in hwb increasing hue, hwb(320deg 30% 40%), hwb(20deg 30% 40%))`, `color-mix(in hwb increasing hue, rgb(153, 77, 128), rgb(153, 102, 77))`);
-
- test_valid_value(`color`, `color-mix(in hwb decreasing hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%))`, `color-mix(in hwb decreasing hue, rgb(153, 128, 77), rgb(153, 153, 77))`);
- test_valid_value(`color`, `color-mix(in hwb decreasing hue, hwb(60deg 30% 40%), hwb(40deg 30% 40%))`, `color-mix(in hwb decreasing hue, rgb(153, 153, 77), rgb(153, 128, 77))`);
- test_valid_value(`color`, `color-mix(in hwb decreasing hue, hwb(50deg 30% 40%), hwb(330deg 30% 40%))`, `color-mix(in hwb decreasing hue, rgb(153, 140, 77), rgb(153, 77, 115))`);
- test_valid_value(`color`, `color-mix(in hwb decreasing hue, hwb(330deg 30% 40%), hwb(50deg 30% 40%))`, `color-mix(in hwb decreasing hue, rgb(153, 77, 115), rgb(153, 140, 77))`);
- test_valid_value(`color`, `color-mix(in hwb decreasing hue, hwb(20deg 30% 40%), hwb(320deg 30% 40%))`, `color-mix(in hwb decreasing hue, rgb(153, 102, 77), rgb(153, 77, 128))`);
- test_valid_value(`color`, `color-mix(in hwb decreasing hue, hwb(320deg 30% 40%), hwb(20deg 30% 40%))`, `color-mix(in hwb decreasing hue, rgb(153, 77, 128), rgb(153, 102, 77))`);
-
- test_valid_value(`color`, `color-mix(in hwb, hwb(none none none), hwb(none none none))`, `color-mix(in hwb, rgb(255, 0, 0), rgb(255, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(none none none), hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(255, 0, 0), rgb(153, 115, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%), hwb(none none none))`, `color-mix(in hwb, rgb(26, 204, 26), rgb(255, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% none), hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 255, 26), rgb(153, 115, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%), hwb(30deg 30% none))`, `color-mix(in hwb, rgb(26, 204, 26), rgb(255, 166, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(none 10% 20%), hwb(30deg none 40%))`, `color-mix(in hwb, rgb(204, 26, 26), rgb(153, 77, 0))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40%))`, `color-mix(in hwb, rgba(26, 204, 26, 0), rgb(153, 115, 77))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40% / 0.5))`, `color-mix(in hwb, rgba(26, 204, 26, 0), rgba(153, 115, 77, 0.5))`);
- test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40% / none))`, `color-mix(in hwb, rgba(26, 204, 26, 0), rgba(153, 115, 77, 0))`);
-
- test_valid_value(`color`, `color-mix(in hwb, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, color(display-p3 0 1 0) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hwb, lab(100 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, lab(100 104.3 -50.9) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hwb, lab(0 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, lab(0 104.3 -50.9) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hwb, lch(100 116 334) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, lch(100 116 334) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hwb, lch(0 116 334) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, lch(0 116 334) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hwb, oklab(100 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, oklab(1 0.365 -0.16) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hwb, oklab(0 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, oklab(0 0.365 -0.16) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hwb, oklch(100 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, oklch(1 0.399 336.3) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hwb, oklch(0 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, oklch(0 0.399 336.3) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hwb, oklab(1 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, oklab(1 0.365 -0.16) 100%, rgb(0, 0, 0))`);
- test_valid_value(`color`, `color-mix(in hwb, oklch(1 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, oklch(1 0.399 336.3) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in srgb, red, blue)`, `color-mix(in srgb, red, blue)`);
+ fuzzy_test_valid_color(`color-mix(in srgb, 70% red, 50% blue)`, `color-mix(in srgb, red 70%, blue 50%)`);
+ fuzzy_test_valid_color(`color-mix(in hsl, red, blue)`, `color-mix(in hsl, red, blue)`);
+ fuzzy_test_valid_color(`color-mix(in hsl, red calc(20%), blue)`, `color-mix(in hsl, red calc(20%), blue)`);
+ fuzzy_test_valid_color(`color-mix(in hsl, red calc(var(--v)*1%), blue)`, `color-mix(in hsl, red calc(var(--v)*1%), blue)`);
+ fuzzy_test_valid_color(`color-mix(in hsl, currentcolor, blue)`, `color-mix(in hsl, currentcolor, blue)`);
+ fuzzy_test_valid_color(`color-mix(in hsl, red 60%, blue 40%)`, `color-mix(in hsl, red 60%, blue)`);
+ fuzzy_test_valid_color(`color-mix(in hsl, red 50%, blue)`, `color-mix(in hsl, red, blue)`);
+ fuzzy_test_valid_color(`color-mix(in hsl, red, blue 50%)`, `color-mix(in hsl, red, blue)`);
+ fuzzy_test_valid_color(`color-mix(in lch decreasing hue, red, hsl(120, 100%, 50%))`, `color-mix(in lch decreasing hue, red, rgb(0, 255, 0))`);
+
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20%), hsl(30deg 30% 40%))`, `color-mix(in hsl, rgb(46, 56, 46), rgb(133, 102, 71))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, 50% hsl(120deg 10% 20%), hsl(30deg 30% 40%))`, `color-mix(in hsl, rgb(46, 56, 46), rgb(133, 102, 71))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20%), 50% hsl(30deg 30% 40%))`, `color-mix(in hsl, rgb(46, 56, 46), rgb(133, 102, 71))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20%) 25%, hsl(30deg 30% 40%))`, `color-mix(in hsl, rgb(46, 56, 46) 25%, rgb(133, 102, 71))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, 25% hsl(120deg 10% 20%), hsl(30deg 30% 40%))`, `color-mix(in hsl, rgb(46, 56, 46) 25%, rgb(133, 102, 71))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20%), 25% hsl(30deg 30% 40%))`, `color-mix(in hsl, rgb(46, 56, 46) 75%, rgb(133, 102, 71))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20%), hsl(30deg 30% 40%) 25%)`, `color-mix(in hsl, rgb(46, 56, 46) 75%, rgb(133, 102, 71))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20%) 25%, hsl(30deg 30% 40%) 75%)`, `color-mix(in hsl, rgb(46, 56, 46) 25%, rgb(133, 102, 71))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20%) 30%, hsl(30deg 30% 40%) 90%)`, `color-mix(in hsl, rgb(46, 56, 46) 30%, rgb(133, 102, 71) 90%)`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20%) 12.5%, hsl(30deg 30% 40%) 37.5%)`, `color-mix(in hsl, rgb(46, 56, 46) 12.5%, rgb(133, 102, 71) 37.5%)`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20%) 0%, hsl(30deg 30% 40%))`, `color-mix(in hsl, rgb(46, 56, 46) 0%, rgb(133, 102, 71))`);
+
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20% / .4), hsl(30deg 30% 40% / .8))`, `color-mix(in hsl, rgba(46, 56, 46, 0.4), rgba(133, 102, 71, 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20%) 25%, hsl(30deg 30% 40% / .8))`, `color-mix(in hsl, rgb(46, 56, 46) 25%, rgba(133, 102, 71, 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, 25% hsl(120deg 10% 20% / .4), hsl(30deg 30% 40% / .8))`, `color-mix(in hsl, rgba(46, 56, 46, 0.4) 25%, rgba(133, 102, 71, 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20% / .4), 25% hsl(30deg 30% 40% / .8))`, `color-mix(in hsl, rgba(46, 56, 46, 0.4) 75%, rgba(133, 102, 71, 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20% / .4), hsl(30deg 30% 40% / .8) 25%)`, `color-mix(in hsl, rgba(46, 56, 46, 0.4) 75%, rgba(133, 102, 71, 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20% / .4) 25%, hsl(30deg 30% 40% / .8) 75%)`, `color-mix(in hsl, rgba(46, 56, 46, 0.4) 25%, rgba(133, 102, 71, 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20% / .4) 30%, hsl(30deg 30% 40% / .8) 90%)`, `color-mix(in hsl, rgba(46, 56, 46, 0.4) 30%, rgba(133, 102, 71, 0.8) 90%)`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20% / .4) 12.5%, hsl(30deg 30% 40% / .8) 37.5%)`, `color-mix(in hsl, rgba(46, 56, 46, 0.4) 12.5%, rgba(133, 102, 71, 0.8) 37.5%)`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 10% 20% / .4) 0%, hsl(30deg 30% 40% / .8))`, `color-mix(in hsl, rgba(46, 56, 46, 0.4) 0%, rgba(133, 102, 71, 0.8))`);
+
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(40deg 50% 50%), hsl(60deg 50% 50%))`, `color-mix(in hsl, rgb(191, 149, 64), rgb(191, 191, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(60deg 50% 50%), hsl(40deg 50% 50%))`, `color-mix(in hsl, rgb(191, 191, 64), rgb(191, 149, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(50deg 50% 50%), hsl(330deg 50% 50%))`, `color-mix(in hsl, rgb(191, 170, 64), rgb(191, 64, 128))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(330deg 50% 50%), hsl(50deg 50% 50%))`, `color-mix(in hsl, rgb(191, 64, 128), rgb(191, 170, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(20deg 50% 50%), hsl(320deg 50% 50%))`, `color-mix(in hsl, rgb(191, 106, 64), rgb(191, 64, 149))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(320deg 50% 50%), hsl(20deg 50% 50%))`, `color-mix(in hsl, rgb(191, 64, 149), rgb(191, 106, 64))`);
+
+ fuzzy_test_valid_color(`color-mix(in hsl shorter hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%))`, `color-mix(in hsl, rgb(191, 149, 64), rgb(191, 191, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl shorter hue, hsl(60deg 50% 50%), hsl(40deg 50% 50%))`, `color-mix(in hsl, rgb(191, 191, 64), rgb(191, 149, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl shorter hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%))`, `color-mix(in hsl, rgb(191, 170, 64), rgb(191, 64, 128))`);
+ fuzzy_test_valid_color(`color-mix(in hsl shorter hue, hsl(330deg 50% 50%), hsl(50deg 50% 50%))`, `color-mix(in hsl, rgb(191, 64, 128), rgb(191, 170, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl shorter hue, hsl(20deg 50% 50%), hsl(320deg 50% 50%))`, `color-mix(in hsl, rgb(191, 106, 64), rgb(191, 64, 149))`);
+ fuzzy_test_valid_color(`color-mix(in hsl shorter hue, hsl(320deg 50% 50%), hsl(20deg 50% 50%))`, `color-mix(in hsl, rgb(191, 64, 149), rgb(191, 106, 64))`);
+
+ fuzzy_test_valid_color(`color-mix(in hsl longer hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%))`, `color-mix(in hsl longer hue, rgb(191, 149, 64), rgb(191, 191, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl longer hue, hsl(60deg 50% 50%), hsl(40deg 50% 50%))`, `color-mix(in hsl longer hue, rgb(191, 191, 64), rgb(191, 149, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl longer hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%))`, `color-mix(in hsl longer hue, rgb(191, 170, 64), rgb(191, 64, 128))`);
+ fuzzy_test_valid_color(`color-mix(in hsl longer hue, hsl(330deg 50% 50%), hsl(50deg 50% 50%))`, `color-mix(in hsl longer hue, rgb(191, 64, 128), rgb(191, 170, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl longer hue, hsl(20deg 50% 50%), hsl(320deg 50% 50%))`, `color-mix(in hsl longer hue, rgb(191, 106, 64), rgb(191, 64, 149))`);
+ fuzzy_test_valid_color(`color-mix(in hsl longer hue, hsl(320deg 50% 50%), hsl(20deg 50% 50%))`, `color-mix(in hsl longer hue, rgb(191, 64, 149), rgb(191, 106, 64))`);
+
+ fuzzy_test_valid_color(`color-mix(in hsl increasing hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%))`, `color-mix(in hsl increasing hue, rgb(191, 149, 64), rgb(191, 191, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl increasing hue, hsl(60deg 50% 50%), hsl(40deg 50% 50%))`, `color-mix(in hsl increasing hue, rgb(191, 191, 64), rgb(191, 149, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl increasing hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%))`, `color-mix(in hsl increasing hue, rgb(191, 170, 64), rgb(191, 64, 128))`);
+ fuzzy_test_valid_color(`color-mix(in hsl increasing hue, hsl(330deg 50% 50%), hsl(50deg 50% 50%))`, `color-mix(in hsl increasing hue, rgb(191, 64, 128), rgb(191, 170, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl increasing hue, hsl(20deg 50% 50%), hsl(320deg 50% 50%))`, `color-mix(in hsl increasing hue, rgb(191, 106, 64), rgb(191, 64, 149))`);
+ fuzzy_test_valid_color(`color-mix(in hsl increasing hue, hsl(320deg 50% 50%), hsl(20deg 50% 50%))`, `color-mix(in hsl increasing hue, rgb(191, 64, 149), rgb(191, 106, 64))`);
+
+ fuzzy_test_valid_color(`color-mix(in hsl decreasing hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%))`, `color-mix(in hsl decreasing hue, rgb(191, 149, 64), rgb(191, 191, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl decreasing hue, hsl(60deg 50% 50%), hsl(40deg 50% 50%))`, `color-mix(in hsl decreasing hue, rgb(191, 191, 64), rgb(191, 149, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl decreasing hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%))`, `color-mix(in hsl decreasing hue, rgb(191, 170, 64), rgb(191, 64, 128))`);
+ fuzzy_test_valid_color(`color-mix(in hsl decreasing hue, hsl(330deg 50% 50%), hsl(50deg 50% 50%))`, `color-mix(in hsl decreasing hue, rgb(191, 64, 128), rgb(191, 170, 64))`);
+ fuzzy_test_valid_color(`color-mix(in hsl decreasing hue, hsl(20deg 50% 50%), hsl(320deg 50% 50%))`, `color-mix(in hsl decreasing hue, rgb(191, 106, 64), rgb(191, 64, 149))`);
+ fuzzy_test_valid_color(`color-mix(in hsl decreasing hue, hsl(320deg 50% 50%), hsl(20deg 50% 50%))`, `color-mix(in hsl decreasing hue, rgb(191, 64, 149), rgb(191, 106, 64))`);
+
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(none none none), hsl(none none none))`, `color-mix(in hsl, rgb(0, 0, 0), rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(none none none), hsl(30deg 40% 80%))`, `color-mix(in hsl, rgb(0, 0, 0), rgb(224, 204, 184))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 20% 40%), hsl(none none none))`, `color-mix(in hsl, rgb(82, 122, 82), rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 20% none), hsl(30deg 40% 60%))`, `color-mix(in hsl, rgb(0, 0, 0), rgb(194, 153, 112))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 20% 40%), hsl(30deg 20% none))`, `color-mix(in hsl, rgb(82, 122, 82), rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(none 20% 40%), hsl(30deg none 80%))`, `color-mix(in hsl, rgb(122, 82, 82), rgb(204, 204, 204))`);
+
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40%))`, `color-mix(in hsl, rgba(61, 143, 61, 0), rgb(143, 61, 61))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40% / 0.5))`, `color-mix(in hsl, rgba(61, 143, 61, 0), rgba(143, 61, 61, 0.5))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40% / none))`, `color-mix(in hsl, rgba(61, 143, 61, 0), rgba(143, 61, 61, 0))`);
+
+ fuzzy_test_valid_color(`color-mix(in hsl, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, color(display-p3 0 1 0) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, lab(100 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, lab(100 104.3 -50.9) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, lab(0 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, lab(0 104.3 -50.9) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, lch(100 116 334) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, lch(100 116 334) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, lch(0 116 334) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, lch(0 116 334) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, oklab(100 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, oklab(1 0.365 -0.16) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, oklab(0 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, oklab(0 0.365 -0.16) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, oklch(100 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, oklch(1 0.399 336.3) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, oklab(1 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, oklab(1 0.365 -0.16) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, oklch(1 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, oklch(1 0.399 336.3) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hsl, oklch(0 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hsl, oklch(0 0.399 336.3) 100%, rgb(0, 0, 0))`);
+
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20%), hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 204, 26), rgb(153, 115, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, 50% hwb(120deg 10% 20%), hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 204, 26), rgb(153, 115, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20%), 50% hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 204, 26), rgb(153, 115, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20%) 25%, hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 204, 26) 25%, rgb(153, 115, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, 25% hwb(120deg 10% 20%), hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 204, 26) 25%, rgb(153, 115, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20%), 25% hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 204, 26) 75%, rgb(153, 115, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20%), hwb(30deg 30% 40%) 25%)`, `color-mix(in hwb, rgb(26, 204, 26) 75%, rgb(153, 115, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20%) 25%, hwb(30deg 30% 40%) 75%)`, `color-mix(in hwb, rgb(26, 204, 26) 25%, rgb(153, 115, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20%) 30%, hwb(30deg 30% 40%) 90%)`, `color-mix(in hwb, rgb(26, 204, 26) 30%, rgb(153, 115, 77) 90%)`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20%) 12.5%, hwb(30deg 30% 40%) 37.5%)`, `color-mix(in hwb, rgb(26, 204, 26) 12.5%, rgb(153, 115, 77) 37.5%)`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20%) 0%, hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 204, 26) 0%, rgb(153, 115, 77))`);
+
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20% / .4), hwb(30deg 30% 40% / .8))`, `color-mix(in hwb, rgba(26, 204, 26, 0.4), rgba(153, 115, 77, 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20% / .4) 25%, hwb(30deg 30% 40% / .8))`, `color-mix(in hwb, rgba(26, 204, 26, 0.4) 25%, rgba(153, 115, 77, 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, 25% hwb(120deg 10% 20% / .4), hwb(30deg 30% 40% / .8))`, `color-mix(in hwb, rgba(26, 204, 26, 0.4) 25%, rgba(153, 115, 77, 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20%), 25% hwb(30deg 30% 40% / .8))`, `color-mix(in hwb, rgb(26, 204, 26) 75%, rgba(153, 115, 77, 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20% / .4), hwb(30deg 30% 40% / .8) 25%)`, `color-mix(in hwb, rgba(26, 204, 26, 0.4) 75%, rgba(153, 115, 77, 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20% / .4) 25%, hwb(30deg 30% 40% / .8) 75%)`, `color-mix(in hwb, rgba(26, 204, 26, 0.4) 25%, rgba(153, 115, 77, 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20% / .4) 30%, hwb(30deg 30% 40% / .8) 90%)`, `color-mix(in hwb, rgba(26, 204, 26, 0.4) 30%, rgba(153, 115, 77, 0.8) 90%)`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20% / .4) 12.5%, hwb(30deg 30% 40% / .8) 37.5%)`, `color-mix(in hwb, rgba(26, 204, 26, 0.4) 12.5%, rgba(153, 115, 77, 0.8) 37.5%)`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20% / .4) 0%, hwb(30deg 30% 40% / .8))`, `color-mix(in hwb, rgba(26, 204, 26, 0.4) 0%, rgba(153, 115, 77, 0.8))`);
+
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(40deg 30% 40%), hwb(60deg 30% 40%))`, `color-mix(in hwb, rgb(153, 128, 77), rgb(153, 153, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(60deg 30% 40%), hwb(40deg 30% 40%))`, `color-mix(in hwb, rgb(153, 153, 77), rgb(153, 128, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(50deg 30% 40%), hwb(330deg 30% 40%))`, `color-mix(in hwb, rgb(153, 140, 77), rgb(153, 77, 115))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(330deg 30% 40%), hwb(50deg 30% 40%))`, `color-mix(in hwb, rgb(153, 77, 115), rgb(153, 140, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(20deg 30% 40%), hwb(320deg 30% 40%))`, `color-mix(in hwb, rgb(153, 102, 77), rgb(153, 77, 128))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(320deg 30% 40%), hwb(20deg 30% 40%))`, `color-mix(in hwb, rgb(153, 77, 128), rgb(153, 102, 77))`);
+
+ fuzzy_test_valid_color(`color-mix(in hwb shorter hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%))`, `color-mix(in hwb, rgb(153, 128, 77), rgb(153, 153, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb shorter hue, hwb(60deg 30% 40%), hwb(40deg 30% 40%))`, `color-mix(in hwb, rgb(153, 153, 77), rgb(153, 128, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb shorter hue, hwb(50deg 30% 40%), hwb(330deg 30% 40%))`, `color-mix(in hwb, rgb(153, 140, 77), rgb(153, 77, 115))`);
+ fuzzy_test_valid_color(`color-mix(in hwb shorter hue, hwb(330deg 30% 40%), hwb(50deg 30% 40%))`, `color-mix(in hwb, rgb(153, 77, 115), rgb(153, 140, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb shorter hue, hwb(20deg 30% 40%), hwb(320deg 30% 40%))`, `color-mix(in hwb, rgb(153, 102, 77), rgb(153, 77, 128))`);
+ fuzzy_test_valid_color(`color-mix(in hwb shorter hue, hwb(320deg 30% 40%), hwb(20deg 30% 40%))`, `color-mix(in hwb, rgb(153, 77, 128), rgb(153, 102, 77))`);
+
+ fuzzy_test_valid_color(`color-mix(in hwb longer hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%))`, `color-mix(in hwb longer hue, rgb(153, 128, 77), rgb(153, 153, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb longer hue, hwb(60deg 30% 40%), hwb(40deg 30% 40%))`, `color-mix(in hwb longer hue, rgb(153, 153, 77), rgb(153, 128, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb longer hue, hwb(50deg 30% 40%), hwb(330deg 30% 40%))`, `color-mix(in hwb longer hue, rgb(153, 140, 77), rgb(153, 77, 115))`);
+ fuzzy_test_valid_color(`color-mix(in hwb longer hue, hwb(330deg 30% 40%), hwb(50deg 30% 40%))`, `color-mix(in hwb longer hue, rgb(153, 77, 115), rgb(153, 140, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb longer hue, hwb(20deg 30% 40%), hwb(320deg 30% 40%))`, `color-mix(in hwb longer hue, rgb(153, 102, 77), rgb(153, 77, 128))`);
+ fuzzy_test_valid_color(`color-mix(in hwb longer hue, hwb(320deg 30% 40%), hwb(20deg 30% 40%))`, `color-mix(in hwb longer hue, rgb(153, 77, 128), rgb(153, 102, 77))`);
+
+ fuzzy_test_valid_color(`color-mix(in hwb increasing hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%))`, `color-mix(in hwb increasing hue, rgb(153, 128, 77), rgb(153, 153, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb increasing hue, hwb(60deg 30% 40%), hwb(40deg 30% 40%))`, `color-mix(in hwb increasing hue, rgb(153, 153, 77), rgb(153, 128, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb increasing hue, hwb(50deg 30% 40%), hwb(330deg 30% 40%))`, `color-mix(in hwb increasing hue, rgb(153, 140, 77), rgb(153, 77, 115))`);
+ fuzzy_test_valid_color(`color-mix(in hwb increasing hue, hwb(330deg 30% 40%), hwb(50deg 30% 40%))`, `color-mix(in hwb increasing hue, rgb(153, 77, 115), rgb(153, 140, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb increasing hue, hwb(20deg 30% 40%), hwb(320deg 30% 40%))`, `color-mix(in hwb increasing hue, rgb(153, 102, 77), rgb(153, 77, 128))`);
+ fuzzy_test_valid_color(`color-mix(in hwb increasing hue, hwb(320deg 30% 40%), hwb(20deg 30% 40%))`, `color-mix(in hwb increasing hue, rgb(153, 77, 128), rgb(153, 102, 77))`);
+
+ fuzzy_test_valid_color(`color-mix(in hwb decreasing hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%))`, `color-mix(in hwb decreasing hue, rgb(153, 128, 77), rgb(153, 153, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb decreasing hue, hwb(60deg 30% 40%), hwb(40deg 30% 40%))`, `color-mix(in hwb decreasing hue, rgb(153, 153, 77), rgb(153, 128, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb decreasing hue, hwb(50deg 30% 40%), hwb(330deg 30% 40%))`, `color-mix(in hwb decreasing hue, rgb(153, 140, 77), rgb(153, 77, 115))`);
+ fuzzy_test_valid_color(`color-mix(in hwb decreasing hue, hwb(330deg 30% 40%), hwb(50deg 30% 40%))`, `color-mix(in hwb decreasing hue, rgb(153, 77, 115), rgb(153, 140, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb decreasing hue, hwb(20deg 30% 40%), hwb(320deg 30% 40%))`, `color-mix(in hwb decreasing hue, rgb(153, 102, 77), rgb(153, 77, 128))`);
+ fuzzy_test_valid_color(`color-mix(in hwb decreasing hue, hwb(320deg 30% 40%), hwb(20deg 30% 40%))`, `color-mix(in hwb decreasing hue, rgb(153, 77, 128), rgb(153, 102, 77))`);
+
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(none none none), hwb(none none none))`, `color-mix(in hwb, rgb(255, 0, 0), rgb(255, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(none none none), hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(255, 0, 0), rgb(153, 115, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20%), hwb(none none none))`, `color-mix(in hwb, rgb(26, 204, 26), rgb(255, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% none), hwb(30deg 30% 40%))`, `color-mix(in hwb, rgb(26, 255, 26), rgb(153, 115, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20%), hwb(30deg 30% none))`, `color-mix(in hwb, rgb(26, 204, 26), rgb(255, 166, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(none 10% 20%), hwb(30deg none 40%))`, `color-mix(in hwb, rgb(204, 26, 26), rgb(153, 77, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40%))`, `color-mix(in hwb, rgba(26, 204, 26, 0), rgb(153, 115, 77))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40% / 0.5))`, `color-mix(in hwb, rgba(26, 204, 26, 0), rgba(153, 115, 77, 0.5))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40% / none))`, `color-mix(in hwb, rgba(26, 204, 26, 0), rgba(153, 115, 77, 0))`);
+
+ fuzzy_test_valid_color(`color-mix(in hwb, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, color(display-p3 0 1 0) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, lab(100 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, lab(100 104.3 -50.9) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, lab(0 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, lab(0 104.3 -50.9) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, lch(100 116 334) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, lch(100 116 334) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, lch(0 116 334) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, lch(0 116 334) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, oklab(100 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, oklab(1 0.365 -0.16) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, oklab(0 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, oklab(0 0.365 -0.16) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, oklch(100 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, oklch(1 0.399 336.3) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, oklch(0 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, oklch(0 0.399 336.3) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, oklab(1 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, oklab(1 0.365 -0.16) 100%, rgb(0, 0, 0))`);
+ fuzzy_test_valid_color(`color-mix(in hwb, oklch(1 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `color-mix(in hwb, oklch(1 0.399 336.3) 100%, rgb(0, 0, 0))`);
// lch()
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg), lch(50 60 70deg))`, `color-mix(in lch, lch(10 20 30), lch(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg) 25%, lch(50 60 70deg))`, `color-mix(in lch, lch(10 20 30) 25%, lch(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lch, 25% lch(10 20 30deg), lch(50 60 70deg))`, `color-mix(in lch, lch(10 20 30) 25%, lch(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg), 25% lch(50 60 70deg))`, `color-mix(in lch, lch(10 20 30) 75%, lch(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg), lch(50 60 70deg) 25%)`, `color-mix(in lch, lch(10 20 30) 75%, lch(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg) 25%, lch(50 60 70deg) 75%)`, `color-mix(in lch, lch(10 20 30) 25%, lch(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg) 30%, lch(50 60 70deg) 90%)`, `color-mix(in lch, lch(10 20 30) 30%, lch(50 60 70) 90%)`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg) 12.5%, lch(50 60 70deg) 37.5%)`, `color-mix(in lch, lch(10 20 30) 12.5%, lch(50 60 70) 37.5%)`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg) 0%, lch(50 60 70deg))`, `color-mix(in lch, lch(10 20 30) 0%, lch(50 60 70))`);
-
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg / .4), lch(50 60 70deg / .8))`, `color-mix(in lch, lch(10 20 30 / 0.4), lch(50 60 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg / .4) 25%, lch(50 60 70deg / .8))`, `color-mix(in lch, lch(10 20 30 / 0.4) 25%, lch(50 60 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in lch, 25% lch(10 20 30deg / .4), lch(50 60 70deg / .8))`, `color-mix(in lch, lch(10 20 30 / 0.4) 25%, lch(50 60 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg / .4), 25% lch(50 60 70deg / .8))`, `color-mix(in lch, lch(10 20 30 / 0.4) 75%, lch(50 60 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg / .4), lch(50 60 70deg / .8) 25%)`, `color-mix(in lch, lch(10 20 30 / 0.4) 75%, lch(50 60 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg / .4) 25%, lch(50 60 70deg / .8) 75%)`, `color-mix(in lch, lch(10 20 30 / 0.4) 25%, lch(50 60 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg / .4) 30%, lch(50 60 70deg / .8) 90%)`, `color-mix(in lch, lch(10 20 30 / 0.4) 30%, lch(50 60 70 / 0.8) 90%)`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg / .4) 12.5%, lch(50 60 70deg / .8) 37.5%)`, `color-mix(in lch, lch(10 20 30 / 0.4) 12.5%, lch(50 60 70 / 0.8) 37.5%)`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg / .4) 0%, lch(50 60 70deg / .8))`, `color-mix(in lch, lch(10 20 30 / 0.4) 0%, lch(50 60 70 / 0.8))`);
-
- test_valid_value(`color`, `color-mix(in lch, lch(100 0 40deg), lch(100 0 60deg))`, `color-mix(in lch, lch(100 0 40), lch(100 0 60))`);
- test_valid_value(`color`, `color-mix(in lch, lch(100 0 60deg), lch(100 0 40deg))`, `color-mix(in lch, lch(100 0 60), lch(100 0 40))`);
- test_valid_value(`color`, `color-mix(in lch, lch(100 0 50deg), lch(100 0 330deg))`, `color-mix(in lch, lch(100 0 50), lch(100 0 330))`);
- test_valid_value(`color`, `color-mix(in lch, lch(100 0 330deg), lch(100 0 50deg))`, `color-mix(in lch, lch(100 0 330), lch(100 0 50))`);
- test_valid_value(`color`, `color-mix(in lch, lch(100 0 20deg), lch(100 0 320deg))`, `color-mix(in lch, lch(100 0 20), lch(100 0 320))`);
- test_valid_value(`color`, `color-mix(in lch, lch(100 0 320deg), lch(100 0 20deg))`, `color-mix(in lch, lch(100 0 320), lch(100 0 20))`);
-
- test_valid_value(`color`, `color-mix(in lch shorter hue, lch(100 0 40deg), lch(100 0 60deg))`, `color-mix(in lch, lch(100 0 40), lch(100 0 60))`);
- test_valid_value(`color`, `color-mix(in lch shorter hue, lch(100 0 60deg), lch(100 0 40deg))`, `color-mix(in lch, lch(100 0 60), lch(100 0 40))`);
- test_valid_value(`color`, `color-mix(in lch shorter hue, lch(100 0 50deg), lch(100 0 330deg))`, `color-mix(in lch, lch(100 0 50), lch(100 0 330))`);
- test_valid_value(`color`, `color-mix(in lch shorter hue, lch(100 0 330deg), lch(100 0 50deg))`, `color-mix(in lch, lch(100 0 330), lch(100 0 50))`);
- test_valid_value(`color`, `color-mix(in lch shorter hue, lch(100 0 20deg), lch(100 0 320deg))`, `color-mix(in lch, lch(100 0 20), lch(100 0 320))`);
- test_valid_value(`color`, `color-mix(in lch shorter hue, lch(100 0 320deg), lch(100 0 20deg))`, `color-mix(in lch, lch(100 0 320), lch(100 0 20))`);
-
- test_valid_value(`color`, `color-mix(in lch longer hue, lch(100 0 40deg), lch(100 0 60deg))`, `color-mix(in lch longer hue, lch(100 0 40), lch(100 0 60))`);
- test_valid_value(`color`, `color-mix(in lch longer hue, lch(100 0 60deg), lch(100 0 40deg))`, `color-mix(in lch longer hue, lch(100 0 60), lch(100 0 40))`);
- test_valid_value(`color`, `color-mix(in lch longer hue, lch(100 0 50deg), lch(100 0 330deg))`, `color-mix(in lch longer hue, lch(100 0 50), lch(100 0 330))`);
- test_valid_value(`color`, `color-mix(in lch longer hue, lch(100 0 330deg), lch(100 0 50deg))`, `color-mix(in lch longer hue, lch(100 0 330), lch(100 0 50))`);
- test_valid_value(`color`, `color-mix(in lch longer hue, lch(100 0 20deg), lch(100 0 320deg))`, `color-mix(in lch longer hue, lch(100 0 20), lch(100 0 320))`);
- test_valid_value(`color`, `color-mix(in lch longer hue, lch(100 0 320deg), lch(100 0 20deg))`, `color-mix(in lch longer hue, lch(100 0 320), lch(100 0 20))`);
-
- test_valid_value(`color`, `color-mix(in lch increasing hue, lch(100 0 40deg), lch(100 0 60deg))`, `color-mix(in lch increasing hue, lch(100 0 40), lch(100 0 60))`);
- test_valid_value(`color`, `color-mix(in lch increasing hue, lch(100 0 60deg), lch(100 0 40deg))`, `color-mix(in lch increasing hue, lch(100 0 60), lch(100 0 40))`);
- test_valid_value(`color`, `color-mix(in lch increasing hue, lch(100 0 50deg), lch(100 0 330deg))`, `color-mix(in lch increasing hue, lch(100 0 50), lch(100 0 330))`);
- test_valid_value(`color`, `color-mix(in lch increasing hue, lch(100 0 330deg), lch(100 0 50deg))`, `color-mix(in lch increasing hue, lch(100 0 330), lch(100 0 50))`);
- test_valid_value(`color`, `color-mix(in lch increasing hue, lch(100 0 20deg), lch(100 0 320deg))`, `color-mix(in lch increasing hue, lch(100 0 20), lch(100 0 320))`);
- test_valid_value(`color`, `color-mix(in lch increasing hue, lch(100 0 320deg), lch(100 0 20deg))`, `color-mix(in lch increasing hue, lch(100 0 320), lch(100 0 20))`);
-
- test_valid_value(`color`, `color-mix(in lch decreasing hue, lch(100 0 40deg), lch(100 0 60deg))`, `color-mix(in lch decreasing hue, lch(100 0 40), lch(100 0 60))`);
- test_valid_value(`color`, `color-mix(in lch decreasing hue, lch(100 0 60deg), lch(100 0 40deg))`, `color-mix(in lch decreasing hue, lch(100 0 60), lch(100 0 40))`);
- test_valid_value(`color`, `color-mix(in lch decreasing hue, lch(100 0 50deg), lch(100 0 330deg))`, `color-mix(in lch decreasing hue, lch(100 0 50), lch(100 0 330))`);
- test_valid_value(`color`, `color-mix(in lch decreasing hue, lch(100 0 330deg), lch(100 0 50deg))`, `color-mix(in lch decreasing hue, lch(100 0 330), lch(100 0 50))`);
- test_valid_value(`color`, `color-mix(in lch decreasing hue, lch(100 0 20deg), lch(100 0 320deg))`, `color-mix(in lch decreasing hue, lch(100 0 20), lch(100 0 320))`);
- test_valid_value(`color`, `color-mix(in lch decreasing hue, lch(100 0 320deg), lch(100 0 20deg))`, `color-mix(in lch decreasing hue, lch(100 0 320), lch(100 0 20))`);
-
- test_valid_value(`color`, `color-mix(in lch, lch(none none none), lch(none none none))`, `color-mix(in lch, lch(none none none), lch(none none none))`);
- test_valid_value(`color`, `color-mix(in lch, lch(none none none), lch(50 60 70deg))`, `color-mix(in lch, lch(none none none), lch(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg), lch(none none none))`, `color-mix(in lch, lch(10 20 30), lch(none none none))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 none), lch(50 60 70deg))`, `color-mix(in lch, lch(10 20 none), lch(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg), lch(50 60 none))`, `color-mix(in lch, lch(10 20 30), lch(50 60 none))`);
- test_valid_value(`color`, `color-mix(in lch, lch(none 20 30deg), lch(50 none 70deg))`, `color-mix(in lch, lch(none 20 30), lch(50 none 70))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg / none), lch(50 60 70deg))`, `color-mix(in lch, lch(10 20 30 / none), lch(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg / none), lch(50 60 70deg / 0.5))`, `color-mix(in lch, lch(10 20 30 / none), lch(50 60 70 / 0.5))`);
- test_valid_value(`color`, `color-mix(in lch, lch(10 20 30deg / none), lch(50 60 70deg / none))`, `color-mix(in lch, lch(10 20 30 / none), lch(50 60 70 / none))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg), lch(50 60 70deg))`, `color-mix(in lch, lch(10 20 30), lch(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg) 25%, lch(50 60 70deg))`, `color-mix(in lch, lch(10 20 30) 25%, lch(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lch, 25% lch(10 20 30deg), lch(50 60 70deg))`, `color-mix(in lch, lch(10 20 30) 25%, lch(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg), 25% lch(50 60 70deg))`, `color-mix(in lch, lch(10 20 30) 75%, lch(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg), lch(50 60 70deg) 25%)`, `color-mix(in lch, lch(10 20 30) 75%, lch(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg) 25%, lch(50 60 70deg) 75%)`, `color-mix(in lch, lch(10 20 30) 25%, lch(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg) 30%, lch(50 60 70deg) 90%)`, `color-mix(in lch, lch(10 20 30) 30%, lch(50 60 70) 90%)`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg) 12.5%, lch(50 60 70deg) 37.5%)`, `color-mix(in lch, lch(10 20 30) 12.5%, lch(50 60 70) 37.5%)`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg) 0%, lch(50 60 70deg))`, `color-mix(in lch, lch(10 20 30) 0%, lch(50 60 70))`);
+
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg / .4), lch(50 60 70deg / .8))`, `color-mix(in lch, lch(10 20 30 / 0.4), lch(50 60 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg / .4) 25%, lch(50 60 70deg / .8))`, `color-mix(in lch, lch(10 20 30 / 0.4) 25%, lch(50 60 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in lch, 25% lch(10 20 30deg / .4), lch(50 60 70deg / .8))`, `color-mix(in lch, lch(10 20 30 / 0.4) 25%, lch(50 60 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg / .4), 25% lch(50 60 70deg / .8))`, `color-mix(in lch, lch(10 20 30 / 0.4) 75%, lch(50 60 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg / .4), lch(50 60 70deg / .8) 25%)`, `color-mix(in lch, lch(10 20 30 / 0.4) 75%, lch(50 60 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg / .4) 25%, lch(50 60 70deg / .8) 75%)`, `color-mix(in lch, lch(10 20 30 / 0.4) 25%, lch(50 60 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg / .4) 30%, lch(50 60 70deg / .8) 90%)`, `color-mix(in lch, lch(10 20 30 / 0.4) 30%, lch(50 60 70 / 0.8) 90%)`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg / .4) 12.5%, lch(50 60 70deg / .8) 37.5%)`, `color-mix(in lch, lch(10 20 30 / 0.4) 12.5%, lch(50 60 70 / 0.8) 37.5%)`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg / .4) 0%, lch(50 60 70deg / .8))`, `color-mix(in lch, lch(10 20 30 / 0.4) 0%, lch(50 60 70 / 0.8))`);
+
+ fuzzy_test_valid_color(`color-mix(in lch, lch(100 0 40deg), lch(100 0 60deg))`, `color-mix(in lch, lch(100 0 40), lch(100 0 60))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(100 0 60deg), lch(100 0 40deg))`, `color-mix(in lch, lch(100 0 60), lch(100 0 40))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(100 0 50deg), lch(100 0 330deg))`, `color-mix(in lch, lch(100 0 50), lch(100 0 330))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(100 0 330deg), lch(100 0 50deg))`, `color-mix(in lch, lch(100 0 330), lch(100 0 50))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(100 0 20deg), lch(100 0 320deg))`, `color-mix(in lch, lch(100 0 20), lch(100 0 320))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(100 0 320deg), lch(100 0 20deg))`, `color-mix(in lch, lch(100 0 320), lch(100 0 20))`);
+
+ fuzzy_test_valid_color(`color-mix(in lch shorter hue, lch(100 0 40deg), lch(100 0 60deg))`, `color-mix(in lch, lch(100 0 40), lch(100 0 60))`);
+ fuzzy_test_valid_color(`color-mix(in lch shorter hue, lch(100 0 60deg), lch(100 0 40deg))`, `color-mix(in lch, lch(100 0 60), lch(100 0 40))`);
+ fuzzy_test_valid_color(`color-mix(in lch shorter hue, lch(100 0 50deg), lch(100 0 330deg))`, `color-mix(in lch, lch(100 0 50), lch(100 0 330))`);
+ fuzzy_test_valid_color(`color-mix(in lch shorter hue, lch(100 0 330deg), lch(100 0 50deg))`, `color-mix(in lch, lch(100 0 330), lch(100 0 50))`);
+ fuzzy_test_valid_color(`color-mix(in lch shorter hue, lch(100 0 20deg), lch(100 0 320deg))`, `color-mix(in lch, lch(100 0 20), lch(100 0 320))`);
+ fuzzy_test_valid_color(`color-mix(in lch shorter hue, lch(100 0 320deg), lch(100 0 20deg))`, `color-mix(in lch, lch(100 0 320), lch(100 0 20))`);
+
+ fuzzy_test_valid_color(`color-mix(in lch longer hue, lch(100 0 40deg), lch(100 0 60deg))`, `color-mix(in lch longer hue, lch(100 0 40), lch(100 0 60))`);
+ fuzzy_test_valid_color(`color-mix(in lch longer hue, lch(100 0 60deg), lch(100 0 40deg))`, `color-mix(in lch longer hue, lch(100 0 60), lch(100 0 40))`);
+ fuzzy_test_valid_color(`color-mix(in lch longer hue, lch(100 0 50deg), lch(100 0 330deg))`, `color-mix(in lch longer hue, lch(100 0 50), lch(100 0 330))`);
+ fuzzy_test_valid_color(`color-mix(in lch longer hue, lch(100 0 330deg), lch(100 0 50deg))`, `color-mix(in lch longer hue, lch(100 0 330), lch(100 0 50))`);
+ fuzzy_test_valid_color(`color-mix(in lch longer hue, lch(100 0 20deg), lch(100 0 320deg))`, `color-mix(in lch longer hue, lch(100 0 20), lch(100 0 320))`);
+ fuzzy_test_valid_color(`color-mix(in lch longer hue, lch(100 0 320deg), lch(100 0 20deg))`, `color-mix(in lch longer hue, lch(100 0 320), lch(100 0 20))`);
+
+ fuzzy_test_valid_color(`color-mix(in lch increasing hue, lch(100 0 40deg), lch(100 0 60deg))`, `color-mix(in lch increasing hue, lch(100 0 40), lch(100 0 60))`);
+ fuzzy_test_valid_color(`color-mix(in lch increasing hue, lch(100 0 60deg), lch(100 0 40deg))`, `color-mix(in lch increasing hue, lch(100 0 60), lch(100 0 40))`);
+ fuzzy_test_valid_color(`color-mix(in lch increasing hue, lch(100 0 50deg), lch(100 0 330deg))`, `color-mix(in lch increasing hue, lch(100 0 50), lch(100 0 330))`);
+ fuzzy_test_valid_color(`color-mix(in lch increasing hue, lch(100 0 330deg), lch(100 0 50deg))`, `color-mix(in lch increasing hue, lch(100 0 330), lch(100 0 50))`);
+ fuzzy_test_valid_color(`color-mix(in lch increasing hue, lch(100 0 20deg), lch(100 0 320deg))`, `color-mix(in lch increasing hue, lch(100 0 20), lch(100 0 320))`);
+ fuzzy_test_valid_color(`color-mix(in lch increasing hue, lch(100 0 320deg), lch(100 0 20deg))`, `color-mix(in lch increasing hue, lch(100 0 320), lch(100 0 20))`);
+
+ fuzzy_test_valid_color(`color-mix(in lch decreasing hue, lch(100 0 40deg), lch(100 0 60deg))`, `color-mix(in lch decreasing hue, lch(100 0 40), lch(100 0 60))`);
+ fuzzy_test_valid_color(`color-mix(in lch decreasing hue, lch(100 0 60deg), lch(100 0 40deg))`, `color-mix(in lch decreasing hue, lch(100 0 60), lch(100 0 40))`);
+ fuzzy_test_valid_color(`color-mix(in lch decreasing hue, lch(100 0 50deg), lch(100 0 330deg))`, `color-mix(in lch decreasing hue, lch(100 0 50), lch(100 0 330))`);
+ fuzzy_test_valid_color(`color-mix(in lch decreasing hue, lch(100 0 330deg), lch(100 0 50deg))`, `color-mix(in lch decreasing hue, lch(100 0 330), lch(100 0 50))`);
+ fuzzy_test_valid_color(`color-mix(in lch decreasing hue, lch(100 0 20deg), lch(100 0 320deg))`, `color-mix(in lch decreasing hue, lch(100 0 20), lch(100 0 320))`);
+ fuzzy_test_valid_color(`color-mix(in lch decreasing hue, lch(100 0 320deg), lch(100 0 20deg))`, `color-mix(in lch decreasing hue, lch(100 0 320), lch(100 0 20))`);
+
+ fuzzy_test_valid_color(`color-mix(in lch, lch(none none none), lch(none none none))`, `color-mix(in lch, lch(none none none), lch(none none none))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(none none none), lch(50 60 70deg))`, `color-mix(in lch, lch(none none none), lch(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg), lch(none none none))`, `color-mix(in lch, lch(10 20 30), lch(none none none))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 none), lch(50 60 70deg))`, `color-mix(in lch, lch(10 20 none), lch(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg), lch(50 60 none))`, `color-mix(in lch, lch(10 20 30), lch(50 60 none))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(none 20 30deg), lch(50 none 70deg))`, `color-mix(in lch, lch(none 20 30), lch(50 none 70))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg / none), lch(50 60 70deg))`, `color-mix(in lch, lch(10 20 30 / none), lch(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg / none), lch(50 60 70deg / 0.5))`, `color-mix(in lch, lch(10 20 30 / none), lch(50 60 70 / 0.5))`);
+ fuzzy_test_valid_color(`color-mix(in lch, lch(10 20 30deg / none), lch(50 60 70deg / none))`, `color-mix(in lch, lch(10 20 30 / none), lch(50 60 70 / none))`);
// oklch()
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg), oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(0.1 0.2 30), oklch(0.5 0.6 70))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg) 25%, oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(0.1 0.2 30) 25%, oklch(0.5 0.6 70))`);
- test_valid_value(`color`, `color-mix(in oklch, 25% oklch(0.1 0.2 30deg), oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(0.1 0.2 30) 25%, oklch(0.5 0.6 70))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg), 25% oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(0.1 0.2 30) 75%, oklch(0.5 0.6 70))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg), oklch(0.5 0.6 70deg) 25%)`, `color-mix(in oklch, oklch(0.1 0.2 30) 75%, oklch(0.5 0.6 70))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg) 25%, oklch(0.5 0.6 70deg) 75%)`, `color-mix(in oklch, oklch(0.1 0.2 30) 25%, oklch(0.5 0.6 70))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg) 30%, oklch(0.5 0.6 70deg) 90%)`, `color-mix(in oklch, oklch(0.1 0.2 30) 30%, oklch(0.5 0.6 70) 90%)`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg) 12.5%, oklch(0.5 0.6 70deg) 37.5%)`, `color-mix(in oklch, oklch(0.1 0.2 30) 12.5%, oklch(0.5 0.6 70) 37.5%)`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg) 0%, oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(0.1 0.2 30) 0%, oklch(0.5 0.6 70))`);
-
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg / .4), oklch(0.5 0.6 70deg / .8))`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4), oklch(0.5 0.6 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg / .4) 25%, oklch(0.5 0.6 70deg / .8))`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 25%, oklch(0.5 0.6 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in oklch, 25% oklch(0.1 0.2 30deg / .4), oklch(0.5 0.6 70deg / .8))`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 25%, oklch(0.5 0.6 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg / .4), 25% oklch(0.5 0.6 70deg / .8))`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 75%, oklch(0.5 0.6 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg / .4), oklch(0.5 0.6 70deg / .8) 25%)`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 75%, oklch(0.5 0.6 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg / .4) 25%, oklch(0.5 0.6 70deg / .8) 75%)`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 25%, oklch(0.5 0.6 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg / .4) 30%, oklch(0.5 0.6 70deg / .8) 90%)`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 30%, oklch(0.5 0.6 70 / 0.8) 90%)`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg / .4) 12.5%, oklch(0.5 0.6 70deg / .8) 37.5%)`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 12.5%, oklch(0.5 0.6 70 / 0.8) 37.5%)`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg / .4) 0%, oklch(0.5 0.6 70deg / .8))`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 0%, oklch(0.5 0.6 70 / 0.8))`);
-
- test_valid_value(`color`, `color-mix(in oklch, oklch(1 0 40deg), oklch(1 0 60deg))`, `color-mix(in oklch, oklch(1 0 40), oklch(1 0 60))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(1 0 60deg), oklch(1 0 40deg))`, `color-mix(in oklch, oklch(1 0 60), oklch(1 0 40))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(1 0 50deg), oklch(1 0 330deg))`, `color-mix(in oklch, oklch(1 0 50), oklch(1 0 330))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(1 0 330deg), oklch(1 0 50deg))`, `color-mix(in oklch, oklch(1 0 330), oklch(1 0 50))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(1 0 20deg), oklch(1 0 320deg))`, `color-mix(in oklch, oklch(1 0 20), oklch(1 0 320))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(1 0 320deg), oklch(1 0 20deg))`, `color-mix(in oklch, oklch(1 0 320), oklch(1 0 20))`);
-
- test_valid_value(`color`, `color-mix(in oklch shorter hue, oklch(1 0 40deg), oklch(1 0 60deg))`, `color-mix(in oklch, oklch(1 0 40), oklch(1 0 60))`);
- test_valid_value(`color`, `color-mix(in oklch shorter hue, oklch(1 0 60deg), oklch(1 0 40deg))`, `color-mix(in oklch, oklch(1 0 60), oklch(1 0 40))`);
- test_valid_value(`color`, `color-mix(in oklch shorter hue, oklch(1 0 50deg), oklch(1 0 330deg))`, `color-mix(in oklch, oklch(1 0 50), oklch(1 0 330))`);
- test_valid_value(`color`, `color-mix(in oklch shorter hue, oklch(1 0 330deg), oklch(1 0 50deg))`, `color-mix(in oklch, oklch(1 0 330), oklch(1 0 50))`);
- test_valid_value(`color`, `color-mix(in oklch shorter hue, oklch(1 0 20deg), oklch(1 0 320deg))`, `color-mix(in oklch, oklch(1 0 20), oklch(1 0 320))`);
- test_valid_value(`color`, `color-mix(in oklch shorter hue, oklch(1 0 320deg), oklch(1 0 20deg))`, `color-mix(in oklch, oklch(1 0 320), oklch(1 0 20))`);
-
- test_valid_value(`color`, `color-mix(in oklch longer hue, oklch(1 0 40deg), oklch(1 0 60deg))`, `color-mix(in oklch longer hue, oklch(1 0 40), oklch(1 0 60))`);
- test_valid_value(`color`, `color-mix(in oklch longer hue, oklch(1 0 60deg), oklch(1 0 40deg))`, `color-mix(in oklch longer hue, oklch(1 0 60), oklch(1 0 40))`);
- test_valid_value(`color`, `color-mix(in oklch longer hue, oklch(1 0 50deg), oklch(1 0 330deg))`, `color-mix(in oklch longer hue, oklch(1 0 50), oklch(1 0 330))`);
- test_valid_value(`color`, `color-mix(in oklch longer hue, oklch(1 0 330deg), oklch(1 0 50deg))`, `color-mix(in oklch longer hue, oklch(1 0 330), oklch(1 0 50))`);
- test_valid_value(`color`, `color-mix(in oklch longer hue, oklch(1 0 20deg), oklch(1 0 320deg))`, `color-mix(in oklch longer hue, oklch(1 0 20), oklch(1 0 320))`);
- test_valid_value(`color`, `color-mix(in oklch longer hue, oklch(1 0 320deg), oklch(1 0 20deg))`, `color-mix(in oklch longer hue, oklch(1 0 320), oklch(1 0 20))`);
-
- test_valid_value(`color`, `color-mix(in oklch increasing hue, oklch(1 0 40deg), oklch(1 0 60deg))`, `color-mix(in oklch increasing hue, oklch(1 0 40), oklch(1 0 60))`);
- test_valid_value(`color`, `color-mix(in oklch increasing hue, oklch(1 0 60deg), oklch(1 0 40deg))`, `color-mix(in oklch increasing hue, oklch(1 0 60), oklch(1 0 40))`);
- test_valid_value(`color`, `color-mix(in oklch increasing hue, oklch(1 0 50deg), oklch(1 0 330deg))`, `color-mix(in oklch increasing hue, oklch(1 0 50), oklch(1 0 330))`);
- test_valid_value(`color`, `color-mix(in oklch increasing hue, oklch(1 0 330deg), oklch(1 0 50deg))`, `color-mix(in oklch increasing hue, oklch(1 0 330), oklch(1 0 50))`);
- test_valid_value(`color`, `color-mix(in oklch increasing hue, oklch(1 0 20deg), oklch(1 0 320deg))`, `color-mix(in oklch increasing hue, oklch(1 0 20), oklch(1 0 320))`);
- test_valid_value(`color`, `color-mix(in oklch increasing hue, oklch(1 0 320deg), oklch(1 0 20deg))`, `color-mix(in oklch increasing hue, oklch(1 0 320), oklch(1 0 20))`);
-
- test_valid_value(`color`, `color-mix(in oklch decreasing hue, oklch(1 0 40deg), oklch(1 0 60deg))`, `color-mix(in oklch decreasing hue, oklch(1 0 40), oklch(1 0 60))`);
- test_valid_value(`color`, `color-mix(in oklch decreasing hue, oklch(1 0 60deg), oklch(1 0 40deg))`, `color-mix(in oklch decreasing hue, oklch(1 0 60), oklch(1 0 40))`);
- test_valid_value(`color`, `color-mix(in oklch decreasing hue, oklch(1 0 50deg), oklch(1 0 330deg))`, `color-mix(in oklch decreasing hue, oklch(1 0 50), oklch(1 0 330))`);
- test_valid_value(`color`, `color-mix(in oklch decreasing hue, oklch(1 0 330deg), oklch(1 0 50deg))`, `color-mix(in oklch decreasing hue, oklch(1 0 330), oklch(1 0 50))`);
- test_valid_value(`color`, `color-mix(in oklch decreasing hue, oklch(1 0 20deg), oklch(1 0 320deg))`, `color-mix(in oklch decreasing hue, oklch(1 0 20), oklch(1 0 320))`);
- test_valid_value(`color`, `color-mix(in oklch decreasing hue, oklch(1 0 320deg), oklch(1 0 20deg))`, `color-mix(in oklch decreasing hue, oklch(1 0 320), oklch(1 0 20))`);
-
- test_valid_value(`color`, `color-mix(in oklch, oklch(none none none), oklch(none none none))`, `color-mix(in oklch, oklch(none none none), oklch(none none none))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(none none none), oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(none none none), oklch(0.5 0.6 70))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg), oklch(none none none))`, `color-mix(in oklch, oklch(0.1 0.2 30), oklch(none none none))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 none), oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(0.1 0.2 none), oklch(0.5 0.6 70))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg), oklch(0.5 0.6 none))`, `color-mix(in oklch, oklch(0.1 0.2 30), oklch(0.5 0.6 none))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(none 0.2 30deg), oklch(0.5 none 70deg))`, `color-mix(in oklch, oklch(none 0.2 30), oklch(0.5 none 70))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg / none), oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(0.1 0.2 30 / none), oklch(0.5 0.6 70))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg / none), oklch(0.5 0.6 70deg / 0.5))`, `color-mix(in oklch, oklch(0.1 0.2 30 / none), oklch(0.5 0.6 70 / 0.5))`);
- test_valid_value(`color`, `color-mix(in oklch, oklch(0.1 0.2 30deg / none), oklch(0.5 0.6 70deg / none))`, `color-mix(in oklch, oklch(0.1 0.2 30 / none), oklch(0.5 0.6 70 / none))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg), oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(0.1 0.2 30), oklch(0.5 0.6 70))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg) 25%, oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(0.1 0.2 30) 25%, oklch(0.5 0.6 70))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, 25% oklch(0.1 0.2 30deg), oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(0.1 0.2 30) 25%, oklch(0.5 0.6 70))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg), 25% oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(0.1 0.2 30) 75%, oklch(0.5 0.6 70))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg), oklch(0.5 0.6 70deg) 25%)`, `color-mix(in oklch, oklch(0.1 0.2 30) 75%, oklch(0.5 0.6 70))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg) 25%, oklch(0.5 0.6 70deg) 75%)`, `color-mix(in oklch, oklch(0.1 0.2 30) 25%, oklch(0.5 0.6 70))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg) 30%, oklch(0.5 0.6 70deg) 90%)`, `color-mix(in oklch, oklch(0.1 0.2 30) 30%, oklch(0.5 0.6 70) 90%)`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg) 12.5%, oklch(0.5 0.6 70deg) 37.5%)`, `color-mix(in oklch, oklch(0.1 0.2 30) 12.5%, oklch(0.5 0.6 70) 37.5%)`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg) 0%, oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(0.1 0.2 30) 0%, oklch(0.5 0.6 70))`);
+
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg / .4), oklch(0.5 0.6 70deg / .8))`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4), oklch(0.5 0.6 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg / .4) 25%, oklch(0.5 0.6 70deg / .8))`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 25%, oklch(0.5 0.6 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, 25% oklch(0.1 0.2 30deg / .4), oklch(0.5 0.6 70deg / .8))`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 25%, oklch(0.5 0.6 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg / .4), 25% oklch(0.5 0.6 70deg / .8))`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 75%, oklch(0.5 0.6 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg / .4), oklch(0.5 0.6 70deg / .8) 25%)`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 75%, oklch(0.5 0.6 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg / .4) 25%, oklch(0.5 0.6 70deg / .8) 75%)`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 25%, oklch(0.5 0.6 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg / .4) 30%, oklch(0.5 0.6 70deg / .8) 90%)`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 30%, oklch(0.5 0.6 70 / 0.8) 90%)`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg / .4) 12.5%, oklch(0.5 0.6 70deg / .8) 37.5%)`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 12.5%, oklch(0.5 0.6 70 / 0.8) 37.5%)`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg / .4) 0%, oklch(0.5 0.6 70deg / .8))`, `color-mix(in oklch, oklch(0.1 0.2 30 / 0.4) 0%, oklch(0.5 0.6 70 / 0.8))`);
+
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(1 0 40deg), oklch(1 0 60deg))`, `color-mix(in oklch, oklch(1 0 40), oklch(1 0 60))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(1 0 60deg), oklch(1 0 40deg))`, `color-mix(in oklch, oklch(1 0 60), oklch(1 0 40))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(1 0 50deg), oklch(1 0 330deg))`, `color-mix(in oklch, oklch(1 0 50), oklch(1 0 330))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(1 0 330deg), oklch(1 0 50deg))`, `color-mix(in oklch, oklch(1 0 330), oklch(1 0 50))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(1 0 20deg), oklch(1 0 320deg))`, `color-mix(in oklch, oklch(1 0 20), oklch(1 0 320))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(1 0 320deg), oklch(1 0 20deg))`, `color-mix(in oklch, oklch(1 0 320), oklch(1 0 20))`);
+
+ fuzzy_test_valid_color(`color-mix(in oklch shorter hue, oklch(1 0 40deg), oklch(1 0 60deg))`, `color-mix(in oklch, oklch(1 0 40), oklch(1 0 60))`);
+ fuzzy_test_valid_color(`color-mix(in oklch shorter hue, oklch(1 0 60deg), oklch(1 0 40deg))`, `color-mix(in oklch, oklch(1 0 60), oklch(1 0 40))`);
+ fuzzy_test_valid_color(`color-mix(in oklch shorter hue, oklch(1 0 50deg), oklch(1 0 330deg))`, `color-mix(in oklch, oklch(1 0 50), oklch(1 0 330))`);
+ fuzzy_test_valid_color(`color-mix(in oklch shorter hue, oklch(1 0 330deg), oklch(1 0 50deg))`, `color-mix(in oklch, oklch(1 0 330), oklch(1 0 50))`);
+ fuzzy_test_valid_color(`color-mix(in oklch shorter hue, oklch(1 0 20deg), oklch(1 0 320deg))`, `color-mix(in oklch, oklch(1 0 20), oklch(1 0 320))`);
+ fuzzy_test_valid_color(`color-mix(in oklch shorter hue, oklch(1 0 320deg), oklch(1 0 20deg))`, `color-mix(in oklch, oklch(1 0 320), oklch(1 0 20))`);
+
+ fuzzy_test_valid_color(`color-mix(in oklch longer hue, oklch(1 0 40deg), oklch(1 0 60deg))`, `color-mix(in oklch longer hue, oklch(1 0 40), oklch(1 0 60))`);
+ fuzzy_test_valid_color(`color-mix(in oklch longer hue, oklch(1 0 60deg), oklch(1 0 40deg))`, `color-mix(in oklch longer hue, oklch(1 0 60), oklch(1 0 40))`);
+ fuzzy_test_valid_color(`color-mix(in oklch longer hue, oklch(1 0 50deg), oklch(1 0 330deg))`, `color-mix(in oklch longer hue, oklch(1 0 50), oklch(1 0 330))`);
+ fuzzy_test_valid_color(`color-mix(in oklch longer hue, oklch(1 0 330deg), oklch(1 0 50deg))`, `color-mix(in oklch longer hue, oklch(1 0 330), oklch(1 0 50))`);
+ fuzzy_test_valid_color(`color-mix(in oklch longer hue, oklch(1 0 20deg), oklch(1 0 320deg))`, `color-mix(in oklch longer hue, oklch(1 0 20), oklch(1 0 320))`);
+ fuzzy_test_valid_color(`color-mix(in oklch longer hue, oklch(1 0 320deg), oklch(1 0 20deg))`, `color-mix(in oklch longer hue, oklch(1 0 320), oklch(1 0 20))`);
+
+ fuzzy_test_valid_color(`color-mix(in oklch increasing hue, oklch(1 0 40deg), oklch(1 0 60deg))`, `color-mix(in oklch increasing hue, oklch(1 0 40), oklch(1 0 60))`);
+ fuzzy_test_valid_color(`color-mix(in oklch increasing hue, oklch(1 0 60deg), oklch(1 0 40deg))`, `color-mix(in oklch increasing hue, oklch(1 0 60), oklch(1 0 40))`);
+ fuzzy_test_valid_color(`color-mix(in oklch increasing hue, oklch(1 0 50deg), oklch(1 0 330deg))`, `color-mix(in oklch increasing hue, oklch(1 0 50), oklch(1 0 330))`);
+ fuzzy_test_valid_color(`color-mix(in oklch increasing hue, oklch(1 0 330deg), oklch(1 0 50deg))`, `color-mix(in oklch increasing hue, oklch(1 0 330), oklch(1 0 50))`);
+ fuzzy_test_valid_color(`color-mix(in oklch increasing hue, oklch(1 0 20deg), oklch(1 0 320deg))`, `color-mix(in oklch increasing hue, oklch(1 0 20), oklch(1 0 320))`);
+ fuzzy_test_valid_color(`color-mix(in oklch increasing hue, oklch(1 0 320deg), oklch(1 0 20deg))`, `color-mix(in oklch increasing hue, oklch(1 0 320), oklch(1 0 20))`);
+
+ fuzzy_test_valid_color(`color-mix(in oklch decreasing hue, oklch(1 0 40deg), oklch(1 0 60deg))`, `color-mix(in oklch decreasing hue, oklch(1 0 40), oklch(1 0 60))`);
+ fuzzy_test_valid_color(`color-mix(in oklch decreasing hue, oklch(1 0 60deg), oklch(1 0 40deg))`, `color-mix(in oklch decreasing hue, oklch(1 0 60), oklch(1 0 40))`);
+ fuzzy_test_valid_color(`color-mix(in oklch decreasing hue, oklch(1 0 50deg), oklch(1 0 330deg))`, `color-mix(in oklch decreasing hue, oklch(1 0 50), oklch(1 0 330))`);
+ fuzzy_test_valid_color(`color-mix(in oklch decreasing hue, oklch(1 0 330deg), oklch(1 0 50deg))`, `color-mix(in oklch decreasing hue, oklch(1 0 330), oklch(1 0 50))`);
+ fuzzy_test_valid_color(`color-mix(in oklch decreasing hue, oklch(1 0 20deg), oklch(1 0 320deg))`, `color-mix(in oklch decreasing hue, oklch(1 0 20), oklch(1 0 320))`);
+ fuzzy_test_valid_color(`color-mix(in oklch decreasing hue, oklch(1 0 320deg), oklch(1 0 20deg))`, `color-mix(in oklch decreasing hue, oklch(1 0 320), oklch(1 0 20))`);
+
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(none none none), oklch(none none none))`, `color-mix(in oklch, oklch(none none none), oklch(none none none))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(none none none), oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(none none none), oklch(0.5 0.6 70))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg), oklch(none none none))`, `color-mix(in oklch, oklch(0.1 0.2 30), oklch(none none none))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 none), oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(0.1 0.2 none), oklch(0.5 0.6 70))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg), oklch(0.5 0.6 none))`, `color-mix(in oklch, oklch(0.1 0.2 30), oklch(0.5 0.6 none))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(none 0.2 30deg), oklch(0.5 none 70deg))`, `color-mix(in oklch, oklch(none 0.2 30), oklch(0.5 none 70))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg / none), oklch(0.5 0.6 70deg))`, `color-mix(in oklch, oklch(0.1 0.2 30 / none), oklch(0.5 0.6 70))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg / none), oklch(0.5 0.6 70deg / 0.5))`, `color-mix(in oklch, oklch(0.1 0.2 30 / none), oklch(0.5 0.6 70 / 0.5))`);
+ fuzzy_test_valid_color(`color-mix(in oklch, oklch(0.1 0.2 30deg / none), oklch(0.5 0.6 70deg / none))`, `color-mix(in oklch, oklch(0.1 0.2 30 / none), oklch(0.5 0.6 70 / none))`);
// lab()
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30), lab(50 60 70))`, `color-mix(in lab, lab(10 20 30), lab(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30) 25%, lab(50 60 70))`, `color-mix(in lab, lab(10 20 30) 25%, lab(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lab, 25% lab(10 20 30), lab(50 60 70))`, `color-mix(in lab, lab(10 20 30) 25%, lab(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30), 25% lab(50 60 70))`, `color-mix(in lab, lab(10 20 30) 75%, lab(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30), lab(50 60 70) 25%)`, `color-mix(in lab, lab(10 20 30) 75%, lab(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30) 25%, lab(50 60 70) 75%)`, `color-mix(in lab, lab(10 20 30) 25%, lab(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30) 30%, lab(50 60 70) 90%)`, `color-mix(in lab, lab(10 20 30) 30%, lab(50 60 70) 90%)`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30) 12.5%, lab(50 60 70) 37.5%)`, `color-mix(in lab, lab(10 20 30) 12.5%, lab(50 60 70) 37.5%)`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30) 0%, lab(50 60 70))`, `color-mix(in lab, lab(10 20 30) 0%, lab(50 60 70))`);
-
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30 / .4), lab(50 60 70 / .8))`, `color-mix(in lab, lab(10 20 30 / 0.4), lab(50 60 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30 / .4) 25%, lab(50 60 70 / .8))`, `color-mix(in lab, lab(10 20 30 / 0.4) 25%, lab(50 60 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in lab, 25% lab(10 20 30 / .4), lab(50 60 70 / .8))`, `color-mix(in lab, lab(10 20 30 / 0.4) 25%, lab(50 60 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30 / .4), 25% lab(50 60 70 / .8))`, `color-mix(in lab, lab(10 20 30 / 0.4) 75%, lab(50 60 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30 / .4), lab(50 60 70 / .8) 25%)`, `color-mix(in lab, lab(10 20 30 / 0.4) 75%, lab(50 60 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30 / .4) 25%, lab(50 60 70 / .8) 75%)`, `color-mix(in lab, lab(10 20 30 / 0.4) 25%, lab(50 60 70 / 0.8))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30 / .4) 30%, lab(50 60 70 / .8) 90%)`, `color-mix(in lab, lab(10 20 30 / 0.4) 30%, lab(50 60 70 / 0.8) 90%)`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30 / .4) 12.5%, lab(50 60 70 / .8) 37.5%)`, `color-mix(in lab, lab(10 20 30 / 0.4) 12.5%, lab(50 60 70 / 0.8) 37.5%)`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30 / .4) 0%, lab(50 60 70 / .8))`, `color-mix(in lab, lab(10 20 30 / 0.4) 0%, lab(50 60 70 / 0.8))`);
-
- test_valid_value(`color`, `color-mix(in lab, lab(none none none), lab(none none none))`, `color-mix(in lab, lab(none none none), lab(none none none))`);
- test_valid_value(`color`, `color-mix(in lab, lab(none none none), lab(50 60 70))`, `color-mix(in lab, lab(none none none), lab(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30), lab(none none none))`, `color-mix(in lab, lab(10 20 30), lab(none none none))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 none), lab(50 60 70))`, `color-mix(in lab, lab(10 20 none), lab(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30), lab(50 60 none))`, `color-mix(in lab, lab(10 20 30), lab(50 60 none))`);
- test_valid_value(`color`, `color-mix(in lab, lab(none 20 30), lab(50 none 70))`, `color-mix(in lab, lab(none 20 30), lab(50 none 70))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30 / none), lab(50 60 70))`, `color-mix(in lab, lab(10 20 30 / none), lab(50 60 70))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30 / none), lab(50 60 70 / 0.5))`, `color-mix(in lab, lab(10 20 30 / none), lab(50 60 70 / 0.5))`);
- test_valid_value(`color`, `color-mix(in lab, lab(10 20 30 / none), lab(50 60 70 / none))`, `color-mix(in lab, lab(10 20 30 / none), lab(50 60 70 / none))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30), lab(50 60 70))`, `color-mix(in lab, lab(10 20 30), lab(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30) 25%, lab(50 60 70))`, `color-mix(in lab, lab(10 20 30) 25%, lab(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lab, 25% lab(10 20 30), lab(50 60 70))`, `color-mix(in lab, lab(10 20 30) 25%, lab(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30), 25% lab(50 60 70))`, `color-mix(in lab, lab(10 20 30) 75%, lab(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30), lab(50 60 70) 25%)`, `color-mix(in lab, lab(10 20 30) 75%, lab(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30) 25%, lab(50 60 70) 75%)`, `color-mix(in lab, lab(10 20 30) 25%, lab(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30) 30%, lab(50 60 70) 90%)`, `color-mix(in lab, lab(10 20 30) 30%, lab(50 60 70) 90%)`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30) 12.5%, lab(50 60 70) 37.5%)`, `color-mix(in lab, lab(10 20 30) 12.5%, lab(50 60 70) 37.5%)`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30) 0%, lab(50 60 70))`, `color-mix(in lab, lab(10 20 30) 0%, lab(50 60 70))`);
+
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30 / .4), lab(50 60 70 / .8))`, `color-mix(in lab, lab(10 20 30 / 0.4), lab(50 60 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30 / .4) 25%, lab(50 60 70 / .8))`, `color-mix(in lab, lab(10 20 30 / 0.4) 25%, lab(50 60 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in lab, 25% lab(10 20 30 / .4), lab(50 60 70 / .8))`, `color-mix(in lab, lab(10 20 30 / 0.4) 25%, lab(50 60 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30 / .4), 25% lab(50 60 70 / .8))`, `color-mix(in lab, lab(10 20 30 / 0.4) 75%, lab(50 60 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30 / .4), lab(50 60 70 / .8) 25%)`, `color-mix(in lab, lab(10 20 30 / 0.4) 75%, lab(50 60 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30 / .4) 25%, lab(50 60 70 / .8) 75%)`, `color-mix(in lab, lab(10 20 30 / 0.4) 25%, lab(50 60 70 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30 / .4) 30%, lab(50 60 70 / .8) 90%)`, `color-mix(in lab, lab(10 20 30 / 0.4) 30%, lab(50 60 70 / 0.8) 90%)`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30 / .4) 12.5%, lab(50 60 70 / .8) 37.5%)`, `color-mix(in lab, lab(10 20 30 / 0.4) 12.5%, lab(50 60 70 / 0.8) 37.5%)`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30 / .4) 0%, lab(50 60 70 / .8))`, `color-mix(in lab, lab(10 20 30 / 0.4) 0%, lab(50 60 70 / 0.8))`);
+
+ fuzzy_test_valid_color(`color-mix(in lab, lab(none none none), lab(none none none))`, `color-mix(in lab, lab(none none none), lab(none none none))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(none none none), lab(50 60 70))`, `color-mix(in lab, lab(none none none), lab(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30), lab(none none none))`, `color-mix(in lab, lab(10 20 30), lab(none none none))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 none), lab(50 60 70))`, `color-mix(in lab, lab(10 20 none), lab(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30), lab(50 60 none))`, `color-mix(in lab, lab(10 20 30), lab(50 60 none))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(none 20 30), lab(50 none 70))`, `color-mix(in lab, lab(none 20 30), lab(50 none 70))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30 / none), lab(50 60 70))`, `color-mix(in lab, lab(10 20 30 / none), lab(50 60 70))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30 / none), lab(50 60 70 / 0.5))`, `color-mix(in lab, lab(10 20 30 / none), lab(50 60 70 / 0.5))`);
+ fuzzy_test_valid_color(`color-mix(in lab, lab(10 20 30 / none), lab(50 60 70 / none))`, `color-mix(in lab, lab(10 20 30 / none), lab(50 60 70 / none))`);
// oklab()
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 25%, oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 25%, oklab(0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in oklab, 25% oklab(0.1 0.2 0.3), oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 25%, oklab(0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3), 25% oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 75%, oklab(0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 0.7) 25%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 75%, oklab(0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 25%, oklab(0.5 0.6 0.7) 75%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 25%, oklab(0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 30%, oklab(0.5 0.6 0.7) 90%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 30%, oklab(0.5 0.6 0.7) 90%)`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 12.5%, oklab(0.5 0.6 0.7) 37.5%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 12.5%, oklab(0.5 0.6 0.7) 37.5%)`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 0%, oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 0%, oklab(0.5 0.6 0.7))`);
-
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / .4), oklab(0.5 0.6 0.7 / .8))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4), oklab(0.5 0.6 0.7 / 0.8))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / .4) 25%, oklab(0.5 0.6 0.7 / .8))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 25%, oklab(0.5 0.6 0.7 / 0.8))`);
- test_valid_value(`color`, `color-mix(in oklab, 25% oklab(0.1 0.2 0.3 / .4), oklab(0.5 0.6 0.7 / .8))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 25%, oklab(0.5 0.6 0.7 / 0.8))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / .4), 25% oklab(0.5 0.6 0.7 / .8))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 75%, oklab(0.5 0.6 0.7 / 0.8))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / .4), oklab(0.5 0.6 0.7 / .8) 25%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 75%, oklab(0.5 0.6 0.7 / 0.8))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / .4) 25%, oklab(0.5 0.6 0.7 / .8) 75%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 25%, oklab(0.5 0.6 0.7 / 0.8))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / .4) 30%, oklab(0.5 0.6 0.7 / .8) 90%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 30%, oklab(0.5 0.6 0.7 / 0.8) 90%)`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / .4) 12.5%, oklab(0.5 0.6 0.7 / .8) 37.5%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 12.5%, oklab(0.5 0.6 0.7 / 0.8) 37.5%)`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / .4) 0%, oklab(0.5 0.6 0.7 / .8))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 0%, oklab(0.5 0.6 0.7 / 0.8))`);
-
- test_valid_value(`color`, `color-mix(in oklab, oklab(none none none), oklab(none none none))`, `color-mix(in oklab, oklab(none none none), oklab(none none none))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(none none none), oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(none none none), oklab(0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(none none none))`, `color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(none none none))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 none), oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(0.1 0.2 none), oklab(0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 none))`, `color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 none))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(none 0.2 0.3), oklab(0.5 none 0.7))`, `color-mix(in oklab, oklab(none 0.2 0.3), oklab(0.5 none 0.7))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / none), oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / none), oklab(0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / none), oklab(0.5 0.6 0.7 / 0.5))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / none), oklab(0.5 0.6 0.7 / 0.5))`);
- test_valid_value(`color`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / none), oklab(0.5 0.6 0.7 / none))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / none), oklab(0.5 0.6 0.7 / none))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3) 25%, oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 25%, oklab(0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, 25% oklab(0.1 0.2 0.3), oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 25%, oklab(0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3), 25% oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 75%, oklab(0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 0.7) 25%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 75%, oklab(0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3) 25%, oklab(0.5 0.6 0.7) 75%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 25%, oklab(0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3) 30%, oklab(0.5 0.6 0.7) 90%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 30%, oklab(0.5 0.6 0.7) 90%)`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3) 12.5%, oklab(0.5 0.6 0.7) 37.5%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 12.5%, oklab(0.5 0.6 0.7) 37.5%)`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3) 0%, oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(0.1 0.2 0.3) 0%, oklab(0.5 0.6 0.7))`);
+
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3 / .4), oklab(0.5 0.6 0.7 / .8))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4), oklab(0.5 0.6 0.7 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3 / .4) 25%, oklab(0.5 0.6 0.7 / .8))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 25%, oklab(0.5 0.6 0.7 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, 25% oklab(0.1 0.2 0.3 / .4), oklab(0.5 0.6 0.7 / .8))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 25%, oklab(0.5 0.6 0.7 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3 / .4), 25% oklab(0.5 0.6 0.7 / .8))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 75%, oklab(0.5 0.6 0.7 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3 / .4), oklab(0.5 0.6 0.7 / .8) 25%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 75%, oklab(0.5 0.6 0.7 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3 / .4) 25%, oklab(0.5 0.6 0.7 / .8) 75%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 25%, oklab(0.5 0.6 0.7 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3 / .4) 30%, oklab(0.5 0.6 0.7 / .8) 90%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 30%, oklab(0.5 0.6 0.7 / 0.8) 90%)`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3 / .4) 12.5%, oklab(0.5 0.6 0.7 / .8) 37.5%)`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 12.5%, oklab(0.5 0.6 0.7 / 0.8) 37.5%)`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3 / .4) 0%, oklab(0.5 0.6 0.7 / .8))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / 0.4) 0%, oklab(0.5 0.6 0.7 / 0.8))`);
+
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(none none none), oklab(none none none))`, `color-mix(in oklab, oklab(none none none), oklab(none none none))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(none none none), oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(none none none), oklab(0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(none none none))`, `color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(none none none))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 none), oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(0.1 0.2 none), oklab(0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 none))`, `color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 none))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(none 0.2 0.3), oklab(0.5 none 0.7))`, `color-mix(in oklab, oklab(none 0.2 0.3), oklab(0.5 none 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3 / none), oklab(0.5 0.6 0.7))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / none), oklab(0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3 / none), oklab(0.5 0.6 0.7 / 0.5))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / none), oklab(0.5 0.6 0.7 / 0.5))`);
+ fuzzy_test_valid_color(`color-mix(in oklab, oklab(0.1 0.2 0.3 / none), oklab(0.5 0.6 0.7 / none))`, `color-mix(in oklab, oklab(0.1 0.2 0.3 / none), oklab(0.5 0.6 0.7 / none))`);
for (const colorSpace of [ "srgb", "srgb-linear", "display-p3", "a98-rgb", "prophoto-rgb", "rec2020", "xyz", "xyz-d50", "xyz-d65" ]) {
const resultColorSpace = colorSpace == "xyz" ? "xyz-d65" : colorSpace;
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3), color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3), color(${resultColorSpace} 0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, 50% color(${colorSpace} .1 .2 .3), color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3), color(${resultColorSpace} 0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3), 50% color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3), color(${resultColorSpace} 0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3) 25%, color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3) 25%, color(${resultColorSpace} 0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3), color(${colorSpace} .5 .6 .7) 25%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3) 75%, color(${resultColorSpace} 0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3) 25%, color(${colorSpace} .5 .6 .7) 75%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3) 25%, color(${resultColorSpace} 0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3) 30%, color(${colorSpace} .5 .6 .7) 90%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3) 30%, color(${resultColorSpace} 0.5 0.6 0.7) 90%)`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3) 12.5%, color(${colorSpace} .5 .6 .7) 37.5%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3) 12.5%, color(${resultColorSpace} 0.5 0.6 0.7) 37.5%)`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3) 0%, color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3) 0%, color(${resultColorSpace} 0.5 0.6 0.7))`);
-
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / .5), color(${colorSpace} .5 .6 .7 / .8))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / 0.5), color(${resultColorSpace} 0.5 0.6 0.7 / 0.8))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / .4) 25%, color(${colorSpace} .5 .6 .7 / .8))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / 0.4) 25%, color(${resultColorSpace} 0.5 0.6 0.7 / 0.8))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / .4), color(${colorSpace} .5 .6 .7 / .8) 25%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / 0.4) 75%, color(${resultColorSpace} 0.5 0.6 0.7 / 0.8))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / .4) 25%, color(${colorSpace} .5 .6 .7 / .8) 75%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / 0.4) 25%, color(${resultColorSpace} 0.5 0.6 0.7 / 0.8))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / .4) 30%, color(${colorSpace} .5 .6 .7 / .8) 90%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / 0.4) 30%, color(${resultColorSpace} 0.5 0.6 0.7 / 0.8) 90%)`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / .4) 12.5%, color(${colorSpace} .5 .6 .7 / .8) 37.5%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / 0.4) 12.5%, color(${resultColorSpace} 0.5 0.6 0.7 / 0.8) 37.5%)`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / .4) 0%, color(${colorSpace} .5 .6 .7 / .8))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / 0.4) 0%, color(${resultColorSpace} 0.5 0.6 0.7 / 0.8))`);
-
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} 2 3 4 / 5), color(${colorSpace} 4 6 8 / 10))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 2 3 4), color(${resultColorSpace} 4 6 8))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} -2 -3 -4), color(${colorSpace} -4 -6 -8))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} -2 -3 -4), color(${resultColorSpace} -4 -6 -8))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} -2 -3 -4 / -5), color(${colorSpace} -4 -6 -8 / -10))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} -2 -3 -4 / 0), color(${resultColorSpace} -4 -6 -8 / 0))`);
-
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} none none none), color(${colorSpace} none none none))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} none none none), color(${resultColorSpace} none none none))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} none none none), color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} none none none), color(${resultColorSpace} 0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3), color(${colorSpace} none none none))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3), color(${resultColorSpace} none none none))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 none), color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 none), color(${resultColorSpace} 0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3), color(${colorSpace} .5 .6 none))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3), color(${resultColorSpace} 0.5 0.6 none))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} none .2 .3), color(${colorSpace} .5 none .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} none 0.2 0.3), color(${resultColorSpace} 0.5 none 0.7))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / none), color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / none), color(${resultColorSpace} 0.5 0.6 0.7))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / none), color(${colorSpace} .5 .6 .7 / 0.5))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / none), color(${resultColorSpace} 0.5 0.6 0.7 / 0.5))`);
- test_valid_value(`color`, `color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / none), color(${colorSpace} .5 .6 .7 / none))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / none), color(${resultColorSpace} 0.5 0.6 0.7 / none))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3), color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3), color(${resultColorSpace} 0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, 50% color(${colorSpace} .1 .2 .3), color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3), color(${resultColorSpace} 0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3), 50% color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3), color(${resultColorSpace} 0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3) 25%, color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3) 25%, color(${resultColorSpace} 0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3), color(${colorSpace} .5 .6 .7) 25%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3) 75%, color(${resultColorSpace} 0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3) 25%, color(${colorSpace} .5 .6 .7) 75%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3) 25%, color(${resultColorSpace} 0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3) 30%, color(${colorSpace} .5 .6 .7) 90%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3) 30%, color(${resultColorSpace} 0.5 0.6 0.7) 90%)`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3) 12.5%, color(${colorSpace} .5 .6 .7) 37.5%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3) 12.5%, color(${resultColorSpace} 0.5 0.6 0.7) 37.5%)`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3) 0%, color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3) 0%, color(${resultColorSpace} 0.5 0.6 0.7))`);
+
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / .5), color(${colorSpace} .5 .6 .7 / .8))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / 0.5), color(${resultColorSpace} 0.5 0.6 0.7 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / .4) 25%, color(${colorSpace} .5 .6 .7 / .8))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / 0.4) 25%, color(${resultColorSpace} 0.5 0.6 0.7 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / .4), color(${colorSpace} .5 .6 .7 / .8) 25%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / 0.4) 75%, color(${resultColorSpace} 0.5 0.6 0.7 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / .4) 25%, color(${colorSpace} .5 .6 .7 / .8) 75%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / 0.4) 25%, color(${resultColorSpace} 0.5 0.6 0.7 / 0.8))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / .4) 30%, color(${colorSpace} .5 .6 .7 / .8) 90%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / 0.4) 30%, color(${resultColorSpace} 0.5 0.6 0.7 / 0.8) 90%)`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / .4) 12.5%, color(${colorSpace} .5 .6 .7 / .8) 37.5%)`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / 0.4) 12.5%, color(${resultColorSpace} 0.5 0.6 0.7 / 0.8) 37.5%)`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / .4) 0%, color(${colorSpace} .5 .6 .7 / .8))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / 0.4) 0%, color(${resultColorSpace} 0.5 0.6 0.7 / 0.8))`);
+
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} 2 3 4 / 5), color(${colorSpace} 4 6 8 / 10))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 2 3 4), color(${resultColorSpace} 4 6 8))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} -2 -3 -4), color(${colorSpace} -4 -6 -8))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} -2 -3 -4), color(${resultColorSpace} -4 -6 -8))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} -2 -3 -4 / -5), color(${colorSpace} -4 -6 -8 / -10))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} -2 -3 -4 / 0), color(${resultColorSpace} -4 -6 -8 / 0))`);
+
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} none none none), color(${colorSpace} none none none))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} none none none), color(${resultColorSpace} none none none))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} none none none), color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} none none none), color(${resultColorSpace} 0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3), color(${colorSpace} none none none))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3), color(${resultColorSpace} none none none))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 none), color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 none), color(${resultColorSpace} 0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3), color(${colorSpace} .5 .6 none))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3), color(${resultColorSpace} 0.5 0.6 none))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} none .2 .3), color(${colorSpace} .5 none .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} none 0.2 0.3), color(${resultColorSpace} 0.5 none 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / none), color(${colorSpace} .5 .6 .7))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / none), color(${resultColorSpace} 0.5 0.6 0.7))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / none), color(${colorSpace} .5 .6 .7 / 0.5))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / none), color(${resultColorSpace} 0.5 0.6 0.7 / 0.5))`);
+ fuzzy_test_valid_color(`color-mix(in ${colorSpace}, color(${colorSpace} .1 .2 .3 / none), color(${colorSpace} .5 .6 .7 / none))`, `color-mix(in ${resultColorSpace}, color(${resultColorSpace} 0.1 0.2 0.3 / none), color(${resultColorSpace} 0.5 0.6 0.7 / none))`);
}
</script>
</body>
diff --git a/testing/web-platform/tests/css/css-color/t424-hsl-clip-outside-gamut-b.xht b/testing/web-platform/tests/css/css-color/t424-hsl-clip-outside-gamut-b.xht
deleted file mode 100644
index d66c2db925..0000000000
--- a/testing/web-platform/tests/css/css-color/t424-hsl-clip-outside-gamut-b.xht
+++ /dev/null
@@ -1,60 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <title>CSS Test: hsl() clipping outside device gamut</title>
- <link rel="author" title="L. David Baron" href="https://dbaron.org/" />
- <link rel="author" title="Mozilla Corporation" href="http://mozilla.com/" />
- <link rel="help" href="http://www.w3.org/TR/css3-color/#hsl-color" />
- <link rel="match" href="t424-hsl-clip-outside-gamut-b-ref.html" />
- <meta name="assert" content="Test clipping of hsl() values outside the device gamut." />
- <style type="text/css"><![CDATA[
-
- table { border-spacing: 0 2px; padding: 0; border: none; }
- td { border: none; padding: 0; height: 1.2em; }
-
- ]]></style>
- </head>
- <body>
-
- <p><strong>WARNING: This test assumes that the device gamut is sRGB
- (as it will be for many CRT monitors).</strong></p>
-
- <p>Every row in this table should have both columns the same color:</p>
-
- <table>
- <tr>
- <th style="background:white; color: black">Column 1</th>
- <th style="background:black; color: white">Column 2</th>
- </tr>
- <tr>
- <td style="background: hsl(240, 100%, -100%)">&nbsp;</td>
- <td style="background: black">&nbsp;</td>
- </tr>
- <tr>
- <td style="background: hsl(240, 75%, -20%)">&nbsp;</td>
- <td style="background: black">&nbsp;</td>
- </tr>
- <tr>
- <td style="background: hsl(240, 75%, 120%)">&nbsp;</td>
- <td style="background: white">&nbsp;</td>
- </tr>
- <tr>
- <td style="background: hsl(240, 130%, 50%)">&nbsp;</td>
- <td style="background: rgb(0, 0, 255)">&nbsp;</td>
- </tr>
- <tr>
- <td style="background: hsl(264, 130%, 50%)">&nbsp;</td>
- <td style="background: rgb(102, 0, 255)">&nbsp;</td>
- </tr>
- <tr>
- <td style="background: hsl(0, -50%, 40%)">&nbsp;</td>
- <td style="background: rgb(102, 102, 102)">&nbsp;</td>
- </tr>
- <tr>
- <td style="background: hsl(30, -50%, 60%)">&nbsp;</td>
- <td style="background: rgb(153, 153, 153)">&nbsp;</td>
- </tr>
- </table>
-
- </body>
-</html>
diff --git a/testing/web-platform/tests/css/css-color/t425-hsla-clip-outside-device-gamut-b.xht b/testing/web-platform/tests/css/css-color/t425-hsla-clip-outside-device-gamut-b.xht
deleted file mode 100644
index d30cf8c483..0000000000
--- a/testing/web-platform/tests/css/css-color/t425-hsla-clip-outside-device-gamut-b.xht
+++ /dev/null
@@ -1,61 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <title>CSS Test: hsla() clipping outside device gamut</title>
- <link rel="author" title="L. David Baron" href="https://dbaron.org/" />
- <link rel="author" title="Mozilla Corporation" href="http://mozilla.com/" />
- <link rel="help" href="http://www.w3.org/TR/css3-color/#hsla-color" />
- <link rel="match" href="t425-hsla-clip-outside-device-gamut-b-ref.html" />
- <meta name="assert" content="Test clipping of hsla() values outside the device gamut." />
- <style type="text/css"><![CDATA[
-
- body { background: white; color: black; }
- table { border-spacing: 0 2px; padding: 0; border: none; }
- td { border: none; padding: 0; height: 1.2em; }
-
- ]]></style>
- </head>
- <body>
-
- <p><strong>WARNING: This test assumes that the device gamut is sRGB
- (as it will be for many CRT monitors).</strong></p>
-
- <p>Every row in this table should have both columns the same color:</p>
-
- <table>
- <tr>
- <th style="background:white; color: black">Column 1</th>
- <th style="background:black; color: white">Column 2</th>
- </tr>
- <tr>
- <td style="background: hsla(240, 100%, -100%, 0.6)">&nbsp;</td>
- <td style="background: rgb(102, 102, 102)">&nbsp;</td>
- </tr>
- <tr>
- <td style="background: hsla(240, 75%, -20%, 0.4)">&nbsp;</td>
- <td style="background: rgb(153, 153, 153)">&nbsp;</td>
- </tr>
- <tr>
- <td style="background: hsla(240, 75%, 120%, 0.6)">&nbsp;</td>
- <td style="background: white">&nbsp;</td>
- </tr>
- <tr>
- <td style="background: hsla(240, 130%, 50%, 0.4)">&nbsp;</td>
- <td style="background: rgb(153, 153, 255)">&nbsp;</td>
- </tr>
- <tr>
- <td style="background: hsla(264, 130%, 50%, 0.6)">&nbsp;</td>
- <td style="background: rgb(163, 102, 255)">&nbsp;</td>
- </tr>
- <tr>
- <td style="background: hsla(0, -50%, 40%, 0.4)">&nbsp;</td>
- <td style="background: rgb(194, 194, 194)">&nbsp;</td>
- </tr>
- <tr>
- <td style="background: hsla(30, -50%, 60%, 0.6)">&nbsp;</td>
- <td style="background: rgb(194, 194, 194)">&nbsp;</td>
- </tr>
- </table>
-
- </body>
-</html>
diff --git a/testing/web-platform/tests/css/css-contain/contain-layout-baseline-005.html b/testing/web-platform/tests/css/css-contain/contain-layout-baseline-005.html
index 0971402e6b..fb706cbf03 100644
--- a/testing/web-platform/tests/css/css-contain/contain-layout-baseline-005.html
+++ b/testing/web-platform/tests/css/css-contain/contain-layout-baseline-005.html
@@ -43,7 +43,6 @@ fieldset, details {
<div class="wrapper">
<canvas></canvas>
<div class="inline-block">foo</div>
- <button>foo</button>
<select><option>foo</option></select>
<select multiple style="height: 40px;"><option>foo</option></select>
<textarea style="height: 40px;"></textarea>
diff --git a/testing/web-platform/tests/css/css-contain/contain-layout-button-001.html b/testing/web-platform/tests/css/css-contain/contain-layout-button-001.tentative.html
index b53b28879e..f5a664e566 100644
--- a/testing/web-platform/tests/css/css-contain/contain-layout-button-001.html
+++ b/testing/web-platform/tests/css/css-contain/contain-layout-button-001.tentative.html
@@ -5,7 +5,7 @@
<link rel="help" href="https://drafts.csswg.org/css-contain-1/#containment-layout">
<link rel="help" href="https://drafts.csswg.org/css2/visudet.html#propdef-vertical-align">
<link rel="match" href="reference/contain-layout-button-001-ref.html">
-<meta name=assert content="Layout containment does apply to buttons, thus their baseline is their margin-bottom edge.">
+<meta name=assert content="Layout containment does apply to buttons, but in an inline context their baseline is synthesized from their content-box.">
<style>
button {
border: 5px solid green;
diff --git a/testing/web-platform/tests/css/css-contain/contain-layout-button-002.tentative.html b/testing/web-platform/tests/css/css-contain/contain-layout-button-002.tentative.html
new file mode 100644
index 0000000000..15026d55f8
--- /dev/null
+++ b/testing/web-platform/tests/css/css-contain/contain-layout-button-002.tentative.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-contain-1/#containment-layout">
+<link rel="help" href="https://github.com/web-platform-tests/wpt/issues/45889">
+<link rel="match" href="reference/contain-layout-button-002-ref.html">
+<meta name=assert content="An empty button and a button with layout containment should align the same.">
+<style>
+button {
+ border: 5px solid green;
+ padding: 10px;
+ margin-bottom: 2px;
+ color: transparent;
+ width: 20px;
+ height: 20px;
+}
+</style>
+
+<p>This test passes if it has the same output as the reference.</p>
+<button></button> <button style="contain:layout"></button>
diff --git a/testing/web-platform/tests/css/css-contain/container-queries/custom-property-style-queries.html b/testing/web-platform/tests/css/css-contain/container-queries/custom-property-style-queries.html
index 4ae5efca91..d9152432ed 100644
--- a/testing/web-platform/tests/css/css-contain/container-queries/custom-property-style-queries.html
+++ b/testing/web-platform/tests/css/css-contain/container-queries/custom-property-style-queries.html
@@ -206,6 +206,9 @@
--inherit-no: bar;
--unset-no: baz;
--initial-no: baz;
+ --space-no: baz;
+ --explicit-initial: initial;
+ --space: ;
}
@container style(--initial: initial) {
#initial { color: green; }
@@ -225,6 +228,18 @@
@container not style(--inherit-no: inherit) {
#inherit-no { color: green; }
}
+ @container style(--explicit-initial: initial) {
+ #explicit-initial { color: green; }
+ }
+ @container not style(--explicit-initial) {
+ #explicit-initial-implicit { color: green; }
+ }
+ @container style(--space: ) {
+ #space { color: green; }
+ }
+ @container not style(--space-no: ) {
+ #space-no { color: green; }
+ }
@container style(--unset: unset) {
#unset { color: green; }
}
@@ -238,6 +253,10 @@
<div id="initial-implicit"></div>
<div id="initial-no"></div>
<div id="initial-no-implicit"></div>
+ <div id="explicit-initial"></div>
+ <div id="explicit-initial-implicit"></div>
+ <div id="space"></div>
+ <div id="space-no"></div>
<div id="inherit"></div>
<div id="inherit-no"></div>
<div id="unset"></div>
@@ -262,6 +281,22 @@
}, "Style query matching value-less query against non-initial value");
test(() => {
+ assert_equals(getComputedStyle(document.querySelector("#explicit-initial")).color, green);
+ }, "Style query 'initial' matching (with explicit 'initial' value)");
+
+ test(() => {
+ assert_equals(getComputedStyle(document.querySelector("#explicit-initial-implicit")).color, green);
+ }, "Style query matching negated value-less query against initial value (with explicit 'initial' value)");
+
+ test(() => {
+ assert_equals(getComputedStyle(document.querySelector("#space")).color, green);
+ }, "Style query 'space' matching");
+
+ test(() => {
+ assert_equals(getComputedStyle(document.querySelector("#space-no")).color, green);
+ }, "Style query 'space' not matching");
+
+ test(() => {
assert_equals(getComputedStyle(document.querySelector("#inherit")).color, green);
}, "Style query 'inherit' matching");
diff --git a/testing/web-platform/tests/css/css-contain/container-queries/registered-color-style-queries.html b/testing/web-platform/tests/css/css-contain/container-queries/registered-color-style-queries.html
new file mode 100644
index 0000000000..6e2bfb896b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-contain/container-queries/registered-color-style-queries.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>CSS Container Queries Test: registered color syntax style queries</title>
+<link rel="help" href="https://drafts.csswg.org/css-contain-3/#style-container">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="support/cq-testcommon.js"></script>
+<style>
+ @property --reg-color {
+ syntax: "<color>";
+ inherits: false;
+ initial-value: red;
+ }
+
+ #light { color-scheme: light; }
+ #dark { color-scheme: dark; }
+ .container { --reg-color: light-dark(white, black); }
+
+ @container style(--reg-color: white) {
+ #t1 { color: green; }
+ }
+ @container style(--reg-color: black) {
+ #t2 { color: green; }
+ }
+</style>
+<div id="light" class="container">
+ <div id="t1"></div>
+</div>
+<div id="dark" class="container">
+ <div id="t2"></div>
+</div>
+<script>
+ const green = "rgb(0, 128, 0)";
+
+ test(() => {
+ assert_equals(getComputedStyle(t1).color, green);
+ }, "Registered color with light color-scheme and light-dark()");
+
+ test(() => {
+ assert_equals(getComputedStyle(t2).color, green);
+ }, "Registered color with dark color-scheme and light-dark()");
+</script>
diff --git a/testing/web-platform/tests/css/css-contain/content-visibility/content-visibility-background-clip-crash.html b/testing/web-platform/tests/css/css-contain/content-visibility/content-visibility-background-clip-crash.html
new file mode 100644
index 0000000000..1fa645f457
--- /dev/null
+++ b/testing/web-platform/tests/css/css-contain/content-visibility/content-visibility-background-clip-crash.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<body>
+ <style>
+ body {
+ content-visibility: hidden;
+ background-clip: text;
+ position: absolute;
+ border-right-style: dashed;
+ border-top-style: ridge;
+ background-color: green;
+ }
+ </style>
+ <script>
+ var p1 = document.createElement('p');
+ var p2 = document.createElement('p');
+ document.body.appendChild(p1);
+ document.body.appendChild(p2);
+ p1.scroll();
+ p2.remove();
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-contain/reference/contain-layout-baseline-005-ref.html b/testing/web-platform/tests/css/css-contain/reference/contain-layout-baseline-005-ref.html
index 360652c939..83340137ac 100644
--- a/testing/web-platform/tests/css/css-contain/reference/contain-layout-baseline-005-ref.html
+++ b/testing/web-platform/tests/css/css-contain/reference/contain-layout-baseline-005-ref.html
@@ -39,7 +39,6 @@ fieldset, details {
<div class="wrapper">
<canvas></canvas>
<div class="inline-block">foo</div>
- <button>foo</button>
<select><option>foo</option></select>
<select multiple style="height: 40px;"><option>foo</option></select>
<textarea style="height: 40px;"></textarea>
diff --git a/testing/web-platform/tests/css/css-contain/reference/contain-layout-button-001-ref.html b/testing/web-platform/tests/css/css-contain/reference/contain-layout-button-001-ref.html
index da83204dce..c20efbbb39 100644
--- a/testing/web-platform/tests/css/css-contain/reference/contain-layout-button-001-ref.html
+++ b/testing/web-platform/tests/css/css-contain/reference/contain-layout-button-001-ref.html
@@ -7,7 +7,7 @@ div.fakeButton {
display: inline-block;
border: 5px solid green;
padding: 0;
- margin-bottom: 2px;
+ margin-bottom: -5px;
color: transparent;
width: 0;
height: 0px;
diff --git a/testing/web-platform/tests/css/css-contain/reference/contain-layout-button-002-ref.html b/testing/web-platform/tests/css/css-contain/reference/contain-layout-button-002-ref.html
new file mode 100644
index 0000000000..85b98ee4a6
--- /dev/null
+++ b/testing/web-platform/tests/css/css-contain/reference/contain-layout-button-002-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<style>
+button {
+ border: 5px solid green;
+ padding: 10px;
+ margin-bottom: 2px;
+ color: transparent;
+ width: 20px;
+ height: 20px;
+}
+</style>
+<p>This test passes if it has the same output as the reference.</p>
+<button></button> <button></button>
diff --git a/testing/web-platform/tests/css/css-content/parsing/content-valid.html b/testing/web-platform/tests/css/css-content/parsing/content-valid.html
index 5c75586107..60d03a4d4c 100644
--- a/testing/web-platform/tests/css/css-content/parsing/content-valid.html
+++ b/testing/web-platform/tests/css/css-content/parsing/content-valid.html
@@ -42,7 +42,7 @@ test_valid_value_combinations("content", `counters(counter-name, ".", counter-st
test_valid_value_combinations("content", `counters(counter-name, ".", dECiMaL)`, `counters(counter-name, ".")`);
test_valid_value_combinations("content", `counters(counter-name, ".", DECIMAL)`, `counters(counter-name, ".")`);
-test_valid_value_combinations("content", `url("https://www.example.com/picture.svg")`);
+test_valid_value_combinations("content", `url("picture.svg")`);
test_valid_value_combinations("content", `"hello"`);
@@ -52,7 +52,7 @@ test_valid_value_combinations("content", `counter(counter-name) "potato"`);
test_valid_value_combinations("content", `counters(counter-name, ".") "potato"`);
test_valid_value_combinations("content", `"(" counters(counter-name, ".", counter-style) ")"`);
test_valid_value_combinations("content", `open-quote "hello" "world" close-quote`);
-test_valid_value_combinations("content", `url("https://www.example.com/picture.svg") "hello"`);
+test_valid_value_combinations("content", `url("picture.svg") "hello"`);
</script>
</body>
</html>
diff --git a/testing/web-platform/tests/css/css-counter-styles/counter-style-at-rule/WEB_FEATURES.yml b/testing/web-platform/tests/css/css-counter-styles/counter-style-at-rule/WEB_FEATURES.yml
new file mode 100644
index 0000000000..fc866d5c12
--- /dev/null
+++ b/testing/web-platform/tests/css/css-counter-styles/counter-style-at-rule/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: counter-style
+ files: "**"
diff --git a/testing/web-platform/tests/css/css-counter-styles/cssom/WEB_FEATURES.yml b/testing/web-platform/tests/css/css-counter-styles/cssom/WEB_FEATURES.yml
new file mode 100644
index 0000000000..1efb03070d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-counter-styles/cssom/WEB_FEATURES.yml
@@ -0,0 +1,26 @@
+# All of the tests in this directory are for CSSCounterStyleRule, but tests for
+# other parts of CSS Counter Styles could potentially be added here, so list of
+# the (current) tests to avoid matching tests from unrelated features added
+# later.
+features:
+- name: counter-style
+ files:
+ - cssom-additive-symbols-setter-invalid.html
+ - cssom-additive-symbols-setter.html
+ - cssom-fallback-setter-invalid.html
+ - cssom-fallback-setter.html
+ - cssom-name-setter-invalid.html
+ - cssom-name-setter.html
+ - cssom-negative-setter-invalid.html
+ - cssom-negative-setter.html
+ - cssom-pad-setter-invalid.html
+ - cssom-pad-setter.html
+ - cssom-prefix-suffix-setter-invalid.html
+ - cssom-prefix-suffix-setter.html
+ - cssom-range-setter-invalid.html
+ - cssom-range-setter.html
+ - cssom-symbols-setter-invalid.html
+ - cssom-symbols-setter.html
+ - cssom-system-setter-1.html
+ - cssom-system-setter-2.html
+ - cssom-system-setter-invalid.html
diff --git a/testing/web-platform/tests/css/css-display/accessibility/display-contents-role-and-label.html b/testing/web-platform/tests/css/css-display/accessibility/display-contents-role-and-label.html
index 4a06d0ff5b..6b5453e9f6 100644
--- a/testing/web-platform/tests/css/css-display/accessibility/display-contents-role-and-label.html
+++ b/testing/web-platform/tests/css/css-display/accessibility/display-contents-role-and-label.html
@@ -75,7 +75,7 @@
</thead>
<tbody>
<tr style="display: contents;">
- <td data-expectedrole="cell" data-expectedlabel="x" data-testname="td as child of tr with display: contents, within table with display: flex, has cell role" class="ex-role-and-label">x</td>
+ <td data-expectedrole="cell" data-testname="td as child of tr with display: contents, within table with display: flex, has cell role" class="ex-role">x</td>
<td>x</td>
</tr>
</tbody>
@@ -90,7 +90,7 @@
</thead>
<tbody>
<tr style="display: contents;">
- <td data-expectedrole="cell" data-expectedlabel="x" data-testname="td as child of tr with display: contents, within table with role=table with display: flex, has cell role" class="ex-role-and-label">x</td>
+ <td data-expectedrole="cell" data-testname="td as child of tr with display: contents, within table with role=table with display: flex, has cell role" class="ex-role">x</td>
<td>x</td>
</tr>
</tbody>
@@ -105,7 +105,7 @@
</thead>
<tbody>
<tr style="display: contents;">
- <td data-expectedrole="gridcell" data-expectedlabel="x" data-testname="td (no explicit role) as child of tr with display: contents, within table with role=grid with display: flex, has gridcell role" class="ex-role-and-label">x</td>
+ <td data-expectedrole="gridcell" data-testname="td (no explicit role) as child of tr with display: contents, within table with role=grid with display: flex, has gridcell role" class="ex-role">x</td>
<td>x</td>
</tr>
</tbody>
@@ -116,7 +116,7 @@
<h2>x</h2>
<ul style="display: contents" data-expectedrole="list" data-testname="ul with display: contents, as child of div with display: grid, has list role" class="ex-role">
<li>x</li>
- <li data-expectedrole="listitem" data-expectedlabel="x" data-testname="listitem within ul with display: contents, as child of div with display: grid, has listitem role" class="ex-role-and-label">x</li>
+ <li data-expectedrole="listitem" data-testname="listitem within ul with display: contents, as child of div with display: grid, has listitem role" class="ex-role">x</li>
</ul>
</div>
@@ -131,8 +131,8 @@
<!-- Landmarks and regions -->
<header style="display: contents;" data-expectedrole="banner" data-testname="header with display: contents has banner role" class="ex-role">x</header>
- <nav style="display: contents;" aria-label="label" data-expectedrole="navigation" data-testname="nav with display: contents and aria-label has navigation role" class="ex-role-and-label">x</nav>
- <aside style="display: contents;" aria-label="label" data-expectedrole="complementary" data-testname="aside with display: contents and aria-label has complementary role" class="ex-role-and-label">x</aside>
+ <nav style="display: contents;" aria-label="label" data-expectedrole="navigation" data-expectedlabel="label" data-testname="nav with display: contents and aria-label has navigation role" class="ex-role-and-label">x</nav>
+ <aside style="display: contents;" aria-label="label" data-expectedrole="complementary" data-expectedlabel="label" data-testname="aside with display: contents and aria-label has complementary role" class="ex-role-and-label">x</aside>
<main style="display: contents;" data-expectedrole="main" data-testname="main with display: contents has main role" class="ex-role">x</main>
<footer style="display: contents;" data-expectedrole="contentinfo" data-testname="footer with display: contents has contentinfo role" class="ex-role">x</footer>
<form aria-label="label" style="display: contents;" data-expectedrole="form" data-expectedlabel="label" data-testname="form with display: contents has form role" class="ex-role-and-label">x</form>
@@ -140,12 +140,12 @@
<section aria-label="label" style="display: contents;" data-expectedrole="region" data-expectedlabel="label" data-testname="section with aria-label and display: contents has region role" class="ex-role-and-label"></section>
<div role="banner" style="display: contents;" data-expectedrole="banner" data-testname="div with role banner and display: contents has banner role" class="ex-role">x</div>
- <div role="navigation" aria-label="label" style="display: contents;" data-expectedrole="navigation" data-testname="div with role navigation, aria-label and display: contents has navigation role" class="ex-role-and-label">x</div>
- <div role="complementary" aria-label="label" style="display: contents;" data-expectedrole="complementary" data-testname="div with role complementary, aria-label and display: contents has complementary role" class="ex-role-and-label">x</div>
+ <div role="navigation" aria-label="label" style="display: contents;" data-expectedrole="navigation" data-expectedlabel="label" data-testname="div with role navigation, aria-label and display: contents has navigation role" class="ex-role-and-label">x</div>
+ <div role="complementary" aria-label="label" style="display: contents;" data-expectedrole="complementary" data-expectedlabel="label" data-testname="div with role complementary, aria-label and display: contents has complementary role" class="ex-role-and-label">x</div>
<div role="main" style="display: contents;" data-expectedrole="main" data-testname="div with role main and display: contents has main role" class="ex-role">x</div>
<div role="contentinfo" style="display: contents;" data-expectedrole="contentinfo" data-testname="div with role contentinfo and display: contents has contentinfo role" class="ex-role">x</div>
<div role="form" aria-label="label" style="display: contents;" data-expectedrole="form" data-expectedlabel="label" data-testname="div with role form, aria-label and display: contents has form role" class="ex-role-and-label">x</div>
- <div role="search" aria-label="label" style="display: contents;" data-expectedrole="search" data-testname="div with role search and display: contents has search role" class="ex-role-and-label">x</div>
+ <div role="search" aria-label="label" style="display: contents;" data-expectedrole="search" data-expectedlabel="label" data-testname="div with role search and display: contents has search role" class="ex-role-and-label">x</div>
<div role="region" aria-label="label" style="display: contents;" data-expectedrole="region" data-expectedlabel="label" data-testname="div with role region, aria-label and display: contents has region role" class="ex-role-and-label">x</div>
<!-- Links -->
@@ -156,33 +156,33 @@
<!-- Lists -->
<ul role="list" style="display: contents;" data-expectedrole="list" data-testname="ul with role list and display: contents (child li has display: contents) has list role" class="ex-role">
- <li style="display: contents;" data-expectedrole="listitem" data-expectedlabel="x" data-testname="li as child of ul with role list, both with display: contents, has listitem role" class="ex-role-and-label">x</li>
+ <li style="display: contents;" data-expectedrole="listitem" data-testname="li as child of ul with role list, both with display: contents, has listitem role" class="ex-role">x</li>
<li>y</li>
</ul>
<ul role="list" style="display: contents;" data-expectedrole="list" data-testname="ul with role list and display: contents has list role" class="ex-role">
<li>x</li>
- <li data-expectedrole="listitem" data-expectedlabel="y" data-testname="li, as child of ul with role list and display: contents, has listitem role" class="ex-role-and-label">y</li>
+ <li data-expectedrole="listitem" data-testname="li, as child of ul with role list and display: contents, has listitem role" class="ex-role">y</li>
</ul>
<ul role="list">
<li>x</li>
- <li style="display: contents;" data-expectedrole="listitem" data-expectedlabel="y" data-testname="li with display: contents, as child of ul with role list, has listitem role" class="ex-role-and-label">y</li>
+ <li style="display: contents;" data-expectedrole="listitem" data-testname="li with display: contents, as child of ul with role list, has listitem role" class="ex-role">y</li>
</ul>
<ol role="list" style="display: contents;" data-expectedrole="list" data-testname="ol with role list and display: contents has list role (child li has display: contents)" class="ex-role">
- <li style="display: contents;" data-expectedrole="listitem" data-expectedlabel="x" data-testname="li as child of ol with role list, both with display: contents, has listitem role" class="ex-role-and-label">x</li>
+ <li style="display: contents;" data-expectedrole="listitem" data-testname="li as child of ol with role list, both with display: contents, has listitem role" class="ex-role">x</li>
<li>y</li>
</ol>
<ol role="list" style="display: contents;" data-expectedrole="list" data-testname="ol with role list and display: contents has list role" class="ex-role">
<li>x</li>
- <li data-expectedrole="listitem" data-expectedlabel="y" data-testname="li, as child of ol with role list and display: contents, has listitem role" class="ex-role-and-label">y</li>
+ <li data-expectedrole="listitem" data-testname="li, as child of ol with role list and display: contents, has listitem role" class="ex-role">y</li>
</ol>
<ol role="list">
<li>x</li>
- <li style="display: contents;" data-expectedrole="listitem" data-expectedlabel="y" data-testname="li with display: contents, as child of ol with role list, has listitem role" class="ex-role-and-label">y</li>
+ <li style="display: contents;" data-expectedrole="listitem" data-testname="li with display: contents, as child of ol with role list, has listitem role" class="ex-role">y</li>
</ol>
<div role="list" style="display: contents;" data-expectedrole="list" data-testname="div with list role and display: contents has list role" class="ex-role">
@@ -212,7 +212,7 @@
</thead>
<tbody>
<tr>
- <td style="display: contents;" data-expectedrole="cell" data-expectedlabel="x" data-testname="td within tr in table with role table, all with display: contents, has cell role" class="ex-role-and-label">x</td>
+ <td style="display: contents;" data-expectedrole="cell" data-testname="td within tr in table with role table, all with display: contents, has cell role" class="ex-role">x</td>
<td>x</td>
</tr>
</tbody>
@@ -242,7 +242,7 @@
</thead>
<tbody>
<tr>
- <td style="display: contents;" data-expectedrole="gridcell" data-expectedlabel="x" data-testname="td within table with role grid, both with display: contents, has gridcell role" class="ex-role-and-label">x</td>
+ <td style="display: contents;" data-expectedrole="gridcell" data-testname="td within table with role grid, both with display: contents, has gridcell role" class="ex-role">x</td>
<td>x</td>
</tr>
</tbody>
@@ -282,10 +282,10 @@
<div role="treegrid" style="display: contents;" data-expectedrole="treegrid" data-testname="div with role treegrid and display: contents has treegrid role" class="ex-role"></div>
<ul role="tree" style="display: contents;" data-expectedrole="tree" data-testname="ul with role tree and display: contents has tree role" class="ex-role">
- <li role="treeitem" aria-expanded="true" style="display: contents;" data-expectedrole="treeitem" data-expectedlabel="x" data-testname="li with role treeitem and display: contents, within ul with role tree and display: contents, has treeitem role" class="ex-role-and-label">
+ <li role="treeitem" aria-expanded="true" style="display: contents;" data-expectedrole="treeitem" data-testname="li with role treeitem and display: contents, within ul with role tree and display: contents, has treeitem role" class="ex-role">
<span>x</span>
<ul role="group">
- <li role="treeitem" aria-expanded="false" style="display: contents;" data-expectedrole="treeitem" data-expectedlabel="x" data-testname="li with role treeitem and display: contents, within ul with role=group (within ul with role tree and display: contents), has treeitem role" class="ex-role-and-label">
+ <li role="treeitem" aria-expanded="false" style="display: contents;" data-expectedrole="treeitem" data-testname="li with role treeitem and display: contents, within ul with role=group (within ul with role tree and display: contents), has treeitem role" class="ex-role">
<span>x</span>
</li>
</ul>
diff --git a/testing/web-platform/tests/css/css-easing/WEB_FEATURES.yml b/testing/web-platform/tests/css/css-easing/WEB_FEATURES.yml
new file mode 100644
index 0000000000..c17b6206c5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-easing/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: linear-easing
+ files:
+ - linear-timing-*
diff --git a/testing/web-platform/tests/css/css-easing/linear-timing-functions-output.tentative.html b/testing/web-platform/tests/css/css-easing/linear-timing-functions-output.html
index 82ea469052..ef157e2e28 100644
--- a/testing/web-platform/tests/css/css-easing/linear-timing-functions-output.tentative.html
+++ b/testing/web-platform/tests/css/css-easing/linear-timing-functions-output.html
@@ -2,7 +2,7 @@
<meta charset=utf-8>
<meta name="assert" content="This test checks the output of linear timing functions" />
<title>Tests for the output of linear timing functions</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/pull/6533">
+<link rel="help" href="https://drafts.csswg.org/css-easing-2/#the-linear-easing-function">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="support/util.js"></script>
diff --git a/testing/web-platform/tests/css/css-easing/linear-timing-functions-syntax.tentative.html b/testing/web-platform/tests/css/css-easing/linear-timing-functions-syntax.html
index 99b680d0bd..2d1baea4eb 100644
--- a/testing/web-platform/tests/css/css-easing/linear-timing-functions-syntax.tentative.html
+++ b/testing/web-platform/tests/css/css-easing/linear-timing-functions-syntax.html
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<title>CSS Easing: getComputedStyle().animationTimingFunction with linear(...)</title>
-<link rel="help" href="https://github.com/w3c/csswg-drafts/pull/6533">
+<link rel="help" href="https://drafts.csswg.org/css-easing-2/#the-linear-easing-function">
<meta name="assert" content="animation-timing-function: linear(...) parsing tests">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
@@ -34,6 +34,9 @@ test_computed_value("animation-timing-function", "linear(0 calc(min(50%, 60%)),
test_computed_value("animation-timing-function", "linear(0 0% 50%, 1 50% 100%)", "linear(0 0%, 0 50%, 1 50%, 1 100%)");
test_computed_value("animation-timing-function", "linear(0, 0.5 25% 75%, 1 100% 100%)", "linear(0 0%, 0.5 25%, 0.5 75%, 1 100%, 1 100%)");
test_computed_value("animation-timing-function", "linear(0, 1.3, 1, 0.92, 1, 0.99, 1, 0.998, 1 100% 100%)", "linear(0 0%, 1.3 12.5%, 1 25%, 0.92 37.5%, 1 50%, 0.99 62.5%, 1 75%, 0.998 87.5%, 1 100%, 1 100%)");
+
+test_computed_value("animation-timing-function", "linear(0, 0 40%, 1, 0.5, 1)", "linear(0 0%, 0 40%, 1 60%, 0.5 80%, 1 100%)");
+
</script>
</body>
</html>
diff --git a/testing/web-platform/tests/css/css-flexbox/WEB_FEATURES.yml b/testing/web-platform/tests/css/css-flexbox/WEB_FEATURES.yml
new file mode 100644
index 0000000000..69d0875451
--- /dev/null
+++ b/testing/web-platform/tests/css/css-flexbox/WEB_FEATURES.yml
@@ -0,0 +1,7 @@
+features:
+- name: flexbox
+ files: "**"
+# TODO: map *gap* to flexbox-gap. This is currently not possible without the
+# tests being associated with both flexbox and flexbox-gap. All but one of the
+# *gap* tests are passing in all browsers, so lumping them in with flexbox is
+# relatively harmless.
diff --git a/testing/web-platform/tests/css/css-flexbox/intrinsic-size/col-wrap-020.html b/testing/web-platform/tests/css/css-flexbox/intrinsic-size/col-wrap-020.html
new file mode 100644
index 0000000000..ffbfa711c3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-flexbox/intrinsic-size/col-wrap-020.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<link rel="author" title="David Grogan" href="mailto:dgrogan@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox/#intrinsic-sizes">
+<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
+<meta name="assert"
+ content="column-wrap container's max-content width includes gap" />
+
+<style>
+ #reference-overlapped-red {
+ position: absolute;
+ background-color: red;
+ width: 100px;
+ height: 100px;
+ z-index: -1;
+ }
+
+ .item {
+ /* Remove min-height so we don't have to think about it. */
+ min-height: 0px;
+ width: 10px;
+ flex: 0 0 100px;
+ }
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.
+</p>
+
+<div id=reference-overlapped-red></div>
+
+<div
+ style="display: flex; flex-flow: column wrap; height: 100px; width: max-content; column-gap: 80px; background: green;">
+ <div class="item"></div>
+ <div class="item"></div>
+</div>
diff --git a/testing/web-platform/tests/css/css-flexbox/min-size-auto-overflow-clip-ref.html b/testing/web-platform/tests/css/css-flexbox/min-size-auto-overflow-clip-ref.html
new file mode 100644
index 0000000000..2503af6df3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-flexbox/min-size-auto-overflow-clip-ref.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset="utf-8">
+<style>
+ .flex {
+ display: flex;
+ width: 100px;
+ border: 1px solid;
+ }
+</style>
+<div class="flex">
+ <div>
+ <div style="background: green; width: 150px; height: 50px;"></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-flexbox/min-size-auto-overflow-clip.html b/testing/web-platform/tests/css/css-flexbox/min-size-auto-overflow-clip.html
new file mode 100644
index 0000000000..ec3bc5f593
--- /dev/null
+++ b/testing/web-platform/tests/css/css-flexbox/min-size-auto-overflow-clip.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta charset="utf-8">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox/#min-size-auto">
+<link rel="match" href="min-size-auto-overflow-clip-ref.html">
+<title>overflow: visible and clip behave the same for min-size purposes</title>
+<style>
+ .flex {
+ display: flex;
+ width: 100px;
+ border: 1px solid;
+ }
+</style>
+<div class="flex">
+ <div style="overflow: clip">
+ <div style="background: green; width: 150px; height: 50px;"></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-fonts/WEB_FEATURES.yml b/testing/web-platform/tests/css/css-fonts/WEB_FEATURES.yml
index 5e69c923ab..6739fa2d06 100644
--- a/testing/web-platform/tests/css/css-fonts/WEB_FEATURES.yml
+++ b/testing/web-platform/tests/css/css-fonts/WEB_FEATURES.yml
@@ -6,7 +6,19 @@ features:
- palette-values-rule-*
- name: font-synthesis
files:
- - font-synthesis-*
+ - font-synthesis-0*.html
+- name: font-synthesis-position
+ files:
+ - font-synthesis-position*
+- name: font-synthesis-small-caps
+ files:
+ - font-synthesis-small-caps*
+- name: font-synthesis-style
+ files:
+ - font-synthesis-style*
+- name: font-synthesis-weight
+ files:
+ - font-synthesis-weight*
- name: font-variant-alternates
files:
- alternates-order.html
diff --git a/testing/web-platform/tests/css/css-fonts/font-size-adjust-reload.html b/testing/web-platform/tests/css/css-fonts/font-size-adjust-reload.html
index 37d79a68bd..68ccba4398 100644
--- a/testing/web-platform/tests/css/css-fonts/font-size-adjust-reload.html
+++ b/testing/web-platform/tests/css/css-fonts/font-size-adjust-reload.html
@@ -16,8 +16,7 @@ body {
</body>
<script>
const iframe = document.getElementById('iframe');
- // Forcing reload
- iframe.src += '';
+ iframe.contentWindow.location.reload();
iframe.contentWindow.onload = function(){
document.documentElement.classList.remove("reftest-wait");
};
diff --git a/testing/web-platform/tests/css/css-fonts/matching/font-unicode-PUA-primary-font-notref.html b/testing/web-platform/tests/css/css-fonts/matching/font-unicode-PUA-primary-font-notref.html
new file mode 100644
index 0000000000..df9b829a41
--- /dev/null
+++ b/testing/web-platform/tests/css/css-fonts/matching/font-unicode-PUA-primary-font-notref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<link rel="author" title="Vitor Roriz" href="https://github.com/vitorroriz">
+<link rel="help" href="https://drafts.csswg.org/css-fonts-4/#char-handling-issues">
+<style>
+.target {
+ font-family: Times;
+ font-size: 60px;
+}
+</style>
+</head>
+<body>
+"If a given character is a Private-Use Area Unicode codepoint, user agents must only match font families named in the font-family list that are not generic families. If none of the families named in the font-family list contain a glyph for that codepoint, user agents must display some form of missing glyph symbol for that character rather than attempting installed font fallback for that codepoint." - <a href="https://drafts.csswg.org/css-fonts-4/#char-handling-issues">css-fonts-4</a>
+<p class="target">&#xE0AD;&#xE0AE;&#xE0AD;&#xE0AF;&#xE0B0;&#xE0B1;&#xE0C0;&#xE0C1;&#xE0D3;&#xE0D4;</p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-fonts/matching/font-unicode-PUA-primary-font.html b/testing/web-platform/tests/css/css-fonts/matching/font-unicode-PUA-primary-font.html
new file mode 100644
index 0000000000..1b03c98e40
--- /dev/null
+++ b/testing/web-platform/tests/css/css-fonts/matching/font-unicode-PUA-primary-font.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<link rel="author" title="Vitor Roriz" href="https://github.com/vitorroriz">
+<link rel="help" href="https://drafts.csswg.org/css-fonts-4/#char-handling-issues">
+<link rel="mismatch" href="./font-unicode-PUA-primary-font-notref.html">
+<style>
+.target {
+ font-family: Arial;
+ font-size: 60px;
+}
+</style>
+</head>
+<body>
+"If a given character is a Private-Use Area Unicode codepoint, user agents must only match font families named in the font-family list that are not generic families. If none of the families named in the font-family list contain a glyph for that codepoint, user agents must display some form of missing glyph symbol for that character rather than attempting installed font fallback for that codepoint." - <a href="https://drafts.csswg.org/css-fonts-4/#char-handling-issues">css-fonts-4</a>
+<p class="target">&#xE0AD;&#xE0AE;&#xE0AD;&#xE0AF;&#xE0B0;&#xE0B1;&#xE0C0;&#xE0C1;&#xE0D3;&#xE0D4;</p>
+</html>
diff --git a/testing/web-platform/tests/css/css-fonts/parsing/WEB_FEATURES.yml b/testing/web-platform/tests/css/css-fonts/parsing/WEB_FEATURES.yml
index 6a24d269b4..a3bc09cb1f 100644
--- a/testing/web-platform/tests/css/css-fonts/parsing/WEB_FEATURES.yml
+++ b/testing/web-platform/tests/css/css-fonts/parsing/WEB_FEATURES.yml
@@ -8,7 +8,21 @@ features:
- font-palette-values-*
- name: font-synthesis
files:
- - font-synthesis-*
+ - font-synthesis-computed.html
+ - font-synthesis-invalid.html
+ - font-synthesis-valid.html
+- name: font-synthesis-position
+ files:
+ - font-synthesis-position*
+- name: font-synthesis-small-caps
+ files:
+ - font-synthesis-small-caps*
+- name: font-synthesis-style
+ files:
+ - font-synthesis-style*
+- name: font-synthesis-weight
+ files:
+ - font-synthesis-weight*
- name: font-variant-alternates
files:
- font-variant-alternates-*
diff --git a/testing/web-platform/tests/css/css-fonts/parsing/font-palette-values-invalid.html b/testing/web-platform/tests/css/css-fonts/parsing/font-palette-values-invalid.html
index 2056055f34..b93a48fb37 100644
--- a/testing/web-platform/tests/css/css-fonts/parsing/font-palette-values-invalid.html
+++ b/testing/web-platform/tests/css/css-fonts/parsing/font-palette-values-invalid.html
@@ -115,13 +115,33 @@
@font-palette-values --A {
override-colors: 0 canvas;
}
+
+/* 19 */
+@font-palette-values --A {
+ override-colors: 0 currentcolor;
+}
+
+/* 20 */
+@font-palette-values --A {
+ override-colors: 0 light-dark(white, black);
+}
+
+/* 21 */
+@font-palette-values --A {
+ override-colors: 0 color-mix(in lch, red, canvas);
+}
+
+/* 22 */
+@font-palette-values --A {
+ override-colors: 0 light-dark(white, currentcolor);
+}
</style>
</head>
<body>
<script>
let rules = document.getElementById("style").sheet.cssRules;
test(function() {
- assert_equals(rules.length, 19);
+ assert_equals(rules.length, 23);
});
test(function() {
@@ -283,6 +303,34 @@ test(function() {
assert_equals(rule.basePalette, "");
assert_equals(rule.overrideColors, "");
});
+
+test(function() {
+ let text = rules[19].cssText;
+ let rule = rules[19];
+ assert_equals(text.indexOf("override-colors"), -1);
+ assert_equals(rule.overrideColors, "");
+});
+
+test(function() {
+ let text = rules[20].cssText;
+ let rule = rules[20];
+ assert_equals(text.indexOf("override-colors"), -1);
+ assert_equals(rule.overrideColors, "");
+});
+
+test(function() {
+ let text = rules[21].cssText;
+ let rule = rules[21];
+ assert_equals(text.indexOf("override-colors"), -1);
+ assert_equals(rule.overrideColors, "");
+});
+
+test(function() {
+ let text = rules[22].cssText;
+ let rule = rules[22];
+ assert_equals(text.indexOf("override-colors"), -1);
+ assert_equals(rule.overrideColors, "");
+});
</script>
</body>
</html>
diff --git a/testing/web-platform/tests/css/css-fonts/parsing/font-palette-values-valid.html b/testing/web-platform/tests/css/css-fonts/parsing/font-palette-values-valid.html
index 3c0c0626f5..99fceff234 100644
--- a/testing/web-platform/tests/css/css-fonts/parsing/font-palette-values-valid.html
+++ b/testing/web-platform/tests/css/css-fonts/parsing/font-palette-values-valid.html
@@ -103,6 +103,11 @@
@font-palette-values --P {
font-family: foo, bar, baz;
}
+
+/* 17 */
+@font-palette-values --Q {
+ override-colors: 0 color-mix(in lch, red, blue);
+}
</style>
</head>
<body>
@@ -385,6 +390,14 @@ test(function() {
assert_equals(rule.basePalette, "");
assert_equals(rule.overrideColors, "");
});
+
+test(function() {
+ let rule = rules[17];
+ assert_equals(rule.name, "--Q");
+ assert_equals(rule.fontFamily, "");
+ assert_equals(rule.basePalette, "");
+ assert_equals(rule.overrideColors, "0 color-mix(in lch, red, blue)");
+});
</script>
</body>
</html>
diff --git a/testing/web-platform/tests/css/css-fonts/resources/vs/MPLUS1-Regular_without-cmap14-subset.ttf b/testing/web-platform/tests/css/css-fonts/resources/vs/MPLUS1-Regular_without-cmap14-subset.ttf
new file mode 100644
index 0000000000..b77096690d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-fonts/resources/vs/MPLUS1-Regular_without-cmap14-subset.ttf
Binary files differ
diff --git a/testing/web-platform/tests/css/css-fonts/resources/vs/NotoColorEmoji-Regular_subset.ttf b/testing/web-platform/tests/css/css-fonts/resources/vs/NotoColorEmoji-Regular_subset.ttf
new file mode 100644
index 0000000000..24ab79fd05
--- /dev/null
+++ b/testing/web-platform/tests/css/css-fonts/resources/vs/NotoColorEmoji-Regular_subset.ttf
Binary files differ
diff --git a/testing/web-platform/tests/css/css-fonts/resources/vs/NotoEmoji-Regular_subset.ttf b/testing/web-platform/tests/css/css-fonts/resources/vs/NotoEmoji-Regular_subset.ttf
new file mode 100644
index 0000000000..0b054c7c8d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-fonts/resources/vs/NotoEmoji-Regular_subset.ttf
Binary files differ
diff --git a/testing/web-platform/tests/css/css-fonts/resources/vs/NotoEmoji-Regular_without-cmap14-subset.ttf b/testing/web-platform/tests/css/css-fonts/resources/vs/NotoEmoji-Regular_without-cmap14-subset.ttf
new file mode 100644
index 0000000000..3d00ef81de
--- /dev/null
+++ b/testing/web-platform/tests/css/css-fonts/resources/vs/NotoEmoji-Regular_without-cmap14-subset.ttf
Binary files differ
diff --git a/testing/web-platform/tests/css/css-fonts/resources/vs/NotoSansJP-Regular_with-cmap14-subset.ttf b/testing/web-platform/tests/css/css-fonts/resources/vs/NotoSansJP-Regular_with-cmap14-subset.ttf
new file mode 100644
index 0000000000..edcb98e374
--- /dev/null
+++ b/testing/web-platform/tests/css/css-fonts/resources/vs/NotoSansJP-Regular_with-cmap14-subset.ttf
Binary files differ
diff --git a/testing/web-platform/tests/css/css-fonts/resources/vs/NotoSansMath-Regular_without-cmap14-subset.ttf b/testing/web-platform/tests/css/css-fonts/resources/vs/NotoSansMath-Regular_without-cmap14-subset.ttf
new file mode 100644
index 0000000000..5436d06dae
--- /dev/null
+++ b/testing/web-platform/tests/css/css-fonts/resources/vs/NotoSansMath-Regular_without-cmap14-subset.ttf
Binary files differ
diff --git a/testing/web-platform/tests/css/css-fonts/resources/vs/STIXTwoMath-Regular_with-cmap14-subset.ttf b/testing/web-platform/tests/css/css-fonts/resources/vs/STIXTwoMath-Regular_with-cmap14-subset.ttf
new file mode 100644
index 0000000000..c9720cf9ce
--- /dev/null
+++ b/testing/web-platform/tests/css/css-fonts/resources/vs/STIXTwoMath-Regular_with-cmap14-subset.ttf
Binary files differ
diff --git a/testing/web-platform/tests/css/css-fonts/support/css/variation-sequences.css b/testing/web-platform/tests/css/css-fonts/support/css/variation-sequences.css
new file mode 100644
index 0000000000..5977f17b67
--- /dev/null
+++ b/testing/web-platform/tests/css/css-fonts/support/css/variation-sequences.css
@@ -0,0 +1,38 @@
+@font-face {
+ font-family: "MonoEmojiFont";
+ src: url(../../resources/vs/NotoEmoji-Regular_subset.ttf);
+}
+
+@font-face {
+ font-family: "ColorEmojiFont";
+ src: url(../../resources/vs/NotoColorEmoji-Regular_subset.ttf);
+}
+
+@font-face {
+ font-family: "EmojiFontWithBaseCharOnly";
+ src: url(../../resources/vs/NotoEmoji-Regular_without-cmap14-subset.ttf);
+}
+
+@font-face {
+ font-family: "CJKFontWithVS";
+ src: url(../../resources/vs/NotoSansJP-Regular_with-cmap14-subset.ttf);
+}
+
+@font-face {
+ font-family: "CJKFontWithBaseCharOnly";
+ src: url(../../resources/vs/MPLUS1-Regular_without-cmap14-subset.ttf);
+}
+
+@font-face {
+ font-family: "MathFontWithVS";
+ src: url(../../resources/vs/STIXTwoMath-Regular_with-cmap14-subset.ttf);
+}
+
+@font-face {
+ font-family: "MathFontWithBaseCharOnly";
+ src: url(../../resources/vs/NotoSansMath-Regular_without-cmap14-subset.ttf);
+}
+
+body {
+ font-size: 24px;
+} \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-fonts/support/js/variation-sequences.js b/testing/web-platform/tests/css/css-fonts/support/js/variation-sequences.js
new file mode 100644
index 0000000000..84c5a1a9c7
--- /dev/null
+++ b/testing/web-platform/tests/css/css-fonts/support/js/variation-sequences.js
@@ -0,0 +1,125 @@
+var baseChars = {
+ "emoji": "\u{1fae8}",
+ "cjk": "\u{8279}",
+ "math": "\u{2205}"
+};
+
+var variationSelectors = {
+ "emoji": ["\u{fe0e}", "\u{fe0f}"],
+ "cjk": ["", "\u{FE00}", "\u{FE01}", "\u{e0100}", "\u{e0101}",
+ "\u{e0102}"
+ ],
+ "math": ["", "\u{FE00}"]
+};
+
+var families = {
+ "emoji": ["ColorEmojiFont", "MonoEmojiFont",
+ "EmojiFontWithBaseCharOnly",
+ "sans-serif"
+ ],
+ "cjk": ["CJKFontWithVS", "CJKFontWithBaseCharOnly",
+ "sans-serif"
+ ],
+ "math": ["MathFontWithVS", "MathFontWithBaseCharOnly",
+ "sans-serif"
+ ]
+};
+
+var variationSequenceFamilies = new Map([
+ ["\u{1fae8}\u{fe0e}", "MonoEmojiFont"],
+ ["\u{1fae8}\u{fe0f}", "ColorEmojiFont"],
+ ["\u{8279}\u{fe00}", "CJKFontWithVS"],
+ ["\u{8279}\u{fe01}", "CJKFontWithVS"],
+ ["\u{8279}\u{e0100}", "CJKFontWithVS"],
+ ["\u{8279}\u{e0101}", "CJKFontWithVS"],
+ ["\u{8279}\u{e0102}", "CJKFontWithVS"],
+ ["\u{2205}\u{FE00}", "MathFontWithVS"]
+]);
+
+var baseCharFamilies = new Map([
+ ["\u{1fae8}", new Set(["MonoEmojiFont", "ColorEmojiFont",
+ "EmojiFontWithBaseCharOnly"
+ ])],
+ ["\u{8279}", new Set(["CJKFontWithVS",
+ "CJKFontWithBaseCharOnly"
+ ])],
+ ["\u{2205}", new Set(["MathFontWithVS",
+ "MathFontWithBaseCharOnly"
+ ])]
+]);
+
+const range = function*(l) {
+ for (let i = 0; i < l; i += 1) yield i;
+}
+const isEmpty = arr =>
+ arr.length === 0;
+
+const permutations =
+ function*(a) {
+ const r = arguments[1] || [];
+ if (isEmpty(a))
+ yield r;
+ for (let i of range(a.length)) {
+ const aa = [...a];
+ const rr = [...r, ...aa.splice(i, 1)];
+ yield* permutations(aa, rr);
+ }
+}
+
+function getMatchedFamilyForVariationSequence(
+ familyList, baseCharacter, variationSelector) {
+ const variationSequence = baseCharacter + variationSelector;
+ // First try to find a match for the whole variation sequence.
+ if (variationSequenceFamilies.has(variationSequence)) {
+ const matchedFamily = variationSequenceFamilies.get(variationSequence);
+ if (familyList.includes(matchedFamily)) {
+ return matchedFamily;
+ }
+ }
+ // If failed, try to match only the base character from the
+ // variation sequence.
+ if (baseCharFamilies.has(baseCharacter)) {
+ const eligibleFamilies = baseCharFamilies.get(baseCharacter);
+ const matchedFamilies =
+ familyList.filter(value => eligibleFamilies.has(value));
+ if (matchedFamilies.length) {
+ return matchedFamilies[0];
+ }
+ }
+ // We should not reach here, we should always match one of the
+ // specified web fonts in the tests.
+ return "";
+}
+
+function generateContent(
+ families, baseChar, variationSelectors, getFontFamilyValue) {
+ var rootElem = document.createElement('div');
+ // We want to test all possible combinations of variation
+ // selectors and font-family list values. For the refs,
+ // we explicitly specify the font that we expect to be
+ // matched from the maps at the beginning of the files.
+ const allFamiliesLists = permutations(families);
+ for (const familyList of allFamiliesLists) {
+ for (const variationSelector of variationSelectors) {
+ const contentSpan = document.createElement("span");
+ contentSpan.textContent = baseChar + variationSelector;
+ contentSpan.style.fontFamily =
+ getFontFamilyValue(familyList, baseChar, variationSelector);
+ rootElem.appendChild(contentSpan);
+ }
+ }
+ document.body.appendChild(rootElem);
+}
+
+function generateVariationSequenceTests(type) {
+ var getFontFamilyValue = (familyList, baseChar, variationSelector) => {
+ return familyList.join(', ');
+ }
+ generateContent(families[type], baseChars[type], variationSelectors[type], getFontFamilyValue);
+}
+
+function generateVariationSequenceRefs(type) {
+ generateContent(
+ families[type], baseChars[type], variationSelectors[type],
+ getMatchedFamilyForVariationSequence);
+}
diff --git a/testing/web-platform/tests/css/css-fonts/variation-sequences-ref.html b/testing/web-platform/tests/css/css-fonts/variation-sequences-ref.html
new file mode 100644
index 0000000000..a44f18bb3e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-fonts/variation-sequences-ref.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="UTF-8" />
+<title>CSS Test: Cluster Matching Variation Sequences</title>
+<link rel="stylesheet" type="text/css" href="support/css/variation-sequences.css" />
+<script type="text/javascript" src="support/js/variation-sequences.js"></script>
+<body></body>
+<script>
+ generateVariationSequenceRefs("emoji");
+ generateVariationSequenceRefs("cjk");
+ generateVariationSequenceRefs("math");
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-fonts/variation-sequences.html b/testing/web-platform/tests/css/css-fonts/variation-sequences.html
new file mode 100644
index 0000000000..91e46a84d7
--- /dev/null
+++ b/testing/web-platform/tests/css/css-fonts/variation-sequences.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="UTF-8" />
+<title>CSS Test: Cluster Matching Variation Sequences</title>
+<link rel="help" href="https://www.w3.org/TR/css-fonts-4/#cluster-matching" />
+<link rel="help" href="https://unicode.org/reports/tr51/" />
+<link rel="help" href="https://unicode.org/reports/tr37/" />
+<link rel="help" href="https://www.unicode.org/Public/UNIDATA/StandardizedVariants.txt" />
+<link rel="help" href="https://www.unicode.org/versions/Unicode15.1.0/ch23.pdf#G19053" />
+<link rel="match" href="variation-sequences-ref.html">
+<meta name="assert" content="Variation sequences should be taken into account during cluster matching.">
+<link rel="stylesheet" type="text/css" href="support/css/variation-sequences.css" />
+<script type="text/javascript" src="support/js/variation-sequences.js"></script>
+<body></body>
+<script>
+ generateVariationSequenceTests("emoji");
+ generateVariationSequenceTests("cjk");
+ generateVariationSequenceTests("math");
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-001-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-001-ref.html
index fff6cf2138..fff6cf2138 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-001-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-001-ref.html
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-001.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-001.html
index dfefd998c6..387ee9da5e 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-001.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-001.html
@@ -5,9 +5,13 @@
-->
<html><head>
<meta charset="utf-8">
- <title>CSS Grid Test: Masonry layout with `align-content` in masonry axis</title>
+ <title>CSS Grid Test: Masonry layout with `align-content` in masonry axis (horizontal writing mode)</title>
+ <meta name="assert"
+ content="Test passes if align-content shifts content
+ in the masonry axis of a masonry grid container
+ and the grid container's baseline is also shifted accordingly.">
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#alignment">
<link rel="match" href="masonry-align-content-001-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-002-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-002-ref.html
index 67318b323a..67318b323a 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-002-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-002-ref.html
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-002.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-002.html
index 75b82654fe..be7e7a813e 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-002.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-002.html
@@ -5,9 +5,12 @@
-->
<html><head>
<meta charset="utf-8">
- <title>CSS Grid Test: Masonry layout with `align-content` in masonry axis</title>
+ <title>CSS Grid Test: Masonry layout with `align-content` in masonry axis (vertical writing mode)</title>
+ <meta name="assert"
+ content="Test passes if align-content shifts content
+ in the masonry axis of a vertical writing-mode masonry grid container.">
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#alignment">
<link rel="match" href="masonry-align-content-002-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-003-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-003-ref.html
index 9780d5f5dc..9da15c0f40 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-003-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-003-ref.html
@@ -8,31 +8,31 @@
<title>Reference: Masonry layout with `align-content` in grid axis</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
<style>
-html,body {
- color:black; background-color:white; font:15px/1 monospace; padding:0; margin:0;
-}
+ html,body {
+ color:black; background-color:white; font:15px/1 monospace; padding:0; margin:0;
+ }
-grid {
- display: inline-grid;
- gap: 1px 2px;
- grid-template-columns: 1ch auto;
- grid-template-rows: repeat(4,auto);
- background: content-box silver;
- border: 1px solid;
- padding: 0 3px 2px 0;
- width: 100px;
- height: 120px;
- align-content: center;
- justify-items: start;
-}
+ grid {
+ display: inline-grid;
+ gap: 1px 2px;
+ grid-template-columns: 1ch auto;
+ grid-template-rows: repeat(4,19px);
+ background: content-box silver;
+ border: 1px solid;
+ padding: 0 3px 2px 0;
+ width: 100px;
+ height: 120px;
+ align-content: center;
+ justify-items: start;
+ }
-item {
- background-color: #444;
- color: #fff;
-}
+ item {
+ background-color: #444;
+ color: #fff;
+ }
-.tall { grid-row: span 2; padding: 3px 11px 1px 13px; }
-</style>
+ .tall { grid-row: span 2; padding: 3px 11px 1px 13px; }
+ </style>
</head>
<body>
@@ -90,7 +90,7 @@ item {
<item style="grid-row:3/4">6</item>
</grid>
-<grid style="align-content:stretch">
+<grid style="align-content:stretch; grid-template-rows: repeat(4,29.25px);">
<item class="tall" style="grid-row:1/2">1</item>
<item style="grid-row:2/3">2</item>
<item style="grid-row:3/4">3</item>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-003.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-003.html
index ae58e79cb2..818d7e2d84 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-003.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-003.html
@@ -5,9 +5,13 @@
-->
<html><head>
<meta charset="utf-8">
- <title>CSS Grid Test: Masonry layout with `align-content` in grid axis</title>
+ <title>CSS Grid Test: Masonry layout with `align-content` in grid axis (horizontal writing mode)</title>
+ <meta name="assert"
+ content="Test passes if align-content distributes tracks
+ in the grid axis of a masonry grid container
+ and the grid container's baseline is also shifted accordingly.">
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#alignment">
<link rel="match" href="masonry-align-content-003-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-004-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-004-ref.html
index de11c836b8..de11c836b8 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-004-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-004-ref.html
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-004.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-004.html
index 3f07aa1fe7..747420ba46 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/align-content/masonry-align-content-004.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-align-content-004.html
@@ -5,9 +5,12 @@
-->
<html><head>
<meta charset="utf-8">
- <title>CSS Grid Test: Masonry layout with `align-content` in grid axis</title>
+ <title>CSS Grid Test: Masonry layout with `align-content` in grid axis (vertical writing mode)</title>
+ <meta name="assert"
+ content="Test passes if align-content distributes tracks
+ in the grid axis of a vertical writing-mode masonry grid container.">
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#alignment">
<link rel="match" href="masonry-align-content-004-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-001-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-001-ref.html
index f7d9ccf48f..f7d9ccf48f 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-001-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-001-ref.html
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-001.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-001.html
index 3d60ac19fe..36ecd4f3b3 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-001.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-001.html
@@ -5,9 +5,12 @@
-->
<html><head>
<meta charset="utf-8">
- <title>CSS Grid Test: Masonry layout with `justify-content` in grid axis</title>
+ <title>CSS Grid Test: Masonry layout with `justify-content` in grid axis (horizontal writing mode)</title>
+ <meta name="assert"
+ content="Test passes if justify-content distributes tracks
+ in the grid axis of a masonry grid container.">
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#alignment">
<link rel="match" href="masonry-justify-content-001-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-002-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-002-ref.html
index 6889af7eac..6889af7eac 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-002-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-002-ref.html
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-002.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-002.html
index b1db084d4e..fc3696282c 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-002.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-002.html
@@ -5,9 +5,12 @@
-->
<html><head>
<meta charset="utf-8">
- <title>CSS Grid Test: Masonry layout with `justify-content` in grid axis</title>
+ <title>CSS Grid Test: Masonry layout with `justify-content` in grid axis (vertical writing mode)</title>
+ <meta name="assert"
+ content="Test passes if justify-content distributes tracks
+ in the grid axis of a vertical writing-mode masonry grid container.">
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#alignment">
<link rel="match" href="masonry-justify-content-002-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-003-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-003-ref.html
index 81d0fba410..81d0fba410 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-003-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-003-ref.html
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-003.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-003.html
index 772984b9e0..e2e6ead819 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-003.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-003.html
@@ -5,9 +5,12 @@
-->
<html><head>
<meta charset="utf-8">
- <title>CSS Grid Test: Masonry layout with `justify-content` in masonry axis</title>
+ <title>CSS Grid Test: Masonry layout with `justify-content` in masonry axis (horizontal writing mode)</title>
+ <meta name="assert"
+ content="Test passes if justify-content shifts content
+ in the masonry axis of a masonry grid container.">
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#alignment">
<link rel="match" href="masonry-justify-content-003-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-004-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-004-ref.html
index 43af71fc01..43af71fc01 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-004-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-004-ref.html
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-004.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-004.html
index 7d04ffeb4b..e309ef8870 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/justify-content/masonry-justify-content-004.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/alignment/masonry-justify-content-004.html
@@ -5,9 +5,12 @@
-->
<html><head>
<meta charset="utf-8">
- <title>CSS Grid Test: Masonry layout with `justify-content` in masonry axis</title>
+ <title>CSS Grid Test: Masonry layout with `justify-content` in masonry axis (vertical writing mode)</title>
+ <meta name="assert"
+ content="Test passes if justify-content shifts content
+ in the masonry axis of a vertical writing-mode masonry grid container.">
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#alignment">
<link rel="match" href="masonry-justify-content-004-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-001.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-001.html
index 69182c5425..24a5b29061 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-001.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-001.html
@@ -7,7 +7,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry layout fragmentation</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3">
<link rel="match" href="masonry-fragmentation-001-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-002.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-002.html
index 01c4de00b5..76a0540fa9 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-002.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-002.html
@@ -7,7 +7,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry layout fragmentation</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3">
<link rel="match" href="masonry-fragmentation-002-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-003.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-003.html
index fe150e7f83..7252fc169e 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-003.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/fragmentation/masonry-fragmentation-003.html
@@ -7,7 +7,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Grid axis fragmentation with column masonry layout</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3">
<link rel="match" href="masonry-fragmentation-003-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-001-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-001-ref.html
index 031629e926..7157460267 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-001-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-001-ref.html
@@ -15,7 +15,7 @@ html,body {
grid {
display: inline-grid;
gap: 10px 20px;
- grid-template-columns: auto min-content repeat(2,auto);
+ grid-template-columns: auto min-content repeat(2,auto);
color: #444;
border: 1px solid;
padding: 2px;
@@ -27,6 +27,8 @@ item {
padding: 20px;
margin: 3px;
border: 5px solid blue;
+ place-self: start;
+ grid-row: span 2;
}
</style>
</head>
@@ -34,9 +36,8 @@ item {
<grid>
<item>1</item>
- <item style="padding:0; place-self:start; min-width:0">2</item>
+ <item style="padding:0; grid-row: span 1">2</item>
<item>3</item>
<item>4</item>
- <item style="order:1; width:0; margin-right:-23px; margin-top:-37px; align-self:start">5</item>
- <item>6</item>
+ <item style="padding:0; grid-column: 2; grid-row: 2">5</item>
</grid>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-001.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-001.html
index 673bbe40e4..628c00135d 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-001.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-001.html
@@ -7,7 +7,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry layout with definite `gap` in both axes</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#alignment">
<link rel="match" href="masonry-gap-001-ref.html">
<style>
html,body {
@@ -17,7 +17,7 @@ html,body {
grid {
display: inline-grid;
gap: 10px 20px;
- grid-template-columns: repeat(4,auto);
+ grid-template-columns: repeat(4,auto);
grid-template-rows: masonry;
color: #444;
border: 1px solid;
@@ -36,10 +36,9 @@ item {
<body>
<grid>
- <item>1</item>
- <item style="padding:0">2</item>
- <item>3</item>
- <item>4</item>
- <item>5</item>
- <item>6</item>
+ <item style="grid-column: 1;">1</item>
+ <item style="padding:0; grid-column: 2;">2</item>
+ <item style="grid-column: 3;">3</item>
+ <item style="grid-column: 4;">4</item>
+ <item style="padding:0; grid-column: 2;">5</item>
</grid>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-002-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-002-ref.html
new file mode 100644
index 0000000000..834a884f76
--- /dev/null
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-002-ref.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html><head>
+ <meta charset="utf-8">
+ <title>Reference: Masonry layout with normal `gap` in both axes</title>
+ <link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
+ <link rel="author" title="Elika J. Etemad" href="https://fantasai.inkedblade.net/contact">
+ <style>
+html,body {
+ color:black; background-color:white; font:25px/1 "Courier New", monospace; padding:0; margin:0;
+}
+
+grid {
+ display: inline-grid;
+ gap: 0;
+ grid-template-columns: auto min-content repeat(2,auto);
+ color: #444;
+ border: 1px solid;
+ padding: 2px;
+}
+
+item {
+ background-color: #444;
+ color: #fff;
+ padding: 20px;
+ margin: 3px;
+ border: 5px solid blue;
+ place-self: start;
+ grid-row: span 2;
+ display: block;
+}
+</style>
+</head>
+<body>
+
+<grid>
+ <item>1</item>
+ <item style="padding:0; grid-row: span 1">2</item>
+ <item>3</item>
+ <item>4</item>
+ <item style="padding:0; grid-column: 2; grid-row: 2">5</item>
+</grid>
+
+<grid>
+ <item>1</item>
+ <item style="padding:0; grid-row: span 1">2</item>
+ <item>3</item>
+ <item>4</item>
+ <item style="padding:0; grid-column: 2; grid-row: 2">5</item>
+</grid>
+
+<grid>
+ <div style="columns: 3; column-gap: 1em;">
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ </div>
+</grid>
+
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-002.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-002.html
new file mode 100644
index 0000000000..6dae0a1a72
--- /dev/null
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/gap/masonry-gap-002.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html><head>
+ <meta charset="utf-8">
+ <title>CSS Grid Test: Masonry layout with normal `gap` in both axes</title>
+ <meta name="assert"
+ content="Test passes if a 'normal' gap is the initial value,
+ is resolved to zero,
+ does not allow margin collapsing,
+ and inherits as 'normal' in masonry layout">
+ <link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
+ <link rel="author" title="Elika J. Etemad" href="https://fantasai.inkedblade.net/contact">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#alignment">
+ <link rel="match" href="masonry-gap-001-ref.html">
+ <style>
+html,body {
+ color:black; background-color:white; font:25px/1 "Courier New", monospace; padding:0; margin:0;
+}
+
+grid {
+ display: inline-grid;
+ grid-template-columns: repeat(4,auto);
+ grid-template-rows: masonry;
+ color: #444;
+ border: 1px solid;
+ padding: 2px;
+}
+
+item {
+ background-color: #444;
+ color: #fff;
+ padding: 20px;
+ margin: 3px;
+ border: 5px solid blue;
+ display: block;
+}
+
+</style>
+</head>
+<body>
+
+<!-- test initial value of normal -->
+<grid>
+ <item style="grid-column: 1;">1</item>
+ <item style="padding:0; grid-column: 2;">2</item>
+ <item style="grid-column: 3;">3</item>
+ <item style="grid-column: 4;">4</item>
+ <item style="padding:0; grid-column: 2;">5</item>
+</grid>
+
+<!-- test that 'normal' is supported and also resolves to zero -->
+<grid style="gap: 10px; gap: normal">
+ <item style="grid-column: 1;">1</item>
+ <item style="padding:0; grid-column: 2;">2</item>
+ <item style="grid-column: 3;">3</item>
+ <item style="grid-column: 4;">4</item>
+ <item style="padding:0; grid-column: 2;">5</item>
+</grid>
+
+<!-- test inheritance of normal as keyword -->
+
+<grid style="gap: 10px; gap: normal">
+ <div style="columns: 3; gap: inherit;">
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ </div>
+</grid>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-001-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-001-ref.html
index c99b117f2e..2aefa719dc 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-001-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-001-ref.html
@@ -5,7 +5,7 @@
-->
<html><head>
<meta charset="utf-8">
- <title>Reference: Placement involving named lines</title>
+ <title>Reference: Definite column placement in a masonry grid (single edge)</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
<style>
body,html { color:black; background:white; font-size:15px/1 monospace; padding:0; margin:0; }
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-001.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-001.html
index b6745f6973..8bb78e307d 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-001.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-001.html
@@ -5,8 +5,9 @@
-->
<html><head>
<meta charset="utf-8">
- <title>CSS Grid Test: Placement involving named lines</title>
+ <title>CSS Grid Test: Definite column placement in a masonry grid (single edge)</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#grid-template-masonry">
<link rel="help" href="https://drafts.csswg.org/css-grid/#grid-placement-int">
<link rel="match" href="masonry-grid-placement-named-lines-001-ref.html">
<style>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-002-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-002-ref.html
index dd589f90c5..cfe1369e87 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-002-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-002-ref.html
@@ -5,7 +5,7 @@
-->
<html><head>
<meta charset="utf-8">
- <title>Reference: Placement involving named lines</title>
+ <title>Reference: Definite column placement in a masonry grid (double edge)</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
<style>
body,html { color:black; background:white; font:15px/1 monospace; padding:0; margin:0; }
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-002.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-002.html
index 0ad0dfcea3..4e51f09f94 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-002.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/grid-placement/masonry-grid-placement-named-lines-002.html
@@ -5,8 +5,9 @@
-->
<html><head>
<meta charset="utf-8">
- <title>CSS Grid Test: Placement involving named lines</title>
+ <title>CSS Grid Test: Definite column placement in a masonry grid (double edge)</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#grid-template-masonry">
<link rel="help" href="https://drafts.csswg.org/css-grid/#grid-placement-int">
<link rel="match" href="masonry-grid-placement-named-lines-002-ref.html">
<style>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-001-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-001-ref.html
index 898bff16d8..1a9f65aaf6 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-001-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-001-ref.html
@@ -3,41 +3,38 @@
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
-<html><head>
+<html>
<meta charset="utf-8">
<title>Reference: Masonry layout intrinsic sizing</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
<style>
-html,body {
- color:black; background-color:white; font:15px/1 Ahem; padding:0; margin:0;
-}
+
+@import "support/masonry-intrinsic-sizing-visual.css";
grid {
display: inline-grid;
gap: 1px 2px;
- grid-template-columns: repeat(4,auto);
- grid-auto-rows: 1em;
+ grid-auto-rows: auto;
border: 1px solid;
padding: 0 1px 0 2px;
vertical-align: top;
}
-.fr {
+.auto grid {
+ grid-template-columns: repeat(4,auto);
+}
+.fr grid {
grid-template-columns: 1fr 2fr 1fr 1fr;
}
-.mixed {
+.mixed grid {
grid-template-columns: 1fr 2fr min-content max-content;
}
-
-item {
- background-color: #444;
- color: blue;
-}
</style>
-</head>
+
<body>
-<grid>
+<section class="auto">
+<grid style="grid-template-columns: repeat(4, 2ch)">
<item style="width:2ch">1</item>
<item>2</item>
<item>3</item>
@@ -45,7 +42,7 @@ item {
<item>5</item>
</grid>
-<grid style="grid-template-columns: 1ch repeat(3,auto)">
+<grid style="grid-template-columns: repeat(4, 2ch)">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -58,7 +55,7 @@ item {
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:2ch; grid-column:2">5</item>
+ <item style="width:2ch; grid-area:1/2">5</item>
</grid>
<grid>
@@ -66,26 +63,28 @@ item {
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:4ch; grid-column:2/span 2">5</item>
+ <item style="visibility: hidden">4</item>
+ <item style="width:4ch; grid-area:1/2/2/4">5</item>
</grid>
<grid>
- <item>1</item>
+ <item style="grid-column: 4">1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="grid-column:2/span 2">5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item style="visibility: hidden">4</item>
+ <item style="grid-area: 1/2/2/span 2">5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
</grid>
-
<grid>
- <item>1</item>
+ <item style="grid-column: 4">1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:3ch; grid-column:2/span 2">5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item style="visibility: hidden">4</item>
+ <item style="width:3ch; grid-area:1/2/2/4">5</item>
+ <item style="width:5ch; grid-area:2/1/3/4">6</item>
</grid>
<grid>
@@ -104,17 +103,20 @@ item {
<item style="width:6ch; grid-column:span 4">5</item>
</grid>
-<grid style="grid-template-columns: repeat(4,1ch)">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:6ch; grid-column:span 3">5</item>
+ <item style="width:6ch; grid-area: 2/1/3/4">5</item>
+ <item style="width:6ch; grid-area: 2/2/3/5; visibility: hidden">5</item>
</grid>
+</section>
<!-- ditto with 'fr' sizing -->
-<grid class="fr">
+<section class="fr">
+<grid>
<item style="width:2ch">1</item>
<item>2</item>
<item>3</item>
@@ -122,7 +124,7 @@ item {
<item>5</item>
</grid>
-<grid class="fr" style="grid-template-columns: 1ch 2fr 1fr 1fr">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
@@ -130,42 +132,43 @@ item {
<item style="width:2ch">5</item>
</grid>
-<grid class="fr">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:2ch; grid-column:2">5</item>
+ <item style="width:2ch; grid-area: 1/2">5</item>
</grid>
-<grid class="fr">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:4ch; grid-column:2/span 2">5</item>
+ <item style="width:4ch; grid-area: 1/2/2/4">5</item>
</grid>
-<grid class="fr">
- <item>1</item>
+<grid>
+ <item style="grid-column: 4">1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="grid-column:2/span 2">5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item style="visibility: hidden">4</item>
+ <item style="grid-area: 1/2/2/span 2">5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
</grid>
-
-<grid class="fr">
- <item>1</item>
+<grid>
+ <item style="grid-column: 4">1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:3ch; grid-column:2/span 2">5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item style="visibility: hidden">4</item>
+ <item style="width:3ch; grid-area:1/2/2/4">5</item>
+ <item style="width:5ch; grid-area:2/1/3/4">6</item>
</grid>
-<grid class="fr">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
@@ -173,7 +176,7 @@ item {
<item style="grid-column:span 4">5</item>
</grid>
-<grid class="fr">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
@@ -181,18 +184,20 @@ item {
<item style="width:6ch; grid-column:span 4">5</item>
</grid>
-<grid class="fr" style="grid-template-columns: 1ch 2ch 1ch 1ch">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:6ch; grid-column:span 3">5</item>
+ <item style="width:6ch; grid-area: 2/1/3/4">5</item>
+ <item style="width:6ch; grid-area: 2/2/3/5; visibility: hidden">5</item>
</grid>
-
+</section>
<!-- ditto with mixed sizing -->
-<grid class="mixed" style="grid-template-columns: 2ch 4ch 1ch 1ch">
+<section class="mixed">
+<grid style="grid-template-columns: 2ch 4ch 2ch 2ch">
<item style="width:2ch">1</item>
<item>2</item>
<item>3</item>
@@ -200,7 +205,7 @@ item {
<item>5</item>
</grid>
-<grid class="mixed" style="grid-template-columns: 1ch 2ch 1ch 1ch">
+<grid style="grid-template-columns: 2ch 4ch 2ch 2ch">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -208,42 +213,44 @@ item {
<item style="width:2ch">5</item>
</grid>
-<grid class="mixed">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:2ch; grid-column:2">5</item>
+ <item style="width:2ch; grid-area: 1/2">5</item>
</grid>
-<grid class="mixed">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:4ch; grid-column:2/span 2">5</item>
+ <item style="visibility: hidden">4</item>
+ <item style="width:4ch; grid-area: 1/2/2/4">5</item>
</grid>
-<grid class="mixed">
- <item>1</item>
+<grid>
+ <item style="grid-column: 4">1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="grid-column:2/span 2">5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item style="visibility: hidden">4</item>
+ <item style="grid-area: 1/2/2/span 2">5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
</grid>
-
-<grid class="mixed">
- <item>1</item>
+<grid>
+ <item style="grid-column: 4">1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:3ch; grid-column:2/span 2">5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item style="visibility: hidden">4</item>
+ <item style="width:3ch; grid-area:1/2/2/4">5</item>
+ <item style="width:5ch; grid-area:2/1/3/4">6</item>
</grid>
-<grid class="mixed">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
@@ -251,7 +258,7 @@ item {
<item style="grid-column:span 4">5</item>
</grid>
-<grid class="mixed">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
@@ -259,13 +266,13 @@ item {
<item style="width:6ch; grid-column:span 4">5</item>
</grid>
-<grid class="mixed" style="grid-template-columns: 1ch 2ch 1ch 1ch">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:6ch; grid-column:span 3">5</item>
+ <item style="width:6ch; grid-area: 2/1/3/4">5</item>
+ <item style="width:6ch; grid-area: 2/2/3/5; visibility: hidden">5</item>
</grid>
+</section>
-</body>
-</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-001.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-001.html
index 0f6cdb1495..cc0608eafe 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-001.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-001.html
@@ -3,43 +3,40 @@
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
-<html><head>
+<html>
<meta charset="utf-8">
- <title>CSS Grid Test: Masonry layout intrinsic sizing</title>
+ <title>CSS Grid Test: Masonry layout column sizing - intrinsic</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#track-sizing">
<link rel="match" href="masonry-intrinsic-sizing-001-ref.html">
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
<style>
-html,body {
- color:black; background-color:white; font:15px/1 Ahem; padding:0; margin:0;
-}
+
+@import "support/masonry-intrinsic-sizing-visual.css";
grid {
display: inline-grid;
gap: 1px 2px;
- grid-template-columns: repeat(4,auto);
grid-template-rows: masonry;
border: 1px solid;
padding: 0 1px 0 2px;
vertical-align: top;
}
-.fr {
+.auto grid {
+ grid-template-columns: repeat(4,auto);
+}
+.fr grid {
grid-template-columns: 1fr 2fr 1fr 1fr;
}
-.mixed {
+.mixed grid {
grid-template-columns: 1fr 2fr min-content max-content;
}
-
-item {
- background-color: #444;
- color: blue;
-}
</style>
-</head>
+
<body>
-<grid>
+<section class="auto">
+<grid title="Wider 1st item 2ch">
<item style="width:2ch">1</item>
<item>2</item>
<item>3</item>
@@ -47,7 +44,7 @@ item {
<item>5</item>
</grid>
-<grid>
+<grid title="Wider 5th item 2ch">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -55,7 +52,7 @@ item {
<item style="width:2ch">5</item>
</grid>
-<grid>
+<grid title="Wider 5th item 2ch in col 2">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -63,7 +60,7 @@ item {
<item style="width:2ch; grid-column:2">5</item>
</grid>
-<grid>
+<grid title="Wider 5th item 4ch in col 2-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -71,7 +68,7 @@ item {
<item style="width:4ch; grid-column:2/span 2">5</item>
</grid>
-<grid>
+<grid title="5th item in col 2-3, wider 6th item 5ch in col 1-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -80,8 +77,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-
-<grid>
+<grid title="5th item 3ch in 2-3, 6th item 5ch in 1-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -90,7 +86,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid>
+<grid title="5th item span 4">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -98,7 +94,7 @@ item {
<item style="grid-column:span 4">5</item>
</grid>
-<grid>
+<grid title="5th item 6ch span 4">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -106,17 +102,19 @@ item {
<item style="width:6ch; grid-column:span 4">5</item>
</grid>
-<grid>
+<grid title="5th item 6ch span 3">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item style="width:6ch; grid-column:span 3">5</item>
</grid>
+</section>
<!-- ditto with 'fr' sizing -->
-<grid class="fr">
+<section class="fr">
+<grid title="Wider 1st item 2ch">
<item style="width:2ch">1</item>
<item>2</item>
<item>3</item>
@@ -124,7 +122,7 @@ item {
<item>5</item>
</grid>
-<grid class="fr">
+<grid title="Wider 5th item 2ch">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -132,7 +130,7 @@ item {
<item style="width:2ch">5</item>
</grid>
-<grid class="fr">
+<grid title="Wider 5th item 2ch in col 2">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -140,7 +138,7 @@ item {
<item style="width:2ch; grid-column:2">5</item>
</grid>
-<grid class="fr">
+<grid title="Wider 5th item 4ch in col 2-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -148,7 +146,7 @@ item {
<item style="width:4ch; grid-column:2/span 2">5</item>
</grid>
-<grid class="fr">
+<grid title="5th item in col 2-3, wider 6th item 5ch in col 1-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -157,8 +155,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-
-<grid class="fr">
+<grid title="5th item 3ch in 2-3, 6th item 5ch in 1-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -167,7 +164,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid class="fr">
+<grid title="5th item span 4">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -175,7 +172,7 @@ item {
<item style="grid-column:span 4">5</item>
</grid>
-<grid class="fr">
+<grid title="5th item 6ch span 4">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -183,18 +180,19 @@ item {
<item style="width:6ch; grid-column:span 4">5</item>
</grid>
-<grid class="fr">
+<grid title="5th item 6ch span 3">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item style="width:6ch; grid-column:span 3">5</item>
</grid>
-
+</section>
<!-- ditto with mixed sizing -->
-<grid class="mixed">
+<section class="mixed">
+<grid title="Wider 1st item 2ch">
<item style="width:2ch">1</item>
<item>2</item>
<item>3</item>
@@ -202,7 +200,7 @@ item {
<item>5</item>
</grid>
-<grid class="mixed">
+<grid title="Wider 5th item 2ch">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -210,7 +208,7 @@ item {
<item style="width:2ch">5</item>
</grid>
-<grid class="mixed">
+<grid title="Wider 5th item 2ch in col 2">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -218,7 +216,7 @@ item {
<item style="width:2ch; grid-column:2">5</item>
</grid>
-<grid class="mixed">
+<grid title="Wider 5th item 4ch in col 2-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -226,7 +224,7 @@ item {
<item style="width:4ch; grid-column:2/span 2">5</item>
</grid>
-<grid class="mixed">
+<grid title="5th item in col 2-3, wider 6th item 5ch in col 1-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -236,7 +234,7 @@ item {
</grid>
-<grid class="mixed">
+<grid title="5th item 3ch in 2-3, 6th item 5ch in 1-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -245,7 +243,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid class="mixed">
+<grid title="5th item span 4">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -253,7 +251,7 @@ item {
<item style="grid-column:span 4">5</item>
</grid>
-<grid class="mixed">
+<grid title="5th item 6ch span 4">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -261,13 +259,12 @@ item {
<item style="width:6ch; grid-column:span 4">5</item>
</grid>
-<grid class="mixed">
+<grid title="5th item 6ch span 3">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item style="width:6ch; grid-column:span 3">5</item>
</grid>
+</section>
-</body>
-</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-002-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-002-ref.html
index 4f6519db94..ddae8e7fe3 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-002-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-002-ref.html
@@ -3,42 +3,39 @@
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
-<html><head>
+<html>
<meta charset="utf-8">
<title>Reference: Masonry layout intrinsic sizing</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
<style>
-html,body {
- color:black; background-color:white; font:15px/1 Ahem; padding:0; margin:0;
-}
+
+@import "support/masonry-intrinsic-sizing-visual.css";
grid {
display: inline-grid;
gap: 1px 2px;
- grid-template-columns: repeat(4,auto);
grid-auto-rows: 1em;
border: 1px solid;
padding: 0 1px 0 2px;
vertical-align: top;
width: min-content;
}
-.fr {
+.auto grid {
+ grid-template-columns: repeat(4,auto);
+}
+.fr grid {
grid-template-columns: 1fr 2fr 1fr 1fr;
}
-.mixed {
+.mixed grid {
grid-template-columns: 1fr 2fr min-content max-content;
}
-
-item {
- background-color: #444;
- color: blue;
-}
</style>
-</head>
+
<body>
-<grid>
+<section class="auto">
+<grid style="grid-template-columns: repeat(4, 2ch)">
<item style="width:2ch">1</item>
<item>2</item>
<item>3</item>
@@ -46,7 +43,7 @@ item {
<item>5</item>
</grid>
-<grid style="grid-template-columns: 1ch repeat(3,auto)">
+<grid style="grid-template-columns: repeat(4, 2ch)">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -59,7 +56,7 @@ item {
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:2ch; grid-column:2">5</item>
+ <item style="width:2ch; grid-area:1/2">5</item>
</grid>
<grid>
@@ -67,26 +64,28 @@ item {
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:4ch; grid-column:2/span 2">5</item>
+ <item style="visibility: hidden">4</item>
+ <item style="width:4ch; grid-area:1/2/2/4">5</item>
</grid>
<grid>
- <item>1</item>
+ <item style="grid-column: 4">1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="grid-column:2/span 2">5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item style="visibility: hidden">4</item>
+ <item style="grid-area: 1/2/2/span 2">5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
</grid>
-
<grid>
- <item>1</item>
+ <item style="grid-column: 4">1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:3ch; grid-column:2/span 2">5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item style="visibility: hidden">4</item>
+ <item style="width:3ch; grid-area:1/2/2/4">5</item>
+ <item style="width:5ch; grid-area:2/1/3/4">6</item>
</grid>
<grid>
@@ -105,17 +104,20 @@ item {
<item style="width:6ch; grid-column:span 4">5</item>
</grid>
-<grid style="grid-template-columns: repeat(4,1ch)">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:6ch; grid-column:span 3">5</item>
+ <item style="width:6ch; grid-area: 2/1/3/4">5</item>
+ <item style="width:6ch; grid-area: 2/2/3/5; visibility: hidden">5</item>
</grid>
+</section>
<!-- ditto with 'fr' sizing -->
-<grid class="fr">
+<section class="fr">
+<grid style="grid-template-columns: repeat(4, 2ch)">
<item style="width:2ch">1</item>
<item>2</item>
<item>3</item>
@@ -123,7 +125,7 @@ item {
<item>5</item>
</grid>
-<grid class="fr" style="grid-template-columns: 1ch 2fr 1fr 1fr">
+<grid style="grid-template-columns: repeat(4, 2ch)">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -131,42 +133,43 @@ item {
<item style="width:2ch">5</item>
</grid>
-<grid class="fr">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:2ch; grid-column:2">5</item>
+ <item style="width:2ch; grid-area: 1/2">5</item>
</grid>
-<grid class="fr">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:4ch; grid-column:2/span 2">5</item>
+ <item style="width:4ch; grid-area: 1/2/2/4">5</item>
</grid>
-<grid class="fr">
- <item>1</item>
+<grid>
+ <item style="grid-column: 4">1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="grid-column:2/span 2">5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item style="visibility: hidden">4</item>
+ <item style="grid-area: 1/2/2/span 2">5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
</grid>
-
-<grid class="fr">
- <item>1</item>
+<grid>
+ <item style="grid-column: 4">1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:3ch; grid-column:2/span 2">5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item style="visibility: hidden">4</item>
+ <item style="width:3ch; grid-area:1/2/2/4">5</item>
+ <item style="width:5ch; grid-area:2/1/3/4">6</item>
</grid>
-<grid class="fr">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
@@ -174,7 +177,7 @@ item {
<item style="grid-column:span 4">5</item>
</grid>
-<grid class="fr">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
@@ -182,18 +185,20 @@ item {
<item style="width:6ch; grid-column:span 4">5</item>
</grid>
-<grid class="fr" style="grid-template-columns: 1ch 1ch 1ch 1ch">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:6ch; grid-column:span 3">5</item>
+ <item style="width:6ch; grid-area: 2/1/3/4">5</item>
+ <item style="width:6ch; grid-area: 2/2/3/5; visibility: hidden">5</item>
</grid>
-
+</section>
<!-- ditto with mixed sizing -->
-<grid class="mixed" style="grid-template-columns: 2ch 1ch 1ch 1ch">
+<section class="mixed">
+<grid style="grid-template-columns: repeat(4, 2ch)">
<item style="width:2ch">1</item>
<item>2</item>
<item>3</item>
@@ -201,7 +206,7 @@ item {
<item>5</item>
</grid>
-<grid class="mixed" style="grid-template-columns: 1ch 1ch 1ch 1ch">
+<grid style="grid-template-columns: repeat(4, 2ch)">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -209,42 +214,44 @@ item {
<item style="width:2ch">5</item>
</grid>
-<grid class="mixed">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:2ch; grid-column:2">5</item>
+ <item style="width:2ch; grid-area: 1/2">5</item>
</grid>
-<grid class="mixed">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:4ch; grid-column:2/span 2">5</item>
+ <item style="visibility: hidden">4</item>
+ <item style="width:4ch; grid-area: 1/2/2/4">5</item>
</grid>
-<grid class="mixed">
- <item>1</item>
+<grid>
+ <item style="grid-column: 4">1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="grid-column:2/span 2">5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item style="visibility: hidden">4</item>
+ <item style="grid-area: 1/2/2/span 2">5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
</grid>
-
-<grid class="mixed">
- <item>1</item>
+<grid>
+ <item style="grid-column: 4">1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:3ch; grid-column:2/span 2">5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item style="visibility: hidden">4</item>
+ <item style="width:3ch; grid-area:1/2/2/4">5</item>
+ <item style="width:5ch; grid-area:2/1/3/4">6</item>
</grid>
-<grid class="mixed">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
@@ -252,7 +259,7 @@ item {
<item style="grid-column:span 4">5</item>
</grid>
-<grid class="mixed">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
@@ -260,13 +267,14 @@ item {
<item style="width:6ch; grid-column:span 4">5</item>
</grid>
-<grid class="mixed" style="grid-template-columns: 1ch 1ch 1ch 1ch">
+<grid>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="width:6ch; grid-column:span 3">5</item>
+ <item style="width:6ch; grid-area: 2/1/3/4">5</item>
+ <item style="width:6ch; grid-area: 2/2/3/5; visibility: hidden">5</item>
</grid>
+</section>
+
-</body>
-</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-002.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-002.html
index db83299bf5..3867bb13c8 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-002.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-002.html
@@ -3,44 +3,42 @@
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
-<html><head>
+<html>
<meta charset="utf-8">
- <title>CSS Grid Test: Masonry layout intrinsic sizing</title>
+ <title>CSS Grid Test: Masonry layout column sizing - min-content constraint</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#track-sizing">
<link rel="match" href="masonry-intrinsic-sizing-002-ref.html">
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
<style>
-html,body {
- color:black; background-color:white; font:15px/1 Ahem; padding:0; margin:0;
-}
+
+@import "support/masonry-intrinsic-sizing-visual.css";
grid {
display: inline-grid;
gap: 1px 2px;
- grid-template-columns: repeat(4,auto);
grid-template-rows: masonry;
border: 1px solid;
padding: 0 1px 0 2px;
vertical-align: top;
width: min-content;
}
-.fr {
+
+.auto grid {
+ grid-template-columns: repeat(4,auto);
+}
+.fr grid {
grid-template-columns: 1fr 2fr 1fr 1fr;
}
-.mixed {
+.mixed grid {
grid-template-columns: 1fr 2fr min-content max-content;
}
-
-item {
- background-color: #444;
- color: blue;
-}
</style>
-</head>
+
<body>
-<grid>
+<section class="auto">
+<grid title="Wider 1st item 2ch">
<item style="width:2ch">1</item>
<item>2</item>
<item>3</item>
@@ -48,7 +46,7 @@ item {
<item>5</item>
</grid>
-<grid>
+<grid title="Wider 5th item 2ch">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -56,7 +54,7 @@ item {
<item style="width:2ch">5</item>
</grid>
-<grid>
+<grid title="Wider 5th item 2ch in col 2">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -64,7 +62,7 @@ item {
<item style="width:2ch; grid-column:2">5</item>
</grid>
-<grid>
+<grid title="Wider 5th item 4ch in col 2-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -72,7 +70,7 @@ item {
<item style="width:4ch; grid-column:2/span 2">5</item>
</grid>
-<grid>
+<grid title="5th item in col 2-3, wider 6th item 5ch in col 1-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -81,8 +79,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-
-<grid>
+<grid title="5th item 3ch in 2-3, 6th item 5ch in 1-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -91,7 +88,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid>
+<grid title="5th item spanning 4">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -99,7 +96,7 @@ item {
<item style="grid-column:span 4">5</item>
</grid>
-<grid>
+<grid title="5th item 6ch spanning 4">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -107,17 +104,19 @@ item {
<item style="width:6ch; grid-column:span 4">5</item>
</grid>
-<grid>
+<grid title="5th item 6ch spanning 3">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item style="width:6ch; grid-column:span 3">5</item>
</grid>
+</section>
<!-- ditto with 'fr' sizing -->
-<grid class="fr">
+<section class="fr">
+<grid title="Wider 1st item 2ch">
<item style="width:2ch">1</item>
<item>2</item>
<item>3</item>
@@ -125,7 +124,7 @@ item {
<item>5</item>
</grid>
-<grid class="fr">
+<grid title="Wider 5th item 2ch">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -133,7 +132,7 @@ item {
<item style="width:2ch">5</item>
</grid>
-<grid class="fr">
+<grid title="Wider 5th item 2ch in col 2">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -141,7 +140,7 @@ item {
<item style="width:2ch; grid-column:2">5</item>
</grid>
-<grid class="fr">
+<grid title="Wider 5th item 4ch in col 2-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -149,7 +148,7 @@ item {
<item style="width:4ch; grid-column:2/span 2">5</item>
</grid>
-<grid class="fr">
+<grid title="5th item in col 2-3, wider 6th item 5ch in col 1-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -158,8 +157,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-
-<grid class="fr">
+<grid title="5th item 3ch in 2-3, 6th item 5ch in 1-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -168,7 +166,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid class="fr">
+<grid title="5th item span 4">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -176,7 +174,7 @@ item {
<item style="grid-column:span 4">5</item>
</grid>
-<grid class="fr">
+<grid title="5th item 6ch span 4">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -184,18 +182,19 @@ item {
<item style="width:6ch; grid-column:span 4">5</item>
</grid>
-<grid class="fr">
+<grid title="5th item 6ch span 3">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item style="width:6ch; grid-column:span 3">5</item>
</grid>
-
+</section>
<!-- ditto with mixed sizing -->
-<grid class="mixed">
+<section class="mixed">
+<grid title="Wider 1st item 2ch">
<item style="width:2ch">1</item>
<item>2</item>
<item>3</item>
@@ -203,7 +202,7 @@ item {
<item>5</item>
</grid>
-<grid class="mixed">
+<grid title="Wider 5th item 2ch">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -211,7 +210,7 @@ item {
<item style="width:2ch">5</item>
</grid>
-<grid class="mixed">
+<grid title="Wider 5th item 2ch in col 2">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -219,7 +218,7 @@ item {
<item style="width:2ch; grid-column:2">5</item>
</grid>
-<grid class="mixed">
+<grid title="Wider 5th item 4ch in col 2-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -227,7 +226,7 @@ item {
<item style="width:4ch; grid-column:2/span 2">5</item>
</grid>
-<grid class="mixed">
+<grid title="5th item in col 2-3, wider 6th item 5ch in col 1-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -236,8 +235,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-
-<grid class="mixed">
+<grid title="5th item 3ch in 2-3, 6th item 5ch in 1-3">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -246,7 +244,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid class="mixed">
+<grid title="5th item spanning 4">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -254,7 +252,7 @@ item {
<item style="grid-column:span 4">5</item>
</grid>
-<grid class="mixed">
+<grid title="5th item 6ch spanning 4">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -262,13 +260,12 @@ item {
<item style="width:6ch; grid-column:span 4">5</item>
</grid>
-<grid class="mixed">
+<grid title="5th item 6ch spanning 3">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item style="width:6ch; grid-column:span 3">5</item>
</grid>
+</section>
-</body>
-</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-003-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-003-ref.html
index 61eb21570d..044750dc45 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-003-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-003-ref.html
@@ -3,267 +3,331 @@
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
-<html><head>
+<html>
<meta charset="utf-8">
<title>Reference: Masonry layout min-content sizing</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
<style>
-html,body {
- color:black; background-color:white; font:15px/1 Ahem; padding:0; margin:0;
-}
+
+@import "support/masonry-intrinsic-sizing-visual.css";
grid {
display: inline-grid;
gap: 1px 2px;
- grid-template-columns: repeat(4,auto);
- grid-auto-rows: 1em;
border: 1px solid;
padding: 0 1px 0 2px;
vertical-align: top;
width: min-content;
}
-.fr {
+.auto grid {
+ grid-template-columns: repeat(4,auto);
+}
+.fr grid {
grid-template-columns: 1fr 2fr 1fr 1fr;
}
-.mixed {
+.mixed grid {
grid-template-columns: 1fr 2fr min-content max-content;
}
item {
- background-color: #444;
- color: blue;
+ align-self: start;
+}
+.hidden {
+ visibility: hidden;
+ height: 0;
}
-item.start { align-self: start; }
</style>
-</head>
+
<body>
-<grid style="grid-template-rows: 1em 2em">
+<section class="auto">
+<grid style="grid-template-columns: repeat(4, 2ch)">
<item style="width:2ch">1</item>
- <item>2 2</item>
- <item>3 3</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
<item>4</item>
- <item>5 5</item>
+ <item style="grid-row: span 2">5 5</item>
</grid>
-<grid style="grid: 1em 2em / 1ch repeat(3,auto); ">
+<grid style="grid-template-columns: repeat(4, 2ch)">
<item>1</item>
- <item>2 2</item>
- <item>3 3</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
<item>4</item>
- <item style="width:2ch">5 5</item>
+ <item style="grid-row: span 2">5 5</item>
</grid>
-<grid style="grid-template-rows: 2em 2em">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:2ch; grid-column:2">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:2ch; grid-area: 1/2/3">5 5</item>
</grid>
-<grid style="grid-template-rows: 2em 1em">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:4ch; grid-column:2/span 2">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:4ch; grid-area: 1/2/auto/4">5 5</item>
+
+ <item class="hidden">0</item>
</grid>
-<grid style="grid-template-rows: 2em 1em">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="grid-column:2/span 2">5 5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+<grid>
+ <item style="grid-column: 4">1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="grid-area: 1/2/2/4">5 5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
+
+ <item class="hidden">0</item>
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
</grid>
-<grid style="grid-template-rows: 2em 1em">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:3ch; grid-column:2/span 2">5 5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+<grid>
+ <item style="grid-column: 4">1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:3ch; grid-area: 1/2/2/4">5 5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
+
+ <item class="hidden">0</item>
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
</grid>
-<grid style="grid-template-rows: 2em 1em">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="grid-column:span 4">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="grid-area: 3/1/4/5">5 5</item>
+
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
</grid>
-<grid style="grid-template-rows: 2em 1em">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:6ch; grid-column:span 4">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:6ch; grid-area: 3/1/4/5">5 5</item>
+
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
</grid>
-<grid style="grid: 2em 1em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:6ch; grid-column:span 3">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:6ch; grid-area: 3/1/4/4">5 5</item>
+
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
+ <item class="hidden" style="width:6ch; grid-area: 3/2/4/5">0 0</item>
</grid>
+</section>
<!-- ditto with 'fr' sizing -->
-<grid class="fr" style="grid: 1em 2em / 2ch 2fr 1fr 1fr">
- <item style="width:2ch" class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item>5 5</item>
+<section class="fr">
+<grid style="grid-template-columns: repeat(4, 2ch)">
+ <item style="width:2ch">1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="grid-row: span 2">5 5</item>
</grid>
-<grid class="fr" style="grid: 1em 2em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:2ch">5 5</item>
+<grid style="grid-template-columns: repeat(4, 2ch)">
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="grid-row: span 2">5 5</item>
</grid>
-<grid class="fr" style="grid: 2em 2em / 1ch 2ch repeat(2,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:2ch; grid-column:2">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:2ch; grid-area: 1/2/3">5 5</item>
</grid>
-<grid class="fr" style="grid: 2em 1em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:4ch; grid-column:2/span 2">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:4ch; grid-area: 1/2/auto/4">5 5</item>
+ <item class="hidden">0</item>
</grid>
-<grid class="fr" style="grid: 2em 2em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="grid-column:2/span 2">5 5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+<grid>
+ <item style="grid-column: 4">1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="grid-area: 1/2/2/4">5 5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
+ <item class="hidden">0</item>
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
</grid>
-<grid class="fr" style="grid: 2em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:3ch; grid-column:2/span 2">5 5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+<grid>
+ <item style="grid-column: 4">1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:3ch; grid-area: 1/2/2/4">5 5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
+
+ <item class="hidden">0</item>
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
</grid>
-<grid class="fr" style="grid: 2em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="grid-column:span 4">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="grid-area: 3/1/4/5">5 5</item>
+
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
</grid>
-<grid class="fr" style="grid: 2em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:6ch; grid-column:span 4">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:6ch; grid-area: 3/1/4/5">5 5</item>
+
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
</grid>
-<grid class="fr" style="grid: 2em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:6ch; grid-column:span 3">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:6ch; grid-area: 3/1/4/4">5 5</item>
+
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
+ <item class="hidden" style="width:6ch; grid-area: 3/2/4/5">0 0</item>
</grid>
+</section>
<!-- ditto with mixed sizing -->
-<grid class="mixed" style="grid: 1em 2em / 2ch repeat(3,1ch)">
- <item style="width:2ch" class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item>5 5</item>
+<section class="mixed">
+<grid style="grid-template-columns: repeat(3, 2ch) max-content">
+ <item style="width:2ch">1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="grid-row: span 2">5 5</item>
+ <item class="hidden" style="grid-area: 2/4/4">0 0</item>
</grid>
-<grid class="mixed" style="grid: 1em 2em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:2ch">5 5</item>
+<grid style="grid-template-columns: repeat(3, 2ch) max-content">
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="grid-row: span 2">5 5</item>
+ <item class="hidden" style="grid-area: 2/4/4">0 0</item>
</grid>
-<grid class="mixed" style="grid: 2em 2em / 1ch 2ch repeat(2,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:2ch; grid-column:2">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:2ch; grid-area: 1/2/3">5 5</item>
</grid>
-<grid class="mixed" style="grid: 2em 1em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:4ch; grid-column:2/span 2">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:4ch; grid-area: 1/2/auto/4">5 5</item>
+ <item class="hidden">0</item>
</grid>
-<grid class="mixed" style="grid: 2em 2em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="grid-column:2/span 2">5 5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+<grid>
+ <item style="grid-column: 4">1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="grid-area: 1/2/2/4">5 5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
+ <item class="hidden">0</item>
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
</grid>
-<grid class="mixed" style="grid: 2em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:3ch; grid-column:2/span 2">5 5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+<grid>
+ <item style="grid-column: 4">1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:3ch; grid-area: 1/2/2/4">5 5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
+
+ <item class="hidden">0</item>
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
</grid>
-<grid class="mixed" style="grid: 2em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="grid-column:span 4">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="grid-area: 3/1/4/5">5 5</item>
+
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
</grid>
-<grid class="mixed" style="grid: 2em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:6ch; grid-column:span 4">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:6ch; grid-area: 3/1/4/5">5 5</item>
+
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
</grid>
-<grid class="mixed" style="grid: 2em / repeat(4,1ch)">
- <item class="start">1</item>
- <item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:6ch; grid-column:span 3">5 5</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:6ch; grid-area: 3/1/4/4">5 5</item>
+
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
+ <item class="hidden" style="width:6ch; grid-area: 3/2/4/5">0 0</item>
</grid>
+</section>
-</body>
-</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-003.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-003.html
index e43bc86c9b..48489d3c86 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-003.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-003.html
@@ -3,44 +3,41 @@
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
-<html><head>
+<html>
<meta charset="utf-8">
- <title>CSS Grid Test: Masonry layout min-content sizing</title>
+ <title>CSS Grid Test: Masonry layout column sizing - min-content</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#track-sizing">
<link rel="match" href="masonry-intrinsic-sizing-003-ref.html">
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
<style>
-html,body {
- color:black; background-color:white; font:15px/1 Ahem; padding:0; margin:0;
-}
+
+@import "support/masonry-intrinsic-sizing-visual.css";
grid {
display: inline-grid;
gap: 1px 2px;
- grid-template-columns: repeat(4,auto);
grid-template-rows: masonry;
border: 1px solid;
padding: 0 1px 0 2px;
vertical-align: top;
width: min-content;
}
-.fr {
+.auto grid {
+ grid-template-columns: repeat(4,auto);
+}
+.fr grid {
grid-template-columns: 1fr 2fr 1fr 1fr;
}
-.mixed {
+.mixed grid {
grid-template-columns: 1fr 2fr min-content max-content;
}
-
-item {
- background-color: #444;
- color: blue;
-}
</style>
-</head>
+
<body>
-<grid>
+<section class="auto">
+<grid title="Wider 1st item 2ch">
<item style="width:2ch">1</item>
<item>2 2</item>
<item>3 3</item>
@@ -48,7 +45,7 @@ item {
<item>5 5</item>
</grid>
-<grid>
+<grid title="Wrapped 5th item 2ch">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -56,7 +53,7 @@ item {
<item style="width:2ch">5 5</item>
</grid>
-<grid>
+<grid title="Wrapped 5th item 2ch col 2">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -64,7 +61,7 @@ item {
<item style="width:2ch; grid-column:2">5 5</item>
</grid>
-<grid>
+<grid title="5th item 4ch in col 2-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -72,7 +69,7 @@ item {
<item style="width:4ch; grid-column:2/span 2">5 5</item>
</grid>
-<grid>
+<grid title="5th item in col 2-3, 6th item 5ch in col 1-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -81,7 +78,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid>
+<grid title="5th item 3ch in col 2-3, 6th item 5ch in col 1-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -90,7 +87,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid>
+<grid title="5th item span 4">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -98,7 +95,7 @@ item {
<item style="grid-column:span 4">5 5</item>
</grid>
-<grid>
+<grid title="5th item 6ch span 4">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -106,17 +103,19 @@ item {
<item style="width:6ch; grid-column:span 4">5 5</item>
</grid>
-<grid>
+<grid title="5th item 6ch span 3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
<item>4</item>
<item style="width:6ch; grid-column:span 3">5 5</item>
</grid>
+</section>
<!-- ditto with 'fr' sizing -->
-<grid class="fr">
+<section class="fr">
+<grid title="Wider first item 2ch">
<item style="width:2ch">1</item>
<item>2 2</item>
<item>3 3</item>
@@ -124,7 +123,7 @@ item {
<item>5 5</item>
</grid>
-<grid class="fr">
+<grid title="Wrapped last item 2ch">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -132,7 +131,7 @@ item {
<item style="width:2ch">5 5</item>
</grid>
-<grid class="fr">
+<grid title="Wrapped last item 2ch col 2">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -140,7 +139,7 @@ item {
<item style="width:2ch; grid-column:2">5 5</item>
</grid>
-<grid class="fr">
+<grid title="Last item 4ch col 2-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -148,7 +147,7 @@ item {
<item style="width:4ch; grid-column:2/span 2">5 5</item>
</grid>
-<grid class="fr">
+<grid title="Item col 2-3 + Item 5ch col 1-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -157,7 +156,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid class="fr">
+<grid title="Item 3ch col 2-3 + Item 5ch col 1-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -166,7 +165,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid class="fr">
+<grid title="Last item span 4">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -174,7 +173,7 @@ item {
<item style="grid-column:span 4">5 5</item>
</grid>
-<grid class="fr">
+<grid title="Last item 6ch span 4">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -182,17 +181,19 @@ item {
<item style="width:6ch; grid-column:span 4">5 5</item>
</grid>
-<grid class="fr">
+<grid title="Last item 6ch span 3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
<item>4</item>
<item style="width:6ch; grid-column:span 3">5 5</item>
</grid>
+</section>
<!-- ditto with mixed sizing -->
-<grid class="mixed">
+<section class="mixed">
+<grid title="Wider first item 2ch">
<item style="width:2ch">1</item>
<item>2 2</item>
<item>3 3</item>
@@ -200,7 +201,7 @@ item {
<item>5 5</item>
</grid>
-<grid class="mixed">
+<grid title="Wrapped last item 2ch">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -208,7 +209,7 @@ item {
<item style="width:2ch">5 5</item>
</grid>
-<grid class="mixed">
+<grid title="Wrapped last item 2ch col 2">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -216,7 +217,7 @@ item {
<item style="width:2ch; grid-column:2">5 5</item>
</grid>
-<grid class="mixed">
+<grid title="Last item 4ch col 2-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -224,7 +225,7 @@ item {
<item style="width:4ch; grid-column:2/span 2">5 5</item>
</grid>
-<grid class="mixed">
+<grid title="Item col 2-3 + Item 5ch col 1-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -233,7 +234,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid class="mixed">
+<grid title="Item 3ch col 2-3 + Item 5ch col 1-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -242,7 +243,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid class="mixed">
+<grid title="Last item span 4">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -250,7 +251,7 @@ item {
<item style="grid-column:span 4">5 5</item>
</grid>
-<grid class="mixed">
+<grid title="Last item 6ch span 4">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -258,13 +259,12 @@ item {
<item style="width:6ch; grid-column:span 4">5 5</item>
</grid>
-<grid class="mixed">
+<grid title="Last item 6ch span 3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
<item>4</item>
<item style="width:6ch; grid-column:span 3">5 5</item>
</grid>
+</section>
-</body>
-</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-004-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-004-ref.html
index 6a5d81fedb..302dcad116 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-004-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-004-ref.html
@@ -3,268 +3,355 @@
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
-<html><head>
+<html>
<meta charset="utf-8">
<title>Reference: Masonry layout max-content sizing</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
<style>
-html,body {
- color:black; background-color:white; font:15px/1 Ahem; padding:0; margin:0;
-}
+
+@import "support/masonry-intrinsic-sizing-visual.css";
grid {
display: inline-grid;
gap: 1px 2px;
- grid-template-columns: 1ch 3ch 3ch 1ch;
- grid-auto-rows: 1em;
border: 1px solid;
padding: 0 1px 0 2px;
vertical-align: top;
align-items: start;
width: max-content;
}
-.fr {
+.auto grid {
+ grid-template-columns: repeat(4, auto);
+}
+.fr grid {
grid-template-columns: 1fr 2fr 1fr 1fr;
}
-.mixed {
+.mixed grid {
grid-template-columns: 1fr 2fr min-content max-content;
}
item {
- background-color: #444;
- color: blue;
+ align-self: start;
+}
+.hidden {
+ visibility: hidden;
+ height: 0;
}
-item.start { align-self: start; }
</style>
-</head>
+
<body>
-<grid style="grid: 1em 2em / 2ch 3ch 3ch 1ch">
+<section class="auto">
+<grid>
<item style="width:2ch">1</item>
<item>2 2</item>
<item>3 3</item>
<item>4</item>
<item>5 5</item>
+
+ <item class="hidden" style="grid-column: 4">0 0</item>
</grid>
-<grid style="grid: 1em 2em / 1ch 3ch 3ch 1ch; ">
+<grid>
<item>1</item>
<item>2 2</item>
<item>3 3</item>
<item>4</item>
- <item style="width:2ch">5 5</item>
+ <item style="width: 2ch; grid-area: 2/1">5 5</item>
+
+ <item class="hidden" style="grid-area: 2/1">0 0</item>
+ <item class="hidden" style="grid-area: 2/4">0 0</item>
</grid>
-<grid style="grid-template-rows: 1em 2em">
- <item class="start">1</item>
+<grid>
+ <item>1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="width:2ch; grid-column:2">5 5</item>
+ <item style="grid-area: 2/1">4</item>
+ <item style="width:2ch; grid-area: 1/2/3">5 5</item>
+
+ <item class="hidden" style="grid-area: 2/1">0 0</item>
+ <item class="hidden" style="grid-area: 2/2">0 0</item>
</grid>
<grid>
- <item class="start">1</item>
+ <item>1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="width:4ch; grid-column:2/span 2">5 5</item>
+ <item style="grid-area: 2/2">4</item>
+ <item style="width:4ch; grid-area: 1/2/auto/4">5 5</item>
+
+ <item class="hidden" style="grid-area: 2/2">0 0</item>
+ <item class="hidden" style="grid-area: 2/3">0 0</item>
</grid>
<grid>
- <item class="start">1</item>
+ <item style="grid-area: 1/4">1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="grid-column:2/span 2">5 5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item>4</item>
+ <item style="grid-area: 1/2/2/4">5 5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
+
+ <item class="hidden" style="grid-area: 1/1">0</item>
+ <item class="hidden" style="grid-area: 2/2">0 0</item>
+ <item class="hidden" style="grid-area: 2/3">0 0</item>
+ <item class="hidden" style="grid-area: 1/4">0 0</item>
</grid>
<grid>
- <item class="start">1</item>
+ <item style="grid-area: 1/4">1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="width:3ch; grid-column:2/span 2">5 5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item>4</item>
+ <item style="width:3ch; grid-area: 1/2/2/4">5 5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
+
+ <item class="hidden" style="grid-area: 1/1">0</item>
+ <item class="hidden" style="grid-area: 2/2">0 0</item>
+ <item class="hidden" style="grid-area: 2/3">0 0</item>
+ <item class="hidden" style="grid-area: 1/4">0 0</item>
</grid>
<grid>
- <item class="start">1</item>
+ <item>1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="grid-column:span 4">5 5</item>
+ <item>4</item>
+ <item style="grid-area: 2/1/3/5">5 5</item>
+
+ <item class="hidden" style="grid-area: 2/1">0 0</item>
+ <item class="hidden" style="grid-area: 2/4">0 0</item>
</grid>
<grid>
- <item class="start">1</item>
+ <item>1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="width:6ch; grid-column:span 4">5 5</item>
+ <item>4</item>
+ <item style="width:6ch; grid-area: 2/1/3/5">5 5</item>
+
+ <item class="hidden" style="grid-area: 2/1">0 0</item>
+ <item class="hidden" style="grid-area: 2/4">0 0</item>
</grid>
<grid>
- <item class="start">1</item>
+ <item>1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="width:6ch; grid-column:span 3">5 5</item>
+ <item>4</item>
+ <item style="width:6ch; grid-area: 2/1/3/4">5 5</item>
+
+ <item class="hidden" style="grid-area: 2/1">0 0</item>
+ <item class="hidden" style="grid-area: 2/4">0 0</item>
+ <item class="hidden" style="width:6ch; grid-area: 2/2/3/5">0 0</item>
</grid>
+</section>
<!-- ditto with 'fr' sizing -->
-<grid class="fr">
- <item style="width:2ch" class="start">1</item>
+<section class="fr">
+<grid>
+ <item style="width:2ch">1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
+ <item>4</item>
<item>5 5</item>
+ <item class="hidden" style="grid-area: 2/4/4">0 0</item>
</grid>
-<grid class="fr" style="grid: 1em 2em / 3ch 6ch 3ch 3ch">
- <item class="start">1</item>
+<grid>
+ <item>1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="width:2ch">5 5</item>
+ <item>4</item>
+ <item style="width: 2ch">5 5</item>
+ <item class="hidden" style="grid-area: 2/4">0 0</item>
</grid>
-<grid class="fr" style="grid: 1em 2em / 3ch 6ch 3ch 3ch">
- <item class="start">1</item>
+<grid>
+ <item>1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="width:2ch; grid-column:2">5 5</item>
+ <item>4</item>
+ <item style="width:2ch; grid-area: 1/2/3">5 5</item>
</grid>
-<grid class="fr">
- <item class="start">1</item>
+<grid>
+ <item>1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="width:4ch; grid-column:2/span 2">5 5</item>
+ <item>4</item>
+ <item style="width:4ch; grid-area: 1/2/auto/4">5 5</item>
+ <item class="hidden">0</item>
</grid>
-<grid class="fr">
- <item class="start">1</item>
+<grid>
+ <item style="grid-area: 1/4">1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="grid-column:2/span 2">5 5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item>4</item>
+ <item style="grid-area: 1/2/2/4">5 5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
+
+ <item class="hidden" style="grid-area: 1/1">0</item>
+ <item class="hidden" style="grid-area: 2/2">0 0</item>
+ <item class="hidden" style="grid-area: 2/3">0 0</item>
+ <item class="hidden" style="grid-area: 1/4">0 0</item>
</grid>
-<grid class="fr">
- <item class="start">1</item>
+<grid>
+ <item style="grid-area: 1/4">1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="width:3ch; grid-column:2/span 2">5 5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+ <item>4</item>
+ <item style="width:3ch; grid-area: 1/2/2/4">5 5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
+
+ <item class="hidden" style="grid-area: 1/1">0</item>
+ <item class="hidden" style="grid-area: 2/2">0 0</item>
+ <item class="hidden" style="grid-area: 2/3">0 0</item>
+ <item class="hidden" style="grid-area: 1/4">0 0</item>
</grid>
-<grid class="fr">
- <item class="start">1</item>
+<grid>
+ <item>1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="grid-column:span 4">5 5</item>
+ <item>4</item>
+ <item style="grid-area: 2/1/3/5">5 5</item>
+
+ <item class="hidden" style="grid-area: 2/1">0 0</item>
+ <item class="hidden" style="grid-area: 2/4">0 0</item>
</grid>
-<grid class="fr">
- <item class="start">1</item>
+<grid>
+ <item>1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="width:6ch; grid-column:span 4">5 5</item>
+ <item>4</item>
+ <item style="width:6ch; grid-area: 2/1/3/5">5 5</item>
+
+ <item class="hidden" style="grid-area: 2/1">0 0</item>
+ <item class="hidden" style="grid-area: 2/4">0 0</item>
</grid>
-<grid class="fr">
- <item class="start">1</item>
+<grid>
+ <item>1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="width:6ch; grid-column:span 3">5 5</item>
+ <item>4</item>
+ <item style="width:6ch; grid-area: 2/1/3/4">5 5</item>
+
+ <item class="hidden" style="grid-area: 2/1">0 0</item>
+ <item class="hidden" style="grid-area: 2/4">0 0</item>
+ <item class="hidden" style="width:6ch; grid-area: 2/2/3/5">0 0</item>
</grid>
+</section>
<!-- ditto with mixed sizing -->
-<grid class="mixed" style="grid: 1em 2em / 2ch 4ch 1ch 1ch">
- <item style="width:2ch" class="start">1</item>
+<section class="mixed">
+<grid>
+ <item style="width:2ch">1</item>
<item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item>5 5</item>
+ <item style="grid-area: 1/3/3">3 3</item>
+ <item>4</item>
+ <item style="grid-area: 2/1">5 5</item>
+
+ <item class="hidden" style="grid-area: 2/3; width: 2ch">0</item>
+ <item class="hidden" style="grid-area: 2/4">0 0</item>
</grid>
-<grid class="mixed" style="grid: 1em 2em / calc(3ch/2) 3ch 1ch 1ch">
- <item class="start">1</item>
+<grid>
+ <item>1</item>
<item>2 2</item>
- <item>3 3</item>
- <item class="start">4</item>
- <item style="width:2ch">5 5</item>
+ <item style="grid-area: 1/3/3">3 3</item>
+ <item>4</item>
+ <item style="width: 2ch; grid-area: 2/1">5 5</item>
+
+ <item class="hidden" style="grid-area: 2/3; width: 2ch">0</item>
+ <item class="hidden" style="grid-area: 2/4">0 0</item>
</grid>
-<grid class="mixed" style="grid: 1em 2em / calc(3ch/2) 3ch 1ch 1ch">
- <item class="start">1</item>
+<grid>
+ <item>1</item>
+ <item style="grid-row: span 2">2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item style="grid-area: 2/1">4</item>
+ <item style="width:2ch; grid-area: 1/2/3">5 5</item>
+
+ <item class="hidden" style="grid-area: 2/1">0 0</item>
+</grid>
+
+<grid>
+ <item>1</item>
<item>2 2</item>
<item>3 3</item>
- <item class="start">4</item>
- <item style="width:2ch; grid-column:2">5 5</item>
-</grid>
+ <item>4</item>
+ <item style="width:4ch; grid-area: 1/2/auto/4">5 5</item>
-<grid class="mixed" style="grid: 2em 1em / calc(3ch/2) 3ch 1ch 1ch">
- <item class="start">1</item>
- <item class="start">2 2</item>
- <item class="start">3 3</item>
- <item class="start">4</item>
- <item style="width:4ch; grid-column:2/span 2">5 5</item>
+ <item class="hidden">0</item>
</grid>
-<grid class="mixed" style="grid: 2em 1em 1em / calc(3ch/2) 3ch 1ch 1ch">
- <item class="start">1</item>
- <item class="start">2 2</item>
- <item class="start">3 3</item>
- <item class="start">4</item>
- <item style="grid-column:2/span 2">5 5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+<grid>
+ <item style="grid-column: 4">1</item>
+ <item>2 2</item>
+ <item>3 3</item>
+ <item>4</item>
+ <item style="grid-area: 1/2/2/4">5 5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
+
+ <item class="hidden">0 0</item>
+ <item class="hidden" style="grid-area: 2/2">0 0</item>
</grid>
-<grid class="mixed" style="grid: 2em 1em / calc(3ch/2) 3ch 1ch 1ch">
- <item class="start">1</item>
- <item class="start">2 2</item>
- <item class="start">3 3</item>
- <item class="start">4</item>
- <item style="width:3ch; grid-column:2/span 2">5 5</item>
- <item style="width:5ch; grid-column:1/span 3">6</item>
+<grid>
+ <item style="grid-column: 4">1</item>
+ <item>2 2</item>
+ <item>3 3</item>
+ <item>4</item>
+ <item style="width:3ch; grid-area: 1/2/2/4">5 5</item>
+ <item style="width:5ch; grid-area: 2/1/3/4">6</item>
+
+ <item class="hidden" style="grid-area: 2/3">0 0</item>
</grid>
-<grid class="mixed" style="grid: 2em 1em / calc(3ch/2) 3ch 1ch 1ch">
- <item class="start">1</item>
- <item class="start">2 2</item>
- <item class="start">3 3</item>
- <item class="start">4</item>
- <item style="grid-column:span 4">5 5</item>
+<grid>
+ <item>1</item>
+ <item>2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="grid-area: 3/1/4/5">5 5</item>
+
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
</grid>
-<grid class="mixed" style="grid: 2em 1em / calc(3ch/2) 3ch 1ch 1ch">
- <item class="start">1</item>
- <item class="start">2 2</item>
- <item class="start">3 3</item>
- <item class="start">4</item>
- <item style="width:6ch; grid-column:span 4">5 5</item>
+<grid>
+ <item>1</item>
+ <item>2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:6ch; grid-area: 3/1/4/5">5 5</item>
+
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
</grid>
-<grid class="mixed" style="grid: 2em 1em / calc(3ch/2) 3ch 1ch 1ch">
- <item class="start">1</item>
- <item class="start">2 2</item>
- <item class="start">3 3</item>
- <item class="start">4</item>
- <item style="width:6ch; grid-column:span 3">5 5</item>
+<grid>
+ <item>1</item>
+ <item>2 2</item>
+ <item style="grid-row: span 2">3 3</item>
+ <item>4</item>
+ <item style="width:6ch; grid-area: 3/1/4/4">5 5</item>
+
+ <item class="hidden">0 0</item>
+ <item class="hidden">0 0</item>
+ <item class="hidden" style="width:6ch; grid-area: 3/2/4/5">0 0</item>
</grid>
+</section>
-</body>
-</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-004.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-004.html
index 5365208c00..c8bc671bb3 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-004.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-004.html
@@ -3,44 +3,41 @@
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
-<html><head>
+<html>
<meta charset="utf-8">
- <title>CSS Grid Test: Masonry layout max-content sizing</title>
+ <title>CSS Grid Test: Masonry layout column sizing - max-content</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3">
<link rel="match" href="masonry-intrinsic-sizing-004-ref.html">
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
<style>
-html,body {
- color:black; background-color:white; font:15px/1 Ahem; padding:0; margin:0;
-}
+
+@import "support/masonry-intrinsic-sizing-visual.css";
grid {
display: inline-grid;
gap: 1px 2px;
- grid-template-columns: repeat(4,auto);
grid-template-rows: masonry;
border: 1px solid;
padding: 0 1px 0 2px;
vertical-align: top;
width: max-content;
}
-.fr {
+.auto grid {
+ grid-template-columns: repeat(4,auto);
+}
+.fr grid {
grid-template-columns: 1fr 2fr 1fr 1fr;
}
-.mixed {
+.mixed grid {
grid-template-columns: 1fr 2fr min-content max-content;
}
-
-item {
- background-color: #444;
- color: blue;
-}
</style>
-</head>
+
<body>
-<grid>
+<section class="auto">
+<grid title="Wider 1st item 2ch">
<item style="width:2ch">1</item>
<item>2 2</item>
<item>3 3</item>
@@ -48,7 +45,7 @@ item {
<item>5 5</item>
</grid>
-<grid>
+<grid title="Wrapped 5th item 2ch">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -56,7 +53,7 @@ item {
<item style="width:2ch">5 5</item>
</grid>
-<grid>
+<grid title="Wrapped 5th item 2ch in col 2">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -64,7 +61,7 @@ item {
<item style="width:2ch; grid-column:2">5 5</item>
</grid>
-<grid>
+<grid title="5th item 4ch in col 2-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -72,7 +69,7 @@ item {
<item style="width:4ch; grid-column:2/span 2">5 5</item>
</grid>
-<grid>
+<grid title="4th item in col 2-3, 5th item 5ch in col 1-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -81,7 +78,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid>
+<grid title="5th item 3ch in col 2-3, 6th item 5ch in col 1-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -90,7 +87,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid>
+<grid title="5th item span 4">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -98,7 +95,7 @@ item {
<item style="grid-column:span 4">5 5</item>
</grid>
-<grid>
+<grid title="5th item 6ch span 4">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -106,17 +103,19 @@ item {
<item style="width:6ch; grid-column:span 4">5 5</item>
</grid>
-<grid>
+<grid title="5th item 6ch span 3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
<item>4</item>
<item style="width:6ch; grid-column:span 3">5 5</item>
</grid>
+</section>
<!-- ditto with 'fr' sizing -->
-<grid class="fr">
+<section class="fr">
+<grid title="Wider 1st item 2ch">
<item style="width:2ch">1</item>
<item>2 2</item>
<item>3 3</item>
@@ -124,7 +123,7 @@ item {
<item>5 5</item>
</grid>
-<grid class="fr">
+<grid title="Wrapped 5th item 2ch">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -132,7 +131,7 @@ item {
<item style="width:2ch">5 5</item>
</grid>
-<grid class="fr">
+<grid title="Wrapped 5th item 2ch in col 2">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -140,7 +139,7 @@ item {
<item style="width:2ch; grid-column:2">5 5</item>
</grid>
-<grid class="fr">
+<grid title="5th item 4ch in col 2-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -148,7 +147,7 @@ item {
<item style="width:4ch; grid-column:2/span 2">5 5</item>
</grid>
-<grid class="fr">
+<grid title="4th item in col 2-3, 5th item 5ch in col 1-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -157,7 +156,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid class="fr">
+<grid title="5th item 3ch in col 2-3, 6th item 5ch in col 1-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -166,7 +165,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid class="fr">
+<grid title="5th item span 4">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -174,7 +173,7 @@ item {
<item style="grid-column:span 4">5 5</item>
</grid>
-<grid class="fr">
+<grid title="5th item 6ch span 4">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -182,17 +181,19 @@ item {
<item style="width:6ch; grid-column:span 4">5 5</item>
</grid>
-<grid class="fr">
+<grid title="5th item 6ch span 3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
<item>4</item>
<item style="width:6ch; grid-column:span 3">5 5</item>
</grid>
+</section>
<!-- ditto with mixed sizing -->
-<grid class="mixed">
+<section class="mixed">
+<grid title="Wider 1st item 2ch">
<item style="width:2ch">1</item>
<item>2 2</item>
<item>3 3</item>
@@ -200,7 +201,7 @@ item {
<item>5 5</item>
</grid>
-<grid class="mixed">
+<grid title="Wrapped 5th item 2ch">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -208,7 +209,7 @@ item {
<item style="width:2ch">5 5</item>
</grid>
-<grid class="mixed">
+<grid title="Wrapped 5th item 2ch in col 2">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -216,7 +217,7 @@ item {
<item style="width:2ch; grid-column:2">5 5</item>
</grid>
-<grid class="mixed">
+<grid title="5th item 4ch in col 2-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -224,7 +225,7 @@ item {
<item style="width:4ch; grid-column:2/span 2">5 5</item>
</grid>
-<grid class="mixed">
+<grid title="4th item in col 2-3, 5th item 5ch in col 1-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -233,7 +234,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid class="mixed">
+<grid title="5th item 3ch in col 2-3, 6th item 5ch in col 1-3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -242,7 +243,7 @@ item {
<item style="width:5ch; grid-column:1/span 3">6</item>
</grid>
-<grid class="mixed">
+<grid title="5th item span 4">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -250,7 +251,7 @@ item {
<item style="grid-column:span 4">5 5</item>
</grid>
-<grid class="mixed">
+<grid title="5th item 6ch span 4">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
@@ -258,13 +259,12 @@ item {
<item style="width:6ch; grid-column:span 4">5 5</item>
</grid>
-<grid class="mixed">
+<grid title="5th item 6ch span 3">
<item>1</item>
<item>2 2</item>
<item>3 3</item>
<item>4</item>
<item style="width:6ch; grid-column:span 3">5 5</item>
</grid>
+</section>
-</body>
-</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-005-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-005-ref.html
index e564fb1b3e..725f646ebd 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-005-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-005-ref.html
@@ -3,35 +3,32 @@
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
-<html><head>
+<html>
<meta charset="utf-8">
- <title>Reference: Masonry layout intrinsic sizing</title>
+ <title>Reference: Masonry layout row sizing (vertical writing mode)</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
<style>
-html,body {
- color:black; background-color:white; font:15px/1 Ahem; padding:0; margin:0;
-}
+
+@import "support/masonry-intrinsic-sizing-visual.css";
grid {
display: inline-grid;
gap: 1px 2px;
+ grid-template-rows: repeat(4,auto);
grid-auto-flow: column;
border: 1px solid;
padding: 0 1px 0 2px;
vertical-align: top;
}
-
item {
- background-color: #444;
- color: blue;
writing-mode: vertical-lr;
}
</style>
-</head>
+
<body>
-<grid style="grid-template-rows: 3ch repeat(3,1ch)">
+<grid style="grid-template-rows: repeat(4, 3ch)">
<item style="height:3ch">1</item>
<item>2</item>
<item>3</item>
@@ -43,7 +40,7 @@ item {
<item>9 9</item>
</grid>
-<grid style="grid-template-rows: repeat(4,1ch)">
+<grid style="grid-template-rows: repeat(4,3ch)">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -79,5 +76,3 @@ item {
<item>9 9</item>
</grid>
-</body>
-</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-005.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-005.html
index cf9b680869..83afd5bafa 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-005.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-005.html
@@ -3,17 +3,16 @@
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
-<html><head>
+<html>
<meta charset="utf-8">
- <title>CSS Grid Test: Masonry layout intrinsic sizing</title>
+ <title>CSS Grid Test: Masonry layout row sizing (vertical writing mode)</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#track-sizing">
<link rel="match" href="masonry-intrinsic-sizing-005-ref.html">
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
<style>
-html,body {
- color:black; background-color:white; font:15px/1 Ahem; padding:0; margin:0;
-}
+
+@import "support/masonry-intrinsic-sizing-visual.css";
grid {
display: inline-grid;
@@ -26,15 +25,13 @@ grid {
}
item {
- background-color: #444;
- color: blue;
writing-mode: vertical-lr;
}
</style>
-</head>
+
<body>
-<grid>
+<grid title="First item 3ch">
<item style="height:3ch">1</item>
<item>2</item>
<item>3</item>
@@ -46,7 +43,7 @@ item {
<item>9 9</item>
</grid>
-<grid>
+<grid title="Fifth item 3ch">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -58,7 +55,7 @@ item {
<item>9 9</item>
</grid>
-<grid>
+<grid title="Fifth item 3ch row 1">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -70,7 +67,7 @@ item {
<item>9 9</item>
</grid>
-<grid>
+<grid title="Fifth item 3ch col 1 (ignored)">
<item>1</item>
<item>2</item>
<item>3</item>
@@ -82,5 +79,3 @@ item {
<item>9 9</item>
</grid>
-</body>
-</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-006-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-006-ref.html
index e1cb015cfe..1a88c2b4cb 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-006-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-006-ref.html
@@ -3,58 +3,49 @@
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
-<html><head>
+<html>
<meta charset="utf-8">
- <title>Reference: Masonry layout intrinsic sizing</title>
+ <title>Reference: Masonry layout row auto-fill sizing</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
<style>
-html,body {
- color:black; background-color:white; font:15px/1 Ahem; padding:0; margin:0;
-}
+
+@import "support/masonry-intrinsic-sizing-visual.css";
grid {
display: inline-grid;
gap: 1px 2px;
grid-auto-flow: column;
- grid-template-columns: 1ch;
grid-template-rows: repeat(4,1em);
border: 1px solid;
padding: 0 1px 0 2px;
vertical-align: top;
}
-
-item {
- background-color: #444;
- color: blue;
-}
</style>
-</head>
+
<body>
<grid>
- <item style="width:3ch">1 1</item>
+ <item style="width:3ch; grid-column: span 2">1 1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="grid-row:2">5 5</item>
+ <item style="grid-column: span 2">5 5</item>
</grid>
<grid>
- <item style="width:3ch">1 1</item>
+ <item style="width:3ch; grid-column: span 2">1 1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="grid-row:2">5 5</item>
+ <item style="grid-column: span 2">5 5</item>
</grid>
<grid style="height:5em">
- <item style="width:3ch">1 1</item>
+ <item style="width:3ch; grid-column: span 2">1 1</item>
<item>2</item>
<item>3</item>
<item>4</item>
- <item style="grid-row:2">5 5</item>
+ <item style="grid-column: span 2">5 5</item>
</grid>
-</body>
-</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-006.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-006.html
index 49fd53bb79..ad54800921 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-006.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/masonry-intrinsic-sizing-006.html
@@ -3,17 +3,16 @@
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
-<html><head>
+<html>
<meta charset="utf-8">
- <title>CSS Grid Test: Masonry layout intrinsic sizing</title>
+ <title>CSS Grid Test: Masonry layout row auto-fill sizing</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#track-sizing">
<link rel="match" href="masonry-intrinsic-sizing-006-ref.html">
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
<style>
-html,body {
- color:black; background-color:white; font:15px/1 Ahem; padding:0; margin:0;
-}
+
+@import "support/masonry-intrinsic-sizing-visual.css";
grid {
display: inline-grid;
@@ -24,16 +23,12 @@ grid {
padding: 0 1px 0 2px;
vertical-align: top;
}
-
-item {
- background-color: #444;
- color: blue;
-}
</style>
-</head>
+
<body>
-<grid style="max-height:5em; grid-template-rows: repeat(auto-fill,1em);">
+<grid title="max-height 5em"
+ style="max-height:5em; grid-template-rows: repeat(auto-fill,1em);">
<item>1 1</item>
<item>2</item>
<item>3</item>
@@ -41,7 +36,8 @@ item {
<item>5 5</item>
</grid>
-<grid style="min-height:4em; grid-template-rows: repeat(auto-fill,1em);">
+<grid title="min-height 4em"
+ style="min-height:4em; grid-template-rows: repeat(auto-fill,1em);">
<item>1 1</item>
<item>2</item>
<item>3</item>
@@ -49,7 +45,8 @@ item {
<item>5 5</item>
</grid>
-<grid style="height:5em; grid-template-rows: repeat(auto-fill,1em);">
+<grid title="height 5em"
+ style="height:5em; grid-template-rows: repeat(auto-fill,1em);">
<item>1 1</item>
<item>2</item>
<item>3</item>
@@ -57,5 +54,3 @@ item {
<item>5 5</item>
</grid>
-</body>
-</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/support/masonry-intrinsic-sizing-visual.css b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/support/masonry-intrinsic-sizing-visual.css
new file mode 100644
index 0000000000..150f0f2679
--- /dev/null
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/intrinsic-sizing/support/masonry-intrinsic-sizing-visual.css
@@ -0,0 +1,48 @@
+/* Basic Testing Setup */
+html,body {
+ color: black; background: white; font: 15px/1 Ahem, monospace;
+ padding: 1em 0; margin: 0;
+ max-width: 800px; height: calc(600px - 2em);
+ border-bottom: 1px solid orange; /* Do Not Cross */
+}
+
+
+/* Visualization */
+grid {
+ margin: 0.5em;
+}
+item {
+ background-color: gray;
+ color: blue;
+}
+
+/* Debugging Aid */
+section {
+ border-top: 1px solid gray;
+ counter-reset: grid;
+}
+
+grid {
+ margin-inline-start: 1em;
+ counter-increment: grid;
+}
+grid::before {
+ position: absolute;
+ margin: 0 -1.1em;
+ font: smaller sans-serif;
+ content: counter(grid);
+ color: navy;
+}
+
+grid:hover {
+ outline: navy solid;
+}
+grid:hover item[style] {
+ color: navy;
+}
+grid:hover::after {
+ content: attr(title);
+ position: absolute;
+ inset: 0.1em auto auto 0.5rem;
+ font: bold smaller sans-serif;
+}
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-001.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-001.html
index 649e1edb7c..2438df8bac 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-001.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-001.html
@@ -7,7 +7,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry layout using `grid-column/row`</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3">
<link rel="match" href="masonry-item-placement-001-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-002.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-002.html
index 7d321bf731..8833cdfcfc 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-002.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-002.html
@@ -7,7 +7,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry layout using `grid-column/row`</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3">
<link rel="match" href="masonry-item-placement-002-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-003.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-003.html
index 8a183cffc6..bde2fa0e66 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-003.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-003.html
@@ -7,7 +7,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry layout using `grid-column/row` and `dense`</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3">
<link rel="match" href="masonry-item-placement-003-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-004.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-004.html
index 4d1a454a86..52867cd4a2 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-004.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-004.html
@@ -7,7 +7,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry layout using `grid-column/row`</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3">
<link rel="match" href="masonry-item-placement-004-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-005.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-005.html
index ddfbc9e54e..7d4993fb3a 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-005.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-005.html
@@ -7,7 +7,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry layout using `grid-column/row`</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3">
<link rel="match" href="masonry-item-placement-005-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-006.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-006.html
index 0082d72df2..467e313d33 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-006.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-006.html
@@ -7,7 +7,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry item placement</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3">
<link rel="match" href="masonry-item-placement-006-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-007.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-007.html
index 67a02560f4..4c8053ba66 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-007.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-007.html
@@ -7,7 +7,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry item placement (RTL)</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3">
<link rel="match" href="masonry-item-placement-007-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-008-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-008-ref.html
index e14ca3173a..f3503bf071 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-008-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-008-ref.html
@@ -3,7 +3,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry item placement w/ Images</title>
<link rel="author" title="Brandon Stewart" href="mailto:brandonstewart@apple.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3">
</head>
<body>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-008.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-008.html
index c68a9787b8..bdf651d77b 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-008.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/item-placement/masonry-item-placement-008.html
@@ -3,7 +3,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry item placement w/ Images</title>
<link rel="author" title="Brandon Stewart" href="mailto:brandonstewart@apple.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3">
<link rel="match" href="masonry-item-placement-008-ref.html">
</head>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-columns-item-containing-block-is-grid-content-width.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-columns-item-containing-block-is-grid-content-width.html
index e48b650253..b5eb7f4984 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-columns-item-containing-block-is-grid-content-width.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-columns-item-containing-block-is-grid-content-width.html
@@ -4,7 +4,7 @@
<link rel="author" title="Sammy Gill" href="mailto:sammy.gill@apple.com">
<link rel="help" href="https://drafts.csswg.org/css-grid-3/#containing-block">
<link rel="match" href="/css/reference/ref-filled-green-100px-square-only.html">
-<meta name="assert" content="Svg should use grid's content logical width for its containing block size and get sized to 100px x 100px">
+<meta name="assert" content="Test passes if SVG uses grid's content logical width for its containing block size and get sized to 100px x 100px">
<style>
grid {
display: grid;
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-grid-template-columns-computed-withcontent.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-grid-template-columns-computed-withcontent.html
index b36efb664d..0ee2ee78c0 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-grid-template-columns-computed-withcontent.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-grid-template-columns-computed-withcontent.html
@@ -1,34 +1,34 @@
<!DOCTYPE html>
<html>
<head>
-<meta charset="utf-8">
-<title>CSS Masonry Test: getComputedStyle().gridTemplateColumns</title>
-<link rel="help" href="https://drafts.csswg.org/css-grid-1/#propdef-grid-template-columns">
-<meta name="assert" content="grid-template-columns computed value is the keyword none or a computed track list.">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/css/support/computed-testcommon.js"></script>
-<style>
- #target {
- display: grid;
- grid-template-rows: masonry;
- font-size: 40px;
- min-width: 200px;
- width: 300px;
- max-width: 400px;
- min-height: 500px;
- height: 600px;
- max-height: 700px;
- }
- #child {
- min-width: 20px;
- width: 30px;
- max-width: 40px;
- min-height: 50px;
- height: 60px;
- max-height: 70px;
- }
-</style>
+ <meta charset="utf-8">
+ <title>CSS Masonry Test: getComputedStyle().gridTemplateColumns</title>
+ <link rel="help" href="https://drafts.csswg.org/css-grid-1/#grid-template-masonry">
+ <meta name="assert" content="grid-template-columns computed value is the keyword none or a computed track list.">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/css/support/computed-testcommon.js"></script>
+ <style>
+ #target {
+ display: grid;
+ grid-template-rows: masonry;
+ font-size: 40px;
+ min-width: 200px;
+ width: 300px;
+ max-width: 400px;
+ min-height: 500px;
+ height: 600px;
+ max-height: 700px;
+ }
+ #child {
+ min-width: 20px;
+ width: 30px;
+ max-width: 40px;
+ min-height: 50px;
+ height: 60px;
+ max-height: 70px;
+ }
+ </style>
</head>
<body>
<div id="container">
@@ -37,59 +37,59 @@
</div>
</div>
<script>
-test_computed_value("grid-template-rows", 'masonry', 'masonry');
-test_computed_value("grid-template-columns", 'none', 'none'); // "none" without #child
+ test_computed_value("grid-template-rows", 'masonry', 'masonry');
+ test_computed_value("grid-template-columns", 'none', 'none'); // "none" without #child
-// track-size <fixed-breadth> = <length-percentage> | <flex> | min-content | max-content | auto
-test_computed_value("grid-template-columns", '20%', '60px'); // 20% * width
-test_computed_value("grid-template-columns", 'calc(-0.5em + 10px)', '0px');
-test_computed_value("grid-template-columns", 'calc(0.5em + 10px)', '30px');
-test_computed_value("grid-template-columns", 'calc(30% + 40px)', '130px'); // 30% * width + 40px
-test_computed_value("grid-template-columns", '5fr', '300px'); // width
-test_computed_value("grid-template-columns", 'min-content', '30px');
-test_computed_value("grid-template-columns", 'max-content', '30px');
-test_computed_value("grid-template-columns", 'auto', '300px'); // width
+ // track-size <fixed-breadth> = <length-percentage> | <flex> | min-content | max-content | auto
+ test_computed_value("grid-template-columns", '20%', '60px'); // 20% * width
+ test_computed_value("grid-template-columns", 'calc(-0.5em + 10px)', '0px');
+ test_computed_value("grid-template-columns", 'calc(0.5em + 10px)', '30px');
+ test_computed_value("grid-template-columns", 'calc(30% + 40px)', '130px'); // 30% * width + 40px
+ test_computed_value("grid-template-columns", '5fr', '300px'); // width
+ test_computed_value("grid-template-columns", 'min-content', '30px');
+ test_computed_value("grid-template-columns", 'max-content', '30px');
+ test_computed_value("grid-template-columns", 'auto', '300px'); // width
-// track-size minmax( <inflexible-breadth> , <track-breadth> )
-test_computed_value("grid-template-columns", 'minmax(10px, auto)', '300px'); // width
-test_computed_value("grid-template-columns", 'minmax(20%, max-content)', '60px'); // 20% * width
-test_computed_value("grid-template-columns", 'minmax(min-content, calc(-0.5em + 10px))', '30px');
-test_computed_value("grid-template-columns", 'minmax(auto, 0)', '30px');
+ // track-size minmax( <inflexible-breadth> , <track-breadth> )
+ test_computed_value("grid-template-columns", 'minmax(10px, auto)', '300px'); // width
+ test_computed_value("grid-template-columns", 'minmax(20%, max-content)', '60px'); // 20% * width
+ test_computed_value("grid-template-columns", 'minmax(min-content, calc(-0.5em + 10px))', '30px');
+ test_computed_value("grid-template-columns", 'minmax(auto, 0)', '30px');
-// track-size fit-content( <length-percentage> )
-test_computed_value("grid-template-columns", 'fit-content(70px)', '30px');
-test_computed_value("grid-template-columns", 'fit-content(20%)', '30px');
-test_computed_value("grid-template-columns", 'fit-content(calc(-0.5em + 10px))', '30px');
+ // track-size fit-content( <length-percentage> )
+ test_computed_value("grid-template-columns", 'fit-content(70px)', '30px');
+ test_computed_value("grid-template-columns", 'fit-content(20%)', '30px');
+ test_computed_value("grid-template-columns", 'fit-content(calc(-0.5em + 10px))', '30px');
-// <track-repeat> = repeat( [ <positive-integer> ] , [ <line-names>? <track-size> ]+ <line-names>? )
-test_computed_value("grid-template-columns", 'repeat(1, 10px)', '10px');
-test_computed_value("grid-template-columns", 'repeat(1, [one two] 20%)', '[one two] 60px');
-test_computed_value("grid-template-columns", 'repeat(2, minmax(10px, auto))', '160px 140px');
+ // <track-repeat> = repeat( [ <positive-integer> ] , [ <line-names>? <track-size> ]+ <line-names>? )
+ test_computed_value("grid-template-columns", 'repeat(1, 10px)', '10px');
+ test_computed_value("grid-template-columns", 'repeat(1, [one two] 20%)', '[one two] 60px');
+ test_computed_value("grid-template-columns", 'repeat(2, minmax(10px, auto))', '150px 150px');
-test_computed_value("grid-template-columns", 'repeat(2, fit-content(20%) [three four] 30px 40px [five six])',
- '30px [three four] 30px 40px [five six] 0px [three four] 30px 40px [five six]');
+ test_computed_value("grid-template-columns", 'repeat(2, fit-content(20%) [three four] 30px 40px [five six])',
+ '30px [three four] 30px 40px [five six] 30px [three four] 30px 40px [five six]');
-// <track-list> = [ <line-names>? [ <track-size> | <track-repeat> ] ]+ <line-names>?
-test_computed_value("grid-template-columns", 'min-content repeat(5, minmax(10px, auto))',
- '30px 54px 54px 54px 54px 54px');
-test_computed_value("grid-template-columns", '[] 150px [] 1fr []', '150px 150px');
+ // <track-list> = [ <line-names>? [ <track-size> | <track-repeat> ] ]+ <line-names>?
+ test_computed_value("grid-template-columns", 'min-content repeat(5, minmax(10px, auto))',
+ '30px 54px 54px 54px 54px 54px');
+ test_computed_value("grid-template-columns", '[] 150px [] 1fr []', '150px 150px');
-// <auto-repeat> = repeat( [ auto-fill | auto-fit ] , [ <line-names>? <fixed-size> ]+ <line-names>? )
-test_computed_value("grid-template-columns", 'repeat(auto-fill, 200px)', '200px');
-test_computed_value("grid-template-columns", 'repeat(auto-fit, [one] 20%)',
- '[one] 60px [one] 60px [one] 60px [one] 60px [one] 60px');
-test_computed_value("grid-template-columns", 'repeat(auto-fill, minmax(100px, 5fr) [two])',
- '100px [two] 100px [two] 100px [two]');
-test_computed_value("grid-template-columns", 'repeat(auto-fit, [three] minmax(max-content, 6em) [four])',
- '[three] 240px [four]');
+ // <auto-repeat> = repeat( [ auto-fill | auto-fit ] , [ <line-names>? <fixed-size> ]+ <line-names>? )
+ test_computed_value("grid-template-columns", 'repeat(auto-fill, 200px)', '200px');
+ test_computed_value("grid-template-columns", 'repeat(auto-fit, [one] 20%)',
+ '[one] 60px [one] 60px [one] 60px [one] 60px [one] 60px');
+ test_computed_value("grid-template-columns", 'repeat(auto-fill, minmax(100px, 5fr) [two])',
+ '100px [two] 100px [two] 100px [two]');
+ test_computed_value("grid-template-columns", 'repeat(auto-fit, [three] minmax(max-content, 6em) [four])',
+ '[three] 240px [four]');
-// <auto-track-list> =
-// [ <line-names>? [ <fixed-size> | <fixed-repeat> ] ]* <line-names>?
-// <auto-repeat>
-// [ <line-names>? [ <fixed-size> | <fixed-repeat> ] ]* <line-names>?
+ // <auto-track-list> =
+ // [ <line-names>? [ <fixed-size> | <fixed-repeat> ] ]* <line-names>?
+ // <auto-repeat>
+ // [ <line-names>? [ <fixed-size> | <fixed-repeat> ] ]* <line-names>?
-test_computed_value("grid-template-columns", '[one] repeat(2, minmax(50px, auto)) [two] 30px [three] repeat(auto-fill, 10px) 40px [four five] repeat(2, minmax(200px, auto)) [six]',
- '[one] 50px 50px [two] 30px [three] 10px 40px [four five] 200px 200px [six]');
+ test_computed_value("grid-template-columns", '[one] repeat(2, minmax(50px, auto)) [two] 30px [three] repeat(auto-fill, 10px) 40px [four five] repeat(2, minmax(200px, auto)) [six]',
+ '[one] 50px 50px [two] 30px [three] 10px 40px [four five] 200px 200px [six]');
</script>
</body>
</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-not-inhibited-001.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-not-inhibited-001.html
index 54499d3d69..1c912feb1f 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-not-inhibited-001.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/masonry-not-inhibited-001.html
@@ -2,7 +2,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry layout shouldn't be inhibited simply due to being an independent formatting context (unlike subgrid)</title>
<link rel="author" title="Daniel Holbert" href="mailto:dholbert@mozilla.com">
-<link rel="help" href="https://drafts.csswg.org/css-grid-3/">
+<link rel="help" href="https://drafts.csswg.org/css-grid-3/#grid-template-masonry">
<link rel="match" href="masonry-not-inhibited-001-ref.html">
<style>
grid {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/order/masonry-order-001.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/order/masonry-order-001.html
index d01f52ea04..37b3d01fb4 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/order/masonry-order-001.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/order/masonry-order-001.html
@@ -7,7 +7,8 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry layout using `order`</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#masonry-layout-algorithm">
<link rel="match" href="masonry-order-001-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/order/masonry-order-002.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/order/masonry-order-002.html
index abad3d44b8..5ebdec2719 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/order/masonry-order-002.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/order/masonry-order-002.html
@@ -7,7 +7,8 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry layout using `order` and `masonry-auto-flow: next`</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#masonry-layout-algorithm">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#masonry-auto-flow">
<link rel="match" href="masonry-order-002-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/parsing/masonry-parsing.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/parsing/masonry-parsing.html
index e3245095fa..2204c06f72 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/parsing/masonry-parsing.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/parsing/masonry-parsing.html
@@ -3,7 +3,8 @@
<head>
<meta charset="utf-8">
<title>CSS Masonry Test: parsing properties and shortands</title>
-<link rel="help" href="https://drafts.csswg.org/css-grid-2/#propdef-grid-template-columns">
+<link rel="help" href="https://drafts.csswg.org/css-grid-3/#grid-template-masonry">
+<link rel="help" href="https://drafts.csswg.org/css-grid-3/#masonry-auto-flow">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/css/support/parsing-testcommon.js"></script>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/subgrid/masonry-subgrid-001.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/subgrid/masonry-subgrid-001.html
index de07dfb78d..c71ba68612 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/subgrid/masonry-subgrid-001.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/subgrid/masonry-subgrid-001.html
@@ -7,7 +7,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry layout with a subgrid</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#subgrids">
<link rel="match" href="masonry-subgrid-001-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/subgrid/masonry-subgrid-002.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/subgrid/masonry-subgrid-002.html
index 466e95eb03..167f2d4496 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/subgrid/masonry-subgrid-002.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/subgrid/masonry-subgrid-002.html
@@ -7,7 +7,7 @@
<meta charset="utf-8">
<title>CSS Grid Test: Masonry layout with a subgrid</title>
<link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
- <link rel="help" href="https://drats.csswg.org/css-grid-2">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-3/#subgrids">
<link rel="match" href="masonry-subgrid-002-ref.html">
<style>
html,body {
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize-ref.html
index 7196886492..53447e0dad 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize-ref.html
@@ -1,16 +1,14 @@
<!DOCTYPE html>
<html>
-
+<title>CSS Reference: Masonry grid container size changes when item sizes change</title>
<head>
<meta charset="utf-8">
<link rel="author" title="Brandon Stewart" href="mailto:brandonstewart@apple.com">
-<link rel="help" href="https://drafts.csswg.org/css-grid-3">
-<meta name="assert" content="When the height of an element in the grid changes, ensure the grid is properly resized">
</head>
<style>
grid {
- display: grid;
+ display: inline-grid;
grid-template-rows: masonry;
grid-template-columns: auto;
grid-gap: 10px;
@@ -20,7 +18,7 @@
item1 {
background-color: grey;
height: 125px;
- width: 250px;
+ width: 300px;
}
item2 {
background-color: grey;
@@ -35,4 +33,4 @@
<item2>2</item2>
</grid>
</body>
-</html> \ No newline at end of file
+</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize.html
index 06c2901f27..dbff19f28e 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize.html
@@ -1,17 +1,18 @@
<!DOCTYPE html>
-<html>
+<html class="reftest-wait">
<head>
<meta charset="utf-8">
+<title>CSS Test: Masonry grid container size changes when item sizes change</title>
<link rel="author" title="Brandon Stewart" href="mailto:brandonstewart@apple.com">
<link rel="help" href="https://drafts.csswg.org/css-grid-3">
<link rel="match" href="masonry-track-sizing-check-grid-height-on-resize-ref.html">
-<meta name="assert" content="When the height of an element in the grid changes, ensure the grid is properly resized">
+<meta name="assert" content="Test passes if when the size of an item in the masonry grid changes, the grid container is properly resized">
</head>
<style>
grid {
- display: grid;
+ display: inline-grid;
grid-template-rows: masonry;
grid-template-columns: auto;
grid-gap: 10px;
@@ -32,8 +33,11 @@
</grid>
</body>
<script>
- /* Force a relayout */
- document.body.offsetHeight;
- document.querySelector("item").style["height"] = "125px";
+ document.body.offsetHeight; // Force a relayout
+ var gridItem = document.querySelector('item');
+ gridItem.style.height = '125px';
+ gridItem.style.width = '300px';
+ document
+ document.documentElement.className = '';
</script>
-</html> \ No newline at end of file
+</html>
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-overflow-left-side-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-overflow-left-side-ref.html
index 5a31517183..3e973b7fe7 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-overflow-left-side-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-overflow-left-side-ref.html
@@ -12,7 +12,7 @@
<style>
grid {
display: grid;
- grid-template-columns: 50px 1fr;
+ grid-template-columns: 100px 1fr;
grid-template-rows: 50px 250px;
width: 300px;
height: 100px;
diff --git a/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-overflow-right-side-ref.html b/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-overflow-right-side-ref.html
index 51e657e2a4..eb84b2e6bd 100644
--- a/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-overflow-right-side-ref.html
+++ b/testing/web-platform/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-overflow-right-side-ref.html
@@ -12,7 +12,7 @@
<style>
grid {
display: grid;
- grid-template-columns: 1fr 50px;
+ grid-template-columns: 1fr 100px;
grid-template-rows: 1fr 50px;
width: 300px;
height: 100px;
diff --git a/testing/web-platform/tests/css/css-grid/min-size-auto-overflow-clip-ref.html b/testing/web-platform/tests/css/css-grid/min-size-auto-overflow-clip-ref.html
new file mode 100644
index 0000000000..64e6ff2d99
--- /dev/null
+++ b/testing/web-platform/tests/css/css-grid/min-size-auto-overflow-clip-ref.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta charset="utf-8">
+<style>
+ .flex {
+ display: flex;
+ flex-direction: column;
+ }
+ .grid {
+ display: grid;
+ flex-basis: 20px;
+ border: 1px solid;
+ }
+</style>
+<div class="flex">
+ <div class="grid">
+ <div style="height: 50px;"></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-grid/min-size-auto-overflow-clip.html b/testing/web-platform/tests/css/css-grid/min-size-auto-overflow-clip.html
new file mode 100644
index 0000000000..3dfd2c18f5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-grid/min-size-auto-overflow-clip.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<meta charset="utf-8">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="help" href="https://drafts.csswg.org/css-grid/#min-size-auto">
+<link rel="match" href="min-size-auto-overflow-clip-ref.html">
+<title>overflow: visible and clip behave the same for min-size purposes</title>
+<style>
+ .flex {
+ display: flex;
+ flex-direction: column;
+ }
+ .grid {
+ display: grid;
+ flex-basis: 20px;
+ border: 1px solid;
+ }
+</style>
+<div class="flex">
+ <div class="grid" style="overflow: clip">
+ <div style="height: 50px;"></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-grid/subgrid/line-names-013.html b/testing/web-platform/tests/css/css-grid/subgrid/line-names-013.html
new file mode 100644
index 0000000000..a964cf7813
--- /dev/null
+++ b/testing/web-platform/tests/css/css-grid/subgrid/line-names-013.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Grid Test: Clamping a subgrid's own grid-template-areas</title>
+<link rel="author" title="Ethan Jimenez" href="mailto:ethavar@microsoft.com">
+<link rel="help" href="https://drafts.csswg.org/css-grid-2/#grid-template-areas-property">
+<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
+<style>
+.grid {
+ background: red;
+ display: grid;
+ grid-template: 100px 100px / 100px;
+ height: 100px;
+ width: 100px;
+}
+.subgrid {
+ display: grid;
+ grid-template-areas: "item item"
+ "item item";
+ grid-template-rows: subgrid;
+}
+.item {
+ background: green;
+ grid-area: item;
+}
+</style>
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div class="grid">
+ <div class="subgrid">
+ <div class="item"></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-004-2.html b/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-004-2.html
index 0a612d66d1..1b8ff935a6 100644
--- a/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-004-2.html
+++ b/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-004-2.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-highlight-api-1/">
<link rel="match" href="custom-highlight-painting-004-2-ref.html">
<meta name="assert" value="When painting overlapping highlights with the same .priority, the one added last should be painted on top; and style properties not defined by the one on top (background-color in this case) should follow the rules of the next Highlight from top to bottom until there's one that overwrites default (or use default value otherwise).">
-<meta name="fuzzy" content="0-88;0-1">
+<meta name="fuzzy" content="0-88;0-4">
<style>
::highlight(foo) {
color:blue;
diff --git a/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-020-ref.html b/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-020-ref.html
new file mode 100644
index 0000000000..92cf04c180
--- /dev/null
+++ b/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-020-ref.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Highlight API Test: Highlighing Ligatures Ref</title>
+<style>
+ @font-face {
+ font-family: 'mplus';
+ src: url('../../../fonts/mplus-1p-regular.woff');
+ }
+ div {
+ font-size: 7em;
+ font-family: mplus, sans-serif;
+ }
+ ::selection {
+ color:green;
+ text-decoration: blue 2px line-through;
+ }
+</style>
+<body><div>fii ffi fff</div>
+<script>
+ let textNode = document.body.firstChild.firstChild;
+
+ let r1 = new Range();
+ r1.setStart(textNode, 1);
+ r1.setEnd(textNode, 9);
+ window.getSelection().addRange(r1);
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-020.html b/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-020.html
new file mode 100644
index 0000000000..64ae924aa9
--- /dev/null
+++ b/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-020.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>CSS Highlight API Test: Highlighting Ligatures</title>
+<link rel="author" title="Stephen Chenney" href="mailto:schenney@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-highlight-api-1/">
+<link rel="match" href="custom-highlight-painting-020-ref.html">
+<meta name="assert" value="Highlights should split ligatures.">
+<meta name="fuzzy" content="0-128;0-28">
+<style>
+ @font-face {
+ font-family: 'mplus';
+ src: url('../../../fonts/mplus-1p-regular.woff');
+ }
+ ::highlight(foo) {
+ color:green;
+ text-decoration: blue 2px line-through;
+ }
+ div {
+ font-size: 7em;
+ font-family: mplus, serif;
+ }
+</style>
+<body><div>fii ffi fff</div>
+<script>
+ let textNode = document.body.firstChild.firstChild;
+
+ let r1 = new Range();
+ r1.setStart(textNode, 1);
+ r1.setEnd(textNode, 9);
+ let h1 = new Highlight(r1);
+ CSS.highlights.set("foo", h1);
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-021-ref.html b/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-021-ref.html
new file mode 100644
index 0000000000..1ec68549b8
--- /dev/null
+++ b/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-021-ref.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>CSS Highlight API Test: Highlighting thick text - Reference</title>
+<link rel="author" title="Stephen Chenney" href="mailto:schenney@igalia.com">
+<style>
+ body {
+ -webkit-text-stroke-width: 3px;
+ font-size: 25px;
+ }
+ span {
+ -webkit-text-stroke-width: 0px;
+ background-color: green;
+ }
+</style>
+<body>This<span> thick text </span>should not be highlighted</body>
diff --git a/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-021.html b/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-021.html
new file mode 100644
index 0000000000..29ee123457
--- /dev/null
+++ b/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-021.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>CSS Highlight API Test: Highlighting thick text</title>
+<link rel="author" title="Stephen Chenney" href="mailto:schenney@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-text">
+<link rel="match" href="custom-highlight-painting-021-ref.html">
+<meta name="assert" value="Text with -webkit-text-stroke-width should not be highlighted.">
+<meta name="fuzzy" content="0-128;0-4">
+<style>
+ body {
+ -webkit-text-stroke-width: 3px;
+ font-size: 25px;
+ }
+ ::highlight(foo) {
+ background-color: green;
+ }
+</style>
+<body>This thick text should not be highlighted</body>
+<script>
+ let textNode = document.body.firstChild;
+
+ let r1 = new Range();
+ r1.setStart(textNode, 4);
+ r1.setEnd(textNode, 16);
+
+ let h1 = new Highlight(r1);
+
+ CSS.highlights.set("foo", h1);
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-vertical-writing-mode-001-ref.html b/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-vertical-writing-mode-001-ref.html
index b46c679c9f..c2bf7ae7bd 100644
--- a/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-vertical-writing-mode-001-ref.html
+++ b/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-vertical-writing-mode-001-ref.html
@@ -2,14 +2,25 @@
<link rel="stylesheet" href="../../css-pseudo/support/highlights.css">
<meta charset="utf-8">
<style>
- :root {
+ #verticalRL {
writing-mode: vertical-rl;
}
-
+ #verticalLR {
+ writing-mode: vertical-lr;
+ }
+ #sidewaysRL {
+ writing-mode: sideways-rl;
+ }
+ #sidewaysLR {
+ writing-mode: sideways-lr;
+ }
.highlighted {
background-color: yellow;
color: blue;
}
</style>
<body>
-<div class="highlight_reftest"><span class="highlighted">One two </span><span>three…</span></div>
+<div id="verticalRL" class="highlight_reftest"><span class="highlighted">One two </span><span>three…</span></div>
+<div id="verticalLR" class="highlight_reftest"><span class="highlighted">One two </span><span>three…</span></div>
+<div id="sidewaysRL" class="highlight_reftest"><span class="highlighted">One two </span><span>three…</span></div>
+<div id="sidewaysLR" class="highlight_reftest"><span class="highlighted">One two </span><span>three…</span></div>
diff --git a/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-vertical-writing-mode-001.html b/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-vertical-writing-mode-001.html
index 9fe2bdcd5f..ba25037976 100644
--- a/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-vertical-writing-mode-001.html
+++ b/testing/web-platform/tests/css/css-highlight-api/painting/custom-highlight-painting-vertical-writing-mode-001.html
@@ -5,21 +5,44 @@
<link rel="match" href="custom-highlight-painting-vertical-writing-mode-001-ref.html">
<link rel="stylesheet" href="../../css-pseudo/support/highlights.css">
<meta name="assert" value="::highlight overlay is painted correctly when the highlighted text is in a vertical writing mode">
+<meta name="fuzzy" content="0-255;0-18">
<style>
- :root {
+ #verticalRL {
writing-mode: vertical-rl;
}
+ #verticalLR {
+ writing-mode: vertical-lr;
+ }
+ #sidewaysRL {
+ writing-mode: sideways-rl;
+ }
+ #sidewaysLR {
+ writing-mode: sideways-lr;
+ }
::highlight(example-highlight) {
background-color: yellow;
color: blue;
}
</style>
<body>
-<div class="highlight_reftest"><span>One </span><span>two </span><span>three…</span></div>
+<div id="verticalRL" class="highlight_reftest"><span>One </span><span>two </span><span>three…</span></div>
+<div id="verticalLR" class="highlight_reftest"><span>One </span><span>two </span><span>three…</span></div>
+<div id="sidewaysRL" class="highlight_reftest"><span>One </span><span>two </span><span>three…</span></div>
+<div id="sidewaysLR" class="highlight_reftest"><span>One </span><span>two </span><span>three…</span></div>
<script>
- let r = new Range();
- r.setStart(document.querySelector("div"), 0);
- r.setEnd(document.querySelector("div"), 2);
+ let verticalRLRange = new Range();
+ verticalRLRange.setStart(verticalRL, 0);
+ verticalRLRange.setEnd(verticalRL, 2);
+ let verticalLRRange = new Range();
+ verticalLRRange.setStart(verticalLR, 0);
+ verticalLRRange.setEnd(verticalLR, 2);
+ let sidewaysRLRange = new Range();
+ sidewaysRLRange.setStart(sidewaysRL, 0);
+ sidewaysRLRange.setEnd(sidewaysRL, 2);
+ let sidewaysLRRange = new Range();
+ sidewaysLRRange.setStart(sidewaysLR, 0);
+ sidewaysLRRange.setEnd(sidewaysLR, 2);
- CSS.highlights.set("example-highlight", new Highlight(r));
+ CSS.highlights.set("example-highlight",
+ new Highlight(verticalRLRange, verticalLRRange, sidewaysLRRange, sidewaysRLRange));
</script>
diff --git a/testing/web-platform/tests/css/css-images/gradient/repeating-gradient-hsl-and-oklch.html b/testing/web-platform/tests/css/css-images/gradient/repeating-gradient-hsl-and-oklch.html
index 71e615d919..82e65d1dac 100644
--- a/testing/web-platform/tests/css/css-images/gradient/repeating-gradient-hsl-and-oklch.html
+++ b/testing/web-platform/tests/css/css-images/gradient/repeating-gradient-hsl-and-oklch.html
@@ -1,6 +1,6 @@
<!doctype html>
<html lang="en">
-
+<meta name=fuzzy content="maxDifference=0-1;totalPixels=0-2000">
<head>
<meta charset="utf-8">
<title>Repeating linear gradients in HSL and OKLCH space</title>
diff --git a/testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001c.tentative.html b/testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001c.tentative.html
new file mode 100644
index 0000000000..b17f59f887
--- /dev/null
+++ b/testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001c.tentative.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html class="reftest-wait">
+ <head>
+ <meta charset="utf-8">
+ <title>CSS Test: 'object-fit: contain' and 'contain-intrinsic-size' on a canvas element, drawing a PNG image</title>
+ <link rel="help" href="https://github.com/w3c/csswg-drafts/issues/10116">
+ <link rel="match" href="object-fit-containsize-png-001-ref.tentative.html">
+ <style type="text/css">
+ canvas {
+ border: 1px dashed gray;
+ padding: 1px;
+ image-rendering: pixelated;
+ float: left;
+ object-position: top left;
+ contain: size;
+ }
+ br {
+ clear: both;
+ }
+ .big {
+ contain-intrinsic-width: 32px;
+ contain-intrinsic-height: 48px;
+ }
+ .small {
+ contain-intrinsic-width: 8px;
+ contain-intrinsic-height: 8px;
+ }
+
+ .cover { object-fit: cover }
+ .contain { object-fit: contain }
+ .fill { object-fit: fill }
+ .none { object-fit: none }
+ .scaledown { object-fit: scale-down }
+
+ </style>
+ <script>
+ function drawImageToCanvases(imageURI) {
+ var image = new Image();
+ image.onload = function() {
+ var canvasElems = document.getElementsByTagName("canvas");
+ for (var i = 0; i < canvasElems.length; i++) {
+ var ctx = canvasElems[i].getContext("2d");
+ ctx.drawImage(image, 0, 0);
+ }
+ document.documentElement.removeAttribute("class");
+ }
+ image.src = imageURI;
+ }
+ </script>
+ </head>
+ <body onload="drawImageToCanvases('support/colors-16x8.png')">
+ <!-- big: -->
+ <canvas width="16" height="8" class="big cover"></canvas>
+ <canvas width="16" height="8" class="big contain"></canvas>
+ <canvas width="16" height="8" class="big fill"></canvas>
+ <canvas width="16" height="8" class="big none"></canvas>
+ <canvas width="16" height="8" class="big scaledown"></canvas>
+ <br>
+ <!-- small: -->
+ <canvas width="16" height="8" class="small cover"></canvas>
+ <canvas width="16" height="8" class="small contain"></canvas>
+ <canvas width="16" height="8" class="small fill"></canvas>
+ <canvas width="16" height="8" class="small none"></canvas>
+ <canvas width="16" height="8" class="small scaledown"></canvas>
+ <br>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001e.tentative.html b/testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001e.tentative.html
new file mode 100644
index 0000000000..1b121f4eee
--- /dev/null
+++ b/testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001e.tentative.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CSS Test: 'object-fit: contain' and 'contain-intrinsic-size' on an embed element, embedding a PNG image</title>
+ <link rel="help" href="https://github.com/w3c/csswg-drafts/issues/10116">
+ <link rel="match" href="object-fit-containsize-png-001-ref.tentative.html">
+ <style type="text/css">
+ embed {
+ border: 1px dashed gray;
+ padding: 1px;
+ image-rendering: pixelated;
+ float: left;
+ object-position: top left;
+ contain: size;
+ }
+ br {
+ clear: both;
+ }
+ .big {
+ contain-intrinsic-width: 32px;
+ contain-intrinsic-height: 48px;
+ }
+ .small {
+ contain-intrinsic-width: 8px;
+ contain-intrinsic-height: 8px;
+ }
+
+ .cover { object-fit: cover }
+ .contain { object-fit: contain }
+ .fill { object-fit: fill }
+ .none { object-fit: none }
+ .scaledown { object-fit: scale-down }
+
+ </style>
+ </head>
+ <body>
+ <!-- big: -->
+ <embed src="support/colors-16x8.png" class="big cover"></embed>
+ <embed src="support/colors-16x8.png" class="big contain"></embed>
+ <embed src="support/colors-16x8.png" class="big fill"></embed>
+ <embed src="support/colors-16x8.png" class="big none"></embed>
+ <embed src="support/colors-16x8.png" class="big scaledown"></embed>
+ <br>
+ <!-- small: -->
+ <embed src="support/colors-16x8.png" class="small cover"></embed>
+ <embed src="support/colors-16x8.png" class="small contain"></embed>
+ <embed src="support/colors-16x8.png" class="small fill"></embed>
+ <embed src="support/colors-16x8.png" class="small none"></embed>
+ <embed src="support/colors-16x8.png" class="small scaledown"></embed>
+ <br>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001i.tentative.html b/testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001i.tentative.html
new file mode 100644
index 0000000000..2da201f3d6
--- /dev/null
+++ b/testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001i.tentative.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CSS Test: 'object-fit: contain' and 'contain-intrinsic-size' on img element, embedding a PNG image</title>
+ <link rel="author" title="Eric Portis" href="mailto:e@ericportis.com">
+ <link rel="help" href="https://github.com/w3c/csswg-drafts/issues/10116">
+ <link rel="match" href="object-fit-containsize-png-001-ref.tentative.html">
+ <style type="text/css">
+ img {
+ border: 1px dashed gray;
+ padding: 1px;
+ image-rendering: pixelated;
+ float: left;
+ object-position: top left;
+ contain: size;
+ }
+ br {
+ clear: both;
+ }
+ .big {
+ contain-intrinsic-width: 32px;
+ contain-intrinsic-height: 48px;
+ }
+ .small {
+ contain-intrinsic-width: 8px;
+ contain-intrinsic-height: 8px;
+ }
+
+ .cover { object-fit: cover }
+ .contain { object-fit: contain }
+ .fill { object-fit: fill }
+ .none { object-fit: none }
+ .scaledown { object-fit: scale-down }
+
+ </style>
+ </head>
+ <body>
+ <!-- big: -->
+ <img src="support/colors-16x8.png" class="big cover">
+ <img src="support/colors-16x8.png" class="big contain">
+ <img src="support/colors-16x8.png" class="big fill">
+ <img src="support/colors-16x8.png" class="big none">
+ <img src="support/colors-16x8.png" class="big scaledown">
+ <br>
+ <!-- small: -->
+ <img src="support/colors-16x8.png" class="small cover">
+ <img src="support/colors-16x8.png" class="small contain">
+ <img src="support/colors-16x8.png" class="small fill">
+ <img src="support/colors-16x8.png" class="small none">
+ <img src="support/colors-16x8.png" class="small scaledown">
+ <br>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001p.tentative.html b/testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001p.tentative.html
new file mode 100644
index 0000000000..c2c1697c63
--- /dev/null
+++ b/testing/web-platform/tests/css/css-images/object-fit-containcontainintrinsicsize-png-001p.tentative.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CSS Test: 'object-fit: contain' and 'contain-intrinsic-size' on video element with a PNG poster image</title>
+ <link rel="help" href="https://github.com/w3c/csswg-drafts/issues/10116">
+ <link rel="match" href="object-fit-containsize-png-001-ref.tentative.html">
+ <style type="text/css">
+ video {
+ border: 1px dashed gray;
+ padding: 1px;
+ image-rendering: pixelated;
+ float: left;
+ object-position: top left;
+ contain: size;
+ }
+ br {
+ clear: both;
+ }
+ .big {
+ contain-intrinsic-width: 32px;
+ contain-intrinsic-height: 48px;
+ }
+ .small {
+ contain-intrinsic-width: 8px;
+ contain-intrinsic-height: 8px;
+ }
+
+ .cover { object-fit: cover }
+ .contain { object-fit: contain }
+ .fill { object-fit: fill }
+ .none { object-fit: none }
+ .scaledown { object-fit: scale-down }
+
+ </style>
+ </head>
+ <body>
+ <!-- big: -->
+ <video poster="support/colors-16x8.png" class="big cover"></video>
+ <video poster="support/colors-16x8.png" class="big contain"></video>
+ <video poster="support/colors-16x8.png" class="big fill"></video>
+ <video poster="support/colors-16x8.png" class="big none"></video>
+ <video poster="support/colors-16x8.png" class="big scaledown"></video>
+ <br>
+ <!-- small: -->
+ <video poster="support/colors-16x8.png" class="small cover"></video>
+ <video poster="support/colors-16x8.png" class="small contain"></video>
+ <video poster="support/colors-16x8.png" class="small fill"></video>
+ <video poster="support/colors-16x8.png" class="small none"></video>
+ <video poster="support/colors-16x8.png" class="small scaledown"></video>
+ <br>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001-ref.tentative.html b/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001-ref.tentative.html
new file mode 100644
index 0000000000..0c7c533d31
--- /dev/null
+++ b/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001-ref.tentative.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CSS Reftest Reference</title>
+ <link rel="author" title="Eric Portis" href="mailto:e@ericportis.com">
+ <style type="text/css">
+ .objectOuter {
+ border: 1px dashed gray;
+ padding: 1px;
+ float: left;
+ }
+ .objectOuter > * {
+ background-image: url("support/colors-16x8.png");
+ background-repeat: no-repeat;
+ image-rendering: pixelated;
+ background-position: 0% 0%;
+ }
+ br {
+ clear: both;
+ }
+ .big {
+ width: 32px;
+ height: 48px;
+ }
+ .small {
+ width: 8px;
+ height: 8px;
+ }
+
+ .cover { background-size: cover; }
+ .contain { background-size: contain; }
+ .fill { background-size: 100% 100%; }
+ .none { background-size: auto auto; }
+ .scaledown { background-size: auto auto; }
+ .small.scaledown { background-size: contain; }
+
+ </style>
+ </head>
+ <body>
+ <!-- big: -->
+ <div class="objectOuter"><div class="big cover"></div></div>
+ <div class="objectOuter"><div class="big contain"></div></div>
+ <div class="objectOuter"><div class="big fill"></div></div>
+ <div class="objectOuter"><div class="big none"></div></div>
+ <div class="objectOuter"><div class="big scaledown"></div></div>
+ <br>
+ <!-- small: -->
+ <div class="objectOuter"><div class="small cover"></div></div>
+ <div class="objectOuter"><div class="small contain"></div></div>
+ <div class="objectOuter"><div class="small fill"></div></div>
+ <div class="objectOuter"><div class="small none"></div></div>
+ <div class="objectOuter"><div class="small scaledown"></div></div>
+ <br>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001c.tentative.html b/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001c.tentative.html
new file mode 100644
index 0000000000..43a50adced
--- /dev/null
+++ b/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001c.tentative.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html class="reftest-wait">
+ <head>
+ <meta charset="utf-8">
+ <title>CSS Test: 'object-fit: contain' and 'contain: size' on canvas element, drawing a PNG image</title>
+ <link rel="help" href="https://github.com/w3c/csswg-drafts/issues/10116">
+ <link rel="match" href="object-fit-containsize-png-001-ref.tentative.html">
+ <style type="text/css">
+ canvas {
+ border: 1px dashed gray;
+ padding: 1px;
+ image-rendering: pixelated;
+ float: left;
+ contain: size;
+ object-position: top left;
+ }
+ br {
+ clear: both;
+ }
+ .big {
+ width: 32px;
+ height: 48px;
+ }
+ .small {
+ width: 8px;
+ height: 8px;
+ }
+
+ .cover { object-fit: cover }
+ .contain { object-fit: contain }
+ .fill { object-fit: fill }
+ .none { object-fit: none }
+ .scaledown { object-fit: scale-down }
+
+ </style>
+ <script>
+ function drawImageToCanvases(imageURI) {
+ var image = new Image();
+ image.onload = function() {
+ var canvasElems = document.getElementsByTagName("canvas");
+ for (var i = 0; i < canvasElems.length; i++) {
+ var ctx = canvasElems[i].getContext("2d");
+ ctx.drawImage(image, 0, 0);
+ }
+ document.documentElement.removeAttribute("class");
+ }
+ image.src = imageURI;
+ }
+ </script>
+ </head>
+ <body onload="drawImageToCanvases('support/colors-16x8.png')">
+ <!-- big: -->
+ <canvas width="16" height="8" class="big cover"></canvas>
+ <canvas width="16" height="8" class="big contain"></canvas>
+ <canvas width="16" height="8" class="big fill"></canvas>
+ <canvas width="16" height="8" class="big none"></canvas>
+ <canvas width="16" height="8" class="big scaledown"></canvas>
+ <br>
+ <!-- small: -->
+ <canvas width="16" height="8" class="small cover"></canvas>
+ <canvas width="16" height="8" class="small contain"></canvas>
+ <canvas width="16" height="8" class="small fill"></canvas>
+ <canvas width="16" height="8" class="small none"></canvas>
+ <canvas width="16" height="8" class="small scaledown"></canvas>
+ <br>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001e.tentative.html b/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001e.tentative.html
new file mode 100644
index 0000000000..37e0f1e447
--- /dev/null
+++ b/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001e.tentative.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CSS Test: 'object-fit: contain' and 'contain: size' on an embed element, embedding a PNG image</title>
+ <link rel="help" href="https://github.com/w3c/csswg-drafts/issues/10116">
+ <link rel="match" href="object-fit-containsize-png-001-ref.tentative.html">
+ <style type="text/css">
+ embed {
+ border: 1px dashed gray;
+ padding: 1px;
+ image-rendering: pixelated;
+ float: left;
+ contain: size;
+ object-position: top left;
+ }
+ br {
+ clear: both;
+ }
+ .big {
+ width: 32px;
+ height: 48px;
+ }
+ .small {
+ width: 8px;
+ height: 8px;
+ }
+
+ .cover { object-fit: cover }
+ .contain { object-fit: contain }
+ .fill { object-fit: fill }
+ .none { object-fit: none }
+ .scaledown { object-fit: scale-down }
+
+ </style>
+ </head>
+ <body>
+ <!-- big: -->
+ <embed src="support/colors-16x8.png" class="big cover"></embed>
+ <embed src="support/colors-16x8.png" class="big contain"></embed>
+ <embed src="support/colors-16x8.png" class="big fill"></embed>
+ <embed src="support/colors-16x8.png" class="big none"></embed>
+ <embed src="support/colors-16x8.png" class="big scaledown"></embed>
+ <br>
+ <!-- small: -->
+ <embed src="support/colors-16x8.png" class="small cover"></embed>
+ <embed src="support/colors-16x8.png" class="small contain"></embed>
+ <embed src="support/colors-16x8.png" class="small fill"></embed>
+ <embed src="support/colors-16x8.png" class="small none"></embed>
+ <embed src="support/colors-16x8.png" class="small scaledown"></embed>
+ <br>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001i.tentative.html b/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001i.tentative.html
new file mode 100644
index 0000000000..1654cbf36f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001i.tentative.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CSS Test: 'object-fit: contain' and 'contain: size' on img element, embedding a PNG image</title>
+ <link rel="author" title="Eric Portis" href="mailto:e@ericportis.com">
+ <link rel="help" href="https://github.com/w3c/csswg-drafts/issues/10116">
+ <link rel="match" href="object-fit-containsize-png-001-ref.tentative.html">
+ <style type="text/css">
+ img {
+ border: 1px dashed gray;
+ padding: 1px;
+ image-rendering: pixelated;
+ float: left;
+ contain: size;
+ object-position: top left;
+ }
+ br {
+ clear: both;
+ }
+ .big {
+ width: 32px;
+ height: 48px;
+ }
+ .small {
+ width: 8px;
+ height: 8px;
+ }
+
+ .cover { object-fit: cover }
+ .contain { object-fit: contain }
+ .fill { object-fit: fill }
+ .none { object-fit: none }
+ .scaledown { object-fit: scale-down }
+
+ </style>
+ </head>
+ <body>
+ <!-- big: -->
+ <img src="support/colors-16x8.png" class="big cover">
+ <img src="support/colors-16x8.png" class="big contain">
+ <img src="support/colors-16x8.png" class="big fill">
+ <img src="support/colors-16x8.png" class="big none">
+ <img src="support/colors-16x8.png" class="big scaledown">
+ <br>
+ <!-- small: -->
+ <img src="support/colors-16x8.png" class="small cover">
+ <img src="support/colors-16x8.png" class="small contain">
+ <img src="support/colors-16x8.png" class="small fill">
+ <img src="support/colors-16x8.png" class="small none">
+ <img src="support/colors-16x8.png" class="small scaledown">
+ <br>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001p.tentative.html b/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001p.tentative.html
new file mode 100644
index 0000000000..c3072f9838
--- /dev/null
+++ b/testing/web-platform/tests/css/css-images/object-fit-containsize-png-001p.tentative.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CSS Test: 'object-fit: contain' and 'contain: size' on video element with a PNG poster image</title>
+ <link rel="help" href="https://github.com/w3c/csswg-drafts/issues/10116">
+ <link rel="match" href="object-fit-containsize-png-001-ref.tentative.html">
+ <style type="text/css">
+ video {
+ border: 1px dashed gray;
+ padding: 1px;
+ image-rendering: pixelated;
+ float: left;
+ contain: size;
+ object-position: top left;
+ }
+ br {
+ clear: both;
+ }
+ .big {
+ width: 32px;
+ height: 48px;
+ }
+ .small {
+ width: 8px;
+ height: 8px;
+ }
+
+ .cover { object-fit: cover }
+ .contain { object-fit: contain }
+ .fill { object-fit: fill }
+ .none { object-fit: none }
+ .scaledown { object-fit: scale-down }
+
+ </style>
+ </head>
+ <body>
+ <!-- big: -->
+ <video poster="support/colors-16x8.png" class="big cover"></video>
+ <video poster="support/colors-16x8.png" class="big contain"></video>
+ <video poster="support/colors-16x8.png" class="big fill"></video>
+ <video poster="support/colors-16x8.png" class="big none"></video>
+ <video poster="support/colors-16x8.png" class="big scaledown"></video>
+ <br>
+ <!-- small: -->
+ <video poster="support/colors-16x8.png" class="small cover"></video>
+ <video poster="support/colors-16x8.png" class="small contain"></video>
+ <video poster="support/colors-16x8.png" class="small fill"></video>
+ <video poster="support/colors-16x8.png" class="small none"></video>
+ <video poster="support/colors-16x8.png" class="small scaledown"></video>
+ <br>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-001.html b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-001.html
index 2cbf1c26db..14429b1002 100644
--- a/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-001.html
+++ b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-001.html
@@ -15,6 +15,6 @@
</style>
<div class ="div-parent"">
- <div id="d1"></div>
+
<div id="d2">Testline1<br>Testline2<br>Testline3</div>
</div>
diff --git a/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002-ref.html b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002-ref.html
new file mode 100644
index 0000000000..8c10a80370
--- /dev/null
+++ b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<title>Reference for trimming block-boxes at their first/last formatted lines</title>
+<link rel="help" href="https://drafts.csswg.org/css-inline-3/#leading-trim">
+<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" />
+
+<style>
+.div-parent {
+ outline: 1px solid orange;
+ font-family: Ahem;
+ font-size: 20px;
+ line-height: 1;
+ writing-mode:vertical-lr;
+}
+</style>
+<div class ="div-parent">
+ Test<br><br>
+</div>
diff --git a/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002.html b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002.html
new file mode 100644
index 0000000000..fdf5b71d43
--- /dev/null
+++ b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<title>Tests block boxes's edges are trimmed at text-over/text-under baselines of their first/last formatted lines</title>
+<link rel="help" href="https://drafts.csswg.org/css-inline-3/#leading-trim">
+<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" />
+<link rel="match" href="text-box-trim-half-leading-block-box-002-ref.html">
+
+<style>
+.div-parent {
+ outline: 1px solid orange;
+ font-family: Ahem;
+ font-size: 20px;
+ line-height: 3;
+ writing-mode:vertical-lr;
+}
+</style>
+
+<div class ="div-parent" style="text-box-trim:start">Test</div>
diff --git a/testing/web-platform/tests/css/css-lists/WEB_FEATURES.yml b/testing/web-platform/tests/css/css-lists/WEB_FEATURES.yml
new file mode 100644
index 0000000000..89ef4a2521
--- /dev/null
+++ b/testing/web-platform/tests/css/css-lists/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: counter-set
+ files:
+ - counter-set-*
diff --git a/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error-ref.html b/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error-ref.html
index a069e4d3ae..e9f54f3ad3 100644
--- a/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error-ref.html
+++ b/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error-ref.html
@@ -1,7 +1,6 @@
<!DOCTYPE html>
<style>
.container {
- position: fixed;
width: 70px;
height: 126px;
background-color: green;
diff --git a/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error.html b/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error.html
index 8a02a5b2a6..02a14ad65f 100644
--- a/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error.html
+++ b/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error.html
@@ -2,11 +2,10 @@
<html class="reftest-wait">
<link rel="help" href="https://drafts.csswg.org/css-shapes-1/#basic-shape-interpolation">
<link rel="match" href="clip-path-animation-fixed-position-rounding-error-ref.html">
+<meta name="fuzzy" content="maxDifference=0-64;totalPixels=0-400">
<!--
Test that clip paths on elements with position: fixed draw correctly,
even in scenarios that involve partial pixels
-
- Currently uses fuzzy diff because of crbug.com/1249071
-->
<style>
.container {
diff --git a/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position.html b/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position.html
index 3ffc2a30a7..20fc8e52d8 100644
--- a/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position.html
+++ b/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position.html
@@ -1,6 +1,6 @@
<!DOCTYPE html>
<html class="reftest-wait">
-<meta name=fuzzy content="0-10;0-150">
+<meta name=fuzzy content="0-60;0-350">
<link rel="help" href="https://drafts.csswg.org/css-shapes-1/#basic-shape-interpolation">
<link rel="match" href="clip-path-animation-fixed-position-ref.html">
<!--
diff --git a/testing/web-platform/tests/css/css-masking/clip-path/animations/two-clip-path-animation-diff-length4.html b/testing/web-platform/tests/css/css-masking/clip-path/animations/two-clip-path-animation-diff-length4.html
new file mode 100644
index 0000000000..0a893cb756
--- /dev/null
+++ b/testing/web-platform/tests/css/css-masking/clip-path/animations/two-clip-path-animation-diff-length4.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<link rel="help" href="https://drafts.csswg.org/css-shapes-1/#basic-shape-interpolation">
+<link rel="match" href="two-clip-path-animation-diff-length1-ref.html">
+<style>
+ #container {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ animation: clippath2 0.5s;
+ }
+
+ @keyframes clippath2 {
+ 0% {
+ clip-path: inset(10px 10px);
+ }
+
+ 1% {
+ clip-path: inset(10px 10px);
+ }
+
+ 100% {
+ clip-path: inset(40px 40px);
+ }
+ }
+</style>
+<script src="/common/reftest-wait.js"></script>
+<script src="../../../../web-animations/testcommon.js"></script>
+<script src="../../../../web-animations/resources/timing-utils.js"></script>
+
+<body>
+ <div id="container"></div>
+
+ <script>
+ // This test ensures that a new no-op animation still invalidates paint
+ // and the animation running on the compositor.
+ document.getAnimations()[0].ready.then(() => {
+ let anim = document.getElementById("container").animate(
+ [
+ { clipPath: "inset(10px 10px)" },
+ { clipPath: "inset(10px 10px)" },
+ ],
+ 2000,
+ );
+ anim.ready.then(() => { return requestAnimationFrame(takeScreenshot); });
+ });
+ </script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/css/css-masking/mask-image/mask-image-svg-viewport-changed.html b/testing/web-platform/tests/css/css-masking/mask-image/mask-image-svg-viewport-changed.html
new file mode 100644
index 0000000000..156317b251
--- /dev/null
+++ b/testing/web-platform/tests/css/css-masking/mask-image/mask-image-svg-viewport-changed.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>Change of viewport affecting mask content</title>
+<link rel="help" href="https://drafts.fxtf.org/css-masking-1/#the-mask-image">
+<link rel="help" href="https://drafts.fxtf.org/css-masking-1/#svg-masks">
+<link rel="match" href="../clip-path/reference/green-100x100.html">
+<div id="container" style="width: 0%">
+ <svg style="width: 100%">
+ <mask id="mask">
+ <rect width="100%" height="100%" fill="white"/>
+ </mask>
+ <rect width="100" height="100" fill="red"/>
+ <rect width="100" height="100" fill="green" mask="url(#mask)"/>
+ </svg>
+</div>
+<script>
+ document.body.offsetTop;
+ document.getElementById('container').style.width = '100%';
+</script>
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp-021.tentative.html b/testing/web-platform/tests/css/css-overflow/line-clamp-021.tentative.html
new file mode 100644
index 0000000000..611cd6f890
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/line-clamp-021.tentative.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Overflow: line-clamp hidden content is treated as ink overflow</title>
+<link rel="author" title="Andreu Botella" href="mailto:abotella@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-overflow-4/#line-clamp">
+<link rel="match" href="reference/line-clamp-021-ref.html">
+<meta name="assert" content="Any overflowing content hidden from paint by line-clamp is treated as ink overflow and therefore doesn't cause the scrollable overflow rectangle to grow.">
+<style>
+#scrollContainer {
+ overflow: scroll;
+ font: 16px / 32px serif;
+ height: 3lh;
+ border: 1px solid black;
+}
+.clamp {
+ line-clamp: 4;
+ padding: 4px;
+ background-color: yellow;
+}
+.pre {
+ white-space: pre;
+}
+</style>
+
+<div id="scrollContainer">
+
+ <div class="clamp">
+ <div class="pre">Line 1
+Line 2
+Line 3
+Line 4
+Line 5
+Line 6</div>
+
+ <div>Another div</div>
+
+ <table>
+ <tr>
+ <td>A</td>
+ <td>B</td>
+ </tr>
+ <tr>
+ <td>C</td>
+ <td>D</td>
+ </tr>
+ </table>
+ </div>
+
+</div>
+
+<script>
+ window.addEventListener("load", () => {
+ const scrollContainer = document.getElementById("scrollContainer");
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
+ });
+</script>
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp-with-abspos-011.tentative.html b/testing/web-platform/tests/css/css-overflow/line-clamp-with-abspos-011.tentative.html
new file mode 100644
index 0000000000..ab5102a7cf
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/line-clamp-with-abspos-011.tentative.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Overflow: line-clamp hidden abspos should count as ink overflow</title>
+<link rel="author" title="Andreu Botella" href="mailto:abotella@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-overflow-4/#line-clamp">
+<link rel="match" href="reference/line-clamp-with-abspos-011-ref.html">
+<meta name="assert" content="Any overflowing content hidden from paint by line-clamp is treated as ink overflow, including absolute positioned boxes, and therefore doesn't cause the scrollable overflow rectangle to grow. Meanwhile, non-hidden abspos count as scrollable overflow.">
+<style>
+#scrollContainer {
+ overflow: scroll;
+ position: relative;
+ font: 16px / 32px serif;
+ height: 4lh;
+ border: 1px solid black;
+}
+.clamp {
+ line-clamp: 4;
+ padding: 0 4px;
+ white-space: pre;
+ background-color: yellow;
+}
+.abspos {
+ position: absolute;
+ right: 0;
+ width: 50px;
+ height: 50px;
+ margin: 4px;
+ background-color: skyblue;
+}
+#abspos1 {
+ top: 4.5lh;
+}
+#abspos2 {
+ top: 6lh;
+}
+</style>
+
+<div id="scrollContainer">
+<div class="clamp">Line 1
+Line 2
+Line 3
+Line 4<div class="abspos" id="abspos1"></div>
+Line 5
+Line 6<div class="abspos" id="abspos2"></div></div>
+</div>
+
+<script>
+ window.addEventListener("load", () => {
+ const scrollContainer = document.getElementById("scrollContainer");
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
+ });
+</script>
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-001.tentative.html b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-001.tentative.html
new file mode 100644
index 0000000000..98bbdcb904
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-001.tentative.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Overflow: float at the start of a line-clamp</title>
+<link rel="author" title="Andreu Botella" href="mailto:abotella@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-overflow-4/#line-clamp">
+<link rel="match" href="reference/line-clamp-with-floats-001-ref.html">
+<meta name="assert" content="Floats in an inline formatting context inside a line-clamp container are not hidden if they come before the clamp point.">
+<style>
+.clamp {
+ line-clamp: 4;
+ font: 16px / 32px serif;
+ padding: 0 4px;
+ white-space: pre;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ width: 50px;
+ height: 50px;
+ margin: 4px;
+ background-color: skyblue;
+}
+</style>
+<div class="clamp"><div class="float"></div>Line 1
+Line 2
+Line 3
+Line 4
+Line 5</div>
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-002.tentative.html b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-002.tentative.html
new file mode 100644
index 0000000000..15379a3de3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-002.tentative.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Overflow: float at the start of a line-clamp</title>
+<link rel="author" title="Andreu Botella" href="mailto:abotella@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-overflow-4/#line-clamp">
+<link rel="match" href="reference/line-clamp-with-floats-001-ref.html">
+<meta name="assert" content="Floats in a block formatting context inside a line-clamp container are not hidden if they come before the clamp point.">
+<style>
+.clamp {
+ line-clamp: 4;
+ font: 16px / 32px serif;
+ padding: 0 4px;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ width: 50px;
+ height: 50px;
+ margin: 4px;
+ background-color: skyblue;
+}
+.pre {
+ white-space: pre;
+}
+</style>
+<div class="clamp">
+<div class="float"></div>
+<div class="pre">Line 1
+Line 2
+Line 3
+Line 4
+Line 5</div>
+</div>
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-003.tentative.html b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-003.tentative.html
new file mode 100644
index 0000000000..c203758235
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-003.tentative.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Overflow: float in line-clamp after clamp point</title>
+<link rel="author" title="Andreu Botella" href="mailto:abotella@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-overflow-4/#line-clamp">
+<link rel="match" href="reference/webkit-line-clamp-005-ref.html">
+<meta name="assert" content="Floats in an inline formatting context inside a line-clamp container are always hidden if they come after the clamp point.">
+<style>
+.clamp {
+ line-clamp: 4;
+ font: 16px / 32px serif;
+ padding: 0 4px;
+ white-space: pre;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ width: 50px;
+ height: 50px;
+ margin: 4px;
+ background-color: skyblue;
+}
+</style>
+<div class="clamp">Line 1
+Line 2
+Line 3
+Line 4
+<div class="float"></div>Line 5</div>
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-004.tentative.html b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-004.tentative.html
new file mode 100644
index 0000000000..6213130174
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-004.tentative.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Overflow: float in line-clamp after clamp point</title>
+<link rel="author" title="Andreu Botella" href="mailto:abotella@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-overflow-4/#line-clamp">
+<link rel="match" href="reference/webkit-line-clamp-005-ref.html">
+<meta name="assert" content="Floats in a block formatting context inside a line-clamp container are always hidden if they come after the clamp point.">
+<style>
+.clamp {
+ line-clamp: 4;
+ font: 16px / 32px serif;
+ padding: 0 4px;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ width: 50px;
+ height: 50px;
+ margin: 4px;
+ background-color: skyblue;
+}
+.pre {
+ white-space: pre;
+}
+</style>
+<div class="clamp">
+<div class="pre">Line 1
+Line 2
+Line 3
+Line 4</div>
+<div class="float"></div>
+<div>Line 5</div>
+</div>
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-005.tentative.html b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-005.tentative.html
new file mode 100644
index 0000000000..9689a83fa7
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-005.tentative.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Overflow: float in line-clamp before clamp point which extends past it</title>
+<link rel="author" title="Andreu Botella" href="mailto:abotella@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-overflow-4/#line-clamp">
+<link rel="match" href="reference/line-clamp-with-floats-005-ref.html">
+<meta name="assert" content="Floats in an inline formatting context inside a line-clamp container are not hidden if they come before the clamp point, even if they extend beyond that point">
+<style>
+.clamp {
+ line-clamp: 4;
+ font: 16px / 32px serif;
+ padding: 0 4px;
+ white-space: pre;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ width: 50px;
+ height: 50px;
+ margin: 4px;
+ background-color: skyblue;
+}
+</style>
+<div class="clamp">Line 1
+Line 2
+Line 3
+Line 4<div class="float"></div>
+Line 5</div>
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-006.tentative.html b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-006.tentative.html
new file mode 100644
index 0000000000..0a709bff19
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-006.tentative.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Overflow: float in line-clamp before clamp point which extends past it</title>
+<link rel="author" title="Andreu Botella" href="mailto:abotella@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-overflow-4/#line-clamp">
+<link rel="match" href="reference/line-clamp-with-floats-006-ref.html">
+<meta name="assert" content="Floats in a block formatting context inside a line-clamp container are not hidden if they come before the clamp point, even if they extend beyond that point">
+<style>
+.clamp {
+ line-clamp: 4;
+ font: 16px / 32px serif;
+ padding: 0 4px;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ width: 50px;
+ height: 75px;
+ margin: 4px;
+ background-color: skyblue;
+}
+.pre {
+ white-space: pre;
+}
+</style>
+<div class="clamp">
+<div class="pre">Line 1
+Line 2
+Line 3</div>
+<div class="float"></div>
+<div class="pre">Line 4
+Line 5</div>
+</div>
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-007.tentative.html b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-007.tentative.html
new file mode 100644
index 0000000000..7ee286fbf4
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-007.tentative.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Overflow: line-clamp doesn't propagate to floats</title>
+<link rel="author" title="Andreu Botella" href="mailto:abotella@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-overflow-4/#line-clamp">
+<link rel="match" href="reference/line-clamp-with-floats-007-ref.html">
+<meta name="assert" content="Floats create a new BFC, and line-clamp does not propagate into independent BFCs">
+<style>
+.clamp {
+ line-clamp: 4;
+ font: 16px / 32px serif;
+ padding: 0 4px;
+ white-space: pre;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ margin: 4px;
+ white-space: pre;
+ background-color: skyblue;
+}
+</style>
+<div class="clamp">Line 1
+Line 2
+Line 3
+Line 4<div class="float">Line A
+Line B
+Line C
+Line D
+Line E</div>
+Line 5</div>
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-008.tentative.html b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-008.tentative.html
new file mode 100644
index 0000000000..c62ba5371a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-008.tentative.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Overflow: line-clamp IFC with floats extending past the clamp point</title>
+<link rel="author" title="Andreu Botella" href="mailto:abotella@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-overflow-4/#line-clamp">
+<link rel="match" href="reference/line-clamp-with-floats-008-ref.html">
+<meta name="assert" content="If the line-clamp container is an independent formatting context, it will clear contained floats before the clamp point, even if they extend beyond the last line. Lines past the clamp point will remain hidden.">
+<style>
+.clamp {
+ display: flow-root;
+ line-clamp: 4;
+ font: 16px / 32px serif;
+ padding: 0 4px;
+ white-space: pre;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ width: 50px;
+ height: 50px;
+ margin: 4px;
+ background-color: skyblue;
+}
+</style>
+<div class="clamp">Line 1
+Line 2
+Line 3
+<div class="float"></div>Line 4
+Line 5</div>
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-009.tentative.html b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-009.tentative.html
new file mode 100644
index 0000000000..f25ac381c0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-009.tentative.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Overflow: line-clamp IFC with floats after the clamp point</title>
+<link rel="author" title="Andreu Botella" href="mailto:abotella@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-overflow-4/#line-clamp">
+<link rel="match" href="reference/webkit-line-clamp-005-ref.html">
+<meta name="assert" content="If the line-clamp container is an independent formatting context, it will not clear any floats after the clamp point.">
+<style>
+.clamp {
+ display: flow-root;
+ line-clamp: 4;
+ font: 16px / 32px serif;
+ padding: 0 4px;
+ white-space: pre;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ width: 50px;
+ height: 50px;
+ margin: 4px;
+ background-color: skyblue;
+}
+</style>
+<div class="clamp">Line 1
+Line 2
+Line 3
+Line 4
+Line 5<div class="float"></div></div>
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-010.tentative.html b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-010.tentative.html
new file mode 100644
index 0000000000..a00ff60171
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/line-clamp-with-floats-010.tentative.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Overflow: line-clamp hidden floats should count as ink overflow</title>
+<link rel="author" title="Andreu Botella" href="mailto:abotella@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-overflow-4/#line-clamp">
+<link rel="match" href="reference/line-clamp-with-floats-010-ref.html">
+<meta name="assert" content="Any overflowing content hidden from paint by line-clamp is treated as ink overflow, including floats, and therefore doesn't cause the scrollable overflow rectangle to grow. Meanwhile, non-hidden floats count as scrollable overflow.">
+<style>
+#scrollContainer {
+ overflow: scroll;
+ font: 16px / 32px serif;
+ height: 4lh;
+ border: 1px solid black;
+}
+.clamp {
+ line-clamp: 4;
+ padding: 0 4px;
+ white-space: pre;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ width: 50px;
+ height: 50px;
+ margin: 4px;
+ background-color: skyblue;
+}
+</style>
+
+<div id="scrollContainer">
+<div class="clamp">Line 1
+Line 2
+Line 3
+Line 4<div class="float"></div>
+Line 5
+Line 6<div class="float"></div></div>
+</div>
+
+<script>
+ window.addEventListener("load", () => {
+ const scrollContainer = document.getElementById("scrollContainer");
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
+ });
+</script>
diff --git a/testing/web-platform/tests/css/css-overflow/reference/line-clamp-021-ref.html b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-021-ref.html
new file mode 100644
index 0000000000..d794c76e3c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-021-ref.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Reference</title>
+<style>
+#scrollContainer {
+ overflow: scroll;
+ font: 16px / 32px serif;
+ height: 3lh;
+ border: 1px solid black;
+}
+.clamp {
+ padding: 4px;
+ background-color: yellow;
+}
+.pre {
+ white-space: pre;
+}
+</style>
+
+<div id="scrollContainer">
+
+ <div class="clamp">
+ <div class="pre">Line 1
+Line 2
+Line 3
+Line 4…</div>
+ </div>
+
+</div>
+
+<script>
+ window.addEventListener("load", () => {
+ const scrollContainer = document.getElementById("scrollContainer");
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
+ });
+</script>
diff --git a/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-abspos-011-ref.html b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-abspos-011-ref.html
new file mode 100644
index 0000000000..f08b0270e8
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-abspos-011-ref.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Reference</title>
+<style>
+#scrollContainer {
+ overflow: scroll;
+ position: relative;
+ font: 16px / 32px serif;
+ height: 4lh;
+ border: 1px solid black;
+}
+.clamp {
+ padding: 0 4px;
+ white-space: pre;
+ background-color: yellow;
+}
+.abspos {
+ position: absolute;
+ top: 4.5lh;
+ right: 0;
+ width: 50px;
+ height: 50px;
+ margin: 4px;
+ background-color: skyblue;
+}
+</style>
+
+<div id="scrollContainer">
+<div class="clamp">Line 1
+Line 2
+Line 3
+Line 4…<div class="abspos"></div></div>
+
+<script>
+ window.addEventListener("load", () => {
+ const scrollContainer = document.getElementById("scrollContainer");
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
+ });
+</script>
diff --git a/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-001-ref.html b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-001-ref.html
new file mode 100644
index 0000000000..bcd70aa930
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-001-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Reference</title>
+<style>
+.clamp {
+ font: 16px / 32px serif;
+ padding: 0 4px;
+ white-space: pre;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ width: 50px;
+ height: 50px;
+ margin: 4px;
+ background-color: skyblue;
+}
+</style>
+<div class="clamp"><div class="float"></div>Line 1
+Line 2
+Line 3
+Line 4…</div>
diff --git a/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-005-ref.html b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-005-ref.html
new file mode 100644
index 0000000000..d20d6c53dd
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-005-ref.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Reference</title>
+<style>
+.clamp {
+ display: flow-root;
+ font: 16px / 32px serif;
+ padding: 0 4px;
+ white-space: pre;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ width: 50px;
+ height: 50px;
+ margin: 4px;
+ background-color: skyblue;
+}
+</style>
+<div class="clamp">Line 1
+Line 2
+Line 3
+Line 4…<div class="float"></div></div>
diff --git a/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-006-ref.html b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-006-ref.html
new file mode 100644
index 0000000000..9288c4e36f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-006-ref.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Reference</title>
+<style>
+.clamp {
+ display: flow-root;
+ font: 16px / 32px serif;
+ padding: 0 4px;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ width: 50px;
+ height: 75px;
+ margin: 4px;
+ background-color: skyblue;
+}
+.pre {
+ white-space: pre;
+}
+</style>
+<div class="clamp">
+<div class="pre">Line 1
+Line 2
+Line 3</div>
+<div class="float"></div>
+<div class="pre">Line 4…</div>
+</div>
diff --git a/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-007-ref.html b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-007-ref.html
new file mode 100644
index 0000000000..6d5390246b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-007-ref.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Reference</title>
+<style>
+.clamp {
+ display: flow-root;
+ font: 16px / 32px serif;
+ padding: 0 4px;
+ white-space: pre;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ margin: 4px;
+ white-space: pre;
+ background-color: skyblue;
+}
+</style>
+<div class="clamp">Line 1
+Line 2
+Line 3
+Line 4…<div class="float">Line A
+Line B
+Line C
+Line D
+Line E</div></div>
diff --git a/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-008-ref.html b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-008-ref.html
new file mode 100644
index 0000000000..50b3d53900
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-008-ref.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Reference</title>
+<style>
+.clamp {
+ display: flow-root;
+ font: 16px / 32px serif;
+ padding: 0 4px;
+ white-space: pre;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ width: 50px;
+ height: 50px;
+ margin: 4px;
+ background-color: skyblue;
+}
+</style>
+<div class="clamp">Line 1
+Line 2
+Line 3
+<div class="float"></div>Line 4…</div>
diff --git a/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-010-ref.html b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-010-ref.html
new file mode 100644
index 0000000000..12b8cdc441
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/reference/line-clamp-with-floats-010-ref.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Reference</title>
+<style>
+#scrollContainer {
+ overflow: scroll;
+ font: 16px / 32px serif;
+ height: 4lh;
+ border: 1px solid black;
+}
+.clamp {
+ display: flow-root;
+ padding: 0 4px;
+ white-space: pre;
+ background-color: yellow;
+}
+.float {
+ float: left;
+ width: 50px;
+ height: 50px;
+ margin: 4px;
+ background-color: skyblue;
+}
+</style>
+
+<div id="scrollContainer">
+<div class="clamp">Line 1
+Line 2
+Line 3
+Line 4…<div class="float"></div></div>
+
+<script>
+ window.addEventListener("load", () => {
+ const scrollContainer = document.getElementById("scrollContainer");
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
+ });
+</script>
diff --git a/testing/web-platform/tests/css/css-overflow/scroll-with-ancestor-border-radius.html b/testing/web-platform/tests/css/css-overflow/scroll-with-ancestor-border-radius.html
new file mode 100644
index 0000000000..9192c613e9
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/scroll-with-ancestor-border-radius.html
@@ -0,0 +1,59 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Should not scroll out of rounded corner</title>
+<link rel="help" href="https://crbug.com/40277896">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/dom/events/scrolling/scroll_support.js"></script>
+<style>
+ #container {
+ width: 300px;
+ height: 300px;
+ border-radius: 100px;
+ overflow: hidden;
+ border: 2px solid blue;
+ }
+
+ #scroller {
+ overflow: auto;
+ width: 300px;
+ height: 300px;
+ will-change: scroll-position;
+ }
+
+ .spacer {
+ height: 200vh;
+ }
+
+</style>
+
+<div id="container">
+ <div id="scroller">
+ <div class="spacer"></div>
+ </div>
+</div>
+<div class="spacer"></div>
+
+<script>
+ promise_test(async (t) => {
+ await waitForCompositorCommit();
+ let scrolled = new Promise((resolve) => {
+ let scrollers = [window, document.getElementById("scroller")];
+ let onscroll = (evt) => {
+ for (const scroller of scrollers) {
+ scroller.removeEventListener("scroll", onscroll);
+ }
+ resolve(evt.target.id || "root");
+ }
+ for (const scroller of scrollers) {
+ scroller.addEventListener("scroll", onscroll);
+ }
+ });
+ const actions = new test_driver.Actions().scroll(20, 20, 0, 50, { duration: 50 });
+ actions.send();
+ assert_equals(await scrolled, "root", "Incorrect element scrolled");
+ }, "Wheel-scroll out of rounded corner skips that scroller");
+</script>
diff --git a/testing/web-platform/tests/css/css-overflow/scroll-with-border-radius.html b/testing/web-platform/tests/css/css-overflow/scroll-with-border-radius.html
new file mode 100644
index 0000000000..88bb0f1fa8
--- /dev/null
+++ b/testing/web-platform/tests/css/css-overflow/scroll-with-border-radius.html
@@ -0,0 +1,51 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Should not scroll out of rounded corner</title>
+<link rel="help" href="https://crbug.com/40277896">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/dom/events/scrolling/scroll_support.js"></script>
+<style>
+ #scroller {
+ border-radius: 100px;
+ overflow: auto;
+ width: 300px;
+ height: 300px;
+ border: 2px solid blue;
+ will-change: scroll-position;
+ }
+
+ .spacer {
+ height: 200vh;
+ }
+
+</style>
+
+<div id="scroller">
+ <div class="spacer"></div>
+</div>
+<div class="spacer"></div>
+
+<script>
+ promise_test(async (t) => {
+ await waitForCompositorCommit();
+ let scrolled = new Promise((resolve) => {
+ let scrollers = [window, document.getElementById("scroller")];
+ let onscroll = (evt) => {
+ for (const scroller of scrollers) {
+ scroller.removeEventListener("scroll", onscroll);
+ }
+ resolve(evt.target.id || "root");
+ }
+ for (const scroller of scrollers) {
+ scroller.addEventListener("scroll", onscroll);
+ }
+ });
+ const actions = new test_driver.Actions().scroll(20, 20, 0, 50, { duration: 50 });
+ actions.send();
+ assert_equals(await scrolled, "root", "Incorrect element scrolled");
+ }, "Wheel-scroll out of rounded corner skips that scroller");
+</script>
diff --git a/testing/web-platform/tests/css/css-page/fixedpos-009-print-ref.html b/testing/web-platform/tests/css/css-page/fixedpos-009-print-ref.html
new file mode 100644
index 0000000000..31a6a6724f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-page/fixedpos-009-print-ref.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org">
+<style>
+ body {
+ margin: 0;
+ }
+ .pencil {
+ background-color: black;
+ mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTMgMTcuMjVWMjFoMy43NUwxNy44MSA5Ljk0bC0zLjc1LTMuNzVMMyAxNy4yNXpNMjAuNzEgNy4wNGEuOTk2Ljk5NiAwIDAgMCAwLTEuNDFsLTIuMzQtMi4zNGEuOTk2Ljk5NiAwIDAgMC0xLjQxIDBsLTEuODMgMS44MyAzLjc1IDMuNzUgMS44My0xLjgzeiIvPjxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz48L3N2Zz4=);
+ mask-repeat: no-repeat;
+ width: 36px;
+ height: 36px;
+ }
+ .fakepage {
+ position: relative;
+ height: 100vh;
+ }
+</style>
+<div class="fakepage">
+ <div style="position:absolute; bottom:0; right:0;">
+ <div class="pencil"></div>
+ </div>
+ When printed, there should be two pages. There should be a black pencil in the
+ bottom right corner on both pages.
+</div>
+
+<div class="fakepage">
+ <div style="position:absolute; bottom:0; right:0;">
+ <div class="pencil"></div>
+ </div>
+ Page 2.
+</div>
diff --git a/testing/web-platform/tests/css/css-page/fixedpos-009-print.html b/testing/web-platform/tests/css/css-page/fixedpos-009-print.html
new file mode 100644
index 0000000000..803a077c0a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-page/fixedpos-009-print.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org">
+<link rel="help" href="http://crbug.com/337791467">
+<link rel="match" href="fixedpos-009-print-ref.html">
+<style>
+ body {
+ margin: 0;
+ }
+ .pencil {
+ background-color: black;
+ mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTMgMTcuMjVWMjFoMy43NUwxNy44MSA5Ljk0bC0zLjc1LTMuNzVMMyAxNy4yNXpNMjAuNzEgNy4wNGEuOTk2Ljk5NiAwIDAgMCAwLTEuNDFsLTIuMzQtMi4zNGEuOTk2Ljk5NiAwIDAgMC0xLjQxIDBsLTEuODMgMS44MyAzLjc1IDMuNzUgMS44My0xLjgzeiIvPjxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz48L3N2Zz4=);
+ mask-repeat: no-repeat;
+ width: 36px;
+ height: 36px;
+ }
+</style>
+<div style="position:fixed; bottom:0; right:0;">
+ <div class="pencil"></div>
+</div>
+When printed, there should be two pages. There should be a black pencil in the
+bottom right corner on both pages.
+<div style="break-before:page;">
+ Page 2.
+</div>
diff --git a/testing/web-platform/tests/css/css-page/fixedpos-010-print-ref.html b/testing/web-platform/tests/css/css-page/fixedpos-010-print-ref.html
new file mode 100644
index 0000000000..90bd5e7fc3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-page/fixedpos-010-print-ref.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org">
+<style>
+ @page {
+ size: 400px;
+ margin: 0;
+ }
+ @page large {
+ size: 500px 400px;
+ }
+ body {
+ margin: 0;
+ }
+</style>
+This page should <em>not</em> have a blue box.
+<div style="page:large;">
+ <div style="float:right; margin-top:300px; width:100px; height:100px; background:blue;"></div>
+ <div style="width:400px;">
+ This page should have a blue box in the bottom right corner.
+ </div>
+ <div style="break-before:page;">
+ <div style="float:right; margin-top:300px; width:100px; height:100px; background:blue;"></div>
+ <div style="width:400px;">
+ This page should have a blue box in the bottom right corner.
+ </div>
+ </div>
+</div>
+This page should <em>not</em> have a blue box.
diff --git a/testing/web-platform/tests/css/css-page/fixedpos-010-print.html b/testing/web-platform/tests/css/css-page/fixedpos-010-print.html
new file mode 100644
index 0000000000..cdd50828d8
--- /dev/null
+++ b/testing/web-platform/tests/css/css-page/fixedpos-010-print.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-position-3/#valdef-position-fixed">
+<link rel="match" href="fixedpos-010-print-ref.html">
+<style>
+ @page {
+ size: 400px;
+ margin: 0;
+ }
+ @page large {
+ size: 500px 400px;
+ }
+ body {
+ margin: 0;
+ }
+</style>
+<div style="position:fixed; right:-100px; bottom:0; width:100px; height:100px; background:blue;"></div>
+This page should <em>not</em> have a blue box.
+<div style="page:large; width:400px;">
+ This page should have a blue box in the bottom right corner.
+ <div style="break-before:page;">
+ This page should have a blue box in the bottom right corner.
+ </div>
+</div>
+This page should <em>not</em> have a blue box.
diff --git a/testing/web-platform/tests/css/css-page/page-box-000-print-ref.html b/testing/web-platform/tests/css/css-page/page-box-000-print-ref.html
new file mode 100644
index 0000000000..d39bd73850
--- /dev/null
+++ b/testing/web-platform/tests/css/css-page/page-box-000-print-ref.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org">
+<style>
+ html {
+ box-sizing: border-box;
+ display: grid;
+ place-items: center;
+ height: 100%;
+ border: 20px solid green;
+ }
+</style>
+This page should have a green border.
diff --git a/testing/web-platform/tests/css/css-page/page-box-000-print.html b/testing/web-platform/tests/css/css-page/page-box-000-print.html
new file mode 100644
index 0000000000..aee317ab97
--- /dev/null
+++ b/testing/web-platform/tests/css/css-page/page-box-000-print.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-page-3/#page-properties">
+<link rel="match" href="page-box-000-print-ref.html">
+<style>
+ @page {
+ border: 20px solid green;
+ }
+ html {
+ display: grid;
+ place-items: center;
+ height: 100%;
+ }
+</style>
+This page should have a green border.
diff --git a/testing/web-platform/tests/css/css-page/page-size-012-print-ref.html b/testing/web-platform/tests/css/css-page/page-size-012-print-ref.html
new file mode 100644
index 0000000000..e85e5f0785
--- /dev/null
+++ b/testing/web-platform/tests/css/css-page/page-size-012-print-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org">
+<style>
+ @page {
+ size: 600px 400px;
+ margin: 0;
+ }
+ body { margin: 0; }
+</style>
+<div style="margin-left:auto; margin-right:50px; writing-mode:vertical-rl; height:400px; background:yellow;">
+ 50px to the left of page box right edge.<br>
+ Page widths larger than heights.<br>
+</div>
+<div style="break-before:page;"></div>
+<div style="margin-left:auto; margin-top:50px; writing-mode:vertical-rl; height:350px; background:yellow;">
+ 50px below the page box top edge.<br>
+</div>
diff --git a/testing/web-platform/tests/css/css-page/page-size-012-print.html b/testing/web-platform/tests/css/css-page/page-size-012-print.html
new file mode 100644
index 0000000000..737b37cbc3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-page/page-size-012-print.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<title>Test vertical writing mode with specified page size and logical margins.</title>
+<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org">
+<link rel="help" href="https://www.w3.org/TR/css-page-3/#at-page-rule">
+<link rel="help" href="https://www.w3.org/TR/css-page-3/#typedef-page-size-page-size">
+<link rel="match" href="page-size-012-print-ref.html">
+<style>
+ @page {
+ size: 600px 400px;
+ margin: 0;
+ margin-inline-start: 50px;
+ }
+ @page :first {
+ margin-inline-start: 0;
+ margin-block-start: 50px;
+ }
+ html {
+ writing-mode: vertical-rl;
+ }
+ body { margin: 0; }
+</style>
+<div style="background:yellow;">
+ 50px to the left of page box right edge.<br>
+ Page widths larger than heights.<br>
+</div>
+<div style="break-before:page; background:yellow;">
+ 50px below the page box top edge.<br>
+</div>
diff --git a/testing/web-platform/tests/css/css-position/sticky/position-sticky-left-004.html b/testing/web-platform/tests/css/css-position/sticky/position-sticky-left-004.html
new file mode 100644
index 0000000000..79188e84b0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-position/sticky/position-sticky-left-004.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Position Test: Test position:sticky element with 100% left in a block container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-position/#sticky-pos">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1748891">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1835705">
+<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
+<meta name="assert" content="This test verifies that a position:sticky element with 100% left value does not cause the scrollbar to show when shrinking the width of its overflow container.">
+
+<style>
+#scroll {
+ width: 200px;
+ height: 100px;
+ overflow: auto;
+ background: red;
+}
+.sticky {
+ position: sticky;
+ width: 100px;
+ height: 100px;
+ left: 100%;
+ background: green;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="scroll">
+ <div class="sticky"></div>
+</div>
+
+<script>
+window.onload = () => {
+ document.getElementById("scroll").style.width = "100px";
+};
+</script>
diff --git a/testing/web-platform/tests/css/css-position/sticky/position-sticky-left-005.html b/testing/web-platform/tests/css/css-position/sticky/position-sticky-left-005.html
new file mode 100644
index 0000000000..b029e59363
--- /dev/null
+++ b/testing/web-platform/tests/css/css-position/sticky/position-sticky-left-005.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Position Test: Test position:sticky element with 100% left in a grid container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-position/#sticky-pos">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1748891">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1835705">
+<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
+<meta name="assert" content="This test verifies that a position:sticky element with 100% left value does not cause the scrollbar to show when shrinking the width of its overflow container.">
+
+<style>
+#scroll {
+ display: grid;
+ width: 200px;
+ height: 100px;
+ overflow: auto;
+ background: red;
+}
+.sticky {
+ position: sticky;
+ width: 100px;
+ height: 100px;
+ left: 100%;
+ background: green;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="scroll">
+ <div class="sticky"></div>
+</div>
+
+<script>
+window.onload = () => {
+ document.getElementById("scroll").style.width = "100px";
+};
+</script>
diff --git a/testing/web-platform/tests/css/css-position/sticky/position-sticky-left-006.html b/testing/web-platform/tests/css/css-position/sticky/position-sticky-left-006.html
new file mode 100644
index 0000000000..63fa1ebfd8
--- /dev/null
+++ b/testing/web-platform/tests/css/css-position/sticky/position-sticky-left-006.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Position Test: Test position:sticky element with 100% left in a flex container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-position/#sticky-pos">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1748891">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1835705">
+<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
+<meta name="assert" content="This test verifies that a position:sticky element with 100% left value does not cause the scrollbar to show when shrinking the width of its overflow container.">
+
+<style>
+#scroll {
+ display: flex;
+ width: 200px;
+ height: 100px;
+ overflow: auto;
+ background: red;
+}
+.sticky {
+ position: sticky;
+ width: 100px;
+ height: 100px;
+ left: 100%;
+ background: green;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="scroll">
+ <div class="sticky"></div>
+</div>
+
+<script>
+window.onload = () => {
+ document.getElementById("scroll").style.width = "100px";
+};
+</script>
diff --git a/testing/web-platform/tests/css/css-position/sticky/position-sticky-margins-002.html b/testing/web-platform/tests/css/css-position/sticky/position-sticky-margins-002.html
new file mode 100644
index 0000000000..29833ccae0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-position/sticky/position-sticky-margins-002.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Position Test: Test position:sticky element with auto margin in flex container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-position/#staticpos-rect">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1488080">
+<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
+<meta name="assert" content="This test verifies that a position:sticky element aligning with 'auto' margin still honors the sticky positioning.">
+
+<style>
+#container{
+ width: 100px;
+ height: 100px;
+ overflow: hidden;
+ background: red;
+}
+.flex {
+ display: flex;
+ flex-direction: column;
+ height: 500px;
+}
+.child {
+ width: 100px;
+ height: 100px;
+ background: green;
+ position: sticky;
+ margin-top: auto;
+ bottom: 0;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="container">
+ <div class="flex">
+ <div class="child"></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-position/sticky/position-sticky-margins-003.html b/testing/web-platform/tests/css/css-position/sticky/position-sticky-margins-003.html
new file mode 100644
index 0000000000..2bc1a3ef25
--- /dev/null
+++ b/testing/web-platform/tests/css/css-position/sticky/position-sticky-margins-003.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Position Test: Test position:sticky element with auto margin in block container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-position/#staticpos-rect">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1488080">
+<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
+<meta name="assert" content="This test verifies that a position:sticky element aligning with 'auto' margin still honors the sticky positioning.">
+
+<style>
+#container{
+ width: 100px;
+ height: 100px;
+ overflow: hidden;
+ background: red;
+}
+.block {
+ width: 500px;
+}
+.child {
+ width: 100px;
+ height: 100px;
+ background: green;
+ position: sticky;
+ margin-left: auto;
+ right: 0;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="container">
+ <div class="block">
+ <div class="child">
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-position/sticky/position-sticky-scrolled-remove-sibling-002.html b/testing/web-platform/tests/css/css-position/sticky/position-sticky-scrolled-remove-sibling-002.html
new file mode 100644
index 0000000000..870efa417c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-position/sticky/position-sticky-scrolled-remove-sibling-002.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-position/#sticky-pos">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1612561">
+<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
+<meta name="assert" content="This test verifies that there is no vertical scrollbar after removing the sibling that overflows the scroll container">
+
+<style>
+#scroll {
+ width: 100px;
+ height: 100px;
+ overflow: auto;
+ background: green;
+}
+.sticky {
+ position: sticky;
+ width: 50px;
+ height: 100px;
+ top: 0;
+ margin-bottom: -100px;
+}
+#sibling {
+ width: 50px;
+ height: 500px;
+ background: red;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="scroll">
+ <div class="sticky"></div>
+ <div id="sibling"></div>
+</div>
+
+<script>
+window.onload = () => {
+ document.getElementById("scroll").scrollTop = 200;
+ document.getElementById("sibling").remove();
+};
+</script>
diff --git a/testing/web-platform/tests/css/css-position/sticky/position-sticky-top-004.html b/testing/web-platform/tests/css/css-position/sticky/position-sticky-top-004.html
new file mode 100644
index 0000000000..bf0b87ab36
--- /dev/null
+++ b/testing/web-platform/tests/css/css-position/sticky/position-sticky-top-004.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Position Test: Test position:sticky element with 100% top in a block container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-position/#sticky-pos">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1748891">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1835705">
+<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
+<meta name="assert" content="This test verifies that a position:sticky element with 100% top value does not cause the scrollbar to show when shrinking the height of its container.">
+
+<style>
+#scroll {
+ width: 100px;
+ height: 200px;
+ overflow: auto;
+ background: red;
+}
+.sticky {
+ position: sticky;
+ width: 100px;
+ height: 100px;
+ top: 100%;
+ background: green;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="scroll">
+ <div class="sticky"></div>
+</div>
+
+<script>
+window.onload = () => {
+ document.getElementById("scroll").style.height = "100px";
+};
+</script>
diff --git a/testing/web-platform/tests/css/css-position/sticky/position-sticky-top-005.html b/testing/web-platform/tests/css/css-position/sticky/position-sticky-top-005.html
new file mode 100644
index 0000000000..5f76a722b5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-position/sticky/position-sticky-top-005.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Position Test: Test position:sticky element with 100% top in a grid container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-position/#sticky-pos">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1748891">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1835705">
+<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
+<meta name="assert" content="This test verifies that a position:sticky element with 100% top value does not cause the scrollbar to show when shrinking the height of its container.">
+
+<style>
+#scroll {
+ display: grid;
+ width: 100px;
+ height: 200px;
+ overflow: auto;
+ background: red;
+}
+.sticky {
+ position: sticky;
+ width: 100px;
+ height: 100px;
+ top: 100%;
+ background: green;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="scroll">
+ <div class="sticky"></div>
+</div>
+
+<script>
+window.onload = () => {
+ document.getElementById("scroll").style.height = "100px";
+};
+</script>
diff --git a/testing/web-platform/tests/css/css-position/sticky/position-sticky-top-006.html b/testing/web-platform/tests/css/css-position/sticky/position-sticky-top-006.html
new file mode 100644
index 0000000000..4ac5310bf3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-position/sticky/position-sticky-top-006.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Position Test: Test position:sticky element with 100% top in a flex container</title>
+<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+<link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+<link rel="help" href="https://drafts.csswg.org/css-position/#sticky-pos">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1748891">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1835705">
+<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
+<meta name="assert" content="This test verifies that a position:sticky element with 100% top value does not cause the scrollbar to show when shrinking the height of its container.">
+
+<style>
+#scroll {
+ display: flex;
+ width: 100px;
+ height: 200px;
+ overflow: auto;
+ background: red;
+}
+.sticky {
+ position: sticky;
+ width: 100px;
+ height: 100px;
+ top: 100%;
+ background: green;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="scroll">
+ <div class="sticky"></div>
+</div>
+
+<script>
+window.onload = () => {
+ document.getElementById("scroll").style.height = "100px";
+};
+</script>
diff --git a/testing/web-platform/tests/css/css-properties-values-api/crashtests/computed-property-universal-syntax.html b/testing/web-platform/tests/css/css-properties-values-api/crashtests/computed-property-universal-syntax.html
new file mode 100644
index 0000000000..b87f03b2df
--- /dev/null
+++ b/testing/web-platform/tests/css/css-properties-values-api/crashtests/computed-property-universal-syntax.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset=utf8>
+<style>
+@property --my-registered-property {
+ syntax: "<color>";
+ inherits: false;
+ initial-value: #f0f2f5;
+}
+
+.outer {
+ --unregistered-property: var(--my-registered-property);
+}
+
+.inner {
+ --unregistered-property: #c6cfdb;
+}
+</style>
+<div class="outer">
+ <div class="inner"></div>
+</div>
diff --git a/testing/web-platform/tests/css/css-properties-values-api/registered-property-computation.html b/testing/web-platform/tests/css/css-properties-values-api/registered-property-computation.html
index f4c718b139..8ca9d4499a 100644
--- a/testing/web-platform/tests/css/css-properties-values-api/registered-property-computation.html
+++ b/testing/web-platform/tests/css/css-properties-values-api/registered-property-computation.html
@@ -155,6 +155,14 @@ test_computed_value('<color>', '#badbee33', 'rgba(186, 219, 238, 0.2)');
test_computed_value('<color>', 'tomato', 'rgb(255, 99, 71)');
test_computed_value('<color>', 'plum', 'rgb(221, 160, 221)');
test_computed_value('<color>', 'currentcolor', 'currentcolor');
+test_computed_value('<color>', 'color-mix(in srgb, black, white)', 'color(srgb 0.5 0.5 0.5)');
+test_computed_value('<color>', 'color-mix(in srgb, currentcolor, red)', 'color-mix(in srgb, currentcolor, rgb(255, 0, 0))');
+test_computed_value('<color>', 'color-mix(in srgb, currentcolor, #ffffff 70%)', 'color-mix(in srgb, currentcolor 30%, rgb(255, 255, 255))');
+test_computed_value('<color>', 'color-mix(in srgb, currentcolor 20%, #ffffff 20%)', 'color-mix(in srgb, currentcolor 20%, rgb(255, 255, 255) 20%)');
+test_computed_value('<color>', 'light-dark(currentcolor, red)', 'currentcolor');
+test_computed_value('<color>', 'light-dark(lime, red)', 'rgb(0, 255, 0)');
+test_computed_value('<color>', 'color(from lime srgb g g g)', 'color(srgb 1 1 1)');
+test_computed_value('<color>', 'color(srgb 1 1 1 / calc(NaN))', 'color(srgb 1 1 1 / 0)');
// Custom ident values that look like color keywords should not be converted.
test_computed_value('*', 'tomato', 'tomato');
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-002-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade-002-ref.html
deleted file mode 100644
index 17629a6dba..0000000000
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-002-ref.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!doctype html>
-<meta charset="utf-8">
-<link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com">
-<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
-<style>
- main {
- font-size: 7em;
- margin: 0.5em;
- }
- main::selection {
- color: black;
- background-color: transparent;
- }
- main > .control > span::selection,
- main > .bg > span::selection {
- color: white;
- background-color: green;
- }
- main > .fg > span::selection {
- color: green;
- background-color: white;
- }
-</style>
-<p>Test passes if the words below are (respectively): white on green, green on white, white on green.
-<!--
- The element tree below is intentionally the same shape as the
- test, despite the fact that we might be able to simplify it. This
- is because multiple impls (including Gecko and Blink) split the
- background paints accordingly, which can obscure ink overflow in
- some of the highlighted text (especially “f”).
--->
-<main class="highlight_reftest"
- ><span class="control"><span>foo</span></span
- > <span class="fg"><span>b</span></span
- ><span class="fg"><span>a</span></span
- ><span class="fg"><span>r</span></span
- > <span class="bg"><span>b</span></span
- ><span class="bg"><span>a</span></span
- ><span class="bg"><span>z</span></span
- ></main>
-<script>selectNodeContents(document.querySelector("main"));</script>
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-002.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade-002.html
deleted file mode 100644
index c01d3c796e..0000000000
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-002.html
+++ /dev/null
@@ -1,120 +0,0 @@
-<!doctype html>
-<meta charset="utf-8">
-<title>CSS Pseudo-Elements Test: highlight cascade: custom properties are inherited regardless of inherits flag or inheritedness of referencing property</title>
-<link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com">
-<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade">
-<link rel="match" href="highlight-cascade-002-ref.html">
-<meta name="assert" value="This test verifies that, given ::selection styles referencing custom properties, their substitution values are inherited from the parent ::selection styles, even if the property is registered with inherits set to false (--inherits-false) or the referencing property is not an inherited property (background-color). All custom properties are treated as inherited when used in highlight styles in any way.">
-<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
-<style>
- /*
- Register the custom properties, other than --unregistered,
- which would be an inherited property [css-variables-1].
- */
- @property --control {
- syntax: "<color>";
- initial-value: green;
- inherits: false;
- }
- @property --inherits-false {
- syntax: "<color>";
- initial-value: red;
- inherits: false;
- }
- @property --inherits-true {
- syntax: "<color>";
- initial-value: red;
- inherits: true;
- }
-
- main {
- font-size: 7em;
- margin: 0.5em;
- }
- main::selection {
- /*
- Don’t visibly highlight the spaces between words.
- */
- color: black;
- background-color: transparent;
- }
-
- /*
- Non-highlight control: if this text is white on red (inherit)
- rather than white on green (initial), then @property is not
- supported well enough to make this test meaningful.
- */
- main > .control {
- --control: red;
- }
- main > .control > span {
- color: white;
- background-color: var(--control);
- }
-
- main > *::selection {
- --inherits-false: green;
- --inherits-true: green;
- --unregistered: green;
- }
-
- /*
- Foreground tests: if the foreground of this text is red or
- black (initial) rather than green (inherit), then custom
- properties are not being inherited in highlight styles.
-
- color is an inherited property, but that shouldn’t matter.
- */
- main > .fg > span::selection {
- background-color: white;
- }
- main > .fg.inherits-false > span::selection {
- color: var(--inherits-false);
- }
- main > .fg.inherits-true > span::selection {
- color: var(--inherits-true);
- }
- main > .fg.unregistered > span::selection {
- color: var(--unregistered);
- }
-
- /*
- Background tests: if the background of this text is red or
- black (initial) rather than green (inherit), then custom
- properties are not being inherited in highlight styles.
-
- background-color is not an inherited property, but that
- shouldn’t matter.
- */
- main > .bg > span::selection {
- color: white;
- }
- main > .bg.inherits-false > span::selection {
- background-color: var(--inherits-false);
- }
- main > .bg.inherits-true > span::selection {
- background-color: var(--inherits-true);
- }
- main > .bg.unregistered > span::selection {
- background-color: var(--unregistered);
- }
-</style>
-<p>Test passes if the words below are (respectively): white on green, green on white, white on green.
-<main class="highlight_reftest"
- ><span class="control"><span>foo</span></span
- > <span class="fg inherits-false"><span>b</span></span
- ><span class="fg inherits-true"><span>a</span></span
- ><span class="fg unregistered"><span>r</span></span
- > <span class="bg inherits-false"><span>b</span></span
- ><span class="bg inherits-true"><span>a</span></span
- ><span class="bg unregistered"><span>z</span></span
- ></main>
-<script>
- const main = document.querySelector("main");
- selectRangeWith(range => {
- range.selectNodeContents(main);
- range.setStart(main, 2);
- range.setEnd(main, 9);
- });
-</script>
diff --git a/testing/web-platform/tests/css/css-pseudo/cascade-highlight-001-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-001-ref.html
index 25dbeadb2e..2a2907b10a 100644
--- a/testing/web-platform/tests/css/css-pseudo/cascade-highlight-001-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-001-ref.html
@@ -6,7 +6,7 @@
<link rel="author" title="Gérard Talbot" href="http://www.gtalbot.org/BrowserBugsSection/css21testsuite/">
- <link rel="stylesheet" href="support/highlights.css">
+ <link rel="stylesheet" href="../support/highlights.css">
<style>
div
{
diff --git a/testing/web-platform/tests/css/css-pseudo/cascade-highlight-001.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-001.html
index 6068a076f0..f441adf3b9 100644
--- a/testing/web-platform/tests/css/css-pseudo/cascade-highlight-001.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-001.html
@@ -15,7 +15,7 @@
<meta name="assert" content="This test is an adaptation (or modified version) of Example 11 (#example-c35bf49a). The 'div &gt; span::selection' selector has an higher specificity than the 'span::selection' selector.">
- <link rel="stylesheet" href="support/highlights.css">
+ <link rel="stylesheet" href="../support/highlights.css">
<style>
div
{
diff --git a/testing/web-platform/tests/css/css-pseudo/cascade-highlight-002.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-002.html
index 50be805f01..d10bdb9b04 100644
--- a/testing/web-platform/tests/css/css-pseudo/cascade-highlight-002.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-002.html
@@ -15,7 +15,7 @@
<meta name="assert" content="This test is an adaptation (or modified version) of Example 12 (#example-97480f68). In this test, &ltspan&gt; element's ::selection matches the ::selection { background-color: green; } rule and not the div#test::selection rule because '*' is implied when a tag selector is missing.">
- <link rel="stylesheet" href="support/highlights.css">
+ <link rel="stylesheet" href="../support/highlights.css">
<style>
div
{
diff --git a/testing/web-platform/tests/css/css-pseudo/cascade-highlight-004-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-004-ref.html
index e755283a32..9b8507c4cc 100644
--- a/testing/web-platform/tests/css/css-pseudo/cascade-highlight-004-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-004-ref.html
@@ -6,7 +6,7 @@
<link rel="author" title="Gérard Talbot" href="http://www.gtalbot.org/BrowserBugsSection/css21testsuite/">
- <link rel="stylesheet" href="support/highlights.css">
+ <link rel="stylesheet" href="../support/highlights.css">
<style>
div
{
diff --git a/testing/web-platform/tests/css/css-pseudo/cascade-highlight-004.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-004.html
index 56abba7156..295321a172 100644
--- a/testing/web-platform/tests/css/css-pseudo/cascade-highlight-004.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-004.html
@@ -19,7 +19,7 @@
https://www.w3.org/TR/css-pseudo-4/#highlight-cascade
-->
- <link rel="stylesheet" href="support/highlights.css">
+ <link rel="stylesheet" href="../support/highlights.css">
<style>
div
{
diff --git a/testing/web-platform/tests/css/css-pseudo/reference/cascade-highlight-005-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-005-ref.html
index d1e597e05b..d1e597e05b 100644
--- a/testing/web-platform/tests/css/css-pseudo/reference/cascade-highlight-005-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-005-ref.html
diff --git a/testing/web-platform/tests/css/css-pseudo/cascade-highlight-005.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-005.html
index 2c6ba60270..25ad85fe70 100644
--- a/testing/web-platform/tests/css/css-pseudo/cascade-highlight-005.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/cascade-highlight-005.html
@@ -6,7 +6,7 @@
<link rel="author" title="Gérard Talbot" href="http://www.gtalbot.org/BrowserBugsSection/css21testsuite/">
<link rel="help" href="https://www.w3.org/TR/css-pseudo-4/#highlight-cascade">
- <link rel="match" href="reference/cascade-highlight-005-ref.html">
+ <link rel="match" href="cascade-highlight-005-ref.html">
<meta name="assert" content="In this test, 'background-color' has not been specified a value for the highlight pseudo-element of the span element. Since the span's parent element has an highlight pseudo-element also, then the span's background color for its highlight pseudo-element should be inherited from its parent highlight pseudo-element. Therefore the span element should have a green background color.">
@@ -28,7 +28,7 @@
https://www.w3.org/TR/css-pseudo-4/#highlight-cascade
-->
- <link rel="stylesheet" href="support/highlights.css">
+ <link rel="stylesheet" href="../support/highlights.css">
<style>
div
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-001-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-001-ref.html
index a18690962f..b96ffce5ab 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-001-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-001-ref.html
@@ -2,7 +2,7 @@
<meta charset="utf-8">
<link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com">
<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
main {
font-size: 7em;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-001.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-001.html
index f237e9eca7..18b3635b3f 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-001.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-001.html
@@ -6,7 +6,7 @@
<link rel="match" href="highlight-cascade-001-ref.html">
<meta name="assert" value="This test verifies that, given ::selection styles with both color and background-color declared as unset, both properties inherit their values from the parent ::selection styles. All properties become inherited for the purposes of deciding whether unset should mean initial or inherit.">
<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
main {
font-size: 7em;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-003-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-003-ref.html
index 256c28ba0c..68964149ba 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-003-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-003-ref.html
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<title>Initial custom property values in :root::selection rule</title>
<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade">
-<script src="support/selections.js"></script>
+<script src="../support/selections.js"></script>
<style>
:root::selection {
background-color: green;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-003.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-003.html
index 61bc46d4c5..b29f37528c 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-003.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-003.html
@@ -3,7 +3,7 @@
<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade">
<link rel="match" href="highlight-cascade-003-ref.html">
<meta name="assert" value="This test verifies that the initial value given in a custom property registration is respected, when the property is referenced in ::selection styles but no value is defined. The initial value is not the guaranteed-invalid value, so the fallback value in var() is not used.">
-<script src="support/selections.js"></script>
+<script src="../support/selections.js"></script>
<style>
@property --bg {
syntax: "<color>";
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-004-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-004-ref.html
index f9bf83f9c1..5845e93620 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-004-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-004-ref.html
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<title>Initial custom property values in div::selection rule</title>
<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade">
-<script src="support/selections.js"></script>
+<script src="../support/selections.js"></script>
<style>
div::selection {
background-color: green;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-004.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-004.html
index eb181096ce..f3155bcec6 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-004.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-004.html
@@ -3,7 +3,7 @@
<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade">
<link rel="match" href="highlight-cascade-004-ref.html">
<meta name="assert" value="This test verifies that the initial value given in a custom property registration is respected, when the property is referenced in ::selection styles but no value is defined. The initial value is not the guaranteed-invalid value, so the fallback value in var() is not used.">
-<script src="support/selections.js"></script>
+<script src="../support/selections.js"></script>
<style>
@property --bg {
syntax: "<color>";
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-005-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-005-ref.html
index 20d2b0bdb3..f0ea3faa76 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-005-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-005-ref.html
@@ -3,7 +3,7 @@
<title>CSS Pseudo-Elements Test: highlight cascade: inheritance with both universal and non-universal rules</title>
<link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com">
<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
nav::selection,
span::selection,
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-005.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-005.html
index 008f56aded..958bdf6544 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-005.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-005.html
@@ -6,7 +6,7 @@
<link rel="match" href="highlight-cascade-005-ref.html">
<meta name="assert" content="This test verifies that, given both universal and non-universal ::selection rules, the subject of the non-universal rule has styles from both rules with the non-universal styles overriding the universal styles (due to the cascade), its descendants have styles from both rules with the universal styles overriding the non-universal styles (due to highlight inheritance), and its siblings have the universal styles only.">
<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
/* * (universal) */::selection {
background-color: green;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-006-ref.xhtml b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-006-ref.xhtml
index df0a56123e..643dd79325 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-006-ref.xhtml
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-006-ref.xhtml
@@ -2,7 +2,7 @@
<meta charset="utf-8"/>
<title>CSS Pseudo-Elements Test: highlight cascade: inheritance with both universal and namespace-universal rules</title>
<link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com"/>
-<script src="support/selections.js"></script>
+<script src="../support/selections.js"></script>
<style>
main * { all: initial; display: block; }
::selection { color: green; }
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-006.xhtml b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-006.xhtml
index fb6d07f8f0..4a37af7c25 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-006.xhtml
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-006.xhtml
@@ -5,7 +5,7 @@
<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade"/>
<link rel="match" href="highlight-cascade-006-ref.xhtml"/>
<meta name="assert" content="This test verifies that, given both universal ::selection rules and ::selection rules that are actually non-universal due to an explicit namespace prefix or default @namespace rule, the non-universal rules are not erroneously treated as universal."/>
-<script src="support/selections.js"></script>
+<script src="../support/selections.js"></script>
<style>
main * { all: initial; display: block; }
::selection { color: green; } /* 1. universal (* means *|* if there is no default @namespace) */
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-007.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-007.html
index de0322d910..df79d8dff5 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-007.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-007.html
@@ -5,8 +5,8 @@
<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade">
<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7591">
<meta name="assert" content="This test verifies that non-applicable property declarations are ignored in highlight pseudos, that the computed values of ‘font-size’ and ‘line-height’ in highlight pseudos are taken from the originating element, and that ‘text-shadow’ in highlight pseudos respects these values when given ‘em’ and ‘lh’ units.">
-<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<script src="../support/selections.js"></script>
+<link rel="stylesheet" href="../support/highlights.css">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-008-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-008-ref.html
index c87ddcf93c..70b9d958b5 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-008-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-008-ref.html
@@ -1,6 +1,6 @@
<!DOCTYPE html>
<title>Custom property values from the root element</title>
-<script src="support/selections.js"></script>
+<script src="../support/selections.js"></script>
<style>
div::selection {
background-color: green;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-008.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-008.html
index 10ca924b8f..720e2f0469 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-008.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-008.html
@@ -1,18 +1,19 @@
<!DOCTYPE html>
-<title>Custom property values from the root element</title>
+<title>Custom property values from the originating element</title>
<link rel="author" title="Stephen Chenney" href="mailto:schenney@chromium.org">
<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade">
<link rel="match" href="highlight-cascade-008-ref.html">
-<meta name="assert" value="This test verifies that when a custom property is not found in highlight cascade, its value is taken from the root element.">
+<meta name="assert" value="This test verifies that custom properties from the originating element are used to resolve var() on highlight pseudos.">
<meta name="fuzzy" content="0-255;0-10">
-<script src="support/selections.js"></script>
+<script src="../support/selections.js"></script>
<style>
:root {
--background-color: green;
- --decoration-color: purple;
+ --decoration-color: yellow;
}
::selection {
- --decoration-color: yellow;
+ --background-color: cyan;
+ --decoration-color: magenta;
}
div::selection {
background-color: var(--background-color, red);
@@ -20,8 +21,11 @@
text-decoration-style: line;
text-decoration-color: var(--decoration-color, red);
}
- span::selection {
+ span {
--background-color: blue;
+ }
+ span::selection {
+ --background-color: purple;
background-color: var(--background-color, red);
}
</style>
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-009.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-009.html
index acc48c7c36..e95de9a781 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-cascade-009.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-009.html
@@ -4,18 +4,18 @@
<link rel="author" title="Stephen Chenney" href="mailto:schenney@chromium.org">
<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade">
<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/6641">
-<meta name="assert" content="This test verifies that custom properties used in highlight pseudos are taken from the ::root if not found in the highlight inheritance chain.">
-<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<meta name="assert" content="This test verifies that custom properties used in highlight pseudos are taken from the originating element.">
+<script src="../support/selections.js"></script>
+<link rel="stylesheet" href="../support/highlights.css">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style>
:root {
--background-color: green;
- --decoration-color: purple;
+ --decoration-color: green;
}
::selection {
- --decoration-color: green;
+ --decoration-color: purple;
}
div::selection {
--background-color: blue;
@@ -31,11 +31,11 @@
const body_selection = getComputedStyle(document.querySelector("body"), "::selection");
const div_selection = getComputedStyle(document.querySelector("div"), "::selection");
test(() => void assert_equals(body_selection.getPropertyValue("--background-color"), "green"),
- "body ::selection has the root custom property");
+ "body ::selection uses the originating custom property");
test(() => void assert_equals(body_selection.getPropertyValue("--decoration-color"), "green"),
- "body ::selection uses its own custom property");
+ "body ::selection does not use its own custom property");
test(() => void assert_equals(div_selection.getPropertyValue("--decoration-color"), "green"),
- "div::selection inherits a custom property");
- test(() => void assert_equals(div_selection.getPropertyValue("--background-color"), "blue"),
- "div::selection uses its own custom property");
+ "div::selection uses the originating element custom property");
+ test(() => void assert_equals(div_selection.getPropertyValue("--background-color"), "green"),
+ "div::selection does not use its own custom property");
</script>
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-computed-inheritance.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed-inheritance.html
index d67ae82881..d67ae82881 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-computed-inheritance.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed-inheritance.html
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-computed-visited.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed-visited.html
index 207cb7b7dd..207cb7b7dd 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-computed-visited.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed-visited.html
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-computed.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed.html
index 97c31809df..97c31809df 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-computed.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed.html
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-properties-001-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001-ref.html
index 93e2002f4a..d3a3d49797 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-properties-001-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001-ref.html
@@ -3,7 +3,7 @@
<meta charset="utf-8" />
<title>CSS Pseudo-Elements Test: Reference</title>
<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
div {
color: lime;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-properties-001.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001.html
index 76d80d228a..efdb9016bc 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-properties-001.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001.html
@@ -5,7 +5,7 @@
<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-pseudo/#highlight-text">
<link rel="match" href="highlight-currentcolor-painting-properties-001-ref.html">
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<meta name="fuzzy" content="0-50;0-150">
<style>
div {
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-properties-002-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002-ref.html
index 11ca501065..6f9066cba8 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-properties-002-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002-ref.html
@@ -3,7 +3,7 @@
<meta charset="utf-8" />
<title>CSS Pseudo-Elements Test: Reference</title>
<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
div {
color: lime;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-properties-002.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002.html
index ac3677c1cf..d02dc9e8c5 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-properties-002.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002.html
@@ -5,7 +5,7 @@
<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-pseudo/#highlight-text">
<link rel="match" href="highlight-currentcolor-painting-properties-002-ref.html">
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<meta name="fuzzy" content="0-50;0-150">
<style>
div {
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001-ref.html
index d6ee33cda7..1ab60db5c8 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001-ref.html
@@ -1,5 +1,5 @@
<!DOCTYPE html><meta charset="utf-8">
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
div {
color: lime;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001.html
index 141556f935..c82da8c380 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001.html
@@ -4,7 +4,7 @@
<link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-pseudo/#highlight-text">
<link rel="match" href="highlight-currentcolor-painting-text-shadow-001-ref.html">
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
div {
color: lime;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002-ref.html
index fb2696a55a..6452a34d0e 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002-ref.html
@@ -1,5 +1,5 @@
<!DOCTYPE html><meta charset="utf-8">
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
div {
color: lime;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002.html
index 77858729af..870243f501 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002.html
@@ -4,7 +4,7 @@
<link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-pseudo/#highlight-text">
<link rel="match" href="highlight-currentcolor-painting-text-shadow-002-ref.html">
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
div {
color: lime;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-001-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-001-ref.html
index 794796a88f..70ce6b59a4 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-001-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-001-ref.html
@@ -3,7 +3,7 @@
<meta charset="utf-8" />
<title>CSS Pseudo-Elements Test: Reference</title>
<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
div {
color: lime;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-001.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-001.html
index a1512f014d..1869f8ab53 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-001.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-001.html
@@ -5,7 +5,7 @@
<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-pseudo/#highlight-text">
<link rel="match" href="highlight-currentcolor-root-explicit-default-001-ref.html">
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
div {
color: lime;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-002-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002-ref.html
index 3175948317..b64b007c64 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-002-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002-ref.html
@@ -3,7 +3,7 @@
<meta charset="utf-8" />
<title>CSS Pseudo-Elements Test: Reference</title>
<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
.highlight_reftest {
color: green;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-002.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002.html
index fc5698faa1..fe6d80be2e 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-002.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002.html
@@ -5,7 +5,7 @@
<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-pseudo/#highlight-text">
<link rel="match" href="highlight-currentcolor-root-explicit-default-002-ref.html">
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
.highlight_reftest {
color: green;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-001.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-001.html
index ecf787b9b4..ecf787b9b4 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-001.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-001.html
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-002.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-002.html
index 420cc5ba15..420cc5ba15 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-002.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-002.html
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-ref.html
index 67ecb8df64..67ecb8df64 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-ref.html
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-001-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-001-ref.html
index 14687acb84..102b07b481 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-001-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-001-ref.html
@@ -2,7 +2,7 @@
<meta charset="utf-8">
<link rel="author" name="Delan Azabani" href="mailto:dazabani@igalia.com">
<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
main {
font-size: 7em;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-001.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-001.html
index 09e5abf9a3..7f370238df 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-001.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-001.html
@@ -6,7 +6,7 @@
<link rel="match" href="highlight-paired-cascade-001-ref.html">
<meta name="assert" value="This test verifies that setting color on ::selection suppresses any UA non-initial used value for background-color. These properties are highlight colors, which are subject to paired cascade.">
<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
main {
font-size: 7em;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-002-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-002-ref.html
index 48eb9911a1..19d731af02 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-002-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-002-ref.html
@@ -2,7 +2,7 @@
<meta charset="utf-8">
<link rel="author" name="Delan Azabani" href="mailto:dazabani@igalia.com">
<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
main {
font-size: 7em;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-002.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-002.html
index affbe95629..626fc57558 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-002.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-002.html
@@ -6,7 +6,7 @@
<link rel="match" href="highlight-paired-cascade-002-ref.html">
<meta name="assert" value="This test verifies that setting background-color on ::selection suppresses any UA non-initial used value for color. These properties are highlight colors, which are subject to paired cascade.">
<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
main {
font-size: 7em;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-003-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-003-ref.html
index 18885fdc89..638ae00c68 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-003-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-003-ref.html
@@ -2,7 +2,7 @@
<meta charset="utf-8">
<link rel="author" name="Delan Azabani" href="mailto:dazabani@igalia.com">
<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
main {
font-size: 7em;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-003.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-003.html
index 250e320210..8c621cc777 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-003.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-003.html
@@ -6,7 +6,7 @@
<link rel="match" href="highlight-paired-cascade-003-ref.html">
<meta name="assert" value="This test verifies that setting text-decoration on ::selection does not suppress any UA non-initial used values for color or background-color. While the former is an applicable (shorthand) property for highlight styles, it is not one of the highlight colors (color or background-color), so paired cascade does not apply.">
<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
main {
font-size: 7em;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-004-notref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004-notref.html
index 63472b6758..72490b1461 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-004-notref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004-notref.html
@@ -1,7 +1,7 @@
<!doctype html>
<meta charset="utf-8">
<link rel="author" name="Delan Azabani" href="mailto:dazabani@igalia.com">
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
:link, :visited {
color: blue;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-004.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004.html
index 61e2b7d7f0..0a73d006b5 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-004.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004.html
@@ -5,7 +5,7 @@
<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade">
<link rel="mismatch" href="highlight-paired-cascade-004-notref.html">
<meta name="assert" value="This test verifies that setting color on ::target-text suppresses any UA non-initial used value for background-color. ::target-text is a highlight pseudo with a recommended UA default background-color that is not initial (Mark), so paired cascade can be observed.">
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
:link, :visited {
color: blue;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-005-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-005-ref.html
index 0ac5c02b20..862d06e6f6 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-005-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-005-ref.html
@@ -2,7 +2,7 @@
<meta charset="utf-8">
<link rel="author" name="Delan Azabani" href="mailto:dazabani@igalia.com">
<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
main {
font-size: 7em;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-005.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-005.html
index 50677c811e..4ee6db3a09 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-005.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-005.html
@@ -6,7 +6,7 @@
<link rel="match" href="highlight-paired-cascade-005-ref.html">
<meta name="assert" value="This test verifies that setting color to unset on ::selection suppresses any UA non-initial used value for background-color. The unset value is defined as both “treated as [inherit or initial depending on whether the property is inherited]” and “effectively erases all declared values occurring earlier in the cascade [(inclusive)]”, which are normally equivalent, but under paired cascade, the former wins.">
<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
main {
font-size: 7em;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-006-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-006-ref.html
index 18885fdc89..638ae00c68 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-006-ref.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-006-ref.html
@@ -2,7 +2,7 @@
<meta charset="utf-8">
<link rel="author" name="Delan Azabani" href="mailto:dazabani@igalia.com">
<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
main {
font-size: 7em;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-006.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-006.html
index 20c03282c8..bef3601ccb 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-paired-cascade-006.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-006.html
@@ -6,7 +6,7 @@
<link rel="match" href="highlight-paired-cascade-006-ref.html">
<meta name="assert" value="This test verifies that setting color to revert on ::selection does not suppress any UA non-initial used value for background-color. Because the revert value rolls back the cascade, it destroys its own existence as a cascaded value, and this is also true under paired cascade.">
<script src="support/selections.js"></script>
-<link rel="stylesheet" href="support/highlights.css">
+<link rel="stylesheet" href="../support/highlights.css">
<style>
main {
font-size: 7em;
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-pseudos-computed.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-computed.html
index 4a274e1bbd..4a274e1bbd 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-pseudos-computed.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-computed.html
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-pseudos-inheritance-computed-001.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-inheritance-computed-001.html
index 84c4045a54..84c4045a54 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-pseudos-inheritance-computed-001.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-inheritance-computed-001.html
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-pseudos-visited-computed-001.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-visited-computed-001.html
index a2b18effcc..a2b18effcc 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-pseudos-visited-computed-001.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-visited-computed-001.html
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-custom-properties-dynamic-001-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-custom-properties-dynamic-001-ref.html
new file mode 100644
index 0000000000..836874df05
--- /dev/null
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-custom-properties-dynamic-001-ref.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>CSS Pseudo Test: Dynamic Custom Properties for Highlights</title>
+<link rel="author" title="Stephen Chenney" href="mailto:schenney@igalia.com">
+<head>
+ <style>
+ ::selection {
+ background-color: green;
+ }
+ </style>
+</head>
+<body>Green background when selected</body>
+<script>
+ window.getSelection().selectAllChildren(document.body);
+</script>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-custom-properties-dynamic-001.html b/testing/web-platform/tests/css/css-pseudo/highlight-custom-properties-dynamic-001.html
new file mode 100644
index 0000000000..836ef546fa
--- /dev/null
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-custom-properties-dynamic-001.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>CSS Pseudo Test: Dynamic Custom Properties for Highlights</title>
+<link rel="help" href="https://drafts.csswg.org/css-pseudo/#highlight-styling">
+<link rel="match" href="highlight-custom-properties-dynamic-001-ref.html">
+<link rel="author" title="Stephen Chenney" href="mailto:schenney@igalia.com">
+<meta name="assert" value="Custom property values in highlights update correctly when the property is updated.">
+<head>
+ <style>
+ div {
+ --background-color: red;
+ }
+ ::selection {
+ background-color: var(--background-color, red);
+ }
+ </style>
+</head>
+<div id="originating">Green background when selected</div>
+<script>
+ window.getSelection().selectAllChildren(document.body);
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ originating.style.setProperty("--background-color", "green");
+ requestAnimationFrame(() => takeScreenshot());
+ });
+ });
+</script>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-horizontal-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-horizontal-ref.html
new file mode 100644
index 0000000000..4ff90afb38
--- /dev/null
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-horizontal-ref.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Pseudo-Elements Test: Shadows on highlights horizontal text - reference</title>
+<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com">
+<style>
+ :root {
+ line-height: 1;
+ white-space: break-spaces;
+ }
+ #originating_shadow {
+ font-size: 2em;
+ color: transparent;
+ text-shadow: 0.1em 0.1em rgba(0,0,0,0.5);
+ position: absolute;
+ z-index: 0;
+ top: 10px;
+ left: 10px;
+ }
+ #originating_text {
+ font-size: 2em;
+ color: black;
+ position: absolute;
+ z-index: 7;
+ top: 10px;
+ left: 10px;
+ }
+ #selection_only {
+ font-size: 2em;
+ color: green;
+ text-shadow: -0.25em -0.25em rgba(0,128,0,0.5), 0.1em 0.1em rgba(0,0,0,0.5);
+ position: absolute;
+ z-index: 5;
+ top: 10px;
+ }
+ #target {
+ font-size: 2em;
+ color: blue;
+ text-shadow: 0.25em 0.25em rgba(0,0,128,0.5), 0.1em 0.1em rgba(0,0,0,0.5);
+ position: absolute;
+ z-index: 3;
+ top: 10px;
+ }
+ #both_text {
+ font-size: 2em;
+ color: green;
+ position: absolute;
+ z-index: 4;
+ top: 10px;
+ }
+ #both_shadow {
+ font-size: 2em;
+ color: transparent;
+ text-shadow: 0.25em 0.25em rgba(0,0,128,0.5), -0.25em -0.25em rgba(0,128,0,0.5), 0.1em 0.1em rgba(0,0,0,0.5);
+ position: absolute;
+ z-index: 2;
+ top: 10px;
+ }
+</style>
+<p id="originating_shadow">part</p>
+<p id="originating_text">part</p>
+<p id="selection_only">ially selected </p>
+<p id="both_text">ta</p>
+<p id="both_shadow">ta</p>
+<p id="target">rget</p>
+<script>
+ originatingCS = getComputedStyle(originating_text);
+ selection_only.style.left = (parseFloat(originatingCS.width) + parseFloat(originatingCS.left)).toString() + "px";
+ selectionCS = getComputedStyle(selection_only);
+ both_text.style.left = (parseFloat(selectionCS.width) + parseFloat(selectionCS.left)).toString() + "px";
+ both_shadow.style.left = both_text.style.left;
+ bothCS = getComputedStyle(both_text);
+ target.style.left = (parseFloat(bothCS.width) + parseFloat(bothCS.left)).toString() + "px";
+</script>
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-horizontal.html b/testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-horizontal.html
new file mode 100644
index 0000000000..b1762e85ae
--- /dev/null
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-horizontal.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Pseudo-Elements Test: Shadows on highlights horizontal text</title>
+<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-painting">
+<link rel="match" href="highlight-painting-shadows-horizontal-ref.html">
+<meta name="assert" value="::selection and ::target-text both with shadows are painted in the correct order, including originating element shadows">
+<meta name="fuzzy" content="0-32;0-20">
+<script src="support/selections.js"></script>
+<style>
+ :root {
+ line-height: 1;
+ white-space: pre;
+ }
+ p {
+ font-size: 2em;
+ color: black;
+ text-shadow: 0.1em 0.1em rgba(0,0,0,0.5);
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ }
+ p::selection {
+ color: green;
+ text-shadow: -0.25em -0.25em rgba(0,128,0,0.5);
+ }
+ p::target-text {
+ color: blue;
+ text-shadow: 0.25em 0.25em rgba(0,0,128,0.5);
+ }
+</style>
+<p>partially selected target</p>
+<script>
+ window.location.hash = "#:~:text=target";
+ const target = document.querySelector("p");
+ selectRangeWith(range => {
+ range.selectNodeContents(target);
+ range.setStart(target.childNodes[0], 4);
+ range.setEnd(target.childNodes[0], 21);
+ });
+</script>
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-vertical-ref.html b/testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-vertical-ref.html
new file mode 100644
index 0000000000..00a6f1c808
--- /dev/null
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-vertical-ref.html
@@ -0,0 +1,70 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Pseudo-Elements Test: Shadows on highlights vertical text - reference</title>
+<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com">
+<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
+<style>
+ :root {
+ font-family: Ahem;
+ writing-mode: vertical-lr;
+ line-height: 1;
+ white-space: break-spaces;
+ }
+ #originating_shadow {
+ color: transparent;
+ text-shadow: 0.1em 0.1em rgba(0,0,0,0.5);
+ position: absolute;
+ z-index: 0;
+ top: 10px;
+ left: 10px;
+ }
+ #originating_text {
+ color: black;
+ position: absolute;
+ z-index: 7;
+ top: 10px;
+ left: 10px;
+ }
+ #selection_only {
+ color: green;
+ text-shadow: -0.25em -0.25em rgba(0,128,0,0.5), 0.1em 0.1em rgba(0,0,0,0.5);
+ position: absolute;
+ z-index: 5;
+ left: 10px;
+ }
+ #target {
+ color: blue;
+ text-shadow: 0.25em 0.25em rgba(0,0,128,0.5), 0.1em 0.1em rgba(0,0,0,0.5);
+ position: absolute;
+ z-index: 3;
+ left: 10px;
+ }
+ #both_text {
+ color: green;
+ position: absolute;
+ z-index: 4;
+ left: 10px;
+ }
+ #both_shadow {
+ color: transparent;
+ text-shadow: 0.25em 0.25em rgba(0,0,128,0.5), -0.25em -0.25em rgba(0,128,0,0.5), 0.1em 0.1em rgba(0,0,0,0.5);
+ position: absolute;
+ z-index: 2;
+ left: 10px;
+ }
+</style>
+<p id="originating_shadow">part</p>
+<p id="originating_text">part</p>
+<p id="selection_only">ially selected </p>
+<p id="both_text">ta</p>
+<p id="both_shadow">ta</p>
+<p id="target">rget</p>
+<script>
+ originatingCS = getComputedStyle(originating_text);
+ selection_only.style.top = (parseFloat(originatingCS.height) + parseFloat(originatingCS.top)).toString() + "px";
+ selectionCS = getComputedStyle(selection_only);
+ both_text.style.top = (parseFloat(selectionCS.height) + parseFloat(selectionCS.top)).toString() + "px";
+ both_shadow.style.top = both_text.style.top;
+ bothCS = getComputedStyle(both_text);
+ target.style.top = (parseFloat(bothCS.height) + parseFloat(bothCS.top)).toString() + "px";
+</script>
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-vertical.html b/testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-vertical.html
new file mode 100644
index 0000000000..7187c34da4
--- /dev/null
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-painting-shadows-vertical.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Pseudo-Elements Test: Shadows on highlights vertical text</title>
+<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-painting">
+<link rel="match" href="highlight-painting-shadows-vertical-ref.html">
+<meta name="assert" value="::selection and ::target-text both with shadows are painted in the correct order, including originating element shadows">
+<meta name="fuzzy" content="0-32;0-4">
+<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
+<script src="support/selections.js"></script>
+<style>
+ :root {
+ font-family: Ahem;
+ writing-mode: vertical-lr;
+ line-height: 1;
+ white-space: pre;
+ }
+ p {
+ color: black;
+ text-shadow: 0.1em 0.1em rgba(0,0,0,0.5);
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ }
+ p::selection {
+ color: green;
+ text-shadow: -0.25em -0.25em rgba(0,128,0,0.5);
+ }
+ p::target-text {
+ color: blue;
+ text-shadow: 0.25em 0.25em rgba(0,0,128,0.5);
+ }
+</style>
+<p>partially selected target</p>
+<script>
+ window.location.hash = "#:~:text=target";
+ const target = document.querySelector("p");
+ selectRangeWith(range => {
+ range.selectNodeContents(target);
+ range.setStart(target.childNodes[0], 4);
+ range.setEnd(target.childNodes[0], 21);
+ });
+</script>
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-styling-001.html b/testing/web-platform/tests/css/css-pseudo/highlight-styling-001.html
index 63d8ee1eda..7fe76d1938 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-styling-001.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-styling-001.html
@@ -1,10 +1,10 @@
<!doctype html>
<meta charset="utf-8">
-<title>CSS Pseudo-Elements Test: highlight styling: custom properties are applicable properties in highlight pseudos</title>
+<title>CSS Pseudo-Elements Test: highlight styling: custom properties are not applicable properties in highlight pseudos</title>
<link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-styling">
<link rel="match" href="highlight-styling-001-ref.html">
-<meta name="assert" value="This test verifies that ::selection styles can set and reference custom properties.">
+<meta name="assert" value="This test verifies that ::selection styles cannot set custom properties.">
<script src="support/selections.js"></script>
<link rel="stylesheet" href="support/highlights.css">
<style>
@@ -13,9 +13,9 @@
margin: 0.5em;
}
main::selection {
- --x: green;
+ --x: red;
color: white;
- background-color: var(--x);
+ background-color: var(--x, green);
}
</style>
<p>Test passes if the text below is white on green.
diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-styling-002.html b/testing/web-platform/tests/css/css-pseudo/highlight-styling-002.html
index 2f7cc29128..351eacac88 100644
--- a/testing/web-platform/tests/css/css-pseudo/highlight-styling-002.html
+++ b/testing/web-platform/tests/css/css-pseudo/highlight-styling-002.html
@@ -1,21 +1,21 @@
<!doctype html>
<meta charset="utf-8">
-<title>CSS Pseudo-Elements Test: highlight styling: originating custom property values do not affect highlight pseudos</title>
+<title>CSS Pseudo-Elements Test: highlight styling: originating custom property values are used for highlight pseudos</title>
<link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-styling">
<link rel="match" href="highlight-styling-002-ref.html">
-<meta name="assert" value="This test verifies that custom property values set in originating elements do not participate in the substitution of those properties in ::selection styles.">
+<meta name="assert" value="This test verifies that custom property values on the originating element are used for resolving var() in a selection pseudo.">
<script src="support/selections.js"></script>
<link rel="stylesheet" href="support/highlights.css">
<style>
main {
font-size: 7em;
margin: 0.5em;
- --x: red;
+ --x: green;
}
main::selection {
color: white;
- background-color: var(--x, green);
+ background-color: var(--x, red);
}
</style>
<p>Test passes if the text below is white on green.
diff --git a/testing/web-platform/tests/css/css-pseudo/selection-over-highlight-001-ref.html b/testing/web-platform/tests/css/css-pseudo/selection-over-highlight-001-ref.html
new file mode 100644
index 0000000000..f75017d5ec
--- /dev/null
+++ b/testing/web-platform/tests/css/css-pseudo/selection-over-highlight-001-ref.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Pseudo-Elements Test: selected highlight highlight painting - reference</title>
+<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com">
+<style>
+ div {
+ background-color: lightblue;
+ color: green;
+ width: fit-content;
+ }
+</style>
+<div id="content">When selected, the highlight color should remain.</div>
diff --git a/testing/web-platform/tests/css/css-pseudo/selection-over-highlight-001.html b/testing/web-platform/tests/css/css-pseudo/selection-over-highlight-001.html
new file mode 100644
index 0000000000..57078cf83b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-pseudo/selection-over-highlight-001.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Pseudo-Elements Test: selected highlight highlight painting</title>
+<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-painting">
+<meta name="assert" value="Checks that the highlight colors persist when selected.">
+<link rel="match" href="selection-over-highlight-001-ref.html">
+<style>
+ ::selection {
+ background-color: lightblue;
+ }
+ ::highlight(green) {
+ color: green;
+ }
+</style>
+<div id="content">When selected, the highlight color should remain.</div>
+<script>
+ let range = new Range();
+ range.setStart(content, 0);
+ range.setEnd(content, 1);
+ CSS.highlights.set("green", new Highlight(range));
+
+ document.execCommand("selectAll");
+</script>
diff --git a/testing/web-platform/tests/css/css-pseudo/target-text-dynamic-004.html b/testing/web-platform/tests/css/css-pseudo/target-text-dynamic-004.html
index 279ec674b4..35f2542c76 100644
--- a/testing/web-platform/tests/css/css-pseudo/target-text-dynamic-004.html
+++ b/testing/web-platform/tests/css/css-pseudo/target-text-dynamic-004.html
@@ -18,7 +18,7 @@
</style>
<p>The test passes if the following word has a magenta background.</p>
-<div>Example</div>
+<div><span>Example</span></div>
<script>
location.href = "#:~:text=Example";
diff --git a/testing/web-platform/tests/css/css-pseudo/target-text-shadow-horizontal-ref.html b/testing/web-platform/tests/css/css-pseudo/target-text-shadow-horizontal-ref.html
new file mode 100644
index 0000000000..6189f844b5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-pseudo/target-text-shadow-horizontal-ref.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Pseudo-Elements Test: Shadows on horizontal target text - reference</title>
+<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com">
+<style>
+ :root {
+ line-height: 1;
+ }
+ p {
+ font-size: 2em;
+ color: black;
+ text-shadow: 0.1em 0.1em 3px rgba(0,0,0,0.5);
+ }
+ span {
+ color: green;
+ text-shadow: 0.25em 0.25em 3px rgba(0,0,128,0.5), 0.1em 0.1em 3px rgba(0,0,0,0.5);
+ }
+</style>
+<p>the <span>target</span> should have a shadow</p>
diff --git a/testing/web-platform/tests/css/css-pseudo/target-text-shadow-horizontal.html b/testing/web-platform/tests/css/css-pseudo/target-text-shadow-horizontal.html
new file mode 100644
index 0000000000..1ded1360ff
--- /dev/null
+++ b/testing/web-platform/tests/css/css-pseudo/target-text-shadow-horizontal.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Pseudo-Elements Test: Shadows on horizontal target text</title>
+<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-painting">
+<link rel="match" href="target-text-shadow-horizontal-ref.html">
+<meta name="assert" value="::target-text with a shadow is painted, including originating element shadows">
+<style>
+ :root {
+ line-height: 1;
+ }
+ p {
+ font-size: 2em;
+ color: black;
+ text-shadow: 0.1em 0.1em 3px rgba(0,0,0,0.5);
+ }
+ p::target-text {
+ color: green;
+ text-shadow: 0.25em 0.25em 3px rgba(0,0,128,0.5);
+ }
+</style>
+<p>the target should have a shadow</p>
+<script>
+ window.location.hash = "#:~:text=target";
+</script>
diff --git a/testing/web-platform/tests/css/css-pseudo/target-text-shadow-vertical-ref.html b/testing/web-platform/tests/css/css-pseudo/target-text-shadow-vertical-ref.html
new file mode 100644
index 0000000000..cd9e179053
--- /dev/null
+++ b/testing/web-platform/tests/css/css-pseudo/target-text-shadow-vertical-ref.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Pseudo-Elements Test: Shadows on vertical target text - reference</title>
+<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com">
+<style>
+ :root {
+ line-height: 1;
+ writing-mode: vertical-lr;
+ }
+ p {
+ font-size: 2em;
+ color: black;
+ text-shadow: 0.1em 0.1em 3px rgba(0,0,0,0.5);
+ }
+ span {
+ color: green;
+ text-shadow: 0.25em 0.25em 3px rgba(0,0,128,0.5), 0.1em 0.1em 3px rgba(0,0,0,0.5);
+ }
+</style>
+<p>the <span>target</span> should have a shadow</p>
diff --git a/testing/web-platform/tests/css/css-pseudo/target-text-shadow-vertical.html b/testing/web-platform/tests/css/css-pseudo/target-text-shadow-vertical.html
new file mode 100644
index 0000000000..088f6fc175
--- /dev/null
+++ b/testing/web-platform/tests/css/css-pseudo/target-text-shadow-vertical.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Pseudo-Elements Test: Shadows on vertical target text</title>
+<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-painting">
+<link rel="match" href="target-text-shadow-vertical-ref.html">
+<meta name="assert" value="::target-text with a shadow is painted, including originating element shadows">
+<style>
+ :root {
+ line-height: 1;
+ writing-mode: vertical-lr;
+ }
+ p {
+ font-size: 2em;
+ color: black;
+ text-shadow: 0.1em 0.1em 3px rgba(0,0,0,0.5);
+ }
+ p::target-text {
+ color: green;
+ text-shadow: 0.25em 0.25em 3px rgba(0,0,128,0.5);
+ }
+</style>
+<p>the target should have a shadow</p>
+<script>
+ window.location.hash = "#:~:text=target";
+</script>
diff --git a/testing/web-platform/tests/css/css-ruby/line-spacing.html b/testing/web-platform/tests/css/css-ruby/line-spacing.html
index 9d3c6f23e7..4854e984c4 100644
--- a/testing/web-platform/tests/css/css-ruby/line-spacing.html
+++ b/testing/web-platform/tests/css/css-ruby/line-spacing.html
@@ -149,5 +149,16 @@ test(() => {
assert_greater_than_equal(thirdLine.top - rubyLine.top,
rubyLine.top - firstLine.top + RUBY_EMPHASIS_SIZE);
}, 'Don\'t Consume half-leading of the next line with text-emphasis');
+
+// crbug.com/336592423
+test(() => {
+ const {container, ruby, rt} = renderRuby(
+ '<div style="line-height:1;">' +
+ '<span style="display:inline-block; width:1em; height:4em; vertical-align:top"></span><br>' +
+ '<ruby>base<rt>annotation</rt></ruby></div>');
+ const firstLine = container.querySelector('span').getBoundingClientRect();
+ const rtBox = rt.getBoundingClientRect();
+ assert_greater_than_equal(rtBox.top, firstLine.bottom);
+}, 'An atomic-inline should not overlap with an annotation in the next line');
</script>
</body>
diff --git a/testing/web-platform/tests/css/css-ruby/ruby-dynamic-insertion-005-ref.html b/testing/web-platform/tests/css/css-ruby/ruby-dynamic-insertion-005-ref.html
index 03e5cabe55..79a19130ff 100644
--- a/testing/web-platform/tests/css/css-ruby/ruby-dynamic-insertion-005-ref.html
+++ b/testing/web-platform/tests/css/css-ruby/ruby-dynamic-insertion-005-ref.html
@@ -11,3 +11,12 @@
<p><rb>a</rb><rt>x</rt><rb>b</rb><rt>y</rt></p>
<p><rbc><span>a</span><rb></rb>b</rbc><rt>x</rt><rt></rt><rt>y</rt></p>
<p><rb>a</rb><rb></rb><rb>b</rb><rtc><span>x</span><rt></rt>y</rtc></p>
+
+<p>'a' and 'b c' should be paired with 'x' and 'y z' repectively:</p>
+<p><ruby>a <rt>x</rt><span style="display:block">b</span> c<rt>y z</ruby></p>
+
+<p>'a b' and 'c' should be paired with 'x y' and 'z' repectively:</p>
+<p><ruby>a <span style="display:block">b</span> <rt>x y</rt><span>c</span><rt>z</ruby></p>
+
+<p>'a b' and 'c d' should be paired with 'w x' and 'y z' repectively:</p>
+<p><ruby>a <span style="display:block">b</span> <rt>w x</rt><span>c</span> <span style="display:block">d</span><rt>y z</ruby>
diff --git a/testing/web-platform/tests/css/css-ruby/ruby-dynamic-insertion-005.html b/testing/web-platform/tests/css/css-ruby/ruby-dynamic-insertion-005.html
index a684d665c8..66f7dbdd17 100644
--- a/testing/web-platform/tests/css/css-ruby/ruby-dynamic-insertion-005.html
+++ b/testing/web-platform/tests/css/css-ruby/ruby-dynamic-insertion-005.html
@@ -20,3 +20,12 @@
<p><rbc><span data-insert="after" data-tag="rb">a</span>b</rbc><rt>x</rt><rt></rt><rt>y</rt></p>
<!-- pseudo ruby text -->
<p><rb>a</rb><rb></rb><rb>b</rb><rtc><span data-insert="after" data-tag="rt">x</span>y</rtc></p>
+
+<p>'a' and 'b c' should be paired with 'x' and 'y z' repectively:</p>
+<p><ruby>a <span style="display:block" data-insert="before" data-tag="rt" data-text="x">b</span> c<rt>y z</ruby></p>
+
+<p>'a b' and 'c' should be paired with 'x y' and 'z' repectively:</p>
+<p><ruby>a <span style="display:block">b</span> <span data-insert="before" data-tag="rt" data-text="x y">c</span><rt>z</ruby></p>
+
+<p>'a b' and 'c d' should be paired with 'w x' and 'y z' repectively:</p>
+<p><ruby>a <span style="display:block">b</span> <span data-insert="before" data-tag="rt" data-text="w x">c</span> <span style="display:block">d</span><rt>y z</ruby>
diff --git a/testing/web-platform/tests/css/css-ruby/ruby-dynamic-removal-003-ref.html b/testing/web-platform/tests/css/css-ruby/ruby-dynamic-removal-003-ref.html
index 0067c014f5..113598eff2 100644
--- a/testing/web-platform/tests/css/css-ruby/ruby-dynamic-removal-003-ref.html
+++ b/testing/web-platform/tests/css/css-ruby/ruby-dynamic-removal-003-ref.html
@@ -18,3 +18,8 @@
<p><rbc>ab</rbc><rt>xy</rt></p>
<p><rb>ab</rb><rtc style="letter-spacing: 1px">xy</rtc></p>
+
+<p>'a b c' should be paried with 'x y z':</p>
+<p><ruby>a b <span style="display:block">c</span><rt>x y z</ruby>
+<p><ruby>a <span style="display:block">b</span> c<rt>x y z</ruby>
+<p><ruby><span style="display:block">a</span> b <span style="display:block">c</span><rt>x y z</ruby>
diff --git a/testing/web-platform/tests/css/css-ruby/ruby-dynamic-removal-003.html b/testing/web-platform/tests/css/css-ruby/ruby-dynamic-removal-003.html
index d35b2b968d..ff6c0b4c83 100644
--- a/testing/web-platform/tests/css/css-ruby/ruby-dynamic-removal-003.html
+++ b/testing/web-platform/tests/css/css-ruby/ruby-dynamic-removal-003.html
@@ -31,3 +31,9 @@
<!-- pseudo ruby text -->
<!-- letter-spacing is added here to avoid fuzzy on Windows. See bug 1111891 -->
<p><rb>ab</rb><rtc style="letter-spacing: 1px">x<rt class="remove"></rt>y</rtc></p>
+
+<p>'a b c' should be paried with 'x y z':</p>
+<!-- merge -->
+<p><ruby>a <rt class="remove">w</rt><span>b</span> <span style="display:block">c</span><rt>x y z</ruby>
+<p><ruby>a <span style="display:block">b</span> <rt class="remove">w</rt><span>c</span><rt>x y z</ruby>
+<p><ruby><span style="display:block">a</span> <rt class="remove">w</rt><span>b</span> <span style="display:block">c</span><rt>x y z</ruby>
diff --git a/testing/web-platform/tests/css/css-scoping/font-face-001.html b/testing/web-platform/tests/css/css-scoping/font-face-001.html
index 7e47d18cba..4496786449 100644
--- a/testing/web-platform/tests/css/css-scoping/font-face-001.html
+++ b/testing/web-platform/tests/css/css-scoping/font-face-001.html
@@ -12,7 +12,7 @@
</style>
<div id="host"><span id="in-document">1234567890</span></div>
<script>
-test(function() {
+promise_test(async () => {
host.attachShadow({ mode: "open" }).innerHTML = `
<style>
@font-face {
@@ -27,6 +27,8 @@ test(function() {
<span id="in-shadow">0123456789</span>
`;
+ await document.fonts.ready;
+
assert_not_equals(document.getElementById('in-document').offsetWidth, 160);
assert_equals(host.shadowRoot.getElementById('in-shadow').offsetWidth, 160);
}, "@font-face applies in the shadow tree")
diff --git a/testing/web-platform/tests/css/css-scoping/font-face-002.html b/testing/web-platform/tests/css/css-scoping/font-face-002.html
index 2e3272c44f..3a20d0b808 100644
--- a/testing/web-platform/tests/css/css-scoping/font-face-002.html
+++ b/testing/web-platform/tests/css/css-scoping/font-face-002.html
@@ -13,7 +13,7 @@
</style>
<div id="host"><span id="in-document">1234567890</span></div>
<script>
-test(function() {
+promise_test(async () => {
host.attachShadow({ mode: "open" }).innerHTML = `
<style>
#in-shadow {
@@ -24,6 +24,8 @@ test(function() {
<span id="in-shadow">0123456789</span>
`;
+ await document.fonts.ready;
+
assert_not_equals(document.getElementById('in-document').offsetWidth, 160);
assert_equals(host.shadowRoot.getElementById('in-shadow').offsetWidth, 160);
}, "@font-face from the document applies in the shadow tree");
diff --git a/testing/web-platform/tests/css/css-scoping/font-face-003.html b/testing/web-platform/tests/css/css-scoping/font-face-003.html
index d3f83e4ec3..4e8d8ff482 100644
--- a/testing/web-platform/tests/css/css-scoping/font-face-003.html
+++ b/testing/web-platform/tests/css/css-scoping/font-face-003.html
@@ -13,7 +13,7 @@
</style>
<div id="host"><span id="in-document">1234567890</span></div>
<script>
-test(function() {
+ promise_test(async function () {
host.attachShadow({ mode: "open" }).innerHTML = `
<style>
:host {
@@ -24,6 +24,7 @@ test(function() {
<span id="in-shadow">0123456789</span>
`;
+ await document.fonts.ready;
assert_equals(document.getElementById('in-document').offsetWidth, 160);
assert_equals(host.shadowRoot.getElementById('in-shadow').offsetWidth, 160);
}, "@font-face from document applies to :host");
diff --git a/testing/web-platform/tests/css/css-scoping/font-face-004.html b/testing/web-platform/tests/css/css-scoping/font-face-004.html
index 6ac50bd432..f68e70994f 100644
--- a/testing/web-platform/tests/css/css-scoping/font-face-004.html
+++ b/testing/web-platform/tests/css/css-scoping/font-face-004.html
@@ -14,7 +14,7 @@
</style>
<div id="host"><span id="in-document">1234567890</span></div>
<script>
-test(function() {
+promise_test(async function() {
host.attachShadow({ mode: "open" }).innerHTML = `
<style>
::slotted(#in-document) {
@@ -25,6 +25,7 @@ test(function() {
<span id="in-shadow">0123456789</span>
`;
+ await document.fonts.ready;
assert_equals(document.getElementById('in-document').offsetWidth, 160);
assert_not_equals(host.shadowRoot.getElementById('in-shadow').offsetWidth, 160);
}, "@font-face from document applies to a slotted element");
diff --git a/testing/web-platform/tests/css/css-scoping/font-face-005.html b/testing/web-platform/tests/css/css-scoping/font-face-005.html
index fdf86fb56e..de102f253e 100644
--- a/testing/web-platform/tests/css/css-scoping/font-face-005.html
+++ b/testing/web-platform/tests/css/css-scoping/font-face-005.html
@@ -12,7 +12,7 @@
</style>
<div id="host"><span id="in-document">1234567890</span></div>
<script>
-test(function() {
+promise_test(async () => {
host.attachShadow({ mode: "open" }).innerHTML = `
<style>
@font-face {
@@ -27,6 +27,8 @@ test(function() {
<span id="in-shadow">0123456789</span>
`;
+ await document.fonts.ready;
+
assert_not_equals(document.getElementById('in-document').offsetWidth, 160);
assert_equals(host.shadowRoot.getElementById('in-shadow').offsetWidth, 160);
}, "@font-face should not leak out of shadow tree.");
diff --git a/testing/web-platform/tests/css/css-scoping/font-face-006.html b/testing/web-platform/tests/css/css-scoping/font-face-006.html
index 8c6e341713..e72d4ec2aa 100644
--- a/testing/web-platform/tests/css/css-scoping/font-face-006.html
+++ b/testing/web-platform/tests/css/css-scoping/font-face-006.html
@@ -16,7 +16,7 @@
</style>
<div id="host"></div>
<script>
-promise_test(async function() {
+promise_test(async () => {
host.attachShadow({ mode: "open" }).innerHTML = `
<style>
:host::before, :host::after {
diff --git a/testing/web-platform/tests/css/css-scoping/font-face-007.html b/testing/web-platform/tests/css/css-scoping/font-face-007.html
index ae669f638f..c644a54986 100644
--- a/testing/web-platform/tests/css/css-scoping/font-face-007.html
+++ b/testing/web-platform/tests/css/css-scoping/font-face-007.html
@@ -7,7 +7,7 @@
<div id="host"><span id="in-document">1234567890</span></div>
<script>
-test(function() {
+promise_test(async () => {
host.attachShadow({ mode: "open" }).innerHTML = `
<style>
@font-face {
@@ -22,6 +22,8 @@ test(function() {
<span id="in-shadow">0123456789</span>
`;
+ await document.fonts.ready;
+
assert_equals(document.getElementById('in-document').offsetWidth, 160);
assert_equals(host.shadowRoot.getElementById('in-shadow').offsetWidth, 160);
}, "@font-face from shadow applies to :host");
diff --git a/testing/web-platform/tests/css/css-scoping/font-face-008.html b/testing/web-platform/tests/css/css-scoping/font-face-008.html
index a40b0247d1..52c7869f9f 100644
--- a/testing/web-platform/tests/css/css-scoping/font-face-008.html
+++ b/testing/web-platform/tests/css/css-scoping/font-face-008.html
@@ -8,7 +8,7 @@
<div id="host"><span id="in-document">1234567890</span></div>
<script>
-test(function() {
+promise_test(async () => {
host.attachShadow({ mode: "open" }).innerHTML = `
<style>
@font-face {
@@ -23,6 +23,8 @@ test(function() {
<span id="in-shadow">0123456789</span>
`;
+ await document.fonts.ready;
+
assert_equals(document.getElementById('in-document').offsetWidth, 160);
assert_not_equals(host.shadowRoot.getElementById('in-shadow').offsetWidth, 160);
}, "@font-face from shadow applies to a slotted element");
diff --git a/testing/web-platform/tests/css/css-scoping/font-face-009.html b/testing/web-platform/tests/css/css-scoping/font-face-009.html
index 5d770929ca..4e08162dcd 100644
--- a/testing/web-platform/tests/css/css-scoping/font-face-009.html
+++ b/testing/web-platform/tests/css/css-scoping/font-face-009.html
@@ -12,7 +12,7 @@
</style>
<div id="host"></div>
<script>
-test(function() {
+promise_test(async () => {
host.attachShadow({ mode: "open" }).innerHTML = `
<style>
@font-face {
@@ -27,6 +27,8 @@ test(function() {
<slot></slot>
`;
+ await document.fonts.ready;
+
//shrinkwrapped size for a default font will be a bit more than 80-90
//if the font is applied, it will be a bit more than 160
assert_greater_than(document.getElementById('host').offsetWidth, 160);
diff --git a/testing/web-platform/tests/css/css-scroll-snap-2/resources/common.js b/testing/web-platform/tests/css/css-scroll-snap-2/resources/common.js
index a3591d48ed..8dce29474d 100644
--- a/testing/web-platform/tests/css/css-scroll-snap-2/resources/common.js
+++ b/testing/web-platform/tests/css/css-scroll-snap-2/resources/common.js
@@ -26,13 +26,15 @@ async function snap_test_setup(test, scroller, event_type) {
});
}
-async function test_snap_event(test, test_data, event_type) {
+async function test_snap_event(test, test_data, event_type,
+ use_onsnap_member = false) {
await snap_test_setup(test, test_data.scroller, event_type);
let listener = test_data.scroller ==
document.scrollingElement ? document : test_data.scroller;
- const event_promise = waitForSnapEvent(listener, event_type);
+ const event_promise = waitForSnapEvent(listener, event_type, true,
+ use_onsnap_member);
await test_data.scrolling_function();
let evt = await event_promise;
@@ -45,19 +47,36 @@ async function test_snap_event(test, test_data, event_type) {
"horizontal scroll offset mismatch.");
}
-async function test_snapchanged(test, test_data) {
- await test_snap_event(test, test_data, "snapchanged");
+async function test_snapchanged(test, test_data, use_onsnap_member = false) {
+ await test_snap_event(test, test_data, "snapchanged", use_onsnap_member);
}
-function waitForEventUntil(event_target, event_type, wait_until) {
+function waitForEventUntil(event_target, event_type, wait_until,
+ use_onsnap_member = false) {
return new Promise(resolve => {
let result = null;
const listener = (evt) => {
result = evt;
};
- event_target.addEventListener(event_type, listener);
+ if (use_onsnap_member) {
+ if (event_type === "snapchanging") {
+ event_target.onsnapchanging = listener;
+ } else {
+ event_target.onsnapchanged = listener;
+ }
+ } else {
+ event_target.addEventListener(event_type, listener);
+ }
wait_until.then(() => {
- event_target.removeEventListener(event_type, listener);
+ if (use_onsnap_member) {
+ if (event_type === "snapchanging") {
+ event_target.onsnapchanging = null;
+ } else {
+ event_target.onsnapchanged = null;
+ }
+ } else {
+ event_target.removeEventListener(event_type, listener);
+ }
resolve(result);
});
});
@@ -77,30 +96,19 @@ function waitForEventsUntil(event_target, event_type, wait_until) {
});
}
-function waitForOnSnapchanging(event_target) {
- return new Promise(resolve => {
- let result = null;
- const listener = (evt) => {
- result = evt;
- };
- event_target.onsnapchanging = listener;
- waitForScrollendEventNoTimeout(event_target).then(() => {
- event_target.onsnapchanging = null;
- resolve(result);
- });
- });
-}
-
// Proxy a wait for a snap event. We want to avoid having a test
// timeout in the event of an expected snap event not firing in a particular
// test case as that would cause the entire file to fail.
// Snap events should fire before scrollend, so if a scroll should happen, wait
// for a scrollend event. Otherwise, just do a rAF-based wait.
-function waitForSnapEvent(event_target, event_type, scroll_happens = true) {
+function waitForSnapEvent(event_target, event_type, scroll_happens = true,
+ use_onsnap_member = false) {
return scroll_happens ? waitForEventUntil(event_target, event_type,
- waitForScrollendEventNoTimeout(event_target))
+ waitForScrollendEventNoTimeout(event_target),
+ use_onsnap_member)
: waitForEventUntil(event_target, event_type,
- waitForAnimationFrames(2));
+ waitForAnimationFrames(2),
+ use_onsnap_member);
}
function waitForSnapChangedEvent(event_target, scroll_happens = true) {
diff --git a/testing/web-platform/tests/css/css-scroll-snap-2/resources/programmatic-scroll-common.js b/testing/web-platform/tests/css/css-scroll-snap-2/resources/programmatic-scroll-common.js
deleted file mode 100644
index 8257b98fe3..0000000000
--- a/testing/web-platform/tests/css/css-scroll-snap-2/resources/programmatic-scroll-common.js
+++ /dev/null
@@ -1,26 +0,0 @@
-// Helper functions for snapchanged-on-programmatic-* tests.
-
-// Utility function to test that onsnapchanging is triggered for
-// snapchanging-on-programmatic-* tests which set up a similar layout in which
-// the |scroller| has 3 snap targets that form a vertical column along
-// |scroller|'s middle. onsnapchanging should be triggered by conducting a
-// programmatic scroll to the top of snap_target.
-async function test_programmatic_scroll_onsnapchanging(test,
- scroller,
- event_target,
- snap_target) {
- await snap_test_setup(test, scroller, "snapchanging");
- const expected_snap_targets = { block: snap_target, inline: null };
-
- // Scroll and wait for a snapchanging event.
- const snapchanging_promise = waitForOnSnapchanging(event_target);
- scroller.scrollTo(0, snap_target.offsetTop);
- const snapchanging_event = await snapchanging_promise;
-
- // Assert that snapchanging fired and indicated that snap_target would
- // be snapped to.
- assertSnapEvent(snapchanging_event, expected_snap_targets);
- assert_equals(scroller.scrollLeft, 0, "scrollLeft is zero");
- assert_equals(scroller.scrollTop, snap_target.offsetTop,
- "snapped to snap_target");
-}
diff --git a/testing/web-platform/tests/css/css-scroll-snap-2/resources/user-scroll-common.js b/testing/web-platform/tests/css/css-scroll-snap-2/resources/user-scroll-common.js
index 820f143816..07c1428633 100644
--- a/testing/web-platform/tests/css/css-scroll-snap-2/resources/user-scroll-common.js
+++ b/testing/web-platform/tests/css/css-scroll-snap-2/resources/user-scroll-common.js
@@ -69,32 +69,3 @@ async function test_no_snapchanged(t, scroller, delta) {
async function test_no_snapchanging(t, scroller, delta) {
await test_no_snap_event(t, scroller, delta, "snapchanging");
}
-
-// Utility function to test that onsnapchanging is triggered for
-// snapchanging-on-user-* tests which set up a similar layout in which
-// the |scroller| has 3 snap targets that form a vertical column along
-// |scroller|'s middle. onsnapchanging should be triggered by touch-dragging
-// |scroller|'s content so that |snap_target|'s top aligns to |snap_target|.
-async function test_user_scroll_onsnapchanging(test, scroller, event_target,
- snap_target) {
- await snap_test_setup(test, scroller, "snapchanging");
-
- // Compute touch positions to drag the top of snap_target to the top of
- // the scroller.
- const scroller_middle = Math.round(scroller.clientWidth / 2);
- const start_pos = { x: scroller_middle, y: snap_target.offsetTop };
- const end_pos = { x: scroller_middle, y: 0 };
- const expected_snap_targets = { block: snap_target, inline: null };
-
- // Scroll and wait for a snapchanging event.
- const snapchanging_promise = waitForOnSnapchanging(event_target);
- await snap_event_touch_scroll_helper(start_pos, end_pos);
- const snapchanging_event = await snapchanging_promise;
-
- // Assert that snapchanging fired and indicated that snap_target would
- // be snapped to.
- assertSnapEvent(snapchanging_event, expected_snap_targets);
- assert_equals(scroller.scrollLeft, 0, "scrollLeft is zero");
- assert_equals(scroller.scrollTop, snap_target.offsetTop,
- "snapped to snap_target");
-}
diff --git a/testing/web-platform/tests/css/css-scroll-snap-2/snap-events-with-pseudo-target.tentative.html b/testing/web-platform/tests/css/css-scroll-snap-2/snap-events-with-pseudo-target.tentative.html
new file mode 100644
index 0000000000..baa3efc7ba
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap-2/snap-events-with-pseudo-target.tentative.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title> CSS Scroll Snap 2 Test: Snap Events with pseudo-element targets.</title>
+ <link rel="help" href="https://drafts.csswg.org/css-scroll-snap-2/#snap-events">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-actions.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/css/css-scroll-snap-2/resources/common.js"></script>
+ <script src="/css/css-scroll-snap-2/resources/user-scroll-common.js"></script>
+ <script src="/dom/events/scrolling/scroll_support.js"></script>
+ </head>
+ <body>
+ <style>
+ .scroller {
+ overflow: scroll;
+ width: 200px;
+ height: 200px;
+ border: solid 1px black;
+ scroll-snap-type: y mandatory;
+ position: absolute;
+ resize: both;
+ }
+ .space {
+ height: 300vh;
+ width: 300vw;
+ position: absolute;
+ }
+ .scroller::before, .scroller::after {
+ scroll-snap-align: start;
+ height: 50px;
+ width: 50px;
+ color: white;
+ display: block;
+ }
+ .scroller::before {
+ content: "before";
+ background-color: blue;
+ }
+ .scroller::after {
+ content: "after";
+ background-color: orange;
+ margin-top: 100px;
+ }
+ </style>
+ <div class="scroller" id="scroller">
+ <div class="space"></div>
+ </div>
+
+ <script>
+ const start_pos = {
+ x: scroller.clientWidth / 2,
+ y: scroller.clientHeight / 2,
+ };
+ const end_pos = { x: scroller.clientWidth / 2, y: 0 };
+ // The top of the ::after pseudo element is the sum of the
+ // ::before pseudo-element's height (50) and the ::after
+ // pseudo-element's margin-top (100).
+ const after_pseudo_element_top = 50 + 100;
+ const test_data = {
+ scroller: scroller,
+ scrolling_function: async () => {
+ await snap_event_touch_scroll_helper(start_pos, end_pos);
+ },
+ expected_snap_targets: { block: scroller, inline: null },
+ expected_scroll_offsets: {
+ x: 0,
+ y: after_pseudo_element_top,
+ }
+ };
+
+ promise_test(async (t) => {
+ await test_snapchanged(t, test_data);
+ }, "snapTarget for snapchanged is the owning element when a snap area " +
+ "belongs to a pseudo-element");
+
+ promise_test(async (t) => {
+ await test_snap_event(t, test_data, "snapchanging");
+ }, "snapTarget for snapchanging is the owning element when a snap area " +
+ "belongs to a pseudo-element");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-programmatic-root-scroll.tentative.html b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-programmatic-root-scroll.tentative.html
index 98ec2e5d75..8360369422 100644
--- a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-programmatic-root-scroll.tentative.html
+++ b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-programmatic-root-scroll.tentative.html
@@ -8,6 +8,7 @@
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/css/css-scroll-snap-2/resources/common.js"></script>
+ <script src="/css/css-scroll-snap-2/resources/programmatic-scroll-common.js"></script>
<script src="/dom/events/scrolling/scroll_support.js"></script>
<script src="/web-animations/testcommon.js"></script>
</head>
@@ -61,6 +62,7 @@
<script>
let scroller = document.scrollingElement;
+ let snap_point_2 = document.getElementById("snap_point_2");
promise_test(async (t) => {
await waitForCompositorCommit();
@@ -79,6 +81,23 @@
}, "snapchanged event fires after snap target changes via scrollTo");
promise_test(async (t) => {
+ await waitForCompositorCommit();
+ const test_data = {
+ scroller: scroller,
+ scrolling_function: () => {
+ scroller.scrollTo(snap_point_2.offsetLeft, snap_point_2.offsetTop);
+ },
+ expected_snap_targets: { block: snap_point_2, inline: snap_point_2 },
+ expected_scroll_offsets: {
+ x: snap_point_2.offsetLeft,
+ y: snap_point_2.offsetTop,
+ }
+ };
+ await test_snapchanged(t, test_data, /*use_onsnap_member*/true);
+ }, "Document.onsnapchanged event fires after snap target changes via" +
+ "scrollTo");
+
+ promise_test(async (t) => {
checkSnapEventSupport("snapchanged");
await waitForScrollReset(t, scroller);
await waitForCompositorCommit();
diff --git a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-programmatic-scroll.tentative.html b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-programmatic-scroll.tentative.html
index 9dff856f34..2b2e6a77c5 100644
--- a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-programmatic-scroll.tentative.html
+++ b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-programmatic-scroll.tentative.html
@@ -8,6 +8,7 @@
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/css/css-scroll-snap-2/resources/common.js"></script>
+ <script src="/css/css-scroll-snap-2/resources/programmatic-scroll-common.js"></script>
<script src="/dom/events/scrolling/scroll_support.js"></script>
<script src="/web-animations/testcommon.js"></script>
</head>
@@ -68,6 +69,7 @@
<script>
function runTests () {
let scroller = document.getElementById("scroller");
+ let snap_point_2 = document.getElementById("snap_point_2");
promise_test(async (t) => {
await waitForCompositorCommit();
@@ -86,6 +88,23 @@
}, "snapchanged event fires after snap target changes via scrollTo");
promise_test(async (t) => {
+ await waitForCompositorCommit();
+ const test_data = {
+ scroller: scroller,
+ scrolling_function: () => {
+ scroller.scrollTo(snap_point_2.offsetLeft, snap_point_2.offsetTop);
+ },
+ expected_snap_targets: { block: snap_point_2, inline: snap_point_2 },
+ expected_scroll_offsets: {
+ x: snap_point_2.offsetLeft,
+ y: snap_point_2.offsetTop,
+ }
+ };
+ await test_snapchanged(t, test_data, /*use_onsnap_member*/true);
+ }, "Element.onsnapchanged event fires after snap target changes via" +
+ "scrollTo");
+
+ promise_test(async (t) => {
checkSnapEventSupport("snapchanged");
await waitForScrollReset(t, scroller);
await waitForCompositorCommit();
diff --git a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-user-root-scroll.tentative.html b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-user-root-scroll.tentative.html
index 127376caa2..a59d9c5859 100644
--- a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-user-root-scroll.tentative.html
+++ b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-user-root-scroll.tentative.html
@@ -161,6 +161,29 @@
await test_snapchanged(t, test_data);
}, "snapchanged event fires after snap target changes on keydown press");
+ // Touch scroll test (onsnapchanged variant).
+ promise_test(async (t) => {
+ await waitForCompositorCommit();
+ const start_pos = {
+ x: scroller.clientWidth / 2,
+ y: scroller.clientHeight / 2,
+ };
+ const end_pos = { x: 0, y: 0 };
+ const test_data = {
+ scroller: scroller,
+ scrolling_function: async () => {
+ await snap_event_touch_scroll_helper(start_pos, end_pos);
+ },
+ expected_snap_targets: { block: snap_point_2, inline: snap_point_2 },
+ expected_scroll_offsets: {
+ x: offset_to_snap_point_2.x,
+ y: offset_to_snap_point_2.y,
+ }
+ };
+ await test_snapchanged(t, test_data, /*use_onsnap_memeber*/true);
+ }, "Document.snapchanged event fires after snap target changes on touch " +
+ "scroll");
+
promise_test(async (t) => {
await test_no_snapchanged(t, scroller, /*delta*/10);
}, "snapchanged is not fired if snap target doesn't change on user scroll");
diff --git a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-user-scroll.tentative.html b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-user-scroll.tentative.html
index 91194642b5..d2c2789c88 100644
--- a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-user-scroll.tentative.html
+++ b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-on-user-scroll.tentative.html
@@ -165,6 +165,29 @@
await test_snapchanged(t, test_data);
}, "snapchanged event fires after snap target changes on keydown press");
+ // Touch scroll test (onsnapchanged variant).
+ promise_test(async (t) => {
+ await waitForCompositorCommit();
+ const start_pos = {
+ x: scroller.clientWidth / 2,
+ y: scroller.clientHeight / 2,
+ };
+ const end_pos = { x: 0, y: 0 };
+ const test_data = {
+ scroller: scroller,
+ scrolling_function: async () => {
+ await snap_event_touch_scroll_helper(start_pos, end_pos);
+ },
+ expected_snap_targets: { block: snap_point_2, inline: snap_point_2 },
+ expected_scroll_offsets: {
+ x: offset_to_snap_point_2.x,
+ y: offset_to_snap_point_2.y,
+ }
+ };
+ await test_snapchanged(t, test_data, /*use_onsnap_memeber*/true);
+ }, "Element.onsnapchanged event fires after snap target changes on touch " +
+ "scroll");
+
promise_test(async (t) => {
await test_no_snapchanged(t, scroller, /*delta*/10);
}, "snapchanged is not fired if snap target doesn't change on user scroll");
diff --git a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-scrolling-non-snapping-axis.tentative.html b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-scrolling-non-snapping-axis.tentative.html
new file mode 100644
index 0000000000..e39fc0c44e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanged/snapchanged-scrolling-non-snapping-axis.tentative.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title> CSS Scroll Snap 2 Test: snapchanged events</title>
+ <link rel="help" href="https://drafts.csswg.org/css-scroll-snap-2/#snap-events">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-actions.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/css/css-scroll-snap-2/resources/common.js"></script>
+ <script src="/dom/events/scrolling/scroll_support.js"></script>
+ <script src="/css/css-scroll-snap/support/common.js"></script>
+ </head>
+ <body>
+ <style>
+ .scroller {
+ overflow: scroll;
+ width: 200px;
+ height: 200px;
+ border: solid 1px black;
+ scroll-snap-type: y mandatory;
+ position: absolute;
+ resize: both;
+ }
+ .snaparea {
+ scroll-snap-align: start;
+ height: 50px;
+ width: 50px;
+ color: white;
+ margin-top: 100px;
+ background-color: purple;
+ }
+ .space {
+ height: 300vh;
+ width: 300vw;
+ position: absolute;
+ }
+ </style>
+ <div class="scroller" id="scroller">
+ <div class="space"></div>
+ <div class="snaparea"> Area2</div>
+ <div class="snaparea"> Area1</div>
+ </div>
+
+ <script>
+ promise_test(async (t) => {
+ await waitForCompositorCommit();
+
+ scroller.focus();
+
+ const snapchanged_promise = waitForSnapEvent(scroller, "snapchanged");
+ await test_driver.send_keys(scroller, KEY_CODE_MAP["ArrowRight"]);
+ const snap_event = await snapchanged_promise;
+
+ assert_equals(snap_event, null, "no snapchanged event fired as " +
+ "scroller doesn't snap in the x axis");
+ }, "keyboard scroll on non-snapping axis doesn't trigger snapchanged");
+
+ promise_test(async (t) => {
+ await waitForScrollReset(t, scroller);
+ await waitForCompositorCommit();
+ scroller.focus();
+
+ const snapchanged_promise = waitForSnapEvent(scroller, "snapchanged");
+ const wheel_scroll_amount = 25;
+ new test_driver.Actions().scroll(0, 0,
+ wheel_scroll_amount,
+ 0,
+ { origin: scroller }).send();
+ const snap_event = await snapchanged_promise;
+ assert_equals(snap_event, null, "no snapchanged event fired as " +
+ "scroller doesn't snap in the x axis");
+ }, "wheel scroll on non-snapping axis doesn't trigger snapchanged");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-programmatic-root-scroll.tentative.html b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-programmatic-root-scroll.tentative.html
index b714a6cfb5..54c2c95c50 100644
--- a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-programmatic-root-scroll.tentative.html
+++ b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-programmatic-root-scroll.tentative.html
@@ -87,10 +87,22 @@
" snap targets.");
promise_test(async (t) => {
- await test_programmatic_scroll_onsnapchanging(t, scroller, document,
- snap_area_2);
- }, "programmatic scroll triggers Document.snapchanging when scrolling a " +
- "snap container");
+ await waitForCompositorCommit();
+ const test_data = {
+ scroller: scroller,
+ scrolling_function: async () => {
+ scroller.scrollTo(0, snap_area_2.offsetTop);
+ },
+ expected_snap_targets: { block: snap_area_2, inline: null },
+ expected_scroll_offsets: {
+ x: 0,
+ y: snap_area_2.offsetTop
+ }
+ };
+ await test_snap_event(t, test_data, "snapchanging",
+ /*use_onsnap_member*/true);
+ }, "Document.snapchanging fires on programmatic scrolls that changes a" +
+ "scroller's snap targets.");
promise_test(async (t) => {
checkSnapEventSupport("snapchanging");
diff --git a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-programmatic-scroll.tentative.html b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-programmatic-scroll.tentative.html
index 6e7b0126f7..0bd65f8e42 100644
--- a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-programmatic-scroll.tentative.html
+++ b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-programmatic-scroll.tentative.html
@@ -94,10 +94,22 @@
" snap targets.");
promise_test(async (t) => {
- await test_programmatic_scroll_onsnapchanging(t, scroller, scroller,
- snap_area_2);
- }, "programmatic scroll triggers Element.onsnapchanging when scrolling a " +
- "snap container");
+ await waitForCompositorCommit();
+ const test_data = {
+ scroller: scroller,
+ scrolling_function: async () => {
+ scroller.scrollTo(0, snap_area_2.offsetTop);
+ },
+ expected_snap_targets: { block: snap_area_2, inline: null },
+ expected_scroll_offsets: {
+ x: 0,
+ y: snap_area_2.offsetTop
+ }
+ };
+ await test_snap_event(t, test_data, "snapchanging",
+ /*use_onsnap_member*/true);
+ }, "Element.onsnapchanging fires on programmatic scrolls that changes a " +
+ "scroller's snap targets.");
promise_test(async (t) => {
checkSnapEventSupport("snapchanging");
diff --git a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-user-root-scroll.tentative.html b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-user-root-scroll.tentative.html
index 815c3c0922..8054db548d 100644
--- a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-user-root-scroll.tentative.html
+++ b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-user-root-scroll.tentative.html
@@ -159,10 +159,26 @@
await test_snap_event(t, test_data, "snapchanging");
}, "keyboard scroll triggers snapchanging.");
+ // Touch scroll test (onsnapchanging variant).
promise_test(async (t) => {
- await test_user_scroll_onsnapchanging(t, scroller, document,
- snap_area_2);
- }, "Document.onsnapchanging fires when scrolling a snap container.");
+ await waitForCompositorCommit();
+ const scroller_middle = Math.round(scroller.clientWidth / 2);
+ const test_data = {
+ scroller: scroller,
+ scrolling_function: async () => {
+ const start_pos = { x: scroller_middle, y: snap_area_2.offsetTop };
+ const end_pos = { x: scroller_middle, y: 0 };
+ await snap_event_touch_scroll_helper(start_pos, end_pos);
+ },
+ expected_snap_targets: { block: snap_area_2, inline: null },
+ expected_scroll_offsets: {
+ x: 0,
+ y: snap_area_2.offsetTop
+ }
+ };
+ await test_snap_event(t, test_data, "snapchanging",
+ /*use_onsnap_memeber*/true);
+ }, "touch scrolling fires Document.onsnapchanging.");
// Touch scroll test: peek at snap_area_2 and then drag back to
// snap_area_1.
diff --git a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-user-scroll.tentative.html b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-user-scroll.tentative.html
index 27f52efc71..3755369a2f 100644
--- a/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-user-scroll.tentative.html
+++ b/testing/web-platform/tests/css/css-scroll-snap-2/snapchanging/snapchanging-on-user-scroll.tentative.html
@@ -181,10 +181,26 @@
assertSnapEvent(evts[1], { block: snap_area_1, inline: null });
}, "snapchanging fires as scroll moves through different snap targets.");
+ // Touch scroll test.
promise_test(async (t) => {
- await test_user_scroll_onsnapchanging(t, scroller, scroller,
- snap_area_2);
- }, "Element.onsnapchanging fires when scrolling a snap container.");
+ await waitForCompositorCommit();
+ const scroller_middle = Math.round(scroller.clientWidth / 2);
+ const test_data = {
+ scroller: scroller,
+ scrolling_function: async () => {
+ const start_pos = { x: scroller_middle, y: snap_area_2.offsetTop };
+ const end_pos = { x: scroller_middle, y: 0 };
+ await snap_event_touch_scroll_helper(start_pos, end_pos);
+ },
+ expected_snap_targets: { block: snap_area_2, inline: null },
+ expected_scroll_offsets: {
+ x: 0,
+ y: snap_area_2.offsetTop
+ }
+ };
+ await test_snap_event(t, test_data, "snapchanging",
+ /*use_onsnap_member*/true);
+ }, "touch scrolling fires Element.onsnapchanging.");
// snapchanging doesn't fire test.
promise_test(async (t) => {
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/layout-follows-focused-targeted-block-iframe.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/layout-follows-focused-targeted-block-iframe.html
new file mode 100644
index 0000000000..4f4b4309fb
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/layout-follows-focused-targeted-block-iframe.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <style>
+ .scroller {
+ width: 200px;
+ height: 200px;
+ border: solid 1px black;
+ overflow: scroll;
+ scroll-snap-type: both mandatory;
+ position: relative;
+ resize: both;
+ }
+ .target {
+ scroll-snap-align: start;
+ width: 50px;
+ height: 50px;
+ background-color: green;
+ position: absolute;
+ }
+ .target:target {
+ background-color: blue;
+ }
+ .target:focus {
+ background-color: yellow;
+ }
+ #box1 {
+ left: 150px;
+ top: 0px;
+ }
+ #box2 {
+ left: 0px;
+ top: 150px;
+ }
+ .space {
+ width: 500%;
+ height: 500%;
+ position: absolute;
+ }
+ </style>
+ <div class="scroller" id="scroller">
+ <div tabindex="1" id="box1" class="target">Box 1</div>
+ <div tabindex="1" id="box2" class="target">Box 2</div>
+ <div class="space"></div>
+ </div>
+ </body>
+
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/layout-follows-focused-targeted-block.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/layout-follows-focused-targeted-block.html
new file mode 100644
index 0000000000..57dd52bcea
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/layout-follows-focused-targeted-block.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <link rel="help" href="https://drafts.csswg.org/css-scroll-snap" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/dom/events/scrolling/scroll_support.js"></script>
+ </head>
+ <body>
+ <style>
+ iframe {
+ width: 1000px;
+ height: 1000px;
+ }
+ </style>
+ <script>
+ let scroller;
+ let box1;
+ let box2;
+ let frame;
+
+ const iframe_load_promise = new Promise((resolve) => {
+ frame = document.createElement("iframe");
+ frame.onload = async () => {
+ scroller = frame.contentDocument.getElementById("scroller");
+ box1 = frame.contentDocument.getElementById("box1");
+ box2 = frame.contentDocument.getElementById("box2");
+ resolve();
+ };
+ frame.src = "./layout-follows-focused-targeted-block-iframe.html#box2";
+ document.body.appendChild(frame);
+ });
+
+ const displacement = 150;
+ async function test_resnap(t, target) {
+ // Save box1's position and setup the cleanup.
+ const box1_left = box1.style.left;
+ t.add_cleanup(async () => {
+ // Reset box1's position.
+ box1.style.left = box1_left;
+ // Reset scroller's writing-mode.
+ scroller.style.writingMode = "horizontal-tb";
+ // Reset scroll position.
+ await waitForScrollReset(t, scroller);
+ });
+
+ assert_equals(scroller.scrollTop, 0, "scroll top is reset");
+ assert_equals(scroller.scrollLeft, 0, "scroll left is reset");
+
+ // Move box1 outside the scrollport by translating it 150px
+ // horizontally.
+ const new_left = box1.offsetLeft + displacement;
+ box1.style.left = `${new_left}px`;
+
+ assert_equals(scroller.scrollLeft, target.offsetLeft,
+ `scroller followed ${target.id} in x axis`);
+
+ assert_equals(scroller.scrollTop, target.offsetTop,
+ `scroller followed ${target.id} in y axis`);
+ }
+
+ promise_test(async (t) => {
+ await iframe_load_promise;
+
+ box1.focus();
+ assert_equals(frame.contentDocument.activeElement, box1,
+ "sanity check that box1 is focused.");
+ assert_equals(frame.contentDocument.querySelector(":target"), box2,
+ "sanity check that box2 is targeted.");
+ // box2 is targeted but box1 is focused, so box1 should be
+ // followed.
+ await test_resnap(t, box1);
+
+ // Remove focus from box1.
+ scroller.focus();
+ }, "focused area prefered over targeted area.");
+
+ promise_test(async (t) => {
+ await iframe_load_promise;
+
+ assert_not_equals(frame.contentDocument.activeElement, box1,
+ "sanity check that box1 is not focused.");
+ assert_equals(frame.contentDocument.querySelector(":target"), box2,
+ "sanity check that box2 is targeted.");
+ // box2 is targeted and box1 is not focused, so box2 should be
+ // followed.
+ await test_resnap(t, box2);
+ }, "targeted area prefered over non-focused area.");
+
+ promise_test(async (t) => {
+ await iframe_load_promise;
+
+ // Clear the targeted element.
+ frame.contentDocument.location.hash = "";
+ assert_equals(frame.contentDocument.querySelector(":target"), null,
+ "sanity check that no box is targeted.");
+ assert_not_equals(frame.contentDocument.activeElement, box1,
+ "sanity check that box1 is not focused.");
+
+ // Neither box is targeted or focused; so, the block axis target should
+ // be followed.
+ await test_resnap(t, box1);
+ }, "block axis area is preferred.");
+
+ promise_test(async (t) => {
+ await iframe_load_promise;
+
+ scroller.style.writingMode = "vertical-lr";
+
+ // Clear the targeted element.
+ frame.contentDocument.location.hash = "";
+ assert_equals(frame.contentDocument.querySelector(":target"), null,
+ "sanity check that no box is targeted.");
+ assert_not_equals(frame.contentDocument.activeElement, box1,
+ "sanity check that box1 is not focused.");
+
+ // Neither box is targeted or focused; so, the block (x) axis target
+ // should be followed.
+ await test_resnap(t, box2);
+ }, "block axis area is preferred (vertical writing-mode).");
+ </script>
+ </body>
+
+</html>
diff --git a/testing/web-platform/tests/css/css-shadow-parts/invalidation-part-pseudo.html b/testing/web-platform/tests/css/css-shadow-parts/invalidation-part-pseudo.html
index fca4a964dc..66df33a4b7 100644
--- a/testing/web-platform/tests/css/css-shadow-parts/invalidation-part-pseudo.html
+++ b/testing/web-platform/tests/css/css-shadow-parts/invalidation-part-pseudo.html
@@ -3,7 +3,6 @@
<link rel="help" href="https://drafts.csswg.org/css-shadow-parts" >
<link rel="help" href="https://drafts.csswg.org/selectors/#matches">
<link href="https://drafts.csswg.org/selectors/#matches" rel="help">
-<link rel="match" href="interaction-with-nested-pseudo-class-ref.html">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
diff --git a/testing/web-platform/tests/css/css-shadow-parts/part-after-combinator-invalidation-ref.html b/testing/web-platform/tests/css/css-shadow-parts/part-after-combinator-invalidation-ref.html
new file mode 100644
index 0000000000..d6db0d0e4c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-shadow-parts/part-after-combinator-invalidation-ref.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<meta charset="utf-8">
+<div>What color am I?</div>
diff --git a/testing/web-platform/tests/css/css-shadow-parts/part-after-combinator-invalidation.html b/testing/web-platform/tests/css/css-shadow-parts/part-after-combinator-invalidation.html
new file mode 100644
index 0000000000..76beabe885
--- /dev/null
+++ b/testing/web-platform/tests/css/css-shadow-parts/part-after-combinator-invalidation.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-shadow-parts">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1891296">
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="author" href="https://mozilla.org" title="Mozilla">
+<link rel="match" href="part-after-combinator-invalidation-ref.html">
+<style>
+.inactive > ::part(content) {
+ color: red;
+}
+</style>
+<div class="inactive">
+ <div id="host">
+ <template shadowrootmode="open">
+ <div part="content">What color am I?</div>
+ </template>
+ </div>
+</div>
+<script>
+onload = () => {
+ host.getBoundingClientRect();
+ host.parentNode.className = "";
+ host.getBoundingClientRect();
+};
+</script>
diff --git a/testing/web-platform/tests/css/css-sizing/aspect-ratio/support/2x2-green.webm b/testing/web-platform/tests/css/css-sizing/aspect-ratio/support/2x2-green.webm
index 74af43afeb..d1c021c03d 100644
--- a/testing/web-platform/tests/css/css-sizing/aspect-ratio/support/2x2-green.webm
+++ b/testing/web-platform/tests/css/css-sizing/aspect-ratio/support/2x2-green.webm
Binary files differ
diff --git a/testing/web-platform/tests/css/css-sizing/contain-intrinsic-size/WEB_FEATURES.yml b/testing/web-platform/tests/css/css-sizing/contain-intrinsic-size/WEB_FEATURES.yml
new file mode 100644
index 0000000000..5fc7189215
--- /dev/null
+++ b/testing/web-platform/tests/css/css-sizing/contain-intrinsic-size/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: contain-intrinsic-size
+ files: "**"
diff --git a/testing/web-platform/tests/css/css-syntax/custom-property-rule-ambiguity.html b/testing/web-platform/tests/css/css-syntax/custom-property-rule-ambiguity.html
index b1adce7f9e..04f908acde 100644
--- a/testing/web-platform/tests/css/css-syntax/custom-property-rule-ambiguity.html
+++ b/testing/web-platform/tests/css/css-syntax/custom-property-rule-ambiguity.html
@@ -47,7 +47,7 @@
assert_equals(rules[0].selectorText, 'div');
let div = rules[0];
let x = div.style.getPropertyValue('--x');
- assert_equals(x, 'hover { }\n .b { }');
+ assert_equals(x.trim(), 'hover { }\n .b { }');
let childRules = div.cssRules;
assert_equals(childRules.length, 1);
assert_equals(childRules[0].selectorText, '& .a');
diff --git a/testing/web-platform/tests/css/css-tables/collapsed-border-partial-invalidation-003-ref.html b/testing/web-platform/tests/css/css-tables/collapsed-border-partial-invalidation-003-ref.html
new file mode 100644
index 0000000000..2b19a83814
--- /dev/null
+++ b/testing/web-platform/tests/css/css-tables/collapsed-border-partial-invalidation-003-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<link rel="author" title="David Shin" href="dshin@mozilla.com">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1871609">
+<style>
+table {
+ border-collapse: collapse;
+}
+tr {
+ border: 1px solid grey;
+}
+</style>
+<table>
+ <tr>
+ <td>X</td>
+ <td>X</td>
+ </tr>
+</table>
diff --git a/testing/web-platform/tests/css/css-tables/collapsed-border-partial-invalidation-003.html b/testing/web-platform/tests/css/css-tables/collapsed-border-partial-invalidation-003.html
new file mode 100644
index 0000000000..073f67e669
--- /dev/null
+++ b/testing/web-platform/tests/css/css-tables/collapsed-border-partial-invalidation-003.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<link rel="author" title="David Shin" href="dshin@mozilla.com">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1871609">
+<link rel="match" href="collapsed-border-partial-invalidation-003-ref.html">
+<meta name="assert" content="Invalidating part of a border-collapsed table keeps border styling correctly.">
+<style>
+table {
+ border-collapse: collapse;
+}
+tr {
+ border: 1px solid grey;
+}
+
+.foo {
+ border-right: 20px solid black;
+}
+
+</style>
+<table>
+ <tr>
+ <td id="cell" class="foo">X</td>
+ <td>X</td>
+ </tr>
+</table>
+<script>
+onload = function () {
+ cell.classList.remove("foo");
+ document.documentElement.className = "";
+}
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-near-zero-ref.html b/testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-near-zero-ref.html
new file mode 100644
index 0000000000..6254e4e9fc
--- /dev/null
+++ b/testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-near-zero-ref.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<style>
+ #target {
+ font-size: 100px;
+ color: rgba(0 255 0 / 0.5);
+ text-decoration: line-through;
+ }
+</style>
+<div id="target">X</div>
diff --git a/testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-near-zero.html b/testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-near-zero.html
new file mode 100644
index 0000000000..01cb5f8684
--- /dev/null
+++ b/testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-near-zero.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Text with decoration and a text-shadow with a color with alpha close to 0</title>
+<link rel="help" href="https://drafts.csswg.org/css-text-decor/#text-shadow-property">
+<link rel="help" href="https://drafts.csswg.org/css-text-decor/#text-decoration-property">
+<link rel="match" href="basic-opacity-near-zero-ref.html">
+<style>
+ #target {
+ font-size: 100px;
+ color: rgba(0 255 0 / 0.5);
+ text-shadow: 0 2px 0 rgba(0 0 255 / 0.0001);
+ text-decoration: line-through;
+ }
+</style>
+<div id="target">X</div>
diff --git a/testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-zero-ref.html b/testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-zero-ref.html
new file mode 100644
index 0000000000..6254e4e9fc
--- /dev/null
+++ b/testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-zero-ref.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<style>
+ #target {
+ font-size: 100px;
+ color: rgba(0 255 0 / 0.5);
+ text-decoration: line-through;
+ }
+</style>
+<div id="target">X</div>
diff --git a/testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-zero.html b/testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-zero.html
new file mode 100644
index 0000000000..ff9d507853
--- /dev/null
+++ b/testing/web-platform/tests/css/css-text-decor/text-shadow/basic-opacity-zero.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Text with decoration and a text-shadow with a color with alpha=0</title>
+<link rel="help" href="https://drafts.csswg.org/css-text-decor/#text-shadow-property">
+<link rel="help" href="https://drafts.csswg.org/css-text-decor/#text-decoration-property">
+<link rel="match" href="basic-opacity-zero-ref.html">
+<style>
+ #target {
+ font-size: 100px;
+ color: rgba(0 255 0 / 0.5);
+ text-shadow: 0 2px 0 rgba(255 128 0 / 0);
+ text-decoration: line-through;
+ }
+</style>
+<div id="target">X</div>
diff --git a/testing/web-platform/tests/css/css-text/hyphens/WEB_FEATURES.yml b/testing/web-platform/tests/css/css-text/hyphens/WEB_FEATURES.yml
new file mode 100644
index 0000000000..c8270e62c6
--- /dev/null
+++ b/testing/web-platform/tests/css/css-text/hyphens/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: hyphens
+ files: "**"
diff --git a/testing/web-platform/tests/css/css-text/text-spacing-trim/text-spacing-trim-subset-001-ref.html b/testing/web-platform/tests/css/css-text/text-spacing-trim/text-spacing-trim-subset-001-ref.html
new file mode 100644
index 0000000000..10abd537c9
--- /dev/null
+++ b/testing/web-platform/tests/css/css-text/text-spacing-trim/text-spacing-trim-subset-001-ref.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="support/variant-class.js"></script>
+<style>
+@font-face {
+ font-family: halt-font;
+ src: url('/fonts/noto/cjk/NotoSansCJKjp-Regular-subset-halt-min.otf');
+}
+#container {
+ font-family: halt-font;
+ font-size: 20px;
+ text-spacing-trim: space-all;
+}
+.vrl {
+ writing-mode: vertical-rl;
+}
+halt {
+ font-feature-settings: 'halt' 1, 'vhal' 1;
+}
+</style>
+<div id="container">
+ <div>国(<halt>(</halt>国</div>
+ <div>国)<halt>(</halt>国</div>
+ <div>国<halt>)</halt>)国</div>
+</div>
diff --git a/testing/web-platform/tests/css/css-text/text-spacing-trim/text-spacing-trim-subset-001.html b/testing/web-platform/tests/css/css-text/text-spacing-trim/text-spacing-trim-subset-001.html
new file mode 100644
index 0000000000..caef2b18e4
--- /dev/null
+++ b/testing/web-platform/tests/css/css-text/text-spacing-trim/text-spacing-trim-subset-001.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-text-4/#text-spacing-trim-property">
+<link rel="match" href="text-spacing-trim-subset-001-ref.html">
+<meta name="variant" content="?class=halt,htb">
+<meta name="variant" content="?class=halt,vrl">
+<script src="support/variant-class.js"></script>
+<style>
+@font-face {
+ font-family: halt-font;
+ src: url('/fonts/noto/cjk/NotoSansCJKjp-Regular-subset-halt-min.otf');
+}
+#container {
+ font-family: halt-font;
+ font-size: 20px;
+}
+.vrl {
+ writing-mode: vertical-rl;
+}
+</style>
+<div id="container">
+ <div>国((国</div>
+ <div>国)(国</div>
+ <div>国))国</div>
+</div>
diff --git a/testing/web-platform/tests/css/css-transitions/shadow-root-insertion.html b/testing/web-platform/tests/css/css-transitions/shadow-root-insertion.html
new file mode 100644
index 0000000000..47fc665aa3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transitions/shadow-root-insertion.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Transitions: behavior when a shadow root is inserted while transitioning</title>
+<meta name="assert" content="Checks the addition of a shadow root does not affect an in-flight transition">
+<link rel="help" href="https://drafts.csswg.org/css-transitions/">
+
+<script src="/resources/testharness.js" type="text/javascript"></script>
+<script src="/resources/testharnessreport.js" type="text/javascript"></script>
+<script src="./support/helper.js" type="text/javascript"></script>
+
+</head>
+<body>
+<div id="log"></div>
+<script>
+test(t => {
+ // Start a 100s transition 50% of the way through
+ const div = addDiv(t, {
+ style: 'transition: height 100s -50s linear; height: 0px',
+ });
+ getComputedStyle(div).height;
+ div.style.height = '100px';
+ assert_equals(
+ getComputedStyle(div).height,
+ '50px',
+ 'Transition should be initially 50% complete'
+ );
+
+ // Add a shadow root
+ div.attachShadow({ mode: "open" });
+
+ // The transition on the height property should not have been canceled
+ assert_equals(
+ getComputedStyle(div).height,
+ '50px',
+ 'Transition should not have been canceled'
+ );
+}, 'addition of a shadow root should not cancel in-flight transitions');
+</script>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-transitions/starting-style-size-container.html b/testing/web-platform/tests/css/css-transitions/starting-style-size-container.html
index 92ad6e6125..1ad609dd90 100644
--- a/testing/web-platform/tests/css/css-transitions/starting-style-size-container.html
+++ b/testing/web-platform/tests/css/css-transitions/starting-style-size-container.html
@@ -5,32 +5,58 @@
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/css/css-transitions/support/helper.js"></script>
-<div id="container" style="width: 200px">
- <div id="target" style="display: none"></div>
-</div>
+<body>
+</body>
<style>
#container {
container-type: inline-size;
+ width: 100px;
}
#target {
transition-property: background-color;
transition-duration: 100s;
transition-timing-function: steps(2, start);
background-color: lime;
+ display: none;
}
@container (width > 300px) {
@starting-style {
#target { background-color: white; }
}
}
- @container (width < 300px) {
+ @container ((width > 200px) and (width < 300px)) {
+ #target {
+ display: block;
+ }
+ @starting-style {
+ #target { background-color: white; }
+ }
+ }
+ @container (width < 200px) {
@starting-style {
#target { background-color: red; }
}
}
</style>
<script>
+ function setup(test) {
+ let container = document.createElement("div");
+ container.id = "container";
+ document.body.appendChild(container);
+
+ let target = document.createElement("div");
+ target.id = "target";
+ container.appendChild(target);
+
+ test.add_cleanup(() => {
+ target.remove();
+ container.remove();
+ });
+ return [container, target];
+ }
+
promise_test(async t => {
+ let [container, target] = setup(t);
await waitForAnimationFrames(2);
assert_equals(getComputedStyle(target).backgroundColor, "rgb(0, 255, 0)",
"No transition while display:none");
@@ -38,6 +64,21 @@
target.style.display = "block";
await waitForAnimationFrames(2);
assert_equals(getComputedStyle(target).backgroundColor, "rgb(128, 255, 128)",
- "@starting-style based on the size query evaluation from the same frame");
- }, "Triggered transition from first style update based on up-to-date container query");
+ "@starting-style based on the size query evaluation from " +
+ "the same frame");
+ }, "Triggered transition from first style update based on up-to-date " +
+ "container query");
+
+ promise_test(async t => {
+ let [container, target] = setup(t);
+ await waitForAnimationFrames(2);
+ assert_equals(getComputedStyle(target).backgroundColor, "rgb(0, 255, 0)",
+ "No transition while display:none");
+ container.style.width = "250px";
+ await waitForAnimationFrames(2);
+ assert_equals(getComputedStyle(target).backgroundColor, "rgb(128, 255, 128)",
+ "@starting-style based on the size query evaluation from " +
+ "the same frame");
+ }, "Triggered transition from the display change inside the up-to-date " +
+ "container query");
</script>
diff --git a/testing/web-platform/tests/css/css-typed-om/the-stylepropertymap/properties/animation-delay-end.tentative.html b/testing/web-platform/tests/css/css-typed-om/the-stylepropertymap/properties/animation-delay-end.tentative.html
deleted file mode 100644
index 79b9f94617..0000000000
--- a/testing/web-platform/tests/css/css-typed-om/the-stylepropertymap/properties/animation-delay-end.tentative.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<!DOCTYPE html>
-<title>'animation-delay-end' property</title>
-<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
-<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
-<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../resources/testhelper.js"></script>
-<script src="resources/testsuite.js"></script>
-<body>
-<div id="log"></div>
-<script>
-'use strict';
-
-runListValuedPropertyTests('animation-delay-end', [
- { syntax: '<time>' }
-]);
-
-</script>
diff --git a/testing/web-platform/tests/css/css-typed-om/the-stylepropertymap/properties/animation-delay-start.tentative.html b/testing/web-platform/tests/css/css-typed-om/the-stylepropertymap/properties/animation-delay-start.tentative.html
deleted file mode 100644
index 2fba4d8e51..0000000000
--- a/testing/web-platform/tests/css/css-typed-om/the-stylepropertymap/properties/animation-delay-start.tentative.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<!DOCTYPE html>
-<title>'animation-delay-start' property</title>
-<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
-<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
-<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../resources/testhelper.js"></script>
-<script src="resources/testsuite.js"></script>
-<body>
-<div id="log"></div>
-<script>
-'use strict';
-
-runListValuedPropertyTests('animation-delay-start', [
- { syntax: '<time>' }
-]);
-
-</script>
diff --git a/testing/web-platform/tests/css/css-ui/webkit-appearance-auto-non-html-namespace-001.html b/testing/web-platform/tests/css/css-ui/webkit-appearance-auto-non-html-namespace-001.html
new file mode 100644
index 0000000000..2756749725
--- /dev/null
+++ b/testing/web-platform/tests/css/css-ui/webkit-appearance-auto-non-html-namespace-001.html
@@ -0,0 +1,19 @@
+<!-- DO NOT EDIT THIS FILE.
+Edit the appearance-* file instead and then run:
+ ./tools/appearance-build-webkit-reftests.py
+-->
+<!DOCTYPE html>
+<title>CSS Basic User Interface Test: -webkit-appearance: auto on elements in non-HTML namespace</title>
+<link rel="match" href="nothing-below-ref.html">
+<link rel="help" href="https://drafts.csswg.org/css-ui-4/#appearance-switching">
+<meta name="assert" content="-webkit-appearance: auto should have no effect on non-HTML elements.">
+<style>
+ div * { -webkit-appearance: auto; display: inline-block; width: 1em; height: 1em; }
+</style>
+<p>There should be nothing below:</p>
+<div id=div></div>
+<script>
+for (var tagName of ['button', 'input', 'meter', 'progress', 'select', 'textarea']) {
+ div.appendChild(document.createElementNS('not-html', tagName));
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-values/WEB_FEATURES.yml b/testing/web-platform/tests/css/css-values/WEB_FEATURES.yml
index ca3c0ff91b..dc7331ab95 100644
--- a/testing/web-platform/tests/css/css-values/WEB_FEATURES.yml
+++ b/testing/web-platform/tests/css/css-values/WEB_FEATURES.yml
@@ -5,6 +5,10 @@ features:
- name: cap
files:
- cap-*
+- name: exp-functions
+ files:
+ - exp-log-*
+ - hypot-pow-sqrt-*
- name: ic
files:
- ic-*
diff --git a/testing/web-platform/tests/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html b/testing/web-platform/tests/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html
index 06277376e9..6d15c3f226 100644
--- a/testing/web-platform/tests/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html
+++ b/testing/web-platform/tests/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html
@@ -180,7 +180,7 @@
test_interpolation({
property: 'height',
from: 'calc-size(37px, 200px)',
- to: `calc-size(37px, size * 2 + 3% + 17px)`, /* adds to 100px */
+ to: 'calc-size(37px, size * 2 + 3% + 17px)', /* adds to 100px */
}, [
{ at: -0.25, expect: '225px' },
{ at: 0, expect: '200px' },
@@ -189,4 +189,43 @@
{ at: 1.25, expect: '75px' },
]);
+ test_interpolation({
+ property: 'height',
+ from: 'calc-size(auto, size)',
+ to: '50%',
+ }, [
+ { at: -0.25, expect: '87.5px' },
+ { at: 0, expect: '100px' },
+ { at: 0.75, expect: '137.5px' },
+ { at: 1, expect: '150px' },
+ { at: 1.25, expect: '162.5px' },
+ ]);
+
+ // Rerun roughly the same test with an auto height container.
+ let auto_style_text = `
+ .parent {
+ height: auto;
+ }
+ `;
+ let auto_style_element = document.createElement("style");
+ auto_style_element.append(document.createTextNode(auto_style_text));
+ document.head.append(auto_style_element);
+
+ test_interpolation({
+ property: 'height',
+ from: 'calc-size(auto, size * 2)',
+ to: '50%',
+ }, [
+ { at: -0.25, expect: '250px' },
+ { at: 0, expect: '200px' },
+ { at: 0.75, expect: '50px' },
+ /* TODO(https://crbug.com/40339056): It's questionable whether this should
+ * be the case, particularly for transitions. Perhaps the value at the
+ * end should have its percentage-ness back and be 100px here? */
+ { at: 1, expect: '0px' },
+ { at: 1.25, expect: '0px' },
+ ]);
+
+ auto_style_element.remove();
+
</script>
diff --git a/testing/web-platform/tests/css/css-values/calc-size/calc-size-parsing.tentative.html b/testing/web-platform/tests/css/css-values/calc-size/calc-size-parsing.tentative.html
index afcb200424..422ab3c33e 100644
--- a/testing/web-platform/tests/css/css-values/calc-size/calc-size-parsing.tentative.html
+++ b/testing/web-platform/tests/css/css-values/calc-size/calc-size-parsing.tentative.html
@@ -70,4 +70,16 @@ test_valid_value("width", "calc-size(calc-size(min-content, size), size)");
test_invalid_value("height", "calc(12% + calc-size(any, 31%))");
+// Based on the discussion in https://github.com/w3c/csswg-drafts/issues/10259
+// this presumes parse-time conversion of the one-argument form to the
+// two-argument form, but this isn't yet specified.
+test_valid_value("width", "calc-size(30px)", "calc-size(any, 30px)");
+test_valid_value("width", "calc-size(min(30px, 2em))", "calc-size(any, min(30px, 2em))");
+test_invalid_value("width", "calc-size(any)");
+test_valid_value("width", "calc-size(calc-size(any, 30px))", "calc-size(calc-size(any, 30px), size)");
+test_invalid_value("width", "calc-size(size)");
+test_valid_value("width", "calc-size(fit-content)", "calc-size(fit-content, size)");
+test_valid_value("width", "calc-size(calc-size(fit-content, size * 2))", "calc-size(calc-size(fit-content, size * 2), size)");
+test_valid_value("width", "calc-size(calc-size(30px))", "calc-size(calc-size(any, 30px), size)");
+
</script>
diff --git a/testing/web-platform/tests/css/css-values/container-progress-computed.tentative.html b/testing/web-platform/tests/css/css-values/container-progress-computed.tentative.html
index 9ab537cad6..5c8d12f9cd 100644
--- a/testing/web-platform/tests/css/css-values/container-progress-computed.tentative.html
+++ b/testing/web-platform/tests/css/css-values/container-progress-computed.tentative.html
@@ -1,4 +1,5 @@
<!DOCTYPE html>
+<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="help" href="https://drafts.csswg.org/css-values-5/#container-progress-func">
<link rel="author" title="sakhapov@chromium.org">
<script src="/resources/testharness.js"></script>
@@ -15,6 +16,8 @@
<style>
:root {
font-size: 10px;
+ width: 100vw;
+ height: 100vh;
}
#out-of-scope-container {
container: my-container-3 / size;
@@ -42,8 +45,9 @@
</style>
<script>
-let width = window.innerWidth;
-let height = window.innerHeight;
+// innerWidth and innerHeight have lossy precision, see
+// https://github.com/w3c/csswg-drafts/issues/5260.
+let { width, height } = document.documentElement.getBoundingClientRect();
let extraWidth = 5051;
let extraHeight = 1337;
@@ -68,10 +72,10 @@ test_math_used('calc(container-progress(width of my-container from 0px to 50px)
test_math_used('calc(container-progress(height of my-container from 10px to sign(50px - 500em) * 10px))', (outerHeight - 10) / (-10 - 10), {type:'number'});
// Fallback
-test_math_used('container-progress(width of non-existing-container from 0px to 1px)', width, {type:'number'});
-test_math_used('container-progress(height of non-existing-container from 0px to 1px)', height, {type:'number'});
-test_math_used('container-progress(width of out-of-scope-container from 0px to 1px)', width, {type:'number'});
-test_math_used('container-progress(height of out-of-scope-container from 0px to 1px)', height, {type:'number'});
+test_math_used('container-progress(width of non-existing-container from 0px to 1px)', width, {type:'number', msg: 'container-progress() width fallback for non-existing container name'});
+test_math_used('container-progress(height of non-existing-container from 0px to 1px)', height, {type:'number', msg: 'container-progress() height fallback for non-existing container names'});
+test_math_used('container-progress(width of out-of-scope-container from 0px to 1px)', width, {type:'number', msg: 'container-progress() width fallback for out of scope container'});
+test_math_used('container-progress(height of out-of-scope-container from 0px to 1px)', height, {type:'number', msg: 'container-progress() height fallback for out of scope container'});
// Type checking
test_math_used('calc(container-progress(width from 0px to 1px) * 1px)', innerWidth + 'px');
diff --git a/testing/web-platform/tests/css/css-view-transitions/3d-transform-incoming.html b/testing/web-platform/tests/css/css-view-transitions/3d-transform-incoming.html
index 983ba1d861..4b1411f25d 100644
--- a/testing/web-platform/tests/css/css-view-transitions/3d-transform-incoming.html
+++ b/testing/web-platform/tests/css/css-view-transitions/3d-transform-incoming.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="3d-transform-incoming-ref.html">
-<meta name=fuzzy content="3d-transform-incoming-ref.html:0-255;0-515">
+<meta name=fuzzy content="maxDifference=0-255; totalPixels=0-515">
<script src="/common/reftest-wait.js"></script>
<style>
div { box-sizing: border-box; will-change: transform }
diff --git a/testing/web-platform/tests/css/css-view-transitions/3d-transform-outgoing.html b/testing/web-platform/tests/css/css-view-transitions/3d-transform-outgoing.html
index 398fa97ca9..2fe4887bb1 100644
--- a/testing/web-platform/tests/css/css-view-transitions/3d-transform-outgoing.html
+++ b/testing/web-platform/tests/css/css-view-transitions/3d-transform-outgoing.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="3d-transform-outgoing-ref.html">
-<meta name=fuzzy content="3d-transform-outgoing-ref.html:0-255;0-1200">
+<meta name=fuzzy content="maxDifference=0-255; totalPixels=0-1200">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/block-with-overflowing-text.html b/testing/web-platform/tests/css/css-view-transitions/block-with-overflowing-text.html
index b3f8f42cfd..15d1653bfc 100644
--- a/testing/web-platform/tests/css/css-view-transitions/block-with-overflowing-text.html
+++ b/testing/web-platform/tests/css/css-view-transitions/block-with-overflowing-text.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="block-with-overflowing-text-ref.html">
-<meta name="fuzzy" content="block-with-overflowing-text-ref.html:maxDifference=0-2;totalPixels=0-1200">
+<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-1200">
<script src="/common/reftest-wait.js"></script>
diff --git a/testing/web-platform/tests/css/css-view-transitions/break-inside-avoid-child.html b/testing/web-platform/tests/css/css-view-transitions/break-inside-avoid-child.html
index 7b2a83c776..87d56d33af 100644
--- a/testing/web-platform/tests/css/css-view-transitions/break-inside-avoid-child.html
+++ b/testing/web-platform/tests/css/css-view-transitions/break-inside-avoid-child.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://www.w3.org/TR/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="break-inside-avoid-child-ref.html">
-<meta name="fuzzy" content="break-inside-avoid-child-ref.html:0-5;0-1600">
+<meta name="fuzzy" content="maxDifference=0-5; totalPixels=0-1600">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/capture-with-offscreen-child.html b/testing/web-platform/tests/css/css-view-transitions/capture-with-offscreen-child.html
index 8588968d8a..7f8085cae2 100644
--- a/testing/web-platform/tests/css/css-view-transitions/capture-with-offscreen-child.html
+++ b/testing/web-platform/tests/css/css-view-transitions/capture-with-offscreen-child.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="capture-with-offscreen-child-ref.html">
-<meta name="fuzzy" content="capture-with-offscreen-child-ref.html:0-5;0-200">
+<meta name="fuzzy" content="maxDifference=0-5; totalPixels=0-200">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/capture-with-opacity-zero-child.html b/testing/web-platform/tests/css/css-view-transitions/capture-with-opacity-zero-child.html
index 888d0d1720..2b0563ea31 100644
--- a/testing/web-platform/tests/css/css-view-transitions/capture-with-opacity-zero-child.html
+++ b/testing/web-platform/tests/css/css-view-transitions/capture-with-opacity-zero-child.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="capture-with-visibility-hidden-child-ref.html">
-<meta name="fuzzy" content="capture-with-visibility-hidden-child-ref.html:0-5;0-300">
+<meta name="fuzzy" content="maxDifference=0-5; totalPixels=0-300">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/capture-with-visibility-mixed-descendants.html b/testing/web-platform/tests/css/css-view-transitions/capture-with-visibility-mixed-descendants.html
index 462d267b94..3a4811ada1 100644
--- a/testing/web-platform/tests/css/css-view-transitions/capture-with-visibility-mixed-descendants.html
+++ b/testing/web-platform/tests/css/css-view-transitions/capture-with-visibility-mixed-descendants.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="capture-with-visibility-mixed-descendants-ref.html">
-<meta name=fuzzy content="capture-with-visibility-mixed-descendants-ref.html:0-5;0-500">
+<meta name=fuzzy content="maxDifference=0-5; totalPixels=0-500">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/clip-path-larger-than-border-box-on-child-of-named-element.html b/testing/web-platform/tests/css/css-view-transitions/clip-path-larger-than-border-box-on-child-of-named-element.html
index 4cde9cb586..4a26c50ef8 100644
--- a/testing/web-platform/tests/css/css-view-transitions/clip-path-larger-than-border-box-on-child-of-named-element.html
+++ b/testing/web-platform/tests/css/css-view-transitions/clip-path-larger-than-border-box-on-child-of-named-element.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="clip-path-larger-than-border-box-on-child-of-named-element-ref.html">
-<meta name="fuzzy" content="clip-path-larger-than-border-box-on-child-of-named-element-ref.html:maxDifference=0-255;totalPixels=0-400">
+<meta name="fuzzy" content="maxDifference=0-255;totalPixels=0-400">
<script src="/common/reftest-wait.js"></script>
<style>
.target {
diff --git a/testing/web-platform/tests/css/css-view-transitions/content-with-transform-new-image.html b/testing/web-platform/tests/css/css-view-transitions/content-with-transform-new-image.html
index f86f64843c..a6c444917a 100644
--- a/testing/web-platform/tests/css/css-view-transitions/content-with-transform-new-image.html
+++ b/testing/web-platform/tests/css/css-view-transitions/content-with-transform-new-image.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="content-with-transform-ref.html">
-<meta name="fuzzy" content="content-with-transform-ref.html:0-1;0-500">
+<meta name="fuzzy" content="maxDifference=0-1; totalPixels=0-500">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/content-with-transform-old-image.html b/testing/web-platform/tests/css/css-view-transitions/content-with-transform-old-image.html
index 3755910a1b..c6fda7f988 100644
--- a/testing/web-platform/tests/css/css-view-transitions/content-with-transform-old-image.html
+++ b/testing/web-platform/tests/css/css-view-transitions/content-with-transform-old-image.html
@@ -5,7 +5,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="content-with-transform-ref.html">
-<meta name="fuzzy" content="content-with-transform-ref.html:0-1;0-400">
+<meta name="fuzzy" content="maxDifference=0-1; totalPixels=0-400">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/css-tags-paint-order-with-entry.html b/testing/web-platform/tests/css/css-view-transitions/css-tags-paint-order-with-entry.html
index 2ba73758e2..c8d92ed33f 100644
--- a/testing/web-platform/tests/css/css-view-transitions/css-tags-paint-order-with-entry.html
+++ b/testing/web-platform/tests/css/css-view-transitions/css-tags-paint-order-with-entry.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="css-tags-paint-order-with-entry-ref.html">
-<meta name="fuzzy" content="css-tags-paint-order-with-entry-ref.html:0-120;0-300">
+<meta name="fuzzy" content="maxDifference=0-120; totalPixels=0-300">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/dialog-in-rtl-iframe.html b/testing/web-platform/tests/css/css-view-transitions/dialog-in-rtl-iframe.html
index f5959bf434..05bc98dca9 100644
--- a/testing/web-platform/tests/css/css-view-transitions/dialog-in-rtl-iframe.html
+++ b/testing/web-platform/tests/css/css-view-transitions/dialog-in-rtl-iframe.html
@@ -5,7 +5,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:bokan@chromium.org">
<link rel="match" href="dialog-in-rtl-iframe-ref.html">
- <meta name=fuzzy content="dialog-in-rtl-iframe-ref.html:0-80;0-500">
+ <meta name=fuzzy content="maxDifference=0-80; totalPixels=0-500">
<script src="/common/reftest-wait.js"></script>
<style>
iframe {
diff --git a/testing/web-platform/tests/css/css-view-transitions/event-pseudo-name.html b/testing/web-platform/tests/css/css-view-transitions/event-pseudo-name.html
index a57c1d5108..d77c817345 100644
--- a/testing/web-platform/tests/css/css-view-transitions/event-pseudo-name.html
+++ b/testing/web-platform/tests/css/css-view-transitions/event-pseudo-name.html
@@ -2,7 +2,6 @@
<title>View transitions: event pseudo name</title>
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
-<link rel="match" href="web-animations-api-ref.html">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
diff --git a/testing/web-platform/tests/css/css-view-transitions/far-away-capture.html b/testing/web-platform/tests/css/css-view-transitions/far-away-capture.html
index 97ad9dfb44..9ac1621639 100644
--- a/testing/web-platform/tests/css/css-view-transitions/far-away-capture.html
+++ b/testing/web-platform/tests/css/css-view-transitions/far-away-capture.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="far-away-capture-ref.html">
-<meta name="fuzzy" content="far-away-capture-ref.html:0-1;0-5">
+<meta name="fuzzy" content="maxDifference=0-1; totalPixels=0-5">
<script src="/common/reftest-wait.js"></script>
<style>
.flex {
diff --git a/testing/web-platform/tests/css/css-view-transitions/fractional-box-with-overflow-children-new.html b/testing/web-platform/tests/css/css-view-transitions/fractional-box-with-overflow-children-new.html
index e50e6654a7..14371aca3f 100644
--- a/testing/web-platform/tests/css/css-view-transitions/fractional-box-with-overflow-children-new.html
+++ b/testing/web-platform/tests/css/css-view-transitions/fractional-box-with-overflow-children-new.html
@@ -6,7 +6,7 @@
<link rel="match" href="fractional-box-with-overflow-children-ref.html">
<!-- subpixel differences are ok in this test (in highdpi), but channel difference
should not be perceptible -->
-<meta name=fuzzy content="fractional-box-with-overflow-children-ref.html:0-3;0-100">
+<meta name=fuzzy content="maxDifference=0-3; totalPixels=0-100">
<script src="/common/reftest-wait.js"></script>
<style>
.box {
diff --git a/testing/web-platform/tests/css/css-view-transitions/fractional-box-with-overflow-children-old.html b/testing/web-platform/tests/css/css-view-transitions/fractional-box-with-overflow-children-old.html
index acf72e20df..0d19bf9c29 100644
--- a/testing/web-platform/tests/css/css-view-transitions/fractional-box-with-overflow-children-old.html
+++ b/testing/web-platform/tests/css/css-view-transitions/fractional-box-with-overflow-children-old.html
@@ -6,7 +6,7 @@
<link rel="match" href="fractional-box-with-overflow-children-ref.html">
<!-- subpixel differences are ok in this test (in highdpi), but channel difference
should not be perceptible -->
-<meta name=fuzzy content="fractional-box-with-overflow-children-ref.html:0-3;0-100">
+<meta name=fuzzy content="maxDifference=0-3; totalPixels=0-100">
<script src="/common/reftest-wait.js"></script>
<style>
.box {
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-new-iframe-ref.html b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-new-iframe-ref.html
new file mode 100644
index 0000000000..86bc2a4a36
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-new-iframe-ref.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>View transitions: iframe and main frame transition at the same time (ref)</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:khushalsagar@chromium.org">
+<style>
+iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 50vw;
+ height: 50vh;
+}
+
+body {
+ background: lightgreen;
+}
+</style>
+
+<iframe srcdoc="
+<style>
+body {
+ background: lightblue;
+}
+</style>
+"></iframe>
+
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-new-iframe.html b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-new-iframe.html
new file mode 100644
index 0000000000..89360c0dcc
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-new-iframe.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<title>View transitions: iframe and main frame transition at the same time</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:khushalsagar@chromium.org">
+<link rel="match" href="iframe-and-main-frame-transition-new-main-new-iframe-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<style>
+iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 50vw;
+ height: 50vh;
+}
+
+/* Keep showing the live state for the main frame so we can assert the state of
+ screenshots in the iframe */
+::view-transition-group(root) {
+ animation-duration: 300s;
+}
+::view-transition-old(root) {
+ animation: unset;
+ opacity: 0;
+}
+::view-transition-new(root) {
+ animation: unset;
+ opacity: 1;
+}
+</style>
+
+<iframe srcdoc="
+<style>
+ /* The iframe is showing new live DOM */
+ ::view-transition-group(root) {
+ animation-duration: 300s;
+ }
+ ::view-transition-new(root) {
+ animation: unset;
+ opacity: 1;
+ }
+ ::view-transition-old(root) {
+ animation: unset;
+ opacity: 0;
+ }
+</style>
+<body></body>
+"></iframe>
+<script>
+ onload = runTest;
+
+ async function startTransition(document, oldColor, newColor) {
+ document.documentElement.style.background = oldColor;
+ await document.startViewTransition(() => {
+ document.documentElement.style.background = newColor;
+ }).ready;
+ }
+
+ async function runTest() {
+ await startTransition(document, "green", "lightgreen");
+
+ const iframeDocument = document.querySelector("iframe").contentDocument;
+ await startTransition(iframeDocument, "blue", "lightblue");
+
+ takeScreenshot();
+ }
+</script>
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-old-iframe-ref.html b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-old-iframe-ref.html
new file mode 100644
index 0000000000..baafea3656
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-old-iframe-ref.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>View transitions: iframe and main frame transition at the same time (ref)</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:khushalsagar@chromium.org">
+<style>
+iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 50vw;
+ height: 50vh;
+}
+
+body {
+ background: lightgreen;
+}
+</style>
+
+<iframe srcdoc="
+<style>
+body {
+ background: blue;
+}
+</style>
+"></iframe>
+
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-old-iframe.html b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-old-iframe.html
new file mode 100644
index 0000000000..7a9c53ffc1
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-new-main-old-iframe.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<title>View transitions: iframe and main frame transition at the same time</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:khushalsagar@chromium.org">
+<link rel="match" href="iframe-and-main-frame-transition-new-main-old-iframe-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<style>
+iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 50vw;
+ height: 50vh;
+}
+
+/* Keep showing the live state for the main frame so we can assert the state of
+ screenshots in the iframe */
+::view-transition-group(root) {
+ animation-duration: 300s;
+}
+::view-transition-old(root) {
+ animation: unset;
+ opacity: 0;
+}
+::view-transition-new(root) {
+ animation: unset;
+ opacity: 1;
+}
+</style>
+
+<iframe srcdoc="
+<style>
+ /* The iframe is showing old screenshots */
+ ::view-transition-group(root) {
+ animation-duration: 300s;
+ }
+ ::view-transition-old(root) {
+ animation: unset;
+ opacity: 1;
+ }
+ ::view-transition-new(root) {
+ animation: unset;
+ opacity: 0;
+ }
+</style>
+<body></body>
+"></iframe>
+<script>
+ onload = runTest;
+
+ async function startTransition(document, oldColor, newColor) {
+ document.documentElement.style.background = oldColor;
+ await document.startViewTransition(() => {
+ document.documentElement.style.background = newColor;
+ }).ready;
+ }
+
+ async function runTest() {
+ await startTransition(document, "green", "lightgreen");
+
+ const iframeDocument = document.querySelector("iframe").contentDocument;
+ await startTransition(iframeDocument, "blue", "lightblue");
+
+ takeScreenshot();
+ }
+</script>
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-new-iframe-ref.html b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-new-iframe-ref.html
new file mode 100644
index 0000000000..7033cd9d21
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-new-iframe-ref.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>View transitions: iframe and main frame transition at the same time (ref)</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:khushalsagar@chromium.org">
+<style>
+iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 50vw;
+ height: 50vh;
+}
+
+body {
+ background: green;
+}
+</style>
+
+<iframe srcdoc="
+<style>
+body {
+ background: lightblue;
+}
+</style>
+"></iframe>
+
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-new-iframe.html b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-new-iframe.html
new file mode 100644
index 0000000000..d3681aa434
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-new-iframe.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<title>View transitions: iframe and main frame transition at the same time</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:khushalsagar@chromium.org">
+<link rel="match" href="iframe-and-main-frame-transition-old-main-new-iframe-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<style>
+iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 50vw;
+ height: 50vh;
+}
+
+/* Keep showing the screenshot for the main frame */
+::view-transition-group(root) {
+ animation-duration: 300s;
+}
+::view-transition-old(root) {
+ animation: unset;
+ opacity: 1;
+}
+::view-transition-new(root) {
+ animation: unset;
+ opacity: 0;
+}
+</style>
+
+<iframe srcdoc="
+<style>
+ /* The iframe is showing live DOM */
+ ::view-transition-group(root) {
+ animation-duration: 300s;
+ }
+ ::view-transition-new(root) {
+ animation: unset;
+ opacity: 1;
+ }
+ ::view-transition-old(root) {
+ animation: unset;
+ opacity: 0;
+ }
+</style>
+<body></body>
+"></iframe>
+<script>
+ onload = runTest;
+
+ async function startTransition(document, oldColor, newColor) {
+ document.documentElement.style.background = oldColor;
+ await document.startViewTransition(() => {
+ document.documentElement.style.background = newColor;
+ }).ready;
+ }
+
+ async function runTest() {
+ const iframeDocument = document.querySelector("iframe").contentDocument;
+ await startTransition(iframeDocument, "blue", "lightblue");
+
+ await startTransition(document, "green", "lightgreen");
+
+ takeScreenshot();
+ }
+</script>
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-old-iframe-ref.html b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-old-iframe-ref.html
new file mode 100644
index 0000000000..2824884c4f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-old-iframe-ref.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>View transitions: iframe and main frame transition at the same time (ref)</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:khushalsagar@chromium.org">
+<style>
+iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 50vw;
+ height: 50vh;
+}
+
+body {
+ background: green;
+}
+</style>
+
+<iframe srcdoc="
+<style>
+body {
+ background: blue;
+}
+</style>
+"></iframe>
+
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-old-iframe.html b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-old-iframe.html
new file mode 100644
index 0000000000..bcdc566a26
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-old-iframe.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<title>View transitions: iframe and main frame transition at the same time</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:khushalsagar@chromium.org">
+<link rel="match" href="iframe-and-main-frame-transition-old-main-old-iframe-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<style>
+iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 50vw;
+ height: 50vh;
+}
+
+/* Keep showing the screenshot for the main frame */
+::view-transition-group(root) {
+ animation-duration: 300s;
+}
+::view-transition-old(root) {
+ animation: unset;
+ opacity: 1;
+}
+::view-transition-new(root) {
+ animation: unset;
+ opacity: 0;
+}
+</style>
+
+<iframe srcdoc="
+<style>
+ /* The iframe is showing an old screenshot */
+ ::view-transition-group(root) {
+ animation-duration: 300s;
+ }
+ ::view-transition-old(root) {
+ animation: unset;
+ opacity: 1;
+ }
+ ::view-transition-new(root) {
+ animation: unset;
+ opacity: 0;
+ }
+</style>
+<body></body>
+"></iframe>
+<script>
+ onload = runTest;
+
+ async function startTransition(document, oldColor, newColor) {
+ document.documentElement.style.background = oldColor;
+ await document.startViewTransition(() => {
+ document.documentElement.style.background = newColor;
+ }).ready;
+ }
+
+ async function runTest() {
+ const iframeDocument = document.querySelector("iframe").contentDocument;
+ await startTransition(iframeDocument, "blue", "lightblue");
+
+ await startTransition(document, "green", "lightgreen");
+
+ takeScreenshot();
+ }
+</script>
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-ref.html b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-ref.html
new file mode 100644
index 0000000000..9253bb5f21
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main-ref.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>View transitions: iframe and main frame transition at the same time (ref)</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:khushalsagar@chromium.org">
+<style>
+iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 50vw;
+ height: 50vh;
+}
+
+body {
+ background: green;
+}
+</style>
+
+<iframe srcdoc="
+<style>
+body {
+ background: orange;
+}
+</style>
+"></iframe>
+
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main.html b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main.html
new file mode 100644
index 0000000000..bd58368645
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-old-main.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<title>View transitions: iframe and main frame transition at the same time</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:khushalsagar@chromium.org">
+<link rel="match" href="iframe-and-main-frame-transition-old-main-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<style>
+iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 50vw;
+ height: 50vh;
+}
+
+/* The main frame is showing the old screenshot */
+::view-transition-group(root) {
+ animation-duration: 300s;
+}
+::view-transition-new(root) {
+ animation: unset;
+ opacity: 0;
+}
+::view-transition-old(root) {
+ animation: unset;
+ opacity: 1;
+}
+</style>
+
+<iframe srcdoc="
+<style>
+ body {
+ background: orange;
+ }
+</style>
+<body></body>"></iframe>
+<script>
+ onload = runTest;
+
+ async function startTransition(document, oldColor, newColor) {
+ document.documentElement.style.background = oldColor;
+ await document.startViewTransition(() => {
+ document.documentElement.style.background = newColor;
+ }).ready;
+ }
+
+ async function runTest() {
+ await startTransition(document, "green", "lightgreen");
+
+ // Start an iframe transition while the main frame transition is showing the
+ // old screenshot. This change shouldn't show up visually because the old
+ // screenshot on the main frame still has the iframe's old content.
+ const iframeDocument = document.querySelector("iframe").contentDocument;
+ await startTransition(iframeDocument, "blue", "lightblue");
+
+ takeScreenshot();
+ }
+</script>
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-with-name-on-iframe-ref.html b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-with-name-on-iframe-ref.html
new file mode 100644
index 0000000000..94bd3bdac3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-with-name-on-iframe-ref.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<title>View transitions: iframe and main frame transition at the same time with name on iframe (ref)</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:khushalsagar@chromium.org">
+<style>
+iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 50vw;
+ height: 50vh;
+ border: 1px solid orange;
+}
+
+body {
+ background: green;
+}
+</style>
+
+<iframe srcdoc="
+<style>
+body {
+ background: blue;
+}
+</style>
+"></iframe>
+
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-with-name-on-iframe.html b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-with-name-on-iframe.html
new file mode 100644
index 0000000000..f948e89dc7
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-and-main-frame-transition-with-name-on-iframe.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<title>View transitions: iframe and main frame transition at the same time with name on iframe</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:khushalsagar@chromium.org">
+<link rel="match" href="iframe-and-main-frame-transition-with-name-on-iframe-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<style>
+iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 50vw;
+ height: 50vh;
+ view-transition-name: inner;
+}
+
+.old {
+ border: 1px solid black;
+}
+
+.new {
+ border: 1px solid orange;
+}
+
+/* The main frame is showing the old screenshot for the root */
+::view-transition-group(root) {
+ animation-duration: 300s;
+}
+::view-transition-new(root) {
+ animation: unset;
+ opacity: 0;
+}
+::view-transition-old(root) {
+ animation: unset;
+ opacity: 1;
+}
+
+/* The iframe is showing the live screenshot */
+::view-transition-new(inner) {
+ animation: unset;
+ opacity: 1;
+}
+::view-transition-old(inner) {
+ animation: unset;
+ opacity: 0;
+}
+
+</style>
+
+<iframe id="inner" srcdoc="
+<style>
+ /* The iframe document itself is showing an old screenshot */
+ ::view-transition-group(root) {
+ animation-duration: 300s;
+ }
+ ::view-transition-new(root) {
+ animation: unset;
+ opacity: 0;
+ }
+ ::view-transition-old(root) {
+ animation: unset;
+ opacity: 1;
+ }
+</style>
+<body></body>"></iframe>
+<script>
+ onload = runTest;
+
+ async function startTransition(document, oldColor, newColor, nestedFrame) {
+ document.documentElement.style.background = oldColor;
+ if (nestedFrame != null)
+ nestedFrame.classList.add("old");
+
+ await document.startViewTransition(() => {
+ document.documentElement.style.background = newColor;
+ if (nestedFrame != null) {
+ nestedFrame.classList.remove("old");
+ nestedFrame.classList.add("new");
+ }
+ }).ready;
+ }
+
+ async function runTest() {
+ await startTransition(document, "green", "lightgreen", document.getElementById("inner"));
+
+ const iframeDocument = document.querySelector("iframe").contentDocument;
+ await startTransition(iframeDocument, "blue", "lightblue", null);
+
+ takeScreenshot();
+ }
+</script>
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-new-has-scrollbar.html b/testing/web-platform/tests/css/css-view-transitions/iframe-new-has-scrollbar.html
index 67a57bb885..81072ac38a 100644
--- a/testing/web-platform/tests/css/css-view-transitions/iframe-new-has-scrollbar.html
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-new-has-scrollbar.html
@@ -5,7 +5,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:bokan@chromium.org">
<link rel="match" href="iframe-new-has-scrollbar-ref.html">
- <meta name=fuzzy content="iframe-new-has-scrollbar-ref.html:0-80;0-500">
+ <meta name=fuzzy content="maxDifference=0-80; totalPixels=0-500">
<script src="/common/reftest-wait.js"></script>
<style>
iframe {
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-old-has-scrollbar.html b/testing/web-platform/tests/css/css-view-transitions/iframe-old-has-scrollbar.html
index 96dd75a3df..5d26633398 100644
--- a/testing/web-platform/tests/css/css-view-transitions/iframe-old-has-scrollbar.html
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-old-has-scrollbar.html
@@ -5,7 +5,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:bokan@chromium.org">
<link rel="match" href="iframe-old-has-scrollbar-ref.html">
- <meta name=fuzzy content="iframe-old-has-scrollbar-ref.html:0-80;0-500">
+ <meta name=fuzzy content="maxDifference=0-80; totalPixels=0-500">
<script src="/common/reftest-wait.js"></script>
<style>
iframe {
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-transition-destroyed-document-crash.html b/testing/web-platform/tests/css/css-view-transitions/iframe-transition-destroyed-document-crash.html
index 31f6a10ed6..13e743c3a1 100644
--- a/testing/web-platform/tests/css/css-view-transitions/iframe-transition-destroyed-document-crash.html
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-transition-destroyed-document-crash.html
@@ -1,18 +1,17 @@
<!DOCTYPE html>
-<html class=reftest-wait>
+<html class=test-wait>
<title>View transitions: crash test</title>
<link rel="help" href="https://www.w3.org/TR/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
-<script src="/common/reftest-wait.js"></script>
<html>
<head>
<script>
function eventhandler1() {
- var var00106 = htmlvar00011.contentDocument;
- var var00228 = var00106.startViewTransition();
+ var iframeDoc = iframe.contentDocument;
+ var viewTransition = iframeDoc.startViewTransition();
requestAnimationFrame(() => {
requestAnimationFrame(() => {
- requestAnimationFrame(takeScreenshot);
+ requestAnimationFrame(() => document.documentElement.classList.remove("test-wait"));
})
});
}
@@ -20,8 +19,7 @@ function eventhandler1() {
</script>
</head>
<body>
-<iframe id="htmlvar00011" onunload="eventhandler3()" border="0" srcdoc="A#:^;&lt;gV&lt;&gt;8" style=":{,J" referrerpolicy="unsafe-url" background="!xp&gt;" nohref="nohref" nonce="nonce" inputEncoding="s" offsetX="0.3538512271910753">:+j&amp;;&amp;-^&gt;.7xf\jZ1,xb</iframe>
-<style id="htmlvar00014" nonce="nonce" media="screen and (min-width:0px)" onerror="eventhandler1()" onload="eventhandler1()" dir="N5!" updateRangeEnd="0" abbr="4IvGMN[Wxd" symbols="=d##y#)DA4V8ya}KO.cv" frameBorder="^b*]&amp;:|#lB:" search="N">ynFXo*</style>
+<iframe id="iframe" onunload="eventhandler3()" border="0" srcdoc="A#:^;&lt;gV&lt;&gt;8" style=":{,J" referrerpolicy="unsafe-url" background="!xp&gt;" nohref="nohref" nonce="nonce" inputEncoding="s" offsetX="0.3538512271910753">:+j&amp;;&amp;-^&gt;.7xf\jZ1,xb</iframe>
+<style nonce="nonce" media="screen and (min-width:0px)" onerror="eventhandler1()" onload="eventhandler1()" dir="N5!" updateRangeEnd="0" abbr="4IvGMN[Wxd" symbols="=d##y#)DA4V8ya}KO.cv" frameBorder="^b*]&amp;:|#lB:" search="N">ynFXo*</style>
</body>
</html>
-
diff --git a/testing/web-platform/tests/css/css-view-transitions/iframe-transition.sub.html b/testing/web-platform/tests/css/css-view-transitions/iframe-transition.sub.html
index 8fa361b0fc..5f26a494b2 100644
--- a/testing/web-platform/tests/css/css-view-transitions/iframe-transition.sub.html
+++ b/testing/web-platform/tests/css/css-view-transitions/iframe-transition.sub.html
@@ -6,7 +6,7 @@
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="iframe-transition-ref.html">
<meta name="assert" content="Ensure that iframe root capture is sized and displayed correctly">
-<meta name=fuzzy content="iframe-transition-ref.html:0-200;0-200">
+<meta name=fuzzy content="maxDifference=0-200; totalPixels=0-200">
<script src="/common/reftest-wait.js"></script>
<style>
iframe { width: 500px; height: 500px }
diff --git a/testing/web-platform/tests/css/css-view-transitions/inline-with-offset-from-containing-block.html b/testing/web-platform/tests/css/css-view-transitions/inline-with-offset-from-containing-block.html
index 8640899814..026ecb240a 100644
--- a/testing/web-platform/tests/css/css-view-transitions/inline-with-offset-from-containing-block.html
+++ b/testing/web-platform/tests/css/css-view-transitions/inline-with-offset-from-containing-block.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="inline-with-offset-from-containing-block-ref.html">
-<meta name="fuzzy" content="inline-with-offset-from-containing-block-ref.html:0-255;0-1400">
+<meta name="fuzzy" content="maxDifference=0-255; totalPixels=0-1400">
<script src="/common/reftest-wait.js"></script>
<script src="/common/rendering-utils.js"></script>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-below-and-on-top-of-viewport-partially-onscreen-new.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-below-and-on-top-of-viewport-partially-onscreen-new.html
index 23f5fc22cf..65b14a6c4b 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-below-and-on-top-of-viewport-partially-onscreen-new.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-below-and-on-top-of-viewport-partially-onscreen-new.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-below-and-on-top-of-viewport-partially-onscreen-ref.html">
-<meta name="fuzzy" content="massive-element-below-and-on-top-of-viewport-partially-onscreen-ref.html:maxDifference=0-2;totalPixels=0-330">
+<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-330">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-below-and-on-top-of-viewport-partially-onscreen-old.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-below-and-on-top-of-viewport-partially-onscreen-old.html
index fbc8edadc0..b35222c24a 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-below-and-on-top-of-viewport-partially-onscreen-old.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-below-and-on-top-of-viewport-partially-onscreen-old.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-below-and-on-top-of-viewport-partially-onscreen-ref.html">
-<meta name="fuzzy" content="massive-element-below-and-on-top-of-viewport-partially-onscreen-ref.html:maxDifference=0-2;totalPixels=0-330">
+<meta name="fuzzy" content="maxDifference=0-2; totalPixels=0-700">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-offscreen-new.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-offscreen-new.html
index 611d4da21a..a122cd4b3b 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-offscreen-new.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-offscreen-new.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-below-viewport-offscreen-ref.html">
-<meta name="fuzzy" content="massive-element-below-viewport-offscreen-ref.html:maxDifference=0-3;totalPixels=0-950">
+<meta name="fuzzy" content="maxDifference=0-3;totalPixels=0-950">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-offscreen-old.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-offscreen-old.html
index bda3ebf1b1..567819fadf 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-offscreen-old.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-offscreen-old.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-below-viewport-offscreen-ref.html">
-<meta name="fuzzy" content="massive-element-below-viewport-offscreen-ref.html:maxDifference=0-2;totalPixels=0-445">
+<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-445">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-partially-onscreen-new.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-partially-onscreen-new.html
index e881e19622..42f97555f5 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-partially-onscreen-new.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-partially-onscreen-new.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-below-viewport-partially-onscreen-ref.html">
-<meta name="fuzzy" content="massive-element-below-viewport-partially-onscreen-ref.html:maxDifference=0-2;totalPixels=0-330">
+<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-330">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-partially-onscreen-old.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-partially-onscreen-old.html
index c8c3c53082..87b9a20795 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-partially-onscreen-old.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-below-viewport-partially-onscreen-old.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-below-viewport-partially-onscreen-ref.html">
-<meta name="fuzzy" content="massive-element-below-viewport-partially-onscreen-ref.html:maxDifference=0-2;totalPixels=0-445">
+<meta name="fuzzy" content="maxDifference=0-2; totalPixels=0-1600">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-offscreen-new.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-offscreen-new.html
index c8471032a4..97a3cb41ff 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-offscreen-new.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-offscreen-new.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-left-of-viewport-offscreen-ref.html">
-<meta name="fuzzy" content="massive-element-left-of-viewport-offscreen-ref.html:maxDifference=0-2;totalPixels=0-330">
+<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-330">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-offscreen-old.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-offscreen-old.html
index 04ab58f3aa..a41a738826 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-offscreen-old.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-offscreen-old.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-left-of-viewport-offscreen-ref.html">
-<meta name="fuzzy" content="massive-element-left-of-viewport-offscreen-ref.html:maxDifference=0-3;totalPixels=0-330">
+<meta name="fuzzy" content="maxDifference=0-3;totalPixels=0-330">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-partially-onscreen-new.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-partially-onscreen-new.html
index 15cc94ffe7..accd909158 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-partially-onscreen-new.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-partially-onscreen-new.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-left-of-viewport-partially-onscreen-ref.html">
-<meta name="fuzzy" content="massive-element-left-of-viewport-partially-onscreen-ref.html:maxDifference=0-2;totalPixels=0-330">
+<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-330">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-partially-onscreen-old.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-partially-onscreen-old.html
index 0d2aeec59d..e16806e8f3 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-partially-onscreen-old.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-left-of-viewport-partially-onscreen-old.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-left-of-viewport-partially-onscreen-ref.html">
-<meta name="fuzzy" content="massive-element-left-of-viewport-partially-onscreen-ref.html:maxDifference=0-3;totalPixels=0-330">
+<meta name="fuzzy" content="maxDifference=0-3;totalPixels=0-330">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-offscreen-new.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-offscreen-new.html
index 6ef8edd3b0..24edbc1ec7 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-offscreen-new.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-offscreen-new.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-on-top-of-viewport-offscreen-ref.html">
-<meta name="fuzzy" content="massive-element-on-top-of-viewport-offscreen-ref.html:maxDifference=0-6;totalPixels=0-920">
+<meta name="fuzzy" content="maxDifference=0-6;totalPixels=0-920">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-offscreen-old.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-offscreen-old.html
index 5e303e8286..8dfc8eefe1 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-offscreen-old.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-offscreen-old.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-on-top-of-viewport-offscreen-ref.html">
-<meta name="fuzzy" content="massive-element-on-top-of-viewport-offscreen-ref.html:maxDifference=0-3;totalPixels=0-330">
+<meta name="fuzzy" content="maxDifference=0-3;totalPixels=0-330">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-partially-onscreen-new.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-partially-onscreen-new.html
index a9e5f5842a..c301e47099 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-partially-onscreen-new.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-partially-onscreen-new.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-on-top-of-viewport-partially-onscreen-ref.html">
-<meta name="fuzzy" content="massive-element-on-top-of-viewport-partially-onscreen-ref.html:maxDifference=0-2;totalPixels=0-330">
+<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-330">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-partially-onscreen-old.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-partially-onscreen-old.html
index 41dc622914..1f810af745 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-partially-onscreen-old.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-on-top-of-viewport-partially-onscreen-old.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-on-top-of-viewport-partially-onscreen-ref.html">
-<meta name="fuzzy" content="massive-element-on-top-of-viewport-partially-onscreen-ref.html:maxDifference=0-3;totalPixels=0-330">
+<meta name="fuzzy" content="maxDifference=0-3;totalPixels=0-330">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-right-and-left-of-viewport-partially-onscreen-new.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-right-and-left-of-viewport-partially-onscreen-new.html
index 719701fe88..426751d093 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-right-and-left-of-viewport-partially-onscreen-new.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-right-and-left-of-viewport-partially-onscreen-new.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-right-and-left-of-viewport-partially-onscreen-ref.html">
-<meta name="fuzzy" content="massive-element-right-and-left-of-viewport-partially-onscreen-ref.html:maxDifference=0-2;totalPixels=0-330">
+<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-330">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-right-and-left-of-viewport-partially-onscreen-old.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-right-and-left-of-viewport-partially-onscreen-old.html
index e8eeec3f26..ea10e2471b 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-right-and-left-of-viewport-partially-onscreen-old.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-right-and-left-of-viewport-partially-onscreen-old.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-right-and-left-of-viewport-partially-onscreen-ref.html">
-<meta name="fuzzy" content="massive-element-right-and-left-of-viewport-partially-onscreen-ref.html:maxDifference=0-2;totalPixels=0-330">
+<meta name="fuzzy" content="maxDifference=0-2; totalPixels=0-700">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-offscreen-new.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-offscreen-new.html
index 89d00a53a8..53749f29b2 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-offscreen-new.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-offscreen-new.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-right-of-viewport-offscreen-ref.html">
-<meta name="fuzzy" content="massive-element-right-of-viewport-offscreen-ref.html:maxDifference=0-2;totalPixels=0-445">
+<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-445">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-offscreen-old.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-offscreen-old.html
index 04247af18e..b5337c4491 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-offscreen-old.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-offscreen-old.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-right-of-viewport-offscreen-ref.html">
-<meta name="fuzzy" content="massive-element-right-of-viewport-offscreen-ref.html:maxDifference=0-3;totalPixels=0-445">
+<meta name="fuzzy" content="maxDifference=0-3;totalPixels=0-445">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-partially-onscreen-new.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-partially-onscreen-new.html
index a7b599e5eb..ac3fe48a0a 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-partially-onscreen-new.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-partially-onscreen-new.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-right-of-viewport-partially-onscreen-ref.html">
-<meta name="fuzzy" content="massive-element-right-of-viewport-partially-onscreen-ref.html:maxDifference=0-2;totalPixels=0-330">
+<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-330">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-partially-onscreen-old.html b/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-partially-onscreen-old.html
index 2498f2e1f1..2626910d78 100644
--- a/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-partially-onscreen-old.html
+++ b/testing/web-platform/tests/css/css-view-transitions/massive-element-right-of-viewport-partially-onscreen-old.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="massive-element-right-of-viewport-partially-onscreen-ref.html">
-<meta name="fuzzy" content="massive-element-right-of-viewport-partially-onscreen-ref.html:maxDifference=0-3;totalPixels=0-445">
+<meta name="fuzzy" content="maxDifference=0-3;totalPixels=0-445">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/multiline-span-with-overflowing-text-and-box-decorations.html b/testing/web-platform/tests/css/css-view-transitions/multiline-span-with-overflowing-text-and-box-decorations.html
index e166b3c9df..8fff184b2a 100644
--- a/testing/web-platform/tests/css/css-view-transitions/multiline-span-with-overflowing-text-and-box-decorations.html
+++ b/testing/web-platform/tests/css/css-view-transitions/multiline-span-with-overflowing-text-and-box-decorations.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="multiline-span-with-overflowing-text-and-box-decorations-ref.html">
-<meta name="fuzzy" content="multiline-span-with-overflowing-text-and-box-decorations-ref.html:maxDifference=0-3;totalPixels=0-4900">
+<meta name="fuzzy" content="maxDifference=0-3; totalPixels=0-4900">
<script src="/common/reftest-wait.js"></script>
diff --git a/testing/web-platform/tests/css/css-view-transitions/new-and-old-sizes-match.html b/testing/web-platform/tests/css/css-view-transitions/new-and-old-sizes-match.html
index 094d6963bf..70b6515fb5 100644
--- a/testing/web-platform/tests/css/css-view-transitions/new-and-old-sizes-match.html
+++ b/testing/web-platform/tests/css/css-view-transitions/new-and-old-sizes-match.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="new-and-old-sizes-match-ref.html">
-<meta name="fuzzy" content="new-and-old-sizes-match-ref.html:0-1;0-300">
+<meta name="fuzzy" content="maxDifference=0-1; totalPixels=0-300">
<script src="/common/reftest-wait.js"></script>
<style>
.box {
diff --git a/testing/web-platform/tests/css/css-view-transitions/new-content-ancestor-clipped-ref.html b/testing/web-platform/tests/css/css-view-transitions/new-content-ancestor-clipped-ref.html
new file mode 100644
index 0000000000..caa99f2807
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/new-content-ancestor-clipped-ref.html
@@ -0,0 +1,34 @@
+<!doctype HTML>
+<html>
+<head>
+<style>
+html {
+ background: lightpink;
+}
+div {
+ position: relative;
+ width: 200px;
+ height: 200px;
+}
+
+.outer {
+ background-color: blue;
+ box-shadow: -50px -50px 0px 0px rgba(0,0,0,1);
+ left: 100px;
+ top: 100px;
+}
+
+.inner {
+ background-color: red;
+ left: 50px;
+ top: 50px;
+}
+
+</style>
+</head>
+<body>
+</body>
+<div class="outer">
+ <div class="inner"></div>
+</div>
+</html>
diff --git a/testing/web-platform/tests/css/css-view-transitions/new-content-ancestor-clipped.html b/testing/web-platform/tests/css/css-view-transitions/new-content-ancestor-clipped.html
new file mode 100644
index 0000000000..69a8de5f52
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/new-content-ancestor-clipped.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<title>View transitions: capture opacity elements</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:mattwoodrow@apple.com">
+<link rel="match" href="new-content-ancestor-clipped-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<style>
+.outer {
+ background-color: blue;
+ overflow: hidden;
+ box-shadow: -50px -50px 0px 0px rgba(0,0,0,1);
+ position: relative;
+ left: 100px;
+ top: 100px;
+ width: 200px;
+ height: 200px;
+ view-transition-name: outer;
+}
+.inner {
+ background-color: red;
+ position: relative;
+ left: 50px;
+ top: 50px;
+ width: 200px;
+ height: 200px;
+ view-transition-name: inner;
+}
+/* We're verifying what we capture, so just display the new contents for 5 minutes. */
+html::view-transition-group(*) { animation-duration: 300s; }
+html::view-transition-new(*) { animation: unset; opacity: 1; }
+html::view-transition-old(*) { animation: unset; opacity: 0; }
+/* hide the root so we show transition background to ensure we're in a transition */
+html::view-transition-group(root) { animation: unset; opacity: 0; }
+html::view-transition { background: lightpink; }
+</style>
+<div class="outer">
+ <div class="inner"></div>
+</div>
+<script>
+failIfNot(document.startViewTransition, "Missing document.startViewTransition");
+
+async function runTest() {
+ let t = document.startViewTransition(() => {
+ requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
+ });
+}
+onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest));
+</script>
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/new-content-captures-clip-path.html b/testing/web-platform/tests/css/css-view-transitions/new-content-captures-clip-path.html
index 702bb09bc3..19af2493d0 100644
--- a/testing/web-platform/tests/css/css-view-transitions/new-content-captures-clip-path.html
+++ b/testing/web-platform/tests/css/css-view-transitions/new-content-captures-clip-path.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="new-content-captures-clip-path-ref.html">
-<meta name="fuzzy" content="new-content-captures-clip-path-ref.html:0-1;0-500">
+<meta name="fuzzy" content="maxDifference=0-1; totalPixels=0-500">
<script src="/common/reftest-wait.js"></script>
<style>
.box {
diff --git a/testing/web-platform/tests/css/css-view-transitions/new-content-captures-different-size.html b/testing/web-platform/tests/css/css-view-transitions/new-content-captures-different-size.html
index 740199675d..18f323c320 100644
--- a/testing/web-platform/tests/css/css-view-transitions/new-content-captures-different-size.html
+++ b/testing/web-platform/tests/css/css-view-transitions/new-content-captures-different-size.html
@@ -5,7 +5,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="new-content-captures-different-size-ref.html">
-<meta name=fuzzy content="new-content-captures-different-size-ref.html:0-40;0-30000">
+<meta name=fuzzy content="maxDifference=0-40; totalPixels=0-30000">
<script src="/common/reftest-wait.js"></script>
<style>
.box {
diff --git a/testing/web-platform/tests/css/css-view-transitions/new-content-captures-spans.html b/testing/web-platform/tests/css/css-view-transitions/new-content-captures-spans.html
index 94bef1d6dd..5f4807404c 100644
--- a/testing/web-platform/tests/css/css-view-transitions/new-content-captures-spans.html
+++ b/testing/web-platform/tests/css/css-view-transitions/new-content-captures-spans.html
@@ -4,6 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="new-content-captures-spans-ref.html">
+<meta name="fuzzy" content="maxDifference=0-1; totalPixels=0-400">
<script src="/common/reftest-wait.js"></script>
<style>
span {
diff --git a/testing/web-platform/tests/css/css-view-transitions/new-content-changes-overflow-left-ref.html b/testing/web-platform/tests/css/css-view-transitions/new-content-changes-overflow-left-ref.html
new file mode 100644
index 0000000000..e86e5197da
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/new-content-changes-overflow-left-ref.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>View transitions: capture elements and then change overflow (ref)</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<style>
+body { background: pink }
+#target {
+ position: relative;
+ background: green;
+ left: 10px;
+ width: 100px;
+ height: 100px;
+ view-transition-name: target;
+}
+#target.toggle {
+ outline: 300px solid transparent;
+}
+</style>
+
+<div id=target class=toggle></div>
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/new-content-changes-overflow-left.html b/testing/web-platform/tests/css/css-view-transitions/new-content-changes-overflow-left.html
new file mode 100644
index 0000000000..e362dce76a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/new-content-changes-overflow-left.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<html class=reftest-wait>
+<title>View transitions: capture elements and then change overflow</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:vmpstr@chromium.org">
+<link rel="author" href="mailto:mattwoodrow@apple.com">
+<link rel="match" href="new-content-changes-overflow-left-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<style>
+#target {
+ position: relative;
+ background: green;
+ left: 10px;
+ width: 100px;
+ height: 100px;
+ view-transition-name: target;
+}
+#target.toggle {
+ outline: 300px solid transparent;
+}
+
+html::view-transition-group(*) { animation-duration: 300s; }
+html::view-transition-new(*) { animation: unset; opacity: 1; }
+html::view-transition-old(*) { animation: unset; opacity: 0; }
+html::view-transition-group(root) { animation: unset; opacity: 0; }
+html::view-transition { background: pink; }
+</style>
+
+<div id=target></div>
+<script>
+
+async function runTest() {
+ document.startViewTransition().ready.then(() => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ target.classList.add("toggle");
+ requestAnimationFrame(takeScreenshot);
+ });
+ });
+ });
+}
+onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest));
+</script>
+
+
diff --git a/testing/web-platform/tests/css/css-view-transitions/new-content-has-scrollbars.html b/testing/web-platform/tests/css/css-view-transitions/new-content-has-scrollbars.html
index 3246a7e76f..834d0e8f45 100644
--- a/testing/web-platform/tests/css/css-view-transitions/new-content-has-scrollbars.html
+++ b/testing/web-platform/tests/css/css-view-transitions/new-content-has-scrollbars.html
@@ -6,7 +6,7 @@
<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7859">
<link rel="author" href="mailto:bokan@chromium.org">
<link rel="match" href="new-content-has-scrollbars-ref.html">
-<meta name=fuzzy content="new-content-has-scrollbars-ref.html:0-40;0-30000">
+<meta name=fuzzy content="maxDifference=0-40; totalPixels=0-30000">
<script src="/common/reftest-wait.js"></script>
<style>
html, body {
diff --git a/testing/web-platform/tests/css/css-view-transitions/new-content-is-inline.html b/testing/web-platform/tests/css/css-view-transitions/new-content-is-inline.html
index 46c96acb04..81b261a9bf 100644
--- a/testing/web-platform/tests/css/css-view-transitions/new-content-is-inline.html
+++ b/testing/web-platform/tests/css/css-view-transitions/new-content-is-inline.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://www.w3.org/TR/css-view-transitions-1/">
<link rel="author" href="mailto:bokan@chromium.org">
<link rel="match" href="new-content-is-inline-ref.html">
-<meta name="fuzzy" content="new-content-is-inline-ref.html:0-255;0-1000">
+<meta name="fuzzy" content="maxDifference=0-255; totalPixels=0-1000">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/new-content-object-fit-fill.html b/testing/web-platform/tests/css/css-view-transitions/new-content-object-fit-fill.html
index 04a80409c5..a610d68802 100644
--- a/testing/web-platform/tests/css/css-view-transitions/new-content-object-fit-fill.html
+++ b/testing/web-platform/tests/css/css-view-transitions/new-content-object-fit-fill.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="content-object-fit-fill-ref.html">
-<meta name="fuzzy" content="content-object-fit-fill-ref.html:0-60;0-20">
+<meta name="fuzzy" content="maxDifference=0-60; totalPixels=0-20">
<script src="/common/reftest-wait.js"></script>
<style>
#target {
diff --git a/testing/web-platform/tests/css/css-view-transitions/new-content-scaling.html b/testing/web-platform/tests/css/css-view-transitions/new-content-scaling.html
index bccb760fb5..376b7fd11f 100644
--- a/testing/web-platform/tests/css/css-view-transitions/new-content-scaling.html
+++ b/testing/web-platform/tests/css/css-view-transitions/new-content-scaling.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="new-content-scaling-ref.html">
-<meta name="fuzzy" content="new-content-scaling-ref.html:maxDifference=0-16;totalPixels=0-400">
+<meta name="fuzzy" content="maxDifference=0-16; totalPixels=0-400">
<script src="/common/reftest-wait.js"></script>
<style>
.shared {
diff --git a/testing/web-platform/tests/css/css-view-transitions/object-view-box-old-image.html b/testing/web-platform/tests/css/css-view-transitions/object-view-box-old-image.html
index 335e48fa70..793dfd46c9 100644
--- a/testing/web-platform/tests/css/css-view-transitions/object-view-box-old-image.html
+++ b/testing/web-platform/tests/css/css-view-transitions/object-view-box-old-image.html
@@ -5,7 +5,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="object-view-box-ref.html">
-<meta name="fuzzy" content="object-view-box-ref.html:0-1;0-300">
+<meta name="fuzzy" content="maxDifference=0-1; totalPixels=0-300">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/old-content-captures-clip-path.html b/testing/web-platform/tests/css/css-view-transitions/old-content-captures-clip-path.html
index 7ed5e1ca15..67aa5bf32f 100644
--- a/testing/web-platform/tests/css/css-view-transitions/old-content-captures-clip-path.html
+++ b/testing/web-platform/tests/css/css-view-transitions/old-content-captures-clip-path.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="old-content-captures-clip-path-ref.html">
-<meta name="fuzzy" content="old-content-captures-clip-path-ref.html:0-1;0-500">
+<meta name="fuzzy" content="maxDifference=0-1; totalPixels=0-500">
<script src="/common/reftest-wait.js"></script>
<style>
.box {
diff --git a/testing/web-platform/tests/css/css-view-transitions/old-content-captures-different-size.html b/testing/web-platform/tests/css/css-view-transitions/old-content-captures-different-size.html
index 392247bc95..7f3be742b0 100644
--- a/testing/web-platform/tests/css/css-view-transitions/old-content-captures-different-size.html
+++ b/testing/web-platform/tests/css/css-view-transitions/old-content-captures-different-size.html
@@ -5,7 +5,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="old-content-captures-different-size-ref.html">
-<meta name=fuzzy content="old-content-captures-different-size-ref.html:0-40;0-30000">
+<meta name=fuzzy content="maxDifference=0-40; totalPixels=0-30000">
<script src="/common/reftest-wait.js"></script>
<style>
.box {
diff --git a/testing/web-platform/tests/css/css-view-transitions/old-content-captures-opacity.html b/testing/web-platform/tests/css/css-view-transitions/old-content-captures-opacity.html
index cd71c9dfaf..97b8911644 100644
--- a/testing/web-platform/tests/css/css-view-transitions/old-content-captures-opacity.html
+++ b/testing/web-platform/tests/css/css-view-transitions/old-content-captures-opacity.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="old-content-captures-opacity-ref.html">
-<meta name=fuzzy content="old-content-captures-opacity-ref.html:0-1;0-50000">
+<meta name=fuzzy content="maxDifference=0-1; totalPixels=0-50000">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/old-content-captures-root.html b/testing/web-platform/tests/css/css-view-transitions/old-content-captures-root.html
index 51a22bc136..a1cb332942 100644
--- a/testing/web-platform/tests/css/css-view-transitions/old-content-captures-root.html
+++ b/testing/web-platform/tests/css/css-view-transitions/old-content-captures-root.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="old-content-captures-root-ref.html">
-<meta name="fuzzy" content="old-content-captures-root-ref.html:0-1;0-500">
+<meta name="fuzzy" content="maxDifference=0-1; totalPixels=0-500">
<script src="/common/reftest-wait.js"></script>
<style>
.box {
diff --git a/testing/web-platform/tests/css/css-view-transitions/old-content-has-scrollbars.html b/testing/web-platform/tests/css/css-view-transitions/old-content-has-scrollbars.html
index 13e26c702d..b9638ebb10 100644
--- a/testing/web-platform/tests/css/css-view-transitions/old-content-has-scrollbars.html
+++ b/testing/web-platform/tests/css/css-view-transitions/old-content-has-scrollbars.html
@@ -6,7 +6,7 @@
<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7859">
<link rel="author" href="mailto:bokan@chromium.org">
<link rel="match" href="old-content-has-scrollbars-ref.html">
-<meta name=fuzzy content="old-content-has-scrollbars-ref.html:0-40;0-30000">
+<meta name=fuzzy content="maxDifference=0-40; totalPixels=0-30000">
<script src="/common/reftest-wait.js"></script>
<style>
html, body {
diff --git a/testing/web-platform/tests/css/css-view-transitions/old-content-is-inline.html b/testing/web-platform/tests/css/css-view-transitions/old-content-is-inline.html
index 70ff67c0e0..3333a07a02 100644
--- a/testing/web-platform/tests/css/css-view-transitions/old-content-is-inline.html
+++ b/testing/web-platform/tests/css/css-view-transitions/old-content-is-inline.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://www.w3.org/TR/css-view-transitions-1/">
<link rel="author" href="mailto:bokan@chromium.org">
<link rel="match" href="old-content-is-inline-ref.html">
-<meta name="fuzzy" content="old-content-is-inline-ref.html:0-255;0-500">
+<meta name="fuzzy" content="maxDifference=0-255; totalPixels=0-500">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/old-content-object-fit-fill.html b/testing/web-platform/tests/css/css-view-transitions/old-content-object-fit-fill.html
index 0652b30a07..51023fa27b 100644
--- a/testing/web-platform/tests/css/css-view-transitions/old-content-object-fit-fill.html
+++ b/testing/web-platform/tests/css/css-view-transitions/old-content-object-fit-fill.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="content-object-fit-fill-ref.html">
-<meta name="fuzzy" content="content-object-fit-fill-ref.html:0-60;0-20">
+<meta name="fuzzy" content="maxDifference=0-60; totalPixels=0-20">
<script src="/common/reftest-wait.js"></script>
<style>
#target {
diff --git a/testing/web-platform/tests/css/css-view-transitions/old-content-object-view-box-clip-path-reference.html b/testing/web-platform/tests/css/css-view-transitions/old-content-object-view-box-clip-path-reference.html
index 5e6969d9cc..827ca027dd 100644
--- a/testing/web-platform/tests/css/css-view-transitions/old-content-object-view-box-clip-path-reference.html
+++ b/testing/web-platform/tests/css/css-view-transitions/old-content-object-view-box-clip-path-reference.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="old-content-object-view-box-clip-path-reference-ref.html">
-<meta name="fuzzy" content="old-content-object-view-box-clip-path-reference-ref.html:0-1;0-100">
+<meta name="fuzzy" content="maxDifference=0-1; totalPixels=0-100">
<script src="/common/reftest-wait.js"></script>
<style>
.target {
diff --git a/testing/web-platform/tests/css/css-view-transitions/old-content-object-view-box-clip-path.html b/testing/web-platform/tests/css/css-view-transitions/old-content-object-view-box-clip-path.html
index f894555154..d0d26b9790 100644
--- a/testing/web-platform/tests/css/css-view-transitions/old-content-object-view-box-clip-path.html
+++ b/testing/web-platform/tests/css/css-view-transitions/old-content-object-view-box-clip-path.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="old-content-object-view-box-clip-path-ref.html">
-<meta name="fuzzy" content="old-content-object-view-box-clip-path-ref.html:0-1;0-30">
+<meta name="fuzzy" content="maxDifference=0-1; totalPixels=0-30">
<script src="/common/reftest-wait.js"></script>
<style>
.target {
diff --git a/testing/web-platform/tests/css/css-view-transitions/pseudo-element-overflow-hidden-ref.html b/testing/web-platform/tests/css/css-view-transitions/pseudo-element-overflow-hidden-ref.html
new file mode 100644
index 0000000000..02bcb5bb49
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/pseudo-element-overflow-hidden-ref.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html style="background:pink">
+<title>View transitions: overflow:hidden is respected on pseudo elements</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<style>
+body {
+ margin: 0px;
+}
+div {
+ width: 200px;
+ height: 200px;
+}
+#target {
+ position: absolute;
+ width: 200px;
+ height: 200px;
+ background: green;
+ overflow: hidden;
+}
+#inner {
+ position: relative;
+ left: 100px;
+ top: 100px;
+ background: blue;
+}
+.offset {
+ left: 400px;
+}
+</style>
+
+<div id="target"><div id="inner"></div></div>
+<div id="target" class="offset"><div id="inner"></div></div>
+</html>
diff --git a/testing/web-platform/tests/css/css-view-transitions/pseudo-element-overflow-hidden.html b/testing/web-platform/tests/css/css-view-transitions/pseudo-element-overflow-hidden.html
new file mode 100644
index 0000000000..e40df4f6a8
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/pseudo-element-overflow-hidden.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<title>View transitions: overflow:hidden is respected on pseudo elements</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:mattwoodrow@apple.com">
+<link rel="match" href="pseudo-element-overflow-hidden-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<style>
+body {
+ margin: 0px;
+}
+div {
+ width: 200px;
+ height: 200px;
+}
+#target {
+ width: 200px;
+ height: 200px;
+ background: green;
+ view-transition-name: target;
+}
+#inner {
+ position: relative;
+ left: 100px;
+ top: 100px;
+ background: blue;
+}
+
+/* We're verifying what we capture, so just display both of the captures for 5 minutes. */
+html::view-transition-group(*) { animation-duration: 300s; }
+html::view-transition-new(*) { animation: unset; opacity: 1; }
+html::view-transition-old(*) { animation: unset; opacity: 1; }
+/* hide the root so we show transition background to ensure we're in a transition */
+html::view-transition-group(root) { animation: unset; opacity: 0; }
+html::view-transition { background: pink; }
+
+html::view-transition-new(target) {
+ overflow:hidden;
+}
+html::view-transition-old(target) {
+ left: 400px;
+ overflow: hidden;
+}
+</style>
+
+<div id="target"><div id="inner"></div></div>
+<script>
+failIfNot(document.startViewTransition, "Missing document.startViewTransition");
+
+async function runTest() {
+ let t = document.startViewTransition();
+ t.ready.then(takeScreenshot);
+}
+onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest));
+</script>
+
+</html>
diff --git a/testing/web-platform/tests/css/css-view-transitions/pseudo-element-preserve-3d-ref.html b/testing/web-platform/tests/css/css-view-transitions/pseudo-element-preserve-3d-ref.html
new file mode 100644
index 0000000000..1eefed24b3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/pseudo-element-preserve-3d-ref.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<title>View transitions: transform-style: preserve-3d is respected on pseudo elements</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+
+<style>
+body {
+ background: pink;
+}
+div {
+ width: 200px;
+ height: 200px;
+ background: green;
+}
+
+</style>
+
+<div id="target"></div>
+
+</html>
diff --git a/testing/web-platform/tests/css/css-view-transitions/pseudo-element-preserve-3d.html b/testing/web-platform/tests/css/css-view-transitions/pseudo-element-preserve-3d.html
new file mode 100644
index 0000000000..474f743e1c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-view-transitions/pseudo-element-preserve-3d.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<title>View transitions: transform-style: preserve-3d is respected on pseudo elements</title>
+<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
+<link rel="author" href="mailto:mattwoodrow@apple.com">
+<link rel="match" href="pseudo-element-preserve-3d-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<style>
+div {
+ width: 200px;
+ height: 200px;
+ background: green;
+ view-transition-name: target;
+}
+
+/* We're verifying what we capture, so just display the old contents for 5 minutes. */
+html::view-transition-group(*) { animation-duration: 300s; }
+html::view-transition-group(target) { background: green; }
+html::view-transition-new(*) { animation: unset; opacity: 0; }
+html::view-transition-old(*) { animation: unset; opacity: 1; }
+/* hide the root so we show transition background to ensure we're in a transition */
+html::view-transition-group(root) { animation: unset; opacity: 0; }
+html::view-transition { background: pink; }
+html::view-transition-image-pair(target) {
+ transform: rotateX(90deg);
+ transform-style: preserve-3d;
+}
+html::view-transition-old(target) {
+ transform: rotateX(90deg);
+}
+</style>
+
+<div id="target"></div>
+<script>
+failIfNot(document.startViewTransition, "Missing document.startViewTransition");
+
+async function runTest() {
+ let t = document.startViewTransition();
+ t.ready.then(takeScreenshot);
+}
+onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest));
+</script>
+
+</html>
diff --git a/testing/web-platform/tests/css/css-view-transitions/pseudo-rendering-invalidation.html b/testing/web-platform/tests/css/css-view-transitions/pseudo-rendering-invalidation.html
index e1f1718618..4d492c40d8 100644
--- a/testing/web-platform/tests/css/css-view-transitions/pseudo-rendering-invalidation.html
+++ b/testing/web-platform/tests/css/css-view-transitions/pseudo-rendering-invalidation.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:bokan@chromium.org">
<link rel="match" href="pseudo-rendering-invalidation-ref.html">
-<meta name="fuzzy" content="pseudo-rendering-invalidation-ref.html:0-20;0-300">
+<meta name="fuzzy" content="maxDifference=0-20; totalPixels=0-300">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/pseudo-with-classes-match-wildard.html b/testing/web-platform/tests/css/css-view-transitions/pseudo-with-classes-match-wildcard.html
index 13490d5878..13490d5878 100644
--- a/testing/web-platform/tests/css/css-view-transitions/pseudo-with-classes-match-wildard.html
+++ b/testing/web-platform/tests/css/css-view-transitions/pseudo-with-classes-match-wildcard.html
diff --git a/testing/web-platform/tests/css/css-view-transitions/root-captured-as-different-tag.html b/testing/web-platform/tests/css/css-view-transitions/root-captured-as-different-tag.html
index a4d6f11ad4..1d4d1610d1 100644
--- a/testing/web-platform/tests/css/css-view-transitions/root-captured-as-different-tag.html
+++ b/testing/web-platform/tests/css/css-view-transitions/root-captured-as-different-tag.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="old-content-captures-root-ref.html">
-<meta name="fuzzy" content="old-content-captures-root-ref.html:0-1;0-500">
+<meta name="fuzzy" content="maxDifference=0-1; totalPixels=0-500">
<script src="/common/reftest-wait.js"></script>
<style>
:root { view-transition-name: another-root; }
diff --git a/testing/web-platform/tests/css/css-view-transitions/root-element-display-none-during-transition-crash.html b/testing/web-platform/tests/css/css-view-transitions/root-element-display-none-during-transition-crash.html
index b9c384d94a..d67bb256fd 100644
--- a/testing/web-platform/tests/css/css-view-transitions/root-element-display-none-during-transition-crash.html
+++ b/testing/web-platform/tests/css/css-view-transitions/root-element-display-none-during-transition-crash.html
@@ -1,10 +1,9 @@
<!DOCTYPE html>
-<html class=reftest-wait>
+<html class=test-wait>
<title>View transitions: entry animation from root display none</title>
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
-<script src="/common/reftest-wait.js"></script>
<style>
.hidden {
display: none;
@@ -15,16 +14,14 @@
</style>
<script>
-failIfNot(document.startViewTransition, "Missing document.startViewTransition");
-
async function runTest() {
transition = document.startViewTransition();
- transition.ready.then(
+ transition.ready.then(() => {
requestAnimationFrame(() => {
document.documentElement.classList.toggle("hidden");
- }));
- transition.finished.then(takeScreenshot);
+ });
+ });
+ transition.finished.then(() => document.documentElement.classList.remove("test-wait"));
}
onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest));
</script>
-
diff --git a/testing/web-platform/tests/css/css-view-transitions/root-scrollbar-with-fixed-background.html b/testing/web-platform/tests/css/css-view-transitions/root-scrollbar-with-fixed-background.html
index 2fa0132727..3c3429412b 100644
--- a/testing/web-platform/tests/css/css-view-transitions/root-scrollbar-with-fixed-background.html
+++ b/testing/web-platform/tests/css/css-view-transitions/root-scrollbar-with-fixed-background.html
@@ -4,6 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="root-scrollbar-with-fixed-background-ref.html">
+<meta name="fuzzy" content="maxDifference=0-2; totalPixels=0-4500">
<script src="/common/rendering-utils.js"></script>
<script src="/common/reftest-wait.js"></script>
diff --git a/testing/web-platform/tests/css/css-view-transitions/root-to-shared-animation-incoming-ref.html b/testing/web-platform/tests/css/css-view-transitions/root-to-shared-animation-incoming-ref.html
index df0ec1a9d8..6044ba2636 100644
--- a/testing/web-platform/tests/css/css-view-transitions/root-to-shared-animation-incoming-ref.html
+++ b/testing/web-platform/tests/css/css-view-transitions/root-to-shared-animation-incoming-ref.html
@@ -10,4 +10,4 @@ body {
padding: 0;
margin: 0;
}
-
+</style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/rotated-cat-off-top-edge.html b/testing/web-platform/tests/css/css-view-transitions/rotated-cat-off-top-edge.html
index c7179b7a01..f61b916caa 100644
--- a/testing/web-platform/tests/css/css-view-transitions/rotated-cat-off-top-edge.html
+++ b/testing/web-platform/tests/css/css-view-transitions/rotated-cat-off-top-edge.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="rotated-cat-off-top-edge-ref.html">
-<meta name="fuzzy" content="rotated-cat-off-top-edge-ref.html:0-5;0-1500">
+<meta name="fuzzy" content="maxDifference=0-70; totalPixels=0-22000">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/scroller-child-abspos.html b/testing/web-platform/tests/css/css-view-transitions/scroller-child-abspos.html
index 3b94ffa7bb..d04f87215e 100644
--- a/testing/web-platform/tests/css/css-view-transitions/scroller-child-abspos.html
+++ b/testing/web-platform/tests/css/css-view-transitions/scroller-child-abspos.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://www.w3.org/TR/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="scroller-child-abspos-ref.html">
-<meta name="fuzzy" content="scroller-child-abspos-ref.html:0-5;0-800">
+<meta name="fuzzy" content="maxDifference=0-5; totalPixels=0-800">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/scroller-child.html b/testing/web-platform/tests/css/css-view-transitions/scroller-child.html
index 5cb2f03e70..7d4368fe57 100644
--- a/testing/web-platform/tests/css/css-view-transitions/scroller-child.html
+++ b/testing/web-platform/tests/css/css-view-transitions/scroller-child.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://www.w3.org/TR/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="scroller-child-ref.html">
-<meta name="fuzzy" content="scroller-child-ref.html:0-5;0-800">
+<meta name="fuzzy" content="maxDifference=0-5; totalPixels=0-800">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/scroller.html b/testing/web-platform/tests/css/css-view-transitions/scroller.html
index e61d13b316..9d82046891 100644
--- a/testing/web-platform/tests/css/css-view-transitions/scroller.html
+++ b/testing/web-platform/tests/css/css-view-transitions/scroller.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://www.w3.org/TR/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="scroller-ref.html">
-<meta name="fuzzy" content="scroller-ref.html:0-5;0-10">
+<meta name="fuzzy" content="maxDifference=0-5; totalPixels=0-10">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/set-current-time.html b/testing/web-platform/tests/css/css-view-transitions/set-current-time.html
index f7e802d79b..efe561c6ea 100644
--- a/testing/web-platform/tests/css/css-view-transitions/set-current-time.html
+++ b/testing/web-platform/tests/css/css-view-transitions/set-current-time.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://www.w3.org/TR/css-view-transitions-1/">
<link rel="author" href="mailto:vmpstr@chromium.org">
<link rel="match" href="set-current-time-ref.html">
-<meta name="fuzzy" content="set-current-time-ref.html:0-2;0-40000">
+<meta name="fuzzy" content="maxDifference=0-2; totalPixels=0-40000">
<script src="/common/reftest-wait.js"></script>
<style>
:root { view-transition-name: unset; }
diff --git a/testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-absolute.html b/testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-absolute.html
index 4a619f29cc..91b71f7eec 100644
--- a/testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-absolute.html
+++ b/testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-absolute.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:bokan@chromium.org">
<link rel="match" href="snapshot-containing-block-absolute-ref.html">
-<meta name="fuzzy" content="snapshot-containing-block-absolute-ref.html:0-20;0-100">
+<meta name="fuzzy" content="maxDifference=0-20; totalPixels=0-100">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-includes-scrollbar-gutter.html b/testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-includes-scrollbar-gutter.html
index 8e47a056bd..40feea46ef 100644
--- a/testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-includes-scrollbar-gutter.html
+++ b/testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-includes-scrollbar-gutter.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:bokan@chromium.org">
<link rel="match" href="snapshot-containing-block-includes-scrollbar-gutter-ref.html">
-<meta name="fuzzy" content="snapshot-containing-block-includes-scrollbar-gutter-ref.html:0-20;0-100">
+<meta name="fuzzy" content="maxDifference=0-20; totalPixels=0-100">
<script src="/common/reftest-wait.js"></script>
<style>
:root {
diff --git a/testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-static.html b/testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-static.html
index fc0c7033c9..44fe4cfc5e 100644
--- a/testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-static.html
+++ b/testing/web-platform/tests/css/css-view-transitions/snapshot-containing-block-static.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:bokan@chromium.org">
<link rel="match" href="snapshot-containing-block-static-ref.html">
-<meta name="fuzzy" content="snapshot-containing-block-static-ref.html:0-20;0-100">
+<meta name="fuzzy" content="maxDifference=0-20; totalPixels=0-100">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/css-view-transitions/span-with-overflowing-text-and-box-decorations.html b/testing/web-platform/tests/css/css-view-transitions/span-with-overflowing-text-and-box-decorations.html
index a2bf59ecb0..262970ad5f 100644
--- a/testing/web-platform/tests/css/css-view-transitions/span-with-overflowing-text-and-box-decorations.html
+++ b/testing/web-platform/tests/css/css-view-transitions/span-with-overflowing-text-and-box-decorations.html
@@ -4,8 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="span-with-overflowing-text-and-box-decorations-ref.html">
-<meta name="fuzzy" content="span-with-overflowing-text-and-box-decorations-ref.html:maxDifference=0-3;totalPixels=0-4900">
-
+<meta name="fuzzy" content="maxDifference=0-3;totalPixels=0-4900">
<script src="/common/reftest-wait.js"></script>
<script src="/common/rendering-utils.js"></script>
diff --git a/testing/web-platform/tests/css/css-view-transitions/span-with-overflowing-text.html b/testing/web-platform/tests/css/css-view-transitions/span-with-overflowing-text.html
index f3f0f534e9..5a6268ddf0 100644
--- a/testing/web-platform/tests/css/css-view-transitions/span-with-overflowing-text.html
+++ b/testing/web-platform/tests/css/css-view-transitions/span-with-overflowing-text.html
@@ -4,8 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="span-with-overflowing-text-ref.html">
-<meta name="fuzzy" content="span-with-overflowing-text-ref.html:maxDifference=0-3;totalPixels=0-1100">
-
+<meta name="fuzzy" content="maxDifference=0-3;totalPixels=0-1100">
<script src="/common/reftest-wait.js"></script>
<script src="/common/rendering-utils.js"></script>
diff --git a/testing/web-platform/tests/css/css-view-transitions/transition-in-empty-iframe.html b/testing/web-platform/tests/css/css-view-transitions/transition-in-empty-iframe.html
index 7cd621fbfd..101f7c2a63 100644
--- a/testing/web-platform/tests/css/css-view-transitions/transition-in-empty-iframe.html
+++ b/testing/web-platform/tests/css/css-view-transitions/transition-in-empty-iframe.html
@@ -5,7 +5,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:bokan@chromium.org">
<link rel="match" href="transition-in-empty-iframe-ref.html">
- <meta name=fuzzy content="transition-in-empty-iframe-ref.html:0-80;0-1000">
+ <meta name=fuzzy content="maxDifference=0-80; totalPixels=0-1000">
<script src="/common/reftest-wait.js"></script>
<style>
iframe {
diff --git a/testing/web-platform/tests/css/css-view-transitions/web-animations-api-parse-pseudo-argument.html b/testing/web-platform/tests/css/css-view-transitions/web-animations-api-parse-pseudo-argument.html
index 40c9a0d0c2..dfaac62c17 100644
--- a/testing/web-platform/tests/css/css-view-transitions/web-animations-api-parse-pseudo-argument.html
+++ b/testing/web-platform/tests/css/css-view-transitions/web-animations-api-parse-pseudo-argument.html
@@ -3,7 +3,7 @@
<title>View transitions with web-animation API: full parsing of argument</title>
<link rel="help" href="https://www.w3.org/TR/css-view-transitions-1/">
<link rel="match" href="web-animations-api-ref.html">
-<meta name="fuzzy" content="web-animations-api-ref.html:0-2;0-500">
+<meta name="fuzzy" content="maxDifference=0-2; totalPixels=0-500">
<meta name="variant" content="?first-pseudo=::view-transition-group( first)">
<meta name="variant" content="?first-pseudo=::view-transition-group(first)">
<meta name="variant" content="?first-pseudo=::view-transition-group( first">
diff --git a/testing/web-platform/tests/css/css-view-transitions/web-animations-api.html b/testing/web-platform/tests/css/css-view-transitions/web-animations-api.html
index c739e416c8..23b1504b24 100644
--- a/testing/web-platform/tests/css/css-view-transitions/web-animations-api.html
+++ b/testing/web-platform/tests/css/css-view-transitions/web-animations-api.html
@@ -4,7 +4,7 @@
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:khushalsagar@chromium.org">
<link rel="match" href="web-animations-api-ref.html">
-<meta name="fuzzy" content="web-animations-api-ref.html:0-2;0-500">
+<meta name="fuzzy" content="maxDifference=0-2; totalPixels=0-500">
<script src="/common/reftest-wait.js"></script>
<style>
diff --git a/testing/web-platform/tests/css/zoom/iframe-zoom-nested.html b/testing/web-platform/tests/css/css-viewport/zoom/iframe-zoom-nested.html
index 22a491eb0b..22a491eb0b 100644
--- a/testing/web-platform/tests/css/zoom/iframe-zoom-nested.html
+++ b/testing/web-platform/tests/css/css-viewport/zoom/iframe-zoom-nested.html
diff --git a/testing/web-platform/tests/css/zoom/iframe-zoom.sub.html b/testing/web-platform/tests/css/css-viewport/zoom/iframe-zoom.sub.html
index 82a202161b..a27fb91619 100644
--- a/testing/web-platform/tests/css/zoom/iframe-zoom.sub.html
+++ b/testing/web-platform/tests/css/css-viewport/zoom/iframe-zoom.sub.html
@@ -35,7 +35,7 @@
</div>
<div id="another_with_zoom" style="zoom: 3;">
- <iframe src="http://{{hosts[alt][]}}:{{ports[http][0]}}/css/zoom/tentative/resources/iframe_content.html"></iframe>
+ <iframe src="http://{{hosts[alt][]}}:{{ports[http][0]}}/css-viewport/zoom/resources/iframe_content.html"></iframe>
</div>
</body>
diff --git a/testing/web-platform/tests/css/zoom/reference/iframe-zoom-nested-ref.html b/testing/web-platform/tests/css/css-viewport/zoom/reference/iframe-zoom-nested-ref.html
index b855278516..b855278516 100644
--- a/testing/web-platform/tests/css/zoom/reference/iframe-zoom-nested-ref.html
+++ b/testing/web-platform/tests/css/css-viewport/zoom/reference/iframe-zoom-nested-ref.html
diff --git a/testing/web-platform/tests/css/zoom/reference/iframe-zoom-ref.html b/testing/web-platform/tests/css/css-viewport/zoom/reference/iframe-zoom-ref.html
index 43bc3e24cf..43bc3e24cf 100644
--- a/testing/web-platform/tests/css/zoom/reference/iframe-zoom-ref.html
+++ b/testing/web-platform/tests/css/css-viewport/zoom/reference/iframe-zoom-ref.html
diff --git a/testing/web-platform/tests/css/zoom/resources/iframe_content.html b/testing/web-platform/tests/css/css-viewport/zoom/resources/iframe_content.html
index 58c4d03a46..58c4d03a46 100644
--- a/testing/web-platform/tests/css/zoom/resources/iframe_content.html
+++ b/testing/web-platform/tests/css/css-viewport/zoom/resources/iframe_content.html
diff --git a/testing/web-platform/tests/css/zoom/resources/nested-iframe-no-zoom.html b/testing/web-platform/tests/css/css-viewport/zoom/resources/nested-iframe-no-zoom.html
index 60b1fd6481..60b1fd6481 100644
--- a/testing/web-platform/tests/css/zoom/resources/nested-iframe-no-zoom.html
+++ b/testing/web-platform/tests/css/css-viewport/zoom/resources/nested-iframe-no-zoom.html
diff --git a/testing/web-platform/tests/css/zoom/resources/nested-iframe-with-zoom.html b/testing/web-platform/tests/css/css-viewport/zoom/resources/nested-iframe-with-zoom.html
index e7de64aafb..e7de64aafb 100644
--- a/testing/web-platform/tests/css/zoom/resources/nested-iframe-with-zoom.html
+++ b/testing/web-platform/tests/css/css-viewport/zoom/resources/nested-iframe-with-zoom.html
diff --git a/testing/web-platform/tests/css/css-viewport/zoom/scroll-top-test-with-zoom.html b/testing/web-platform/tests/css/css-viewport/zoom/scroll-top-test-with-zoom.html
new file mode 100644
index 0000000000..9656fe120e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-viewport/zoom/scroll-top-test-with-zoom.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<title>Scroll Top Test with Zoom</title>
+<link rel="help" href="https://drafts.csswg.org/css-viewport/#zoom-property">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #container {
+ width: 200px;
+ height: 100px;
+ border: solid thick;
+ overflow: auto;
+ }
+</style>
+<div id="container">
+ <div style="width: 100px; height: 2000px"></div>
+</div>
+
+<script>
+ var container = document.getElementById('container');
+ container.scrollTop = 77;
+ test(function() {
+ assert_equals(container.scrollTop, 77, "Initial scrollTop should be 77");
+ }, "Initial scrollTop with no zoom");
+
+ document.body.style.zoom = 1.2;
+ document.body.offsetTop;
+
+ document.body.style.zoom = 1;
+ test(function() {
+ assert_equals(container.scrollTop, 77, "scrollTop should remain consistent after resetting zoom");
+ }, "scrollTop after resetting zoom");
+ done();
+</script>
diff --git a/testing/web-platform/tests/css/cssom-view/WEB_FEATURES.yml b/testing/web-platform/tests/css/cssom-view/WEB_FEATURES.yml
index a545dbadcb..d5352fb4a1 100644
--- a/testing/web-platform/tests/css/cssom-view/WEB_FEATURES.yml
+++ b/testing/web-platform/tests/css/cssom-view/WEB_FEATURES.yml
@@ -1,4 +1,7 @@
features:
+- name: check-visibility
+ files:
+ - checkVisibility.html
- name: scroll-into-view
files:
- scrollIntoView-*
diff --git a/testing/web-platform/tests/css/cssom-view/offsetTop-offsetLeft-with-zoom.html b/testing/web-platform/tests/css/cssom-view/offsetTop-offsetLeft-with-zoom.html
index a60a18a431..b0f11ea02e 100644
--- a/testing/web-platform/tests/css/cssom-view/offsetTop-offsetLeft-with-zoom.html
+++ b/testing/web-platform/tests/css/cssom-view/offsetTop-offsetLeft-with-zoom.html
@@ -24,7 +24,6 @@
margin: 1px;
top:10x;
left: 10x;
-
}
.one {
position: relative;
@@ -36,7 +35,7 @@
top: 20px;
left: 20px;
zoom: 2;
- }
+ }
.three {
position: absolute;
@@ -54,18 +53,18 @@
<div id="unzoomed_two" class="square two"></div>
<div id="unzoomed_three" class="square three"></div>
</div>
-<div style="zoom:3" class=outer_div>
+<div style="zoom:3" class="outer_div">
<div id="zoomed_one" class="square one"></div>
<div id="zoomed_two" class="square two"></div>
<div id="zoomed_three" class="square three"></div>
</div>
-<div class ="outer_div" style="margin: 30px;" id="outer_div">
+<div class="outer_div" style="margin: 30px;" id="outer_div">
<div id="zoomed_middle" style="margin: 10px; zoom:2">
<div class="square" id="unzoomed_inner"></div>
</div>
</div>
-<div class = outer_div style="margin: 30px;">
+<div class="outer_div" style="margin: 30px;">
<div id="unzoomed_middle">
<div class="square" id="zoomed_inner" style="zoom:2; width: 100px; height: 100px; border: 1px solid black;"></div>
</div>
@@ -90,12 +89,12 @@ test(() => {
assert_equals(unzoomed_inner.offsetLeft, 11, 'unzoomed_inner.offsetLeft');
assert_equals(zoomed_inner.offsetTop, 0, 'zoomed_inner.offsetTop');
assert_equals(zoomed_inner.offsetLeft, 1, 'zoomed_inner.offsetLeft');
+}, 'Verifies that offsetTop and offsetLeft find the right OffsetParent and return values excluding the target zoom');
- // check that offset is equal between elements when one of them has css zoom
+test(() => {
assert_equals(unzoomed_one.offsetWidth, zoomed_one.offsetWidth, "offsetWidth");
assert_equals(unzoomed_one.offsetHeight, zoomed_one.offsetHeight, "offsetHeight");
assert_equals(zoomed_inner.offsetWidth, outer_div.offsetWidth, "offsetWidth for nested element");
assert_equals(zoomed_inner.offsetHeight, outer_div.offsetHeight, "offsetHeight for nested element");
-
-}, 'Verifies that offsetTop and offsetLeft find the right OffsetParent and return values excluding the target zoom');
+}, 'check that offset is equal between elements when one of them has css zoom');
</script>
diff --git a/testing/web-platform/tests/css/cssom/CSSStyleSheet-constructable-baseURL.tentative.html b/testing/web-platform/tests/css/cssom/CSSStyleSheet-constructable-baseURL.html
index 8997a59e9c..d0f0f828b0 100644
--- a/testing/web-platform/tests/css/cssom/CSSStyleSheet-constructable-baseURL.tentative.html
+++ b/testing/web-platform/tests/css/cssom/CSSStyleSheet-constructable-baseURL.html
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<title>CSSStyleSheet baseURL</title>
<link rel="author" title="Erik Nordin" href="mailto:enordin@mozilla.com">
-<link rel="help" href="https://github.com/WICG/construct-stylesheets/issues/95#issuecomment-593545252">
+<link rel="help" href="https://drafts.csswg.org/cssom-1/#dom-cssstylesheetinit-baseurl">
<div id="target"></div>
<script src='/resources/testharness.js'></script>
<script src='/resources/testharnessreport.js'></script>
diff --git a/testing/web-platform/tests/css/cssom/WEB_FEATURES.yml b/testing/web-platform/tests/css/cssom/WEB_FEATURES.yml
new file mode 100644
index 0000000000..def314c45c
--- /dev/null
+++ b/testing/web-platform/tests/css/cssom/WEB_FEATURES.yml
@@ -0,0 +1,7 @@
+features:
+- name: constructed-stylesheets
+ files:
+ - adoptedstylesheets-observablearray.html
+ - CSSStyleSheet-constructable-*
+ - CSSStyleSheet-constructable.html
+ - CSSStyleSheet-template-adoption.html
diff --git a/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-backdrop-filter.html b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-backdrop-filter.html
new file mode 100644
index 0000000000..6be85d5cf2
--- /dev/null
+++ b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-backdrop-filter.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>backdrop-filter: The backdrop root concept</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel="help" href="https://drafts.fxtf.org/filter-effects-2/#BackdropRoot">
+<link rel=stylesheet href="resources/backdrop-filter-backdrop-root.css">
+<link rel="match" href="backdrop-filter-backdrop-root-ref.html">
+
+<!-- A Backdrop Root is formed, anywhere in the document, by:
+ - An element with a backdrop-filter value other than "none".
+ - An element with a will-change value specifying any property that would create a Backdrop Root on non-initial value. -->
+<div class=container>
+ <div class=testcase>
+ <div style="backdrop-filter: invert(0);"><div></div></div>
+ </div>
+ <div class=testcase>
+ <div style="will-change:backdrop-filter;"><div></div></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-clip-path-ref.html b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-clip-path-ref.html
new file mode 100644
index 0000000000..08a2ad4fc5
--- /dev/null
+++ b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-clip-path-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=stylesheet href="resources/backdrop-filter-backdrop-root.css">
+
+<div class=container>
+ <div class=testcase style="clip-path: xywh(5px 5px 40px 40px);">
+ <div></div>
+ <div class="box2" style="background: #00ffff;"></div>
+ </div>
+ <div class=testcase>
+ <div></div>
+ <div class="box2" style="background: #00ffff;"></div>
+ </div>
+</div>
+
+<style>
+.box2 {
+ top:-50px;
+ left: 50px;
+ border: 1px solid black;
+}
+</style>
diff --git a/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-clip-path.html b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-clip-path.html
new file mode 100644
index 0000000000..172d07be93
--- /dev/null
+++ b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-clip-path.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>backdrop-filter: The backdrop root concept</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel="help" href="https://drafts.fxtf.org/filter-effects-2/#BackdropRoot">
+<link rel=stylesheet href="resources/backdrop-filter-backdrop-root.css">
+<link rel="match" href="backdrop-filter-backdrop-root-clip-path-ref.html">
+
+<!-- A Backdrop Root is formed, anywhere in the document, by:
+ - An element with mask, mask-image, mask-border, or clip-path properties with values other than “none”.
+ - An element with a will-change value specifying any property that would create a Backdrop Root on non-initial value. -->
+<div class=container>
+ <div class=testcase>
+ <div style="clip-path: inset(10%);"><div></div></div>
+ </div>
+ <div class=testcase>
+ <div style="will-change:clip-path;"><div></div></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-filter-ref.html b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-filter-ref.html
new file mode 100644
index 0000000000..ffa4dfcf0e
--- /dev/null
+++ b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-filter-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=stylesheet href="resources/backdrop-filter-backdrop-root.css">
+
+<div class=container>
+ <div class=testcase>
+ <div style="filter: invert(0.1)"></div>
+ <div class="box2" style="border-color: #1A1A1A; background: #00ffff;"></div>
+ </div>
+ <div class=testcase>
+ <div></div>
+ <div class="box2" style="background: #00ffff;"></div>
+ </div>
+</div>
+
+<style>
+.box2 {
+ top:-50px;
+ left: 50px;
+ border: 1px solid black;
+}
+</style>
diff --git a/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-filter.html b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-filter.html
new file mode 100644
index 0000000000..e988b479ce
--- /dev/null
+++ b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-filter.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>backdrop-filter: The backdrop root concept</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel="help" href="https://drafts.fxtf.org/filter-effects-2/#BackdropRoot">
+<link rel=stylesheet href="resources/backdrop-filter-backdrop-root.css">
+<link rel="match" href="backdrop-filter-backdrop-root-filter-ref.html">
+<meta name="fuzzy" content="maxDifference=0-5;totalPixels=0-500">
+
+<!-- A Backdrop Root is formed, anywhere in the document, by:
+ - An element with a filter property other than "none".
+ - An element with a will-change value specifying any property that would create a Backdrop Root on non-initial value. -->
+<div class=container>
+ <div class=testcase>
+ <div style="filter: invert(0.1);"><div></div></div>
+ </div>
+ <div class=testcase>
+ <div style="will-change:filter;"><div></div></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-mix-blend-mode.html b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-mix-blend-mode.html
new file mode 100644
index 0000000000..2d77b61b84
--- /dev/null
+++ b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-mix-blend-mode.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>backdrop-filter: The backdrop root concept</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel="help" href="https://drafts.fxtf.org/filter-effects-2/#BackdropRoot">
+<link rel=stylesheet href="resources/backdrop-filter-backdrop-root.css">
+<link rel="match" href="backdrop-filter-backdrop-root-ref.html">
+
+<!-- A Backdrop Root is formed, anywhere in the document, by:
+ - An element with a mix-blend-mode value other than "normal".
+ - An element with a will-change value specifying any property that would create a Backdrop Root on non-initial value. -->
+<div class=container>
+ <div class=testcase>
+ <div style="mix-blend-mode:darken;"><div></div></div>
+ </div>
+ <div class=testcase>
+ <div style="will-change:mix-blend-mode;"><div></div></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-opacity-ref.html b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-opacity-ref.html
new file mode 100644
index 0000000000..76d4854df6
--- /dev/null
+++ b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-opacity-ref.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=stylesheet href="resources/backdrop-filter-backdrop-root.css">
+
+<div class=container>
+ <div class=testcase>
+ <div style="opacity:0.9"></div>
+ <div class="box2" style="border-color: #021919; background: #00ffff;"></div>
+ </div>
+ <div class=testcase>
+ <div></div>
+ <div class="box2" style="background: #00ffff;"></div>
+ </div>
+</div>
+
+<style>
+.box2 {
+ top:-50px;
+ left: 50px;
+ border: 1px solid black;
+}
+</style>
+
diff --git a/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-opacity.html b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-opacity.html
new file mode 100644
index 0000000000..b11d064cb3
--- /dev/null
+++ b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-opacity.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>backdrop-filter: The backdrop root concept</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel="help" href="https://drafts.fxtf.org/filter-effects-2/#BackdropRoot">
+<link rel=stylesheet href="resources/backdrop-filter-backdrop-root.css">
+<link rel="match" href="backdrop-filter-backdrop-root-opacity-ref.html">
+<meta name="fuzzy" content="maxDifference=0-5;totalPixels=0-500">
+
+<!-- A Backdrop Root is formed, anywhere in the document, by:
+ - An element with an opacity value less than 1.
+ - An element with a will-change value specifying any property that would create a Backdrop Root on non-initial value. -->
+<div class=container>
+ <div class=testcase>
+ <div style="opacity: 0.9;"><div></div></div>
+ </div>
+ <div class=testcase>
+ <div style="will-change:opacity;"><div></div></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-ref.html b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-ref.html
new file mode 100644
index 0000000000..934e08acb9
--- /dev/null
+++ b/testing/web-platform/tests/css/filter-effects/backdrop-filter-backdrop-root-ref.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=stylesheet href="resources/backdrop-filter-backdrop-root.css">
+
+<div class=container>
+ <div class=testcase>
+ <div></div>
+ <div class="box2" style="background: #00ffff;"></div>
+ </div>
+ <div class=testcase>
+ <div></div>
+ <div class="box2" style="background: #00ffff;"></div>
+ </div>
+</div>
+
+<style>
+.box2 {
+ top:-50px;
+ left: 50px;
+ border: 1px solid black;
+}
+</style>
+
diff --git a/testing/web-platform/tests/css/filter-effects/backdrop-filter-isolation-ref.html b/testing/web-platform/tests/css/filter-effects/backdrop-filter-isolation-ref.html
deleted file mode 100644
index fc2bccc36c..0000000000
--- a/testing/web-platform/tests/css/filter-effects/backdrop-filter-isolation-ref.html
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>backdrop-filter: Should not filter outside parent stacking context.</title>
-<link rel="author" href="mailto:masonf@chromium.org">
-
-
-
-<div>
- <p>Expected: Two green boxes overlapped by a yellow box. The overlapped region<br>
- of the right-hand box ONLY should be inverted (pink).</p>
-</div>
-<div class="box outside">
- <div class="box stacking-context">
- <div class="box filter">
- </div>
- </div>
-</div>
-<div class="box overlay"></div>
-<style>
-.box {
- position: absolute;
- width: 100px;
- height: 100px;
- background: green;
-}
-.outside {
- top: 110px;
- left: 10px;
-}
-.stacking-context {
- will-change: transform;
- top: 0px;
- left: 120px;
-}
-.filter {
- width: 160px;
- height: 160px;
- top: 30px;
- left: -90px;
- will-change: transform;
- background: #ff08;
-}
-.overlay {
- background:#ffc377;
- width:70px;
- height:70px;
- top:140px;
- left:130px;
-}
-</style>
-
diff --git a/testing/web-platform/tests/css/filter-effects/backdrop-filter-isolation.html b/testing/web-platform/tests/css/filter-effects/backdrop-filter-isolation.html
deleted file mode 100644
index 06791f73d2..0000000000
--- a/testing/web-platform/tests/css/filter-effects/backdrop-filter-isolation.html
+++ /dev/null
@@ -1,44 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>backdrop-filter: Should not filter outside parent stacking context.</title>
-<link rel="author" href="mailto:masonf@chromium.org">
-<link rel="help" href="https://drafts.fxtf.org/filter-effects-2/#BackdropFilterProperty">
-<link rel="match" href="backdrop-filter-isolation-ref.html">
-
-<div>
- <p>Expected: Two green boxes overlapped by a yellow box. The overlapped region<br>
- of the right-hand box ONLY should be inverted (pink).</p>
-</div>
-<div class="box outside">
- <div class="box stacking-context">
- <div class="box filter">
- </div>
- </div>
-</div>
-
-<style>
-.box {
- position: absolute;
- width: 100px;
- height: 100px;
- background: green;
-}
-.outside {
- top: 110px;
- left: 10px;
-}
-.stacking-context {
- opacity: 0.9999;
- top: 0px;
- left: 120px;
-}
-.filter {
- width: 160px;
- height: 160px;
- top: 30px;
- left: -90px;
- backdrop-filter: invert(1);
- background: #ff08;
-}
-</style>
-
diff --git a/testing/web-platform/tests/css/filter-effects/resources/backdrop-filter-backdrop-root.css b/testing/web-platform/tests/css/filter-effects/resources/backdrop-filter-backdrop-root.css
new file mode 100644
index 0000000000..635e497cdf
--- /dev/null
+++ b/testing/web-platform/tests/css/filter-effects/resources/backdrop-filter-backdrop-root.css
@@ -0,0 +1,30 @@
+.container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ width: 250px;
+ height: 100px;
+ align-content: flex-start;
+ background:#00ffff;
+}
+.testcase {
+ width: 100px;
+ height: 50px;
+}
+.testcase div {
+ width: 50px;
+ height: 50px;
+ position: relative;
+}
+.testcase>div {
+ background: green;
+}
+.testcase>div>div {
+ left: 50px;
+ backdrop-filter: invert(1);
+ background: transparent;
+ border: 1px solid black;
+}
+* {
+ box-sizing: border-box;
+}
diff --git a/testing/web-platform/tests/css/filter-effects/support/hueRotate.svg b/testing/web-platform/tests/css/filter-effects/support/hueRotate.svg
index ed294041e1..0ddcb37d70 100644
--- a/testing/web-platform/tests/css/filter-effects/support/hueRotate.svg
+++ b/testing/web-platform/tests/css/filter-effects/support/hueRotate.svg
@@ -1,6 +1,6 @@
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg" version="1.1">
<defs>
- <filter id="MyFilter">
+ <filter id="MyFilter" color-interpolation-filters="sRGB">
<feColorMatrix type="hueRotate" values="90"/>
</filter>
</defs>
diff --git a/testing/web-platform/tests/css/mediaqueries/WEB_FEATURES.yml b/testing/web-platform/tests/css/mediaqueries/WEB_FEATURES.yml
new file mode 100644
index 0000000000..88f1510e1b
--- /dev/null
+++ b/testing/web-platform/tests/css/mediaqueries/WEB_FEATURES.yml
@@ -0,0 +1,5 @@
+features:
+- name: prefers-color-scheme
+ files:
+ - prefers-color-scheme.html
+ - prefers-color-scheme-*
diff --git a/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/svg-as-image-ref.html b/testing/web-platform/tests/css/mediaqueries/prefers-color-scheme-svg-as-image-ref.html
index 1ff9d88f30..012513ef9f 100644
--- a/testing/web-platform/tests/css/css-color-adjust/rendering/dark-color-scheme/svg-as-image-ref.html
+++ b/testing/web-platform/tests/css/mediaqueries/prefers-color-scheme-svg-as-image-ref.html
@@ -6,14 +6,14 @@
<style>
div {
background-color: blue;
- height: 100px;
- width: 100px;
+ height: 32px;
+ width: 32px;
}
@media (prefers-color-scheme: dark) {
div {
- background-color: green;
+ background-color: purple;
}
}
</style>
-<p>There should be green square below when the preferred color-scheme is dark, and blue otherwise.</p>
+<p>There should be a purple square below when the preferred color-scheme is dark, and blue otherwise.</p>
<div></div>
diff --git a/testing/web-platform/tests/css/mediaqueries/prefers-color-scheme-svg-as-image.html b/testing/web-platform/tests/css/mediaqueries/prefers-color-scheme-svg-as-image.html
new file mode 100644
index 0000000000..65d6556f7a
--- /dev/null
+++ b/testing/web-platform/tests/css/mediaqueries/prefers-color-scheme-svg-as-image.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<head>
+ <title>prefers-color-scheme inside an SVG image</title>
+ <link rel="help" href="https://www.w3.org/TR/mediaqueries-5/#descdef-media-prefers-color-scheme">
+ <link rel="match" href="prefers-color-scheme-svg-as-image-ref.html">
+</head>
+<p>There should be a purple square below when the preferred color-scheme is dark, and blue otherwise.</p>
+<img src='resources/prefers-color-scheme.svg'>
diff --git a/testing/web-platform/tests/css/motion/WEB_FEATURES.yml b/testing/web-platform/tests/css/motion/WEB_FEATURES.yml
new file mode 100644
index 0000000000..0a436801cb
--- /dev/null
+++ b/testing/web-platform/tests/css/motion/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: motion-path
+ files: "**"
diff --git a/testing/web-platform/tests/css/motion/animation/offset-rotate-interpolation.html b/testing/web-platform/tests/css/motion/animation/offset-rotate-interpolation.html
index 55845108eb..10c337244f 100644
--- a/testing/web-platform/tests/css/motion/animation/offset-rotate-interpolation.html
+++ b/testing/web-platform/tests/css/motion/animation/offset-rotate-interpolation.html
@@ -163,15 +163,6 @@
{at: 1.5, expect: '70deg'},
]);
- // Regression test for crbug.com/918430.
- test_interpolation({
- property: 'offset-rotate',
- from: '800deg',
- to: '900deg'
- }, [
- {at: -3.40282e+38, expect: '-3.40282e+38deg'},
- ]);
-
// Interpolation between auto angles.
test_interpolation({
property: 'offset-rotate',
diff --git a/testing/web-platform/tests/css/selectors/WEB_FEATURES.yml b/testing/web-platform/tests/css/selectors/WEB_FEATURES.yml
index 47cf05a2c0..2449bf4808 100644
--- a/testing/web-platform/tests/css/selectors/WEB_FEATURES.yml
+++ b/testing/web-platform/tests/css/selectors/WEB_FEATURES.yml
@@ -5,3 +5,10 @@ features:
- name: has
files:
- has-*
+- name: read-write-pseudos
+ files:
+ - selector-read-write-*
+- name: user-pseudos
+ files:
+ - user-invalid.html
+ - user-valid.html
diff --git a/testing/web-platform/tests/css/selectors/focus-visible-028.html b/testing/web-platform/tests/css/selectors/focus-visible-028.html
new file mode 100644
index 0000000000..a8b704838b
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/focus-visible-028.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>CSS Test (Selectors): Programmatic focus causes :focus-visible to match after keyboard usage when preventDefault() is used</title>
+ <link rel="author" title="Tyler Jones" href="tylerjdev@github.com" />
+ <link rel="help" href="https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <style>
+ @supports not selector(:focus-visible) {
+ :focus {
+ outline: red solid 5px;
+ background-color: red;
+ }
+ }
+
+ :focus-visible {
+ outline: blue solid 5px;
+ }
+
+ :focus:not(:focus-visible) {
+ outline: 0;
+ background-color: lime;
+ }
+ </style>
+</head>
+<body>
+ This test checks that programmatically focusing an element after a click and using keyboard afterwards triggers <code>:focus-visible</code> matching.
+ <ol id="instructions">
+ <li>If the user-agent does not claim to support the <code>:focus-visible</code> pseudo-class then SKIP this test.</li>
+ <li>Click the button below that says "Click me".</li>
+ <li>Once focused on the button that says "I will be focused programmatically", use "Left" or "Right" arrow keys to navigate through the button group</li>
+ <li>If any button within the group has a blue outline after using either arrow key, then the test result is SUCCESS. If the element has a green background after using either arrow key, then the test result is FAILURE.</li>
+ </ol>
+ <br />
+ <button id="button">Click me</button>
+ <div id="container">
+ <button id="btn1">I will be focused programmatically.</button>
+ <button id="btn2">Button 2</button>
+ <button id="btn3">Button 3</button>
+ </div>
+ <script>
+ button.addEventListener("click", () => {
+ btn1.focus();
+ });
+
+ focusTrap(document.querySelector('#container'));
+
+ function focusTrap(container) {
+ container.addEventListener('keydown', (e) => {
+ if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;
+ e.preventDefault();
+
+ const btns = container.querySelectorAll('button');
+ const currentIndex = Array.from(btns).indexOf(document.activeElement);
+ let nextIndex;
+
+ if (e.key === 'ArrowRight') {
+ nextIndex = (currentIndex + 1) % btns.length;
+ } else if (e.key === 'ArrowLeft') {
+ nextIndex = (currentIndex - 1 + btns.length) % btns.length;
+ }
+
+ btns[nextIndex].focus();
+ });
+ }
+
+ promise_test(async t => {
+ const arrow_right = "\uE014";
+ btn2.addEventListener("focus", t.step_func_done((e) => {
+ assert_equals(getComputedStyle(btn2).outlineColor, "rgb(0, 0, 255)", `outlineColor for ${btn2.tagName}#${btn2.id} is blue`);
+ assert_not_equals(getComputedStyle(btn2).backgroundColor, "rgb(0, 255, 0)", `backgroundColor for ${btn2.tagName}#${btn2.id} is NOT lime`);
+ }));
+
+ await test_driver.click(button);
+ assert_equals(document.activeElement, btn1);
+ await test_driver.send_keys(btn1, arrow_right);
+ }, "Programmatic focus after click and keyboard interaction should match :focus-visible");
+ </script>
+</body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/selectors/invalidation/WEB_FEATURES.yml b/testing/web-platform/tests/css/selectors/invalidation/WEB_FEATURES.yml
index 4eaa2f3931..5e6a6d0d5c 100644
--- a/testing/web-platform/tests/css/selectors/invalidation/WEB_FEATURES.yml
+++ b/testing/web-platform/tests/css/selectors/invalidation/WEB_FEATURES.yml
@@ -4,3 +4,6 @@ features:
- has-*
- "*-in-has.*"
- "*-in-has-*"
+- name: user-pseudos
+ files:
+ - user-valid-user-invalid.html
diff --git a/testing/web-platform/tests/css/selectors/user-invalid.html b/testing/web-platform/tests/css/selectors/user-invalid.html
index 5f8fa5811a..c57e764831 100644
--- a/testing/web-platform/tests/css/selectors/user-invalid.html
+++ b/testing/web-platform/tests/css/selectors/user-invalid.html
@@ -29,6 +29,7 @@
<input placeholder="Required field" required id="required-input"><br>
<textarea placeholder="Required field" required id="required-textarea"></textarea><br>
<input type="checkbox" required id="required-checkbox"><br>
+ <input type="date" required id="required-date"><br>
<input type="submit" id="submit-button">
<input type="reset" id="reset-button">
</form>
@@ -80,12 +81,14 @@ promise_test(async () => {
const requiredInput = document.querySelector("#required-input");
const requiredTextarea = document.querySelector("#required-textarea");
const requiredCheckbox = document.querySelector("#required-checkbox");
+ const requiredDate = document.querySelector("#required-date");
const submitButton = document.querySelector("#submit-button");
const resetButton = document.querySelector("#reset-button");
assert_false(requiredInput.validity.valid);
assert_false(requiredTextarea.validity.valid);
assert_false(requiredCheckbox.validity.valid);
+ assert_false(requiredDate.validity.valid);
// The selector can't match because no interaction has happened.
assert_false(requiredInput.matches(":user-valid"), "Initially does not match :user-valid");
assert_false(requiredInput.matches(":user-invalid"), "Initially does not match :user-invalid");
@@ -96,6 +99,9 @@ promise_test(async () => {
assert_false(requiredCheckbox.matches(":user-valid"), "Initially does not match :user-valid");
assert_false(requiredCheckbox.matches(":user-invalid"), "Initially does not match :user-invalid");
+ assert_false(requiredDate.matches(":user-valid"), "Initially does not match :user-valid");
+ assert_false(requiredDate.matches(":user-invalid"), "Initially does not match :user-invalid");
+
submitButton.click();
assert_true(requiredInput.matches(":user-invalid"), "Submitted the form, input is validated");
@@ -107,6 +113,9 @@ promise_test(async () => {
assert_true(requiredCheckbox.matches(":user-invalid"), "Submitted the form, checkbox is validated");
assert_false(requiredCheckbox.matches(":user-valid"), "Submitted the form, checkbox is validated");
+ assert_true(requiredDate.matches(":user-invalid"), "Submitted the form, date input is validated");
+ assert_false(requiredDate.matches(":user-valid"), "Submitted the form, date input is validated");
+
resetButton.click();
assert_false(requiredInput.matches(":user-valid"), "Reset the form, user-interacted flag is reset");
@@ -118,6 +127,9 @@ promise_test(async () => {
assert_false(requiredCheckbox.matches(":user-valid"), "Reset the form, user-interacted flag is reset");
assert_false(requiredCheckbox.matches(":user-invalid"), "Reset the form, user-interacted flag is reset");
+ assert_false(requiredDate.matches(":user-valid"), "Reset the form, user-interacted flag is reset");
+ assert_false(requiredDate.matches(":user-invalid"), "Reset the form, user-interacted flag is reset");
+
// Test programmatic form submission with constraint validation.
form.requestSubmit();
@@ -129,6 +141,9 @@ promise_test(async () => {
assert_true(requiredCheckbox.matches(":user-invalid"), "Called form.requestSubmit(), checkbox is validated");
assert_false(requiredCheckbox.matches(":user-valid"), "Called form.requestSubmit(), checkbox is validated");
+
+ assert_true(requiredDate.matches(":user-invalid"), "Called form.requestSubmit(), date input is validated");
+ assert_false(requiredDate.matches(":user-valid"), "Called form.requestSubmit(), date input is validated");
}, ":user-invalid selector properly interacts with submit & reset buttons");
// historical: https://github.com/w3c/csswg-drafts/issues/1329
@@ -193,4 +208,38 @@ promise_test(async () => {
assert_true(checkbox.matches(':user-invalid'),
'Checkbox should match :user-invalid after clicking twice.');
}, 'A required checkbox should match :user-invalid if the user unchecks it and blurs.');
+
+promise_test(async () => {
+ const date = document.getElementById('required-date');
+
+ const resetButton = document.getElementById('reset-button');
+ resetButton.click();
+ assert_false(date.matches(':user-invalid'),
+ 'date input should not match :user-invalid at the start of the test.');
+ assert_equals(date.value, '',
+ 'date input should not have a value at the start of the test.');
+
+ date.value = '2024-04-15';
+ assert_false(date.matches(':user-invalid'),
+ 'date should not match :user-invalid after programatically changing value.');
+ date.value = '';
+ assert_false(date.matches(':user-invalid'),
+ 'date should not match :user-invalid after programatically changing value.');
+
+ const tabKey = '\uE004';
+ const backspace = '\uE003';
+ date.focus();
+ // Press tab twice at the end to make sure that focus has left the input.
+ await test_driver.send_keys(date, `1${tabKey}1${tabKey}1234${tabKey}${tabKey}`);
+ assert_not_equals(document.activeElement, date,
+ 'Pressing tab twice after typing in the date should have blurred the input.');
+ assert_equals(date.value, '1234-01-01',
+ 'Date input value should match the testdriver input.');
+ date.focus();
+ await test_driver.send_keys(date, backspace);
+ assert_equals(date.value, '',
+ 'Date input value should be cleared when deleting one of the sub-values.');
+ assert_true(date.matches(':user-invalid'),
+ 'Date input should match :user-invalid after typing in an invalid value.');
+}, 'A required date should match :user-invalid if the user unchecks it and blurs.');
</script>
diff --git a/testing/web-platform/tests/css/selectors/user-valid-user-invalid-multifield-inputs.tentative.html b/testing/web-platform/tests/css/selectors/user-valid-user-invalid-multifield-inputs.tentative.html
new file mode 100644
index 0000000000..d6358ea269
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/user-valid-user-invalid-multifield-inputs.tentative.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/selectors/#user-pseudos">
+<link rel="help" href="https://html.spec.whatwg.org/#selector-user-invalid">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<!-- This test asserts specifics of keyboard behavior in multifield inputs,
+ like type=date and type=time, in ways that are not specified. -->
+
+<form>
+ <input id=date type=date required>
+ <input id=time type=time required>
+ <input id=datetime-local type=datetime-local required>
+</form>
+
+<script>
+const tabKey = '\uE004';
+const backspace = '\uE003';
+
+promise_test(async () => {
+ const date = document.getElementById('date');
+ assert_false(date.matches(':user-valid'),
+ 'Date input should not match :user-valid at the start of the test.');
+ assert_false(date.matches(':user-invalid'),
+ 'Date input should not match :user-invalid at the start of the test.');
+ assert_equals(date.value, '',
+ 'Date input should not have a value at the start of the test.');
+
+ date.focus();
+ await test_driver.send_keys(date, `1${tabKey}`);
+ assert_equals(date.value, '',
+ 'Date input value should not be set after partially inputting the date.');
+ assert_false(date.matches(':user-valid'),
+ 'Date input should not match :user-valid after partially inputting the date.');
+ assert_false(date.matches(':user-invalid'),
+ 'Date input should not match :user-invalid after partially inputting the date.');
+
+ await test_driver.send_keys(date, `1${tabKey}1234${tabKey}`);
+ assert_equals(date.value, '1234-01-01',
+ 'Date input value should match the testdriver input.');
+ assert_true(date.matches(':user-valid'),
+ 'Date input should match :user-valid after typing in a complete value.');
+ assert_false(date.matches(':user-invalid'),
+ 'Date input should not match :user-invalid after typing in a complete value.');
+
+ date.blur();
+ date.focus();
+ await test_driver.send_keys(date, backspace);
+ assert_equals(date.value, '',
+ 'Date input value should be cleared when deleting one of the sub-values.');
+ assert_false(date.matches(':user-valid'),
+ 'Date input should not match :user-valid after typing in an invalid value.');
+ assert_true(date.matches(':user-invalid'),
+ 'Date input should match :user-invalid after typing in an invalid value.');
+}, '<input type=date> keyboard behavior for :user-valid/:user-invalid.');
+
+promise_test(async () => {
+ const time = document.getElementById('time');
+ assert_false(time.matches(':user-valid'),
+ 'Time input should not match :user-valid at the start of the test.');
+ assert_false(time.matches(':user-invalid'),
+ 'Time input should not match :user-invalid at the start of the test.');
+ assert_equals(time.value, '',
+ 'Time input should not have a value at the start of the test.');
+
+ time.focus();
+ await test_driver.send_keys(time, `1${tabKey}`);
+ assert_equals(time.value, '',
+ 'Time input value should not be set after partially inputting the time.');
+ assert_false(time.matches(':user-valid'),
+ 'Time input should not match :user-valid after partially inputting the time.');
+ assert_false(time.matches(':user-invalid'),
+ 'Time input should not match :user-invalid after partially inputting the time.');
+
+ await test_driver.send_keys(time, `2${tabKey}a${tabKey}`);
+ assert_equals(time.value, '01:02',
+ 'Time input value should match the testdriver input.');
+ assert_true(time.matches(':user-valid'),
+ 'Time input should match :user-valid after typing in a complete value.');
+ assert_false(time.matches(':user-invalid'),
+ 'Time input should not match :user-invalid after typing in a complete value.');
+
+ time.blur();
+ time.focus();
+ await test_driver.send_keys(time, backspace);
+ assert_equals(time.value, '',
+ 'Time input value should be cleared when deleting one of the sub-values.');
+ assert_false(time.matches(':user-valid'),
+ 'Time input should not match :user-valid after typing in an invalid value.');
+ assert_true(time.matches(':user-invalid'),
+ 'Time input should match :user-invalid after typing in an invalid value.');
+}, '<input type=time> keyboard behavior for :user-valid/:user-invalid.');
+
+promise_test(async () => {
+ const dateTimeLocal = document.getElementById('datetime-local');
+ assert_false(dateTimeLocal.matches(':user-valid'),
+ 'Datetime input should not match :user-valid at the start of the test.');
+ assert_false(dateTimeLocal.matches(':user-invalid'),
+ 'Datetime input should not match :user-invalid at the start of the test.');
+ assert_equals(dateTimeLocal.value, '',
+ 'Datetime input should not have a value at the start of the test.');
+
+ dateTimeLocal.focus();
+ await test_driver.send_keys(dateTimeLocal, `1${tabKey}`);
+ assert_equals(dateTimeLocal.value, '',
+ 'Datetime input value should not be set after partially inputting the dateTimeLocal.');
+ assert_false(dateTimeLocal.matches(':user-valid'),
+ 'Datetime input should not match :user-valid after partially inputting the dateTimeLocal.');
+ assert_false(dateTimeLocal.matches(':user-invalid'),
+ 'Datetime input should not match :user-invalid after partially inputting the dateTimeLocal.');
+
+ await test_driver.send_keys(dateTimeLocal, `1${tabKey}1234${tabKey}1${tabKey}2${tabKey}a${tabKey}`);
+ assert_equals(dateTimeLocal.value, '1234-01-01T01:02',
+ 'Datetime input value should match the testdriver input.');
+ assert_true(dateTimeLocal.matches(':user-valid'),
+ 'Datetime input should match :user-valid after typing in a complete value.');
+ assert_false(dateTimeLocal.matches(':user-invalid'),
+ 'Datetime input should not match :user-invalid after typing in a complete value.');
+
+ dateTimeLocal.blur();
+ dateTimeLocal.focus();
+ await test_driver.send_keys(dateTimeLocal, backspace);
+ assert_equals(dateTimeLocal.value, '',
+ 'Datetime input value should be cleared when deleting one of the sub-values.');
+ assert_false(dateTimeLocal.matches(':user-valid'),
+ 'Datetime input should not match :user-valid after typing in an invalid value.');
+ assert_true(dateTimeLocal.matches(':user-invalid'),
+ 'Datetime input should match :user-invalid after typing in an invalid value.');
+}, '<input type=datetime-local> keyboard behavior for :user-valid/:user-invalid.');
+</script>
diff --git a/testing/web-platform/tests/css/selectors/user-valid.html b/testing/web-platform/tests/css/selectors/user-valid.html
index 7a12eb237d..402003ba5e 100644
--- a/testing/web-platform/tests/css/selectors/user-valid.html
+++ b/testing/web-platform/tests/css/selectors/user-valid.html
@@ -29,6 +29,7 @@
<input placeholder="Optional field" id="optional-input"><br>
<textarea placeholder="Optional field" id="optional-textarea"></textarea><br>
<input type="checkbox" id="optional-checkbox"><br>
+ <input type="date" id="optional-date"><br>
<input required placeholder="Required field"> <!-- Prevent the form from navigating with this invalid input -->
<input type="submit" id="submit-button">
<input type="reset" id="reset-button">
@@ -77,12 +78,14 @@ promise_test(async () => {
const optionalInput = document.querySelector("#optional-input");
const optionalTextarea = document.querySelector("#optional-textarea");
const optionalCheckbox = document.querySelector("#optional-checkbox");
+ const optionalDate = document.querySelector("#optional-date");
const submitButton = document.querySelector("#submit-button");
const resetButton = document.querySelector("#reset-button");
assert_true(optionalInput.validity.valid);
assert_true(optionalTextarea.validity.valid);
assert_true(optionalCheckbox.validity.valid);
+ assert_true(optionalDate.validity.valid);
// The selector can't match because no interaction has happened.
assert_false(optionalInput.matches(":user-valid"), "Initially does not match :user-valid");
assert_false(optionalInput.matches(":user-invalid"), "Initially does not match :user-invalid");
@@ -93,6 +96,9 @@ promise_test(async () => {
assert_false(optionalCheckbox.matches(":user-valid"), "Initially does not match :user-valid");
assert_false(optionalCheckbox.matches(":user-invalid"), "Initially does not match :user-invalid");
+ assert_false(optionalDate.matches(":user-valid"), "Initially does not match :user-valid");
+ assert_false(optionalDate.matches(":user-invalid"), "Initially does not match :user-invalid");
+
submitButton.click();
assert_true(optionalInput.matches(":user-valid"), "Submitted the form, input is validated");
@@ -104,6 +110,9 @@ promise_test(async () => {
assert_true(optionalCheckbox.matches(":user-valid"), "Submitted the form, checkbox is validated");
assert_false(optionalCheckbox.matches(":user-invalid"), "Submitted the form, checkbox is validated");
+ assert_true(optionalDate.matches(":user-valid"), "Submitted the form, date is validated");
+ assert_false(optionalDate.matches(":user-invalid"), "Submitted the form, date is validated");
+
resetButton.click();
assert_false(optionalInput.matches(":user-valid"), "Reset the form, user-interacted flag is reset");
@@ -115,6 +124,9 @@ promise_test(async () => {
assert_false(optionalCheckbox.matches(":user-valid"), "Reset the form, user-interacted flag is reset");
assert_false(optionalCheckbox.matches(":user-invalid"), "Reset the form, user-interacted flag is reset");
+ assert_false(optionalDate.matches(":user-valid"), "Reset the form, user-interacted flag is reset");
+ assert_false(optionalDate.matches(":user-invalid"), "Reset the form, user-interacted flag is reset");
+
// Test programmatic form submission with constraint validation.
form.requestSubmit();
@@ -126,6 +138,9 @@ promise_test(async () => {
assert_true(optionalCheckbox.matches(":user-valid"), "Called form.requestSubmit(), checkbox is validated");
assert_false(optionalCheckbox.matches(":user-invalid"), "Called form.requestSubmit(), checkbox is validated");
+
+ assert_true(optionalDate.matches(":user-valid"), "Called form.requestSubmit(), date is validated");
+ assert_false(optionalDate.matches(":user-invalid"), "Called form.requestSubmit(), date is validated");
}, ":user-valid selector properly interacts with submit & reset buttons");
promise_test(async () => {
@@ -151,4 +166,33 @@ promise_test(async () => {
assert_true(checkbox.matches(':user-valid'),
'Checkbox should match :user-valid after clicking once.');
}, 'Checkboxes should match :user-valid after the user clicks on it.');
+
+promise_test(async () => {
+ const date = document.getElementById('optional-date');
+
+ const resetButton = document.getElementById('reset-button');
+ resetButton.click();
+ assert_false(date.matches(':user-valid'),
+ 'Date input should not match :user-valid at the start of the test.');
+ assert_equals(date.value, '',
+ 'Date input should not have a value at the start of the test.');
+
+ date.value = '2024-04-15';
+ assert_false(date.matches(':user-valid'),
+ 'Date input should not match :user-valid after programatically changing value.');
+ date.value = '';
+ assert_false(date.matches(':user-valid'),
+ 'Date input should not match :user-valid after programatically changing value.');
+
+ const tabKey = '\uE004';
+ date.focus();
+ // Press tab twice at the end to make sure that focus has left the input.
+ await test_driver.send_keys(date, `1${tabKey}1${tabKey}1234${tabKey}${tabKey}`);
+ assert_not_equals(document.activeElement, date,
+ 'Pressing tab twice after typing in the date should have blurred the input.');
+ assert_equals(date.value, '1234-01-01',
+ 'Date input value should match the testdriver input.');
+ assert_true(date.matches(':user-valid'),
+ 'Date input should match :user-valid after typing in a value.');
+}, 'Date inputs should match :user-valid after the user types a value into it.');
</script>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/form-associated-callback.html b/testing/web-platform/tests/custom-elements/form-associated/form-associated-callback.html
index 7feec50fef..329c4d7593 100644
--- a/testing/web-platform/tests/custom-elements/form-associated/form-associated-callback.html
+++ b/testing/web-platform/tests/custom-elements/form-associated/form-associated-callback.html
@@ -122,7 +122,7 @@ test(() => {
}
}
customElements.define('will-be-defined', WillBeDefined);
- customElements.upgrade(container);
+ customElements.upgrade($('#container'));
controls = $('#form1').elements;
assert_equals(controls.length, 3, 'form.elements.length');
diff --git a/testing/web-platform/tests/deprecation-reporting/__dir__.ini b/testing/web-platform/tests/deprecation-reporting/__dir__.ini
new file mode 100644
index 0000000000..79ef6f271a
--- /dev/null
+++ b/testing/web-platform/tests/deprecation-reporting/__dir__.ini
@@ -0,0 +1 @@
+implementation-status: not-implementing \ No newline at end of file
diff --git a/testing/web-platform/tests/device-posture/device-posture-change-event.https.html b/testing/web-platform/tests/device-posture/device-posture-change-event.https.html
new file mode 100644
index 0000000000..eb2fc2d96f
--- /dev/null
+++ b/testing/web-platform/tests/device-posture/device-posture-change-event.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+'use strict';
+
+promise_test(async (t) => {
+ t.add_cleanup(async () => {
+ await test_driver.clear_device_posture();
+ });
+ const watcher = new EventWatcher(t, navigator.devicePosture, ['change']);
+ const postures = ['folded', 'continuous', 'folded'];
+ for (const posture of postures) {
+ await Promise.all([
+ watcher.wait_for('change'),
+ test_driver.set_device_posture(posture)
+ ]);
+ assert_equals(posture, navigator.devicePosture.type);
+ }
+}, 'Tests the Device Posture API change event handler.');
+</script>
diff --git a/testing/web-platform/tests/device-posture/device-posture-clear.https.html b/testing/web-platform/tests/device-posture/device-posture-clear.https.html
new file mode 100644
index 0000000000..319cd7266a
--- /dev/null
+++ b/testing/web-platform/tests/device-posture/device-posture-clear.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+'use strict';
+
+promise_test(async (t) => {
+ t.add_cleanup(async () => {
+ await test_driver.clear_device_posture();
+ });
+ const originalPosture = navigator.devicePosture.type;
+ const posture = originalPosture ? 'folded' : 'continuous';
+
+ const watcher = new EventWatcher(t, navigator.devicePosture, ['change']);
+ await Promise.all([
+ watcher.wait_for('change'),
+ test_driver.set_device_posture(posture)
+ ]);
+ assert_equals(navigator.devicePosture.type, posture);
+
+ await Promise.all([
+ watcher.wait_for('change'),
+ test_driver.clear_device_posture()
+ ]);
+ assert_equals(navigator.devicePosture.type, originalPosture);
+}, 'Tests that device posture override can be removed.');
+</script>
diff --git a/testing/web-platform/tests/device-posture/device-posture-event-listener.https.html b/testing/web-platform/tests/device-posture/device-posture-event-listener.https.html
new file mode 100644
index 0000000000..f4e21c89cc
--- /dev/null
+++ b/testing/web-platform/tests/device-posture/device-posture-event-listener.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+'use strict';
+
+promise_test(async (t) => {
+ t.add_cleanup(async () => {
+ await test_driver.clear_device_posture();
+ });
+ assert_equals(navigator.devicePosture.type, 'continuous');
+
+ const promise = new Promise(resolve => {
+ navigator.devicePosture.addEventListener(
+ 'change',
+ () => { resolve(navigator.devicePosture.type); },
+ { once: true }
+ );
+ });
+ await test_driver.set_device_posture('folded');
+ assert_equals(await promise, 'folded');
+}, 'Tests the Device Posture API addEventListener change event handler.');
+</script>
diff --git a/testing/web-platform/tests/device-posture/device-posture-media-queries.https.html b/testing/web-platform/tests/device-posture/device-posture-media-queries.https.html
new file mode 100644
index 0000000000..e4dbd2e7d8
--- /dev/null
+++ b/testing/web-platform/tests/device-posture/device-posture-media-queries.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+'use strict';
+
+promise_test(async (t) => {
+ t.add_cleanup(async () => {
+ await test_driver.clear_device_posture();
+ });
+ assert_equals(navigator.devicePosture.type, 'continuous');
+ assert_true(matchMedia('(device-posture: continuous)').matches);
+
+ const foldedMQL = window.matchMedia('(device-posture: folded)');
+ const promise = new Promise(resolve => {
+ foldedMQL.addEventListener(
+ 'change',
+ () => { resolve(foldedMQL.matches); },
+ { once: true }
+ );
+ });
+ await test_driver.set_device_posture('folded');
+ assert_true(await promise);
+}, 'Tests the Device Posture API Media Query change event handler.');
+</script>
diff --git a/testing/web-platform/tests/docs/admin/pywebsocket3.rst b/testing/web-platform/tests/docs/admin/pywebsocket3.rst
index af768d6b35..3f742958b3 100644
--- a/testing/web-platform/tests/docs/admin/pywebsocket3.rst
+++ b/testing/web-platform/tests/docs/admin/pywebsocket3.rst
@@ -4,88 +4,88 @@ pywebsocket3: A Standalone WebSocket Server for testing purposes
.. contents::
:local:
-:mod:`mod_pywebsocket`
+:mod:`pywebsocket3`
---------------------------------------------
-.. automodule:: mod_pywebsocket
+.. automodule:: pywebsocket3
:members:
-:mod:`mod_pywebsocket.common`
+:mod:`pywebsocket3.common`
---------------------------------------------
-.. automodule:: mod_pywebsocket.common
+.. automodule:: pywebsocket3.common
:members:
-:mod:`mod_pywebsocket.dispatch`
+:mod:`pywebsocket3.dispatch`
---------------------------------------------
-.. automodule:: mod_pywebsocket.dispatch
+.. automodule:: pywebsocket3.dispatch
:members:
-:mod:`mod_pywebsocket.extensions`
+:mod:`pywebsocket3.extensions`
---------------------------------------------
-.. automodule:: mod_pywebsocket.extensions
+.. automodule:: pywebsocket3.extensions
:members:
-:mod:`mod_pywebsocket.handshake`
+:mod:`pywebsocket3.handshake`
---------------------------------------------
-.. automodule:: mod_pywebsocket.handshake
+.. automodule:: pywebsocket3.handshake
:members:
:imported-members:
-:mod:`mod_pywebsocket.request_handler`
+:mod:`pywebsocket3.request_handler`
---------------------------------------------
-.. automodule:: mod_pywebsocket.request_handler
+.. automodule:: pywebsocket3.request_handler
:members:
-:mod:`mod_pywebsocket.stream`
+:mod:`pywebsocket3.stream`
---------------------------------------------
-.. automodule:: mod_pywebsocket.stream
+.. automodule:: pywebsocket3.stream
:members:
:imported-members:
-:mod:`mod_pywebsocket.http_header_util`
+:mod:`pywebsocket3.http_header_util`
---------------------------------------------
-.. automodule:: mod_pywebsocket.http_header_util
+.. automodule:: pywebsocket3.http_header_util
:members:
-:mod:`mod_pywebsocket.msgutil`
+:mod:`pywebsocket3.msgutil`
---------------------------------------------
-.. automodule:: mod_pywebsocket.msgutil
+.. automodule:: pywebsocket3.msgutil
:members:
-:mod:`mod_pywebsocket.util`
+:mod:`pywebsocket3.util`
---------------------------------------------
-.. automodule:: mod_pywebsocket.util
+.. automodule:: pywebsocket3.util
:members:
-:mod:`mod_pywebsocket.memorizingfile`
+:mod:`pywebsocket3.memorizingfile`
---------------------------------------------
-.. automodule:: mod_pywebsocket.memorizingfile
+.. automodule:: pywebsocket3.memorizingfile
:members:
-:mod:`mod_pywebsocket.websocket_server`
+:mod:`pywebsocket3.websocket_server`
---------------------------------------------
-.. automodule:: mod_pywebsocket.websocket_server
+.. automodule:: pywebsocket3.websocket_server
:members:
-:mod:`mod_pywebsocket.server_util`
+:mod:`pywebsocket3.server_util`
---------------------------------------------
-.. automodule:: mod_pywebsocket.server_util
+.. automodule:: pywebsocket3.server_util
:members:
-:mod:`mod_pywebsocket.standalone`
+:mod:`pywebsocket3.standalone`
---------------------------------------------
-.. automodule:: mod_pywebsocket.standalone
+.. automodule:: pywebsocket3.standalone
:members:
diff --git a/testing/web-platform/tests/docs/running-tests/from-local-system.md b/testing/web-platform/tests/docs/running-tests/from-local-system.md
index 89533ee210..0d524ea356 100644
--- a/testing/web-platform/tests/docs/running-tests/from-local-system.md
+++ b/testing/web-platform/tests/docs/running-tests/from-local-system.md
@@ -16,7 +16,7 @@ cd wpt
Running the tests requires `python` and `pip` as well as updating the
system `hosts` file.
-WPT requires Python 3.7 or higher.
+WPT requires Python 3.8 or higher.
The required setup is different depending on your operating system.
diff --git a/testing/web-platform/tests/docs/writing-tests/file-names.md b/testing/web-platform/tests/docs/writing-tests/file-names.md
index 96296c4ff6..5336bfb528 100644
--- a/testing/web-platform/tests/docs/writing-tests/file-names.md
+++ b/testing/web-platform/tests/docs/writing-tests/file-names.md
@@ -11,10 +11,10 @@ These are individually documented for each flag that supports it.
### Test Type
-These flags must be the last element in the filename before the
-extension e.g. `foo-manual.html` will indicate a manual test, but
-`foo-manual-other.html` will not. Unlike test features, test types
-are mutually exclusive.
+These flags are preceded by a `-` and followed by a `.`, and must be the
+last such element in the filename, e.g. `foo-manual.html` will indicate
+a manual test, but `foo-manual-other.html` will not. Unlike test features,
+test types are mutually exclusive.
`-manual`
@@ -26,8 +26,8 @@ are mutually exclusive.
### Test Features
-These flags are preceded by a `.` in the filename, and must
-themselves precede any test type flag, but are otherwise unordered.
+These flags are preceded and followed by a `.` in the filename, and must themselves
+go after any test type flag, but are otherwise unordered.
`.https`
diff --git a/testing/web-platform/tests/docs/writing-tests/testdriver.md b/testing/web-platform/tests/docs/writing-tests/testdriver.md
index 73af3bb3c8..fcf0199bad 100644
--- a/testing/web-platform/tests/docs/writing-tests/testdriver.md
+++ b/testing/web-platform/tests/docs/writing-tests/testdriver.md
@@ -119,6 +119,12 @@ the global scope.
.. js:autofunction:: test_driver.get_virtual_sensor_information
```
+### Device Posture ###
+```eval_rst
+.. js:autofunction:: test_driver.set_device_posture
+.. js:autofunction:: test_driver.clear_device_posture
+```
+
### Using test_driver in other browsing contexts ###
Testdriver can be used in browsing contexts (i.e. windows or frames)
diff --git a/testing/web-platform/tests/dom/events/scrolling/WEB_FEATURES.yml b/testing/web-platform/tests/dom/events/scrolling/WEB_FEATURES.yml
new file mode 100644
index 0000000000..e4fba841b6
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: scrollend
+ files:
+ - scrollend-*
diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-append-meta-referrer-and-script-from-fragment.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-append-meta-referrer-and-script-from-fragment.tentative.html
index d247797603..e80e3b45b6 100644
--- a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-append-meta-referrer-and-script-from-fragment.tentative.html
+++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-append-meta-referrer-and-script-from-fragment.tentative.html
@@ -5,25 +5,37 @@
<script src=/resources/testharnessreport.js></script>
<script>
promise_test(async t => {
- const script = document.createElement("script");
- const meta = document.createElement("meta");
- meta.name = "referrer";
- meta.content = "no-referrer";
- const fragment = new DocumentFragment();
- const done = new Promise(resolve => {
- window.didFetch = resolve;
- });
- script.textContent = `
- (async function() {
- const response = await fetch("/html/infrastructure/urls/terminology-0/resources/echo-referrer-text.py")
- const text = await response.text();
- window.didFetch(text);
- })();
- `;
- fragment.append(script, meta);
- document.head.append(fragment);
- const result = await done;
- assert_equals(result, "");
-}, "<meta name=referrer> should apply before script, as it is an insertion step " +
- "and not a post-insertion step");
+ const preMetaScript = document.createElement("script");
+ preMetaScript.textContent = `
+ window.preMetaScriptPromise = fetch('/html/infrastructure/urls/terminology-0/resources/echo-referrer-text.py')
+ .then(response => response.text());
+ `;
+
+ const meta = document.createElement("meta");
+ meta.name = "referrer";
+ meta.content = "no-referrer";
+
+ const postMetaScript = document.createElement("script");
+ postMetaScript.textContent = `
+ window.postMetaScriptPromise = fetch('/html/infrastructure/urls/terminology-0/resources/echo-referrer-text.py')
+ .then(response => response.text());
+ `;
+
+ const fragment = new DocumentFragment();
+ fragment.append(preMetaScript, meta, postMetaScript);
+ document.head.append(fragment);
+
+ const preMetaReferrer = await window.preMetaScriptPromise;
+ assert_equals(preMetaReferrer, location.href,
+ "preMetaReferrer is the full URL; by the time the first script runs in " +
+ "its post-insertion steps, the later-inserted meta tag has not run its " +
+ "post-insertion steps, which is where meta tags are processed");
+
+ const postMetaReferrer = await window.postMetaScriptPromise;
+ assert_equals(postMetaReferrer, "",
+ "postMetaReferrer is empty; by the time the second script runs in " +
+ "its post-insertion steps, the later-inserted meta tag has run its " +
+ "post-insertion steps, and observes the meta tag's effect");
+}, "<meta name=referrer> gets processed and applied in the post-insertion " +
+ "steps");
</script>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/Node-moveBefore.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/Node-moveBefore.html
new file mode 100644
index 0000000000..8a1db6f93b
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/Node-moveBefore.html
@@ -0,0 +1,297 @@
+<!DOCTYPE html>
+<title>Node.moveBefore</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<!-- First test shared pre-insertion checks that work similarly for replaceChild
+ and moveBefore -->
+<script>
+ var insertFunc = Node.prototype.moveBefore;
+</script>
+<script src="../../pre-insertion-validation-notfound.js"></script>
+<script src="../../pre-insertion-validation-hierarchy.js"></script>
+<script>
+preInsertionValidateHierarchy("moveBefore");
+
+function testLeafNode(nodeName, createNodeFunction) {
+ test(function() {
+ var node = createNodeFunction();
+ assert_throws_js(TypeError, function() { node.moveBefore(null, null) })
+ }, "Calling moveBefore with a non-Node first argument on a leaf node " + nodeName + " must throw TypeError.")
+ test(function() {
+ var node = createNodeFunction();
+ assert_throws_dom("HIERARCHY_REQUEST_ERR", function() { node.moveBefore(document.createTextNode("fail"), null) })
+ // Would be step 2.
+ assert_throws_dom("HIERARCHY_REQUEST_ERR", function() { node.moveBefore(node, null) })
+ // Would be step 3.
+ assert_throws_dom("HIERARCHY_REQUEST_ERR", function() { node.moveBefore(node, document.createTextNode("child")) })
+ }, "Calling moveBefore an a leaf node " + nodeName + " must throw HIERARCHY_REQUEST_ERR.")
+}
+
+test(function() {
+ // WebIDL: first argument.
+ assert_throws_js(TypeError, function() { document.body.moveBefore(null, null) })
+ assert_throws_js(TypeError, function() { document.body.moveBefore(null, document.body.firstChild) })
+ assert_throws_js(TypeError, function() { document.body.moveBefore({'a':'b'}, document.body.firstChild) })
+}, "Calling moveBefore with a non-Node first argument must throw TypeError.")
+
+test(function() {
+ // WebIDL: second argument.
+ assert_throws_js(TypeError, function() { document.body.moveBefore(document.createTextNode("child")) })
+ assert_throws_js(TypeError, function() { document.body.moveBefore(document.createTextNode("child"), {'a':'b'}) })
+}, "Calling moveBefore with second argument missing, or other than Node, null, or undefined, must throw TypeError.")
+
+testLeafNode("DocumentType", function () { return document.doctype; } )
+testLeafNode("Text", function () { return document.createTextNode("Foo") })
+testLeafNode("Comment", function () { return document.createComment("Foo") })
+testLeafNode("ProcessingInstruction", function () { return document.createProcessingInstruction("foo", "bar") })
+
+test(function() {
+ // Step 2.
+ assert_throws_dom("HIERARCHY_REQUEST_ERR", function() { document.body.moveBefore(document.body, document.getElementById("log")) })
+ assert_throws_dom("HIERARCHY_REQUEST_ERR", function() { document.body.moveBefore(document.documentElement, document.getElementById("log")) })
+}, "Calling moveBefore with an inclusive ancestor of the context object must throw HIERARCHY_REQUEST_ERR.")
+
+// Step 3.
+test(function() {
+ var a = document.createElement("div");
+ var b = document.createElement("div");
+ var c = document.createElement("div");
+ assert_throws_dom("NotFoundError", function() {
+ a.moveBefore(b, c);
+ });
+}, "Calling moveBefore with a reference child whose parent is not the context node must throw a NotFoundError.")
+
+// Step 4.1.
+test(function() {
+ var doc = document.implementation.createHTMLDocument("title");
+ var doc2 = document.implementation.createHTMLDocument("title2");
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(doc2, doc.documentElement);
+ });
+
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(doc.createTextNode("text"), doc.documentElement);
+ });
+}, "If the context node is a document, inserting a document or text node should throw a HierarchyRequestError.")
+
+// Step 4.2.1.
+test(function() {
+ var doc = document.implementation.createHTMLDocument("title");
+ doc.removeChild(doc.documentElement);
+
+ var df = doc.createDocumentFragment();
+ df.appendChild(doc.createElement("a"));
+ df.appendChild(doc.createElement("b"));
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(df, doc.firstChild);
+ });
+
+ df = doc.createDocumentFragment();
+ df.appendChild(doc.createTextNode("text"));
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(df, doc.firstChild);
+ });
+
+ df = doc.createDocumentFragment();
+ df.appendChild(doc.createComment("comment"));
+ df.appendChild(doc.createTextNode("text"));
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(df, doc.firstChild);
+ });
+}, "If the context node is a document, inserting a DocumentFragment that contains a text node or too many elements should throw a HierarchyRequestError.")
+
+// Step 4.2.2.
+test(function() {
+ // The context node has an element child.
+ var doc = document.implementation.createHTMLDocument("title");
+ var comment = doc.appendChild(doc.createComment("foo"));
+ assert_array_equals(doc.childNodes, [doc.doctype, doc.documentElement, comment]);
+
+ var df = doc.createDocumentFragment();
+ df.appendChild(doc.createElement("a"));
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(df, doc.doctype);
+ });
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(df, doc.documentElement);
+ });
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(df, comment);
+ });
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(df, null);
+ });
+}, "If the context node is a document, inserting a DocumentFragment with an element if there already is an element child should throw a HierarchyRequestError.")
+test(function() {
+ // /child/ is a doctype.
+ var doc = document.implementation.createHTMLDocument("title");
+ var comment = doc.moveBefore(doc.createComment("foo"), doc.firstChild);
+ doc.removeChild(doc.documentElement);
+ assert_array_equals(doc.childNodes, [comment, doc.doctype]);
+
+ var df = doc.createDocumentFragment();
+ df.appendChild(doc.createElement("a"));
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(df, doc.doctype);
+ });
+}, "If the context node is a document and a doctype is following the reference child, inserting a DocumentFragment with an element should throw a HierarchyRequestError.")
+test(function() {
+ // /child/ is not null and a doctype is following /child/.
+ var doc = document.implementation.createHTMLDocument("title");
+ var comment = doc.moveBefore(doc.createComment("foo"), doc.firstChild);
+ doc.removeChild(doc.documentElement);
+ assert_array_equals(doc.childNodes, [comment, doc.doctype]);
+
+ var df = doc.createDocumentFragment();
+ df.appendChild(doc.createElement("a"));
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(df, comment);
+ });
+}, "If the context node is a document, inserting a DocumentFragment with an element before the doctype should throw a HierarchyRequestError.")
+
+// Step 4.3.
+test(function() {
+ // The context node has an element child.
+ var doc = document.implementation.createHTMLDocument("title");
+ var comment = doc.appendChild(doc.createComment("foo"));
+ assert_array_equals(doc.childNodes, [doc.doctype, doc.documentElement, comment]);
+
+ var a = doc.createElement("a");
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(a, doc.doctype);
+ });
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(a, doc.documentElement);
+ });
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(a, comment);
+ });
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(a, null);
+ });
+}, "If the context node is a document, inserting an element if there already is an element child should throw a HierarchyRequestError.")
+test(function() {
+ // /child/ is a doctype.
+ var doc = document.implementation.createHTMLDocument("title");
+ var comment = doc.moveBefore(doc.createComment("foo"), doc.firstChild);
+ doc.removeChild(doc.documentElement);
+ assert_array_equals(doc.childNodes, [comment, doc.doctype]);
+
+ var a = doc.createElement("a");
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(a, doc.doctype);
+ });
+}, "If the context node is a document, inserting an element before the doctype should throw a HierarchyRequestError.")
+test(function() {
+ // /child/ is not null and a doctype is following /child/.
+ var doc = document.implementation.createHTMLDocument("title");
+ var comment = doc.moveBefore(doc.createComment("foo"), doc.firstChild);
+ doc.removeChild(doc.documentElement);
+ assert_array_equals(doc.childNodes, [comment, doc.doctype]);
+
+ var a = doc.createElement("a");
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(a, comment);
+ });
+}, "If the context node is a document and a doctype is following the reference child, inserting an element should throw a HierarchyRequestError.")
+
+// Step 4.4.
+test(function() {
+ var doc = document.implementation.createHTMLDocument("title");
+ var comment = doc.moveBefore(doc.createComment("foo"), doc.firstChild);
+ assert_array_equals(doc.childNodes, [comment, doc.doctype, doc.documentElement]);
+
+ var doctype = document.implementation.createDocumentType("html", "", "");
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(doctype, comment);
+ });
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(doctype, doc.doctype);
+ });
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(doctype, doc.documentElement);
+ });
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(doctype, null);
+ });
+}, "If the context node is a document, inserting a doctype if there already is a doctype child should throw a HierarchyRequestError.")
+test(function() {
+ var doc = document.implementation.createHTMLDocument("title");
+ var comment = doc.appendChild(doc.createComment("foo"));
+ doc.removeChild(doc.doctype);
+ assert_array_equals(doc.childNodes, [doc.documentElement, comment]);
+
+ var doctype = document.implementation.createDocumentType("html", "", "");
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(doctype, comment);
+ });
+}, "If the context node is a document, inserting a doctype after the document element should throw a HierarchyRequestError.")
+test(function() {
+ var doc = document.implementation.createHTMLDocument("title");
+ var comment = doc.appendChild(doc.createComment("foo"));
+ doc.removeChild(doc.doctype);
+ assert_array_equals(doc.childNodes, [doc.documentElement, comment]);
+
+ var doctype = document.implementation.createDocumentType("html", "", "");
+ assert_throws_dom("HierarchyRequestError", function() {
+ doc.moveBefore(doctype, null);
+ });
+}, "If the context node is a document with and element child, appending a doctype should throw a HierarchyRequestError.")
+
+// Step 5.
+test(function() {
+ var df = document.createDocumentFragment();
+ var a = df.appendChild(document.createElement("a"));
+
+ var doc = document.implementation.createHTMLDocument("title");
+ assert_throws_dom("HierarchyRequestError", function() {
+ df.moveBefore(doc, a);
+ });
+ assert_throws_dom("HierarchyRequestError", function() {
+ df.moveBefore(doc, null);
+ });
+
+ var doctype = document.implementation.createDocumentType("html", "", "");
+ assert_throws_dom("HierarchyRequestError", function() {
+ df.moveBefore(doctype, a);
+ });
+ assert_throws_dom("HierarchyRequestError", function() {
+ df.moveBefore(doctype, null);
+ });
+}, "If the context node is a DocumentFragment, inserting a document or a doctype should throw a HierarchyRequestError.")
+test(function() {
+ var el = document.createElement("div");
+ var a = el.appendChild(document.createElement("a"));
+
+ var doc = document.implementation.createHTMLDocument("title");
+ assert_throws_dom("HierarchyRequestError", function() {
+ el.moveBefore(doc, a);
+ });
+ assert_throws_dom("HierarchyRequestError", function() {
+ el.moveBefore(doc, null);
+ });
+
+ var doctype = document.implementation.createDocumentType("html", "", "");
+ assert_throws_dom("HierarchyRequestError", function() {
+ el.moveBefore(doctype, a);
+ });
+ assert_throws_dom("HierarchyRequestError", function() {
+ el.moveBefore(doctype, null);
+ });
+}, "If the context node is an element, inserting a document or a doctype should throw a HierarchyRequestError.")
+
+// Step 7.
+test(function() {
+ var a = document.createElement("div");
+ var b = document.createElement("div");
+ var c = document.createElement("div");
+ a.appendChild(b);
+ a.appendChild(c);
+ assert_array_equals(a.childNodes, [b, c]);
+ assert_equals(a.moveBefore(b, b), b);
+ assert_array_equals(a.childNodes, [b, c]);
+ assert_equals(a.moveBefore(c, c), c);
+ assert_array_equals(a.childNodes, [b, c]);
+}, "Inserting a node before itself should not move the node");
+</script>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/chrome-338071841-crash.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/chrome-338071841-crash.html
new file mode 100644
index 0000000000..26adfb1cbf
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/chrome-338071841-crash.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<link rel="help" href="https://crbug.com/338071841">
+<div id="p"><span></span><!-- comment --></div>
+<script>
+ p.moveBefore(p.lastChild, p.firstChild);
+</script>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-animation-left.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-animation-left.html
new file mode 100644
index 0000000000..8c7f73e3c9
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-animation-left.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<title>Node.moveBefore should preserve CSS animation state (left)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+ <section id="old-parent">
+ <div id="item"></div>
+ </section>
+ <section id="new-parent">
+ </section>
+ <style>
+ @keyframes anim {
+ from {
+ left: 100px;
+ }
+
+ to {
+ left: 400px;
+ }
+ }
+
+ section {
+ position: absolute;
+ }
+
+ #item {
+ position: relative;
+ width: 100px;
+ height: 100px;
+ background: green;
+ animation: 1s linear infinite alternate anim;
+ animation-delay: 100ms;
+ }
+ </style>
+ <script>
+ promise_test(async t => {
+ const item = document.querySelector("#item");
+ let num_events = 0;
+ await new Promise(resolve => addEventListener("animationstart", () => {
+ num_events++;
+ resolve();
+ }));
+
+ // Reparent item
+ document.body.querySelector("#new-parent").moveBefore(item, null);
+ await new Promise(resolve => requestAnimationFrame(() => resolve()));
+ assert_equals(num_events, 1);
+ assert_not_equals(getComputedStyle(item).left, "0px");
+ });
+ </script>
+</body>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-animation-transform.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-animation-transform.html
new file mode 100644
index 0000000000..e7a285893a
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-animation-transform.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>Node.moveBefore should preserve CSS animation state (transform)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+ <section id="old-parent">
+ <div id="item"></div>
+ </section>
+ <section id="new-parent">
+ </section>
+ <style>
+ @keyframes anim {
+ from {
+ transform: translateX(100px);
+ }
+
+ to {
+ transform: translateX(400px);
+ }
+ }
+
+ #item {
+ position: relative;
+ width: 100px;
+ height: 100px;
+ background: green;
+ animation: 1s linear infinite alternate anim;
+ animation-delay: 100ms;
+ }
+ </style>
+ <script>
+ promise_test(async t => {
+ const item = document.querySelector("#item");
+ let num_events = 0;
+ await new Promise(resolve => addEventListener("animationstart", () => {
+ num_events++;
+ resolve();
+ }));
+
+ // Reparent item
+ document.body.querySelector("#new-parent").moveBefore(item, null);
+ await new Promise(resolve => requestAnimationFrame(() => resolve()));
+ assert_equals(num_events, 1);
+ assert_not_equals(getComputedStyle(item).transform, "none");
+ });
+ </script>
+</body>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-left-pseudo.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-left-pseudo.html
new file mode 100644
index 0000000000..fa51b16887
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-left-pseudo.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<title>Node.moveBefore should preserve CSS transition state on pseudo-elements (left)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+ <section id="old-parent">
+ <div id="item"></div>
+ </section>
+ <section id="new-parent">
+ </section>
+ <style>
+ #item {
+ width: 100px;
+ height: 100px;
+ background: green;
+ position: absolute;
+ left: 0;
+ }
+
+ #item::before {
+ content: "Foo";
+ width: 100px;
+ height: 100px;
+ background: green;
+ transition: left 60s steps(1, jump-both);
+ left: 0px;
+ position: absolute;
+ }
+
+ #item.big::before {
+ left: 400px;
+ }
+
+ section {
+ position: relative;
+ }
+
+ body {
+ margin-left: 0;
+ }
+ </style>
+ <script>
+ promise_test(async t => {
+ const item = document.querySelector("#item");
+ assert_equals(item.getBoundingClientRect().x, 0);
+ item.classList.add("big");
+ await new Promise(resolve => item.addEventListener("transitionstart", resolve));
+ document.querySelector("#new-parent").moveBefore(item, null);
+ await new Promise(resolve => requestAnimationFrame(() => resolve()));
+ assert_equals(getComputedStyle(item, "::before").left, "200px");
+ });
+ </script>
+</body>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-left.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-left.html
new file mode 100644
index 0000000000..2b8e04b26e
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-left.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>Node.moveBefore should preserve CSS transition state (left)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+ <section id="old-parent">
+ <div id="item"></div>
+ </section>
+ <section id="new-parent">
+ </section>
+ <style>
+ #item {
+ width: 100px;
+ height: 100px;
+ background: green;
+ transition: left 10s;
+ position: absolute;
+ left: 0;
+ }
+
+ section {
+ position: relative;
+ }
+
+ body {
+ margin-left: 0;
+ }
+ </style>
+ <script>
+ promise_test(async t => {
+ const item = document.querySelector("#item");
+ assert_equals(item.getBoundingClientRect().x, 0);
+ item.style.left = "400px";
+ await new Promise(resolve => item.addEventListener("transitionstart", resolve));
+ document.querySelector("#new-parent").moveBefore(item, null);
+ await new Promise(resolve => requestAnimationFrame(() => resolve()));
+ assert_less_than(item.getBoundingClientRect().x, 399);
+ });
+ </script>
+</body>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-transform-pseudo.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-transform-pseudo.html
new file mode 100644
index 0000000000..d02c72561c
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-transform-pseudo.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<title>Node.moveBefore should preserve CSS transition state on pseudo-elements (transform)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+ <section id="old-parent">
+ <div id="item"></div>
+ </section>
+ <section id="new-parent">
+ </section>
+ <style>
+ #item {
+ width: 100px;
+ height: 100px;
+ background: green;
+ position: absolute;
+ left: 0;
+ }
+
+ #item::before {
+ content: "Foo";
+ width: 100px;
+ height: 100px;
+ background: green;
+ transition: transform 60s steps(1, jump-both);
+ transform: none;
+ position: absolute;
+ }
+
+ #item.big::before {
+ transform: translateX(400px);
+ }
+
+ section {
+ position: relative;
+ }
+
+ body {
+ margin-left: 0;
+ }
+ </style>
+ <script>
+ promise_test(async t => {
+ const item = document.querySelector("#item");
+ assert_equals(item.getBoundingClientRect().x, 0);
+ item.classList.add("big");
+ await new Promise(resolve => item.addEventListener("transitionstart", resolve));
+ document.querySelector("#new-parent").moveBefore(item, null);
+ await new Promise(resolve => requestAnimationFrame(() => resolve()));
+ assert_not_equals(getComputedStyle(item, "::before").transform, "none");
+ });
+ </script>
+</body>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-transform.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-transform.html
new file mode 100644
index 0000000000..f09edca144
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/continue-css-transition-transform.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<title>Node.moveBefore should preserve CSS transition state (transform)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+ <section id="old-parent">
+ <div id="item"></div>
+ </section>
+ <section id="new-parent">
+ </section>
+ <style>
+ #item {
+ width: 100px;
+ height: 100px;
+ background: green;
+ transition: transform 60s steps(1, jump-both);
+ }
+
+ body {
+ margin-left: 0;
+ }
+ </style>
+ <script>
+ promise_test(async t => {
+ const item = document.querySelector("#item");
+ assert_equals(item.getBoundingClientRect().x, 0);
+ item.style.transform = "translateX(400px)";
+ await new Promise(resolve => item.addEventListener("transitionstart", resolve));
+ document.querySelector("#new-parent").moveBefore(item, null);
+ await new Promise(resolve => requestAnimationFrame(() => resolve()));
+ assert_equals(item.getBoundingClientRect().x, 200);
+ assert_equals(item.getAnimations().length, 1);
+ });
+ </script>
+</body>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-animation-commit-styles.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-animation-commit-styles.html
new file mode 100644
index 0000000000..86bb7c33e4
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-animation-commit-styles.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<title>Calling commitStyles after Node.moveBefore should commit mid-transition value</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+ <section id="old-parent">
+ <div id="item"></div>
+ </section>
+ <section id="new-parent">
+ </section>
+ <style>
+ @keyframes anim {
+ from {
+ transform: translateX(100px);
+ }
+
+ to {
+ transform: translateX(400px);
+ }
+ }
+
+ #item {
+ position: relative;
+ width: 100px;
+ height: 100px;
+ background: green;
+ animation: 1s linear infinite alternate anim;
+ animation-delay: 100ms;
+ }
+ </style>
+ <script>
+ promise_test(async t => {
+ const item = document.querySelector("#item");
+ await new Promise(resolve => item.addEventListener("animationstart", resolve));
+
+ // Reparent item
+ document.body.querySelector("#new-parent").moveBefore(item, null);
+
+ item.getAnimations()[0].commitStyles();
+ assert_true("transform" in item.style);
+ assert_not_equals(item.style.transform, "none");
+ });
+ </script>
+</body>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-cross-document.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-cross-document.html
new file mode 100644
index 0000000000..f3c8fafbfa
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-cross-document.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<title>Node.moveBefore should not preserve CSS transition state when crossing document boundaries</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+ <iframe id="iframe">
+ </iframe>
+ <section id="new-parent">
+ </section>
+ <style id="style">
+ #item {
+ width: 100px;
+ height: 100px;
+ background: green;
+ transition: left 10s;
+ position: absolute;
+ left: 0;
+ }
+
+ section {
+ position: relative;
+ }
+
+ body {
+ margin-left: 0;
+ }
+ </style>
+ <script>
+ promise_test(async t => {
+ const iframe = document.querySelector("#iframe");
+ const style = document.querySelector("#style");
+ iframe.contentDocument.head.append(style.cloneNode(true));
+ const item = iframe.contentDocument.createElement("div");
+ item.id = "item";
+ iframe.contentDocument.body.append(item);
+ assert_equals(item.getBoundingClientRect().x, 0);
+ item.style.left = "400px";
+ await new Promise(resolve => item.addEventListener("transitionstart", resolve));
+ document.querySelector("#new-parent").moveBefore(item, null);
+ await new Promise(resolve => requestAnimationFrame(() => resolve()));
+ assert_greater_than(item.getBoundingClientRect().x, 399);
+ }, "Moving a transition across documents should reset its state");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-cross-shadow.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-cross-shadow.html
new file mode 100644
index 0000000000..145f40ba50
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-cross-shadow.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Node.moveBefore should not preserve CSS transition state when crossing shadow boundaries</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+ <section id="old-parent">
+ <div id="item"></div>
+ </section>
+ <div id="shadow-container">
+ <template shadowrootmode="open">
+ <style>
+ #item {
+ width: 100px;
+ height: 100px;
+ background: green;
+ transition: left 10s;
+ position: absolute;
+ left: 0;
+ }
+
+ section {
+ position: relative;
+ }
+
+ body {
+ margin-left: 0;
+ }
+ </style>
+ <section id="new-parent">
+ </section>
+ </template>
+ </div>
+ <style>
+ #item {
+ width: 100px;
+ height: 100px;
+ background: green;
+ transition: left 10s;
+ position: absolute;
+ left: 0;
+ }
+
+ section {
+ position: relative;
+ }
+
+ body {
+ margin-left: 0;
+ }
+ </style>
+ <script>
+ promise_test(async t => {
+ const item = document.querySelector("#item");
+ assert_equals(item.getBoundingClientRect().x, 0);
+ item.style.left = "400px";
+ await new Promise(resolve => item.addEventListener("transitionstart", resolve));
+ const shadowContainer = document.querySelector("#shadow-container");
+ shadowContainer.shadowRoot.querySelector("#new-parent").moveBefore(item, null);
+ await new Promise(resolve => requestAnimationFrame(() => resolve()));
+ assert_greater_than(item.getBoundingClientRect().x, 399);
+ }, "Moving an element with a transition across shadow boundaries should reset the transition");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-to-disconnected-document.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-to-disconnected-document.html
new file mode 100644
index 0000000000..537edfe9b6
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-to-disconnected-document.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<title>Node.moveBefore should act like insertBefore when moving to a disconnected document</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+ <section id="old-parent">
+ <div id="item"></div>
+ </section>
+ <section id="new-parent">
+ </section>
+ <style>
+ #item {
+ width: 100px;
+ height: 100px;
+ background: green;
+ transition: left 10s;
+ position: absolute;
+ left: 0;
+ }
+
+ section {
+ position: relative;
+ }
+
+ body {
+ margin-left: 0;
+ }
+ </style>
+
+ <script>
+ promise_test(async t => {
+ const item = document.querySelector("#item");
+ assert_equals(item.getBoundingClientRect().x, 0);
+ item.style.left = "400px";
+ await new Promise(resolve => item.addEventListener("transitionstart", resolve));
+ const doc = document.implementation.createHTMLDocument();
+ doc.body.moveBefore(item, null);
+ await new Promise(resolve => requestAnimationFrame(() => resolve()));
+ assert_equals(item.getBoundingClientRect().x, 0);
+ }, "Moving an element with a transition to a disconnected document should reset the transitionm state");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-trigger.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-trigger.html
new file mode 100644
index 0000000000..0cb5608a69
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/css-transition-trigger.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<title>Node.moveBefore should trigger CSS transition state (left) if needed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+ <section id="old-parent">
+ <div id="item"></div>
+ </section>
+ <section id="new-parent">
+ </section>
+ <style>
+ #item {
+ width: 100px;
+ height: 100px;
+ background: green;
+ transition: left 10s steps(1, jump-both);
+ position: absolute;
+ left: 0;
+ }
+
+ #new-parent #item {
+ left: 400px;
+ }
+
+ section {
+ position: relative;
+ }
+
+ body {
+ margin-left: 0;
+ }
+ </style>
+ <script>
+ promise_test(async t => {
+ const item = document.querySelector("#item");
+ assert_equals(item.getBoundingClientRect().x, 0);
+ document.querySelector("#new-parent").moveBefore(item, null);
+ await new Promise(resolve => item.addEventListener("transitionstart", resolve));
+ assert_equals(item.getBoundingClientRect().x, 200);
+ });
+ </script>
+</body>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/focus-preserve.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/focus-preserve.html
new file mode 100644
index 0000000000..a00e8b7788
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/focus-preserve.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<title>moveBefore should not automatically clear focus</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<section id="old_parent">
+<button id="button" tabindex="1">Button</button>
+</section>
+<section id="new_parent">
+</section>
+<section id="inert_parent" inert>
+</section>
+<section id="hidden_parent" hidden>
+</section>
+<script>
+
+function eventually_blurred(t, item, timeout = 1000) {
+ return new Promise((resolve, reject) => {
+ function onblur() {
+ resolve();
+ item.removeEventListener("blur", onblur);
+ }
+ item.addEventListener("blur", onblur);
+ t.step_timeout(reject, timeout);
+ });
+}
+
+test(t => {
+ const old_parent = document.querySelector("#old_parent");
+ const button = document.querySelector("#button");
+ t.add_cleanup(() => old_parent.append(button));
+ button.focus();
+ assert_equals(document.activeElement, button);
+ new_parent.moveBefore(button, null);
+ assert_equals(document.activeElement, button);
+}, "when reparenting an element, don't automatically reset the document focus");
+
+promise_test(async t => {
+ const old_parent = document.querySelector("#old_parent");
+ const button = document.querySelector("#button");
+ t.add_cleanup(() => old_parent.append(button));
+ const inert_parent = document.querySelector("#inert_parent");
+ button.focus();
+ assert_equals(document.activeElement, button);
+ inert_parent.moveBefore(button, null);
+
+ // The button will still be considered the active element. It will blur asynchronously.
+ assert_equals(document.activeElement, button);
+ await eventually_blurred(t, button);
+ assert_equals(document.activeElement, document.body);
+}, "when reparenting a focused element into an inert parent, reset the document focus");
+
+
+promise_test(async t => {
+ const old_parent = document.querySelector("#old_parent");
+ const button = document.querySelector("#button");
+ t.add_cleanup(() => old_parent.append(button));
+ const hidden_parent = document.querySelector("#hidden_parent");
+ button.focus();
+ assert_equals(document.activeElement, button);
+ hidden_parent.moveBefore(button, null);
+
+ // The button will still be considered the active element. It will blur asynchronously.
+ // This is similar to other operations that can cause a blur due to change in inert trees,
+ // e.g. a style change that makes an ancestor `display: none`.
+ assert_equals(document.activeElement, button);
+ await eventually_blurred(t, button);
+ assert_equals(document.activeElement, document.body);
+}, "when reparenting a focused element into a hidden parent, reset the document focus");
+
+promise_test(async t => {
+ const old_parent = document.querySelector("#old_parent");
+ const button = document.querySelector("#button");
+ t.add_cleanup(() => document.body.append(old_parent));
+ const hidden_parent = document.querySelector("#hidden_parent");
+ button.focus();
+ assert_equals(document.activeElement, button);
+ hidden_parent.moveBefore(old_parent, null);
+
+ // The button will still be considered the active element. It will blur asynchronously.
+ assert_equals(document.activeElement, button);
+ await eventually_blurred(t, button);
+ assert_equals(document.activeElement, document.body);
+}, "when reparenting an ancestor of an focused element into a hidden parent, reset the document focus");
+</script>
diff --git a/testing/web-platform/tests/dom/nodes/moveBefore/tentative/fullscreen-preserve.html b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/fullscreen-preserve.html
new file mode 100644
index 0000000000..810eeac9af
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/moveBefore/tentative/fullscreen-preserve.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>Document#fullscreenElement</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/fullscreen/trusted-click.js"></script>
+<section id="old_parent">
+ <div id="item"></div>
+</section>
+<section id="new_parent">
+ <div id="item"></div>
+</section>
+<script>
+ promise_test(async function (t) {
+ const item = document.querySelector("#item");
+
+ await trusted_click();
+
+ assert_equals(
+ document.fullscreenElement,
+ null,
+ "fullscreenElement before requestFullscreen()"
+ );
+
+ await item.requestFullscreen();
+ assert_equals(
+ document.fullscreenElement,
+ item,
+ "fullscreenElement before moveBefore()"
+ );
+
+ document.querySelector("#new_parent").moveBefore(item, null);
+
+ assert_equals(
+ document.fullscreenElement,
+ item,
+ "fullscreenElement after moveBefore()"
+ );
+
+ await Promise.all([document.exitFullscreen(), fullScreenChange()]);
+
+ assert_equals(
+ document.fullscreenElement,
+ null,
+ "fullscreenElement after exiting fullscreen"
+ );
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-every.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-every.any.js
new file mode 100644
index 0000000000..74a344b8f7
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-every.any.js
@@ -0,0 +1,250 @@
+promise_test(async () => {
+ const source = new Observable(subscriber => {
+ subscriber.next("good");
+ subscriber.next("good");
+ subscriber.next("good");
+ subscriber.complete();
+ });
+
+ const result = await source.every((value) => value === "good");
+
+ assert_true(result, "Promise resolves with true if all values pass the predicate");
+}, "every(): Promise resolves to true if all values pass the predicate");
+
+promise_test(async () => {
+ const source = new Observable(subscriber => {
+ subscriber.next("good");
+ subscriber.next("good");
+ subscriber.next("bad");
+ subscriber.complete();
+ });
+
+ const result = await source.every((value) => value === "good");
+
+ assert_false(result, "Promise resolves with false if any value fails the predicate");
+}, "every(): Promise resolves to false if any value fails the predicate");
+
+promise_test(async () => {
+ let tornDown = false;
+ let subscriberActiveAfterFailingPredicate = true;
+ const source = new Observable(subscriber => {
+ subscriber.addTeardown(() => tornDown = true);
+ subscriber.next("good");
+ subscriber.next("good");
+ subscriber.next("bad");
+ subscriberActiveAfterFailingPredicate = subscriber.active;
+ subscriber.next("good");
+ subscriber.complete();
+ });
+
+ const result = await source.every((value) => value === "good");
+
+ assert_false(result, "Promise resolves with false if any value fails the predicate");
+ assert_false(subscriberActiveAfterFailingPredicate,
+ "Subscriber becomes inactive because every() unsubscribed");
+}, "every(): Abort the subscription to the source if the predicate does not pass");
+
+promise_test(async () => {
+ const logs = [];
+
+ const source = createTestSubject({
+ onSubscribe: () => logs.push("subscribed to source"),
+ onTeardown: () => logs.push("teardown"),
+ });
+
+ const resultPromise = source.every((value, index) => {
+ logs.push(`Predicate called with ${value}, ${index}`);
+ return true;
+ });
+
+ let promiseResolved = false;
+
+ resultPromise.then(() => promiseResolved = true);
+
+ assert_array_equals(logs, ["subscribed to source"],
+ "calling every() subscribes to the source immediately");
+
+ source.next("a");
+ assert_array_equals(logs, [
+ "subscribed to source",
+ "Predicate called with a, 0"
+ ], "Predicate called with the value and the index");
+
+ source.next("b");
+ assert_array_equals(logs, [
+ "subscribed to source",
+ "Predicate called with a, 0",
+ "Predicate called with b, 1",
+ ], "Predicate called with the value and the index");
+
+ // wait a tick, just to prove that you have to wait for complete to be called.
+ await Promise.resolve();
+
+ assert_false(promiseResolved,
+ "Promise should not resolve until after the source completes");
+
+ source.complete();
+ assert_array_equals(logs, [
+ "subscribed to source",
+ "Predicate called with a, 0",
+ "Predicate called with b, 1",
+ "teardown",
+ ], "Teardown function called immediately after the source completes");
+
+ const result = await resultPromise;
+
+ assert_true(result,
+ "Promise resolves with true if all values pass the predicate");
+}, "every(): Lifecycle checks when all values pass the predicate");
+
+promise_test(async () => {
+ const logs = [];
+
+ const source = createTestSubject({
+ onSubscribe: () => logs.push("subscribed to source"),
+ onTeardown: () => logs.push("teardown"),
+ });
+
+ const resultPromise = source.every((value, index) => {
+ logs.push(`Predicate called with ${value}, ${index}`);
+ return value === "good";
+ });
+
+ let promiseResolved = false;
+
+ resultPromise.then(() => promiseResolved = true);
+
+ assert_array_equals(logs, ["subscribed to source"],
+ "calling every() subscribes to the source immediately");
+
+ source.next("good");
+ source.next("good");
+ assert_array_equals(logs, [
+ "subscribed to source",
+ "Predicate called with good, 0",
+ "Predicate called with good, 1",
+ ], "Predicate called with the value and the index");
+
+ assert_false(promiseResolved, "Promise should not resolve until after the predicate fails");
+
+ source.next("bad");
+ assert_array_equals(logs, [
+ "subscribed to source",
+ "Predicate called with good, 0",
+ "Predicate called with good, 1",
+ "Predicate called with bad, 2",
+ "teardown",
+ ], "Predicate called with the value and the index, failing predicate immediately aborts subscription to source");
+
+ const result = await resultPromise;
+
+ assert_false(result, "Promise resolves with false if any value fails the predicate");
+}, "every(): Lifecycle checks when any value fails the predicate");
+
+promise_test(async () => {
+ const source = new Observable(subscriber => {
+ subscriber.complete();
+ });
+
+ const result = await source.every(() => true);
+
+ assert_true(result,
+ "Promise resolves with true if the observable completes without " +
+ "emitting a value");
+}, "every(): Resolves with true if the observable completes without " +
+ "emitting a value");
+
+promise_test(async t => {
+ const error = new Error("error from source");
+ const source = new Observable(subscriber => {
+ subscriber.error(error);
+ });
+
+ promise_rejects_exactly(t, error, source.every(() => true),
+ "Promise rejects with the error emitted from the source observable");
+}, "every(): Rejects with any error emitted from the source observable");
+
+promise_test(async t => {
+ const source = new Observable(subscriber => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ const error = new Error("bad value");
+ const promise = source.every(value => {
+ if (value <= 2) return true;
+ throw error;
+ });
+
+ promise_rejects_exactly(t, error, promise, "Promise rejects with the " +
+ "error thrown from the predicate");
+}, "every(): Rejects with any error thrown from the predicate");
+
+promise_test(async () => {
+ const indices = [];
+
+ const source = new Observable(subscriber => {
+ subscriber.next("a");
+ subscriber.next("b");
+ subscriber.next("c");
+ subscriber.complete();
+ });
+
+ const value = await source.every((value, index) => {
+ indices.push(index);
+ return true;
+ });
+
+ assert_array_equals(indices, [0, 1, 2]);
+
+ assert_true(value,
+ "Promise resolves with true if all values pass the predicate");
+}, "every(): Index is passed into the predicate");
+
+promise_test(async t => {
+ const source = new Observable(subscriber => {});
+
+ const controller = new AbortController();
+ const promise = source.every(() => true, { signal: controller.signal });
+ controller.abort();
+
+ promise_rejects_dom(t, 'AbortError', promise, "Promise rejects with a " +
+ "DOMException if the source Observable is aborted");
+}, "every(): Rejects with a DOMException if the source Observable is aborted");
+
+function createTestSubject(options) {
+ const onTeardown = options?.onTeardown;
+
+ const subscribers = new Set();
+ const subject = new Observable(subscriber => {
+ options?.onSubscribe?.();
+ subscribers.add(subscriber);
+ subscriber.addTeardown(() => subscribers.delete(subscriber));
+ if (onTeardown) {
+ subscriber.addTeardown(onTeardown);
+ }
+ });
+
+ subject.next = (value) => {
+ for (const subscriber of Array.from(subscribers)) {
+ subscriber.next(value);
+ }
+ };
+ subject.error = (error) => {
+ for (const subscriber of Array.from(subscribers)) {
+ subscriber.error(error);
+ }
+ };
+ subject.complete = () => {
+ for (const subscriber of Array.from(subscribers)) {
+ subscriber.complete();
+ }
+ };
+ subject.subscriberCount = () => {
+ return subscribers.size;
+ };
+
+ return subject;
+}
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-filter.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-filter.any.js
index 8a49bcf467..3c1a7d7824 100644
--- a/testing/web-platform/tests/dom/observable/tentative/observable-filter.any.js
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-filter.any.js
@@ -103,3 +103,15 @@ test(() => {
['source teardown', 'source abort event', 'filter observable complete']);
}, "filter(): Upon source completion, source Observable teardown sequence " +
"happens after downstream filter complete() is called");
+
+test(() => {
+ const source = new Observable(subscriber => {
+ subscriber.next('value1');
+ subscriber.next('value2');
+ subscriber.next('value3');
+ });
+
+ const indices = [];
+ source.filter((value, index) => indices.push(index)).subscribe();
+ assert_array_equals(indices, [0, 1, 2]);
+}, "filter(): Index is passed correctly to predicate");
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-find.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-find.any.js
new file mode 100644
index 0000000000..0e09060fc5
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-find.any.js
@@ -0,0 +1,85 @@
+promise_test(async () => {
+ let inactiveAfterB = false;
+ const source = new Observable(subscriber => {
+ subscriber.next("a");
+ subscriber.next("b");
+ inactiveAfterB = !subscriber.active;
+ subscriber.next("c");
+ subscriber.complete();
+ });
+
+ const value = await source.find((value) => value === "b");
+
+ assert_equals(value, "b", "Promise resolves with the first value that passes the predicate");
+
+ assert_true(inactiveAfterB, "subscriber is inactive after the first value that passes the predicate");
+}, "find(): Promise resolves with the first value that passes the predicate");
+
+promise_test(async () => {
+ const source = new Observable(subscriber => {
+ subscriber.next("a");
+ subscriber.next("b");
+ subscriber.next("c");
+ subscriber.complete();
+ });
+
+ const value = await source.find(() => false);
+
+ assert_equals(value, undefined, "Promise resolves with undefined if no value passes the predicate");
+}, "find(): Promise resolves with undefined if no value passes the predicate");
+
+promise_test(async t => {
+ const error = new Error("error from source");
+ const source = new Observable(subscriber => {
+ subscriber.error(error);
+ });
+
+ promise_rejects_exactly(t, error, source.find(() => true), "Promise " +
+ "rejects with the error emitted from the source Observable");
+}, "find(): Promise rejects with the error emitted from the source Observable");
+
+promise_test(async t => {
+ const source = new Observable(subscriber => {
+ subscriber.next("ignored");
+ });
+
+ const error = new Error("thrown from predicate");
+ promise_rejects_exactly(t, error, source.find(() => {throw error}),
+ "Promise rejects with any error thrown from the predicate");
+}, "find(): Promise rejects with any error thrown from the predicate");
+
+promise_test(async () => {
+ let indices = [];
+
+ const source = new Observable(subscriber => {
+ subscriber.next("a");
+ subscriber.next("b");
+ subscriber.next("c");
+ subscriber.complete();
+ });
+
+ const value = await source.find((value, index) => {
+ indices.push(index);
+ return false;
+ });
+
+ assert_equals(value, undefined, "Promise resolves with undefined if no value passes the predicate");
+
+ assert_array_equals(indices, [0, 1, 2], "find(): Passes the index of the value to the predicate");
+}, "find(): Passes the index of the value to the predicate");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const source = new Observable(subscriber => {
+ subscriber.next("a");
+ subscriber.next("b");
+ subscriber.next("c");
+ subscriber.complete();
+ });
+
+ controller.abort();
+ const promise = source.find(() => true, { signal: controller.signal });
+
+ promise_rejects_dom(t, 'AbortError', promise, "Promise rejects with " +
+ "DOMException when the signal is aborted");
+}, "find(): Rejects with AbortError when the signal is aborted");
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-inspect.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-inspect.any.js
new file mode 100644
index 0000000000..8aff741d26
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-inspect.any.js
@@ -0,0 +1,412 @@
+// Because we test that the global error handler is called at various times.
+setup({ allow_uncaught_exception: true });
+
+test(() => {
+ const results = [];
+ let sourceSubscriptionCall = 0;
+ const source = new Observable(subscriber => {
+ sourceSubscriptionCall++;
+ results.push(`source subscribe ${sourceSubscriptionCall}`);
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ let inspectSubscribeCall = 0;
+ const result = source.inspect({
+ subscribe: () => {
+ inspectSubscribeCall++;
+ results.push(`inspect() subscribe ${inspectSubscribeCall}`);
+ },
+ next: (value) => results.push(`inspect() next ${value}`),
+ error: (e) => results.push(`inspect() error ${e.message}`),
+ complete: () => results.push(`inspect() complete`),
+ });
+
+ result.subscribe({
+ next: (value) => results.push(`result next ${value}`),
+ error: (e) => results.push(`result error ${e.message}`),
+ complete: () => results.push(`result complete`),
+ });
+
+ result.subscribe({
+ next: (value) => results.push(`result next ${value}`),
+ error: (e) => results.push(`result error ${e.message}`),
+ complete: () => results.push(`result complete`),
+ });
+
+ assert_array_equals(results,
+ [
+ "inspect() subscribe 1",
+ "source subscribe 1",
+ "inspect() next 1",
+ "result next 1",
+ "inspect() next 2",
+ "result next 2",
+ "inspect() next 3",
+ "result next 3",
+ "inspect() complete",
+ "result complete",
+ "inspect() subscribe 2",
+ "source subscribe 2",
+ "inspect() next 1",
+ "result next 1",
+ "inspect() next 2",
+ "result next 2",
+ "inspect() next 3",
+ "result next 3",
+ "inspect() complete",
+ "result complete",
+ ]);
+}, "inspect(): Provides a pre-subscription subscribe callback");
+
+test(() => {
+ const source = new Observable(subscriber => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ const results = [];
+
+ const result = source.inspect({
+ next: value => results.push(value),
+ error: e => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ result.subscribe();
+ result.subscribe();
+
+ assert_array_equals(results, [1, 2, 3, "complete", 1, 2, 3, "complete"]);
+}, "inspect(): Provides a way to tap into the values and completions of the " +
+ "source observable using an observer");
+
+test(() => {
+ const error = new Error("error from source");
+ const source = new Observable(subscriber => subscriber.error(error));
+
+ const results = [];
+
+ const result = source.inspect({
+ next: value => results.push(value),
+ error: e => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ let errorReported = null;
+ self.addEventListener('error', e => errorReported = e.error, {once: true});
+ result.subscribe();
+
+ assert_array_equals(results, [error]);
+ assert_equals(errorReported, error,
+ "errorReported to global matches error from source Observable");
+}, "inspect(): Error handler does not stop error from being reported to the " +
+ "global, when subscriber does not pass error handler");
+
+test(() => {
+ const error = new Error("error from source");
+ const source = new Observable(subscriber => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.error(error);
+ });
+
+ const results = [];
+
+ const result = source.inspect({
+ next: value => results.push(value),
+ error: e => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ const observer = {
+ error: e => results.push(e),
+ };
+ result.subscribe(observer);
+ result.subscribe(observer);
+
+ assert_array_equals(results, [1, 2, 3, error, error, 1, 2, 3, error, error]);
+}, "inspect(): Provides a way to tap into the values and errors of the " +
+ "source observable using an observer. Errors are passed through");
+
+test(() => {
+ const source = new Observable(subscriber => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ const results = [];
+
+ const result = source.inspect(value => results.push(value));
+
+ result.subscribe();
+ result.subscribe();
+
+ assert_array_equals(results, [1, 2, 3, 1, 2, 3]);
+}, "inspect(): ObserverCallback passed in");
+
+test(() => {
+ const source = new Observable(subscriber => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ });
+
+ const error = new Error("error from inspect() next handler");
+ const result = source.inspect({
+ next: (value) => {
+ if (value === 2) {
+ throw error;
+ }
+ },
+ });
+
+ const results1 = [];
+ result.subscribe({
+ next: (value) => results1.push(value),
+ error: (e) => results1.push(e),
+ complete: () => results1.push("complete"),
+ });
+
+ const results2 = [];
+ result.subscribe({
+ next: (value) => results2.push(value),
+ error: (e) => results2.push(e),
+ complete: () => results2.push("complete"),
+ });
+
+ assert_array_equals(results1, [1, error]);
+ assert_array_equals(results2, [1, error]);
+}, "inspect(): Throwing an error in the observer next handler is caught and " +
+ "sent to the error callback of the result observable");
+
+test(() => {
+ const sourceError = new Error("error from source");
+ const inspectError = new Error("error from inspect() error handler");
+
+ const source = new Observable(subscriber => {
+ subscriber.error(sourceError);
+ });
+
+ const result = source.inspect({
+ error: () => {
+ throw inspectError;
+ },
+ });
+
+ const results = [];
+ result.subscribe({
+ next: () => results.push("next"),
+ error: (e) => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(results, [inspectError]);
+}, "inspect(): Throwing an error in the observer error handler in " +
+ "inspect() is caught and sent to the error callback of the result " +
+ "observable");
+
+test(() => {
+ const source = new Observable(subscriber => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ const error = new Error("error from inspect() complete handler");
+ const result = source.inspect({
+ complete: () => {
+ throw error;
+ },
+ });
+
+ const results = [];
+ result.subscribe({
+ next: (value) => results.push(value),
+ error: (e) => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(results, [1, 2, 3, error]);
+}, "inspect(): Throwing an error in the observer complete handler is caught " +
+ "and sent to the error callback of the result observable");
+
+test(() => {
+ const source = new Observable(subscriber => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ });
+
+ const error = new Error("error from inspect() next handler");
+ const result = source.inspect({
+ next: (value) => {
+ if (value === 2) {
+ throw error;
+ }
+ },
+ });
+
+ const results = [];
+ result.subscribe({
+ next: (value) => results.push(value),
+ error: (e) => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(results, [1, error]);
+}, "inspect(): Throwing an error in the next handler function in do should " +
+ "be caught and sent to the error callback of the result observable");
+
+test(() => {
+ const source = new Observable(subscriber => {});
+
+ const result = source.inspect({
+ subscribe: () => {
+ throw new Error("error from do subscribe handler");
+ },
+ });
+
+ const results = [];
+ result.subscribe({
+ next: () => results.push("next"),
+ error: (e) => results.push(e.message),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(results, ["error from do subscribe handler"]);
+}, "inspect(): Errors thrown in subscribe() Inspector handler subscribe " +
+ "handler are caught and sent to error callback");
+
+test(() => {
+ const results = [];
+ let sourceTeardownCall = 0;
+ const source = new Observable(subscriber => {
+ subscriber.addTeardown(() => {
+ sourceTeardownCall++;
+ results.push(`source teardown ${sourceTeardownCall}`);
+ });
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ let doUnsubscribeCall = 0;
+ const result = source.inspect({
+ abort: (reason) => {
+ doUnsubscribeCall++;
+ results.push(`inspect() abort ${doUnsubscribeCall} ${reason}`);
+ },
+ next: (value) => results.push(`inspect() next ${value}`),
+ error: (e) => results.push(`inspect() error ${e.message}`),
+ complete: () => results.push(`inspect() complete`),
+ });
+
+ const controller = new AbortController();
+ result.subscribe({
+ next: (value) => {
+ results.push(`result next ${value}`);
+ if (value === 2) {
+ controller.abort("abort reason");
+ }
+ },
+ error: (e) => results.push(`result error ${e.message}`),
+ complete: () => results.push(`result complete`),
+ }, { signal: controller.signal });
+
+ assert_array_equals(results, [
+ "inspect() next 1",
+ "result next 1",
+ "inspect() next 2",
+ "result next 2",
+ "inspect() abort 1 abort reason",
+ "source teardown 1",
+ ]);
+}, "inspect(): Provides a way to tap into the moment a source observable is unsubscribed from");
+
+test(() => {
+ const results = [];
+ let sourceTeardownCall = 0;
+ const source = new Observable(subscriber => {
+ subscriber.addTeardown(() => {
+ sourceTeardownCall++;
+ results.push(`source teardown ${sourceTeardownCall}`);
+ });
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ let inspectUnsubscribeCall = 0;
+ const result = source.inspect({
+ next: (value) => results.push(`inspect() next ${value}`),
+ complete: () => results.push(`inspect() complete`),
+ abort: (reason) => {
+ inspectUnsubscribeCall++;
+ results.push(`inspect() abort ${inspectUnsubscribeCall} ${reason}`);
+ },
+ });
+
+ result.subscribe({
+ next: (value) => results.push(`result next ${value}`),
+ complete: () => results.push(`result complete`),
+ });
+
+ assert_array_equals(results, [
+ "inspect() next 1",
+ "result next 1",
+ "inspect() next 2",
+ "result next 2",
+ "inspect() next 3",
+ "result next 3",
+ "source teardown 1",
+ "inspect() complete",
+ "result complete",
+ ]);
+}, "inspect(): Inspector abort() handler is not called if the source " +
+ "completes before the result is unsubscribed from");
+
+test(() => {
+ const source = new Observable(subscriber => {
+ subscriber.next(1);
+ });
+
+ const results = [];
+
+ const result = source.inspect({
+ abort: () => {
+ results.push('abort() handler run');
+ throw new Error("error from inspect() subscribe handler");
+ },
+ });
+
+ const controller = new AbortController();
+
+ self.on('error').take(1).subscribe(e =>
+ results.push(e.message + ', from report exception'));
+
+ result.subscribe({
+ next: (value) => {
+ results.push(value);
+ controller.abort();
+ },
+ // This should not be invoked at all!!
+ error: (e) => results.push(e.message + ', from Observer#error()'),
+ complete: () => results.push("complete"),
+ }, {signal: controller.signal});
+
+ assert_array_equals(results, [1, "abort() handler run",
+ "Uncaught Error: error from inspect() subscribe handler, from report " +
+ "exception"]);
+}, "inspect(): Errors thrown from inspect()'s abort() handler are caught " +
+ "and reported to the global, because the subscription is already closed " +
+ "by the time the handler runs");
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-some.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-some.any.js
new file mode 100644
index 0000000000..b692610df3
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-some.any.js
@@ -0,0 +1,96 @@
+promise_test(async () => {
+ let inactiveAfterFirstGood = true;
+
+ const source = new Observable(subscriber => {
+ subscriber.next("good");
+ inactiveAfterFirstGood = !subscriber.active;
+ subscriber.next("good");
+ subscriber.next("good");
+ subscriber.complete();
+ });
+
+ const result = await source.some((value) => value === "good");
+
+ assert_true(result, "Promise resolves with true if any value passes the predicate");
+
+ assert_true(inactiveAfterFirstGood,
+ "subscriber is inactive after the first value that passes the " +
+ "predicate, because the source was unsubscribed from");
+}, "some(): subscriber is inactive after the first value that passes the predicate, because the source was unsubscribed from");
+
+promise_test(async () => {
+ const source = new Observable(subscriber => {
+ subscriber.next("bad");
+ subscriber.next("bad");
+ subscriber.next("bad");
+ subscriber.complete();
+ });
+
+ const result = await source.some((value) => value === "good");
+
+ assert_false(result, "some(): Promise resolves with false if no value passes the predicate");
+});
+
+promise_test(async () => {
+ const source = new Observable(subscriber => {
+ subscriber.next("bad");
+ subscriber.next("bad");
+ subscriber.next("good");
+ subscriber.complete();
+ });
+
+ const result = await source.some((value) => value === "good");
+
+ assert_true(result, "some(): Promise resolves with true if any value passes the predicate");
+});
+
+promise_test(async t => {
+ const source = new Observable(subscriber => {
+ subscriber.next("not used");
+ });
+
+ const error = new Error("thrown from predicate");
+ promise_rejects_exactly(t, error, source.some(() => {throw error}),
+ "The returned promise rejects with an error if the predicate errors");
+}, "some(): The returned promise rejects with an error if the predicate errors");
+
+promise_test(async t => {
+ const error = new Error("error from source");
+ const source = new Observable(subscriber => {
+ subscriber.error(error);
+ });
+
+ promise_rejects_exactly(t, error, source.some(() => true),
+ "The returned promise rejects with an error if the source observable errors");
+}, "some(): The returned promise rejects with an error if the source observable errors");
+
+promise_test(async () => {
+ const source = new Observable(subscriber => {
+ subscriber.complete();
+ });
+
+ const result = await source.some(() => true);
+
+ assert_false(result,
+ "The returned promise resolves as false if the source observable " +
+ "completes without emitting a value");
+}, "some(): The returned promise resolves as false if the source observable " +
+ "completes without emitting a value");
+
+promise_test(async t => {
+ let teardownCalled = false;
+ const source = new Observable(subscriber => {
+ subscriber.addTeardown(() => {
+ teardownCalled = true;
+ });
+ });
+
+ const controller = new AbortController();
+ const promise = source.some(() => true, { signal: controller.signal });
+
+ controller.abort();
+
+ promise_rejects_dom(t, 'AbortError', promise);
+ assert_true(teardownCalled,
+ "The teardown function is called when the signal is aborted");
+}, "some(): The return promise rejects with a DOMException if the signal is aborted");
diff --git a/testing/web-platform/tests/dom/ranges/Range-isPointInRange-shadowdom.tentative.html b/testing/web-platform/tests/dom/ranges/Range-isPointInRange-shadowdom.tentative.html
new file mode 100644
index 0000000000..a90ddcf584
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-isPointInRange-shadowdom.tentative.html
@@ -0,0 +1,75 @@
+<!doctype html>
+<title>Range.isPointInRange() with ShadowDOM selection tests</title>
+<link rel="author" title="Sean Feng" href=sefeng@mozilla.com>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<span id="start">Start</span>
+<div id="host">
+ <template shadowrootmode="open">
+ <span id="inner1">Inner1</span>
+ <span id="inner2">Inner2</span>
+ </template>
+</div>
+<span id="end">End</span>
+<script>
+"use strict";
+
+test(function() {
+ assert_implements(window.getSelection().getComposedRanges, "GetComposedRanges is not supported");
+ const start = document.getElementById("start");
+ const shadowRoot = document.getElementById("host").shadowRoot;
+
+ const end = shadowRoot.getElementById("inner2");
+ const inner1 = shadowRoot.getElementById("inner1");
+
+ window.getSelection().setBaseAndExtent(start.firstChild, 3, end.firstChild, 3);
+
+ const composedRange = window.getSelection().getComposedRanges(shadowRoot)[0];
+ // Sanity check to make sure we have selected something across the shadow boundary.
+ assert_true(composedRange.startContainer == start.firstChild);
+ assert_true(composedRange.startOffset == 3);
+ assert_true(composedRange.endContainer == end.firstChild);
+ assert_true(composedRange.endOffset == 3);
+
+ assert_true(window.getSelection().isCollapsed, "Selection should be collapsed");
+
+ const range = window.getSelection().getRangeAt(0);
+ assert_false(range.isPointInRange(inner1, 0), "inner1 is in the shadow tree, should not be in the range");
+ assert_true(range.comparePoint(inner1, 0) == -1, "inner1 is in the shadow tree, should return -1 for comparison");
+}, "isPointInRange() test for collapsed selection");
+
+test(function() {
+ assert_implements(window.getSelection().getComposedRanges, "GetComposedRanges is not supported");
+ const start = document.getElementById("start");
+ const shadowRoot = document.getElementById("host").shadowRoot;
+
+ const end = document.getElementById("end");
+ const inner1 = shadowRoot.getElementById("inner1");
+
+ window.getSelection().setBaseAndExtent(start.firstChild, 3, end.firstChild, 3);
+
+ const composedRange = window.getSelection().getRangeAt(0);
+ // Sanity check to make sure we have selected something
+ assert_true(composedRange.startContainer == start.firstChild);
+ assert_true(composedRange.startOffset == 3);
+ assert_true(composedRange.endContainer == end.firstChild);
+ assert_true(composedRange.endOffset == 3);
+
+ assert_false(window.getSelection().isCollapsed, "Range should not be collapsed");
+
+ const range = window.getSelection().getRangeAt(0);
+
+ assert_false(range.isPointInRange(inner1, 0), "inner1 is in the shadow tree, should not be in the range");
+
+ // The selection is not collapsed so inner1 is not in the same tree as the selection.
+ assert_throws_dom("WrongDocumentError", function() {
+ range.comparePoint(inner1, 0);
+ });
+
+ const host = document.getElementById("host");
+ assert_true(range.isPointInRange(host, 0), "host is not in the shadow tree, should be in the range");
+ assert_true(range.comparePoint(host, 0) == 0, "host is not in the shadow tree, should return 0 for comparison");
+}, "isPointInRange() test for non-collapsed selection");
+
+</script>
diff --git a/testing/web-platform/tests/dom/xslt/resources/xml2html.xsl b/testing/web-platform/tests/dom/xslt/resources/xml2html.xsl
index 07b967500f..88b74a9620 100644
--- a/testing/web-platform/tests/dom/xslt/resources/xml2html.xsl
+++ b/testing/web-platform/tests/dom/xslt/resources/xml2html.xsl
@@ -5,7 +5,7 @@ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
<body>
- <h2>My CD Collection</h2>
+ <h2 style="margin-top:0">My CD Collection</h2>
<table border="1">
<tr bgcolor="#9acd32">
<th>Title</th>
diff --git a/testing/web-platform/tests/dpub-aam/role/roles.html b/testing/web-platform/tests/dpub-aam/role/roles.html
index b9840976e1..62e33b95fa 100644
--- a/testing/web-platform/tests/dpub-aam/role/roles.html
+++ b/testing/web-platform/tests/dpub-aam/role/roles.html
@@ -28,6 +28,7 @@ AriaUtils.assignAndVerifyRolesByRoleNames([
"doc-colophon",
"doc-conclusion",
"doc-cover",
+ "doc-credit",
"doc-credits",
"doc-dedication",
// "doc-endnote", // deprecated
@@ -60,4 +61,5 @@ AriaUtils.assignAndVerifyRolesByRoleNames([
</script>
</body>
-</html> \ No newline at end of file
+</html>
+
diff --git a/testing/web-platform/tests/editing/data/delete.js b/testing/web-platform/tests/editing/data/delete.js
index 131c99b1d5..c4d1225ef3 100644
--- a/testing/web-platform/tests/editing/data/delete.js
+++ b/testing/web-platform/tests/editing/data/delete.js
@@ -2044,12 +2044,12 @@ var browserTests = [
{"defaultparagraphseparator":[false,false,"div",false,false,"p"],"delete":[false,false,"",false,false,""]}],
["foo<br><br>{<p>]bar</p>",
[["defaultparagraphseparator","div"],["delete",""]],
- "foo<br>{}bar",
+ "foo<br><p>bar</p>",
[true,true],
{"defaultparagraphseparator":[false,false,"p",false,false,"div"],"delete":[false,false,"",false,false,""]}],
["foo<br><br>{<p>]bar</p>",
[["defaultparagraphseparator","p"],["delete",""]],
- "foo<br>{}bar",
+ "foo<br><p>bar</p>",
[true,true],
{"defaultparagraphseparator":[false,false,"div",false,false,"p"],"delete":[false,false,"",false,false,""]}],
["<p>foo<br>{</p><p>}bar</p>",
@@ -2359,7 +2359,7 @@ var browserTests = [
{"delete":[false,false,"",false,false,""]}],
["<ol><li>foo</ol>[bar<ol><li>]baz</ol>",
[["delete",""]],
- "<ol><li>foo</li></ol>{}baz",
+ "<ol><li>foo</li></ol><ol><li>baz</li></ol>",
[true],
{"delete":[false,false,"",false,false,""]}],
["<ol><li>foo</ol><p>[bar<ol><li>]baz</ol>",
diff --git a/testing/web-platform/tests/editing/data/forwarddelete.js b/testing/web-platform/tests/editing/data/forwarddelete.js
index ea590a4fbb..a881fb6ccf 100644
--- a/testing/web-platform/tests/editing/data/forwarddelete.js
+++ b/testing/web-platform/tests/editing/data/forwarddelete.js
@@ -2009,12 +2009,12 @@ var browserTests = [
{"defaultparagraphseparator":[false,false,"div",false,false,"p"],"forwarddelete":[false,false,"",false,false,""]}],
["foo<br><br>{<p>]bar</p>",
[["defaultparagraphseparator","div"],["forwarddelete",""]],
- "foo<br>{}bar",
+ "foo<br><p>bar</p>",
[true,true],
{"defaultparagraphseparator":[false,false,"p",false,false,"div"],"forwarddelete":[false,false,"",false,false,""]}],
["foo<br><br>{<p>]bar</p>",
[["defaultparagraphseparator","p"],["forwarddelete",""]],
- "foo<br>{}bar",
+ "foo<br><p>bar</p>",
[true,true],
{"defaultparagraphseparator":[false,false,"div",false,false,"p"],"forwarddelete":[false,false,"",false,false,""]}],
["<p>foo<br>{</p><p>}bar</p>",
@@ -2184,7 +2184,7 @@ var browserTests = [
{"forwarddelete":[false,false,"",false,false,""]}],
["<ol><li>foo</ol>{}<br><ol><li>bar</ol>",
[["forwarddelete",""]],
- "<ol><li>foo</li></ol>{}bar",
+ "<ol><li>foo</li></ol><ol><li>bar</li></ol>",
[true],
{"forwarddelete":[false,false,"",false,false,""]}],
["<ol><li>foo</ol><p>{}<br></p><ol><li>bar</ol>",
@@ -2199,22 +2199,22 @@ var browserTests = [
{"forwarddelete":[false,false,"",false,false,""]}],
["<ol id=a><li>foo</ol>{}<br><ol><li>bar</ol>",
[["forwarddelete",""]],
- "<ol id=\"a\"><li>foo</li></ol>{}bar",
+ "<ol id=\"a\"><li>foo</li></ol><ol><li>bar</li></ol>",
[true],
{"forwarddelete":[false,false,"",false,false,""]}],
["<ol><li>foo</ol>{}<br><ol id=b><li>bar</ol>",
[["forwarddelete",""]],
- "<ol><li>foo</li></ol>{}bar",
+ "<ol><li>foo</li></ol><ol id=\"b\"><li>bar</li></ol>",
[true],
{"forwarddelete":[false,false,"",false,false,""]}],
["<ol id=a><li>foo</ol>{}<br><ol id=b><li>bar</ol>",
[["forwarddelete",""]],
- "<ol id=\"a\"><li>foo</li></ol>{}bar",
+ "<ol id=\"a\"><li>foo</li></ol><ol id=\"b\"><li>bar</li></ol>",
[true],
{"forwarddelete":[false,false,"",false,false,""]}],
["<ol class=a><li>foo</ol>{}<br><ol class=b><li>bar</ol>",
[["forwarddelete",""]],
- "<ol class=\"a\"><li>foo</li></ol>{}bar",
+ "<ol class=\"a\"><li>foo</li></ol><ol class=\"b\"><li>bar</li></ol>",
[true],
{"forwarddelete":[false,false,"",false,false,""]}],
["<ol><ol><li>foo</ol><li>{}<br><ol><li>bar</ol></ol>",
@@ -2259,7 +2259,7 @@ var browserTests = [
{"forwarddelete":[false,false,"",false,false,""]}],
["<ol><li>foo</ol>[bar<ol><li>]baz</ol>",
[["forwarddelete",""]],
- "<ol><li>foo</li></ol>{}baz",
+ "<ol><li>foo</li></ol><ol><li>baz</li></ol>",
[true],
{"forwarddelete":[false,false,"",false,false,""]}],
["<ol><li>foo</ol><p>[bar<ol><li>]baz</ol>",
@@ -2269,12 +2269,12 @@ var browserTests = [
{"forwarddelete":[false,false,"",false,false,""]}],
["<ol><li>foo</ol><p>[bar<ol><li><p>]baz</ol>",
[["defaultparagraphseparator","div"],["forwarddelete",""]],
- "<ol><li>foo</li></ol><p>{}baz</p>",
+ "<ol><li>foo</li></ol><ol><li><p>{}baz</p></li></ol>",
[true,true],
{"defaultparagraphseparator":[false,false,"p",false,false,"div"],"forwarddelete":[false,false,"",false,false,""]}],
["<ol><li>foo</ol><p>[bar<ol><li><p>]baz</ol>",
[["defaultparagraphseparator","p"],["forwarddelete",""]],
- "<ol><li>foo</li></ol><p>{}baz</p>",
+ "<ol><li>foo</li></ol><ol><li><p>{}baz</p></li></ol>",
[true,true],
{"defaultparagraphseparator":[false,false,"div",false,false,"p"],"forwarddelete":[false,false,"",false,false,""]}],
["<ol><li>foo</ol><ol><li>[]bar</ol>",
@@ -2289,7 +2289,7 @@ var browserTests = [
{"forwarddelete":[false,false,"",false,false,""]}],
["<ul><li>foo</ul>{}<br><ul><li>bar</ul>",
[["forwarddelete",""]],
- "<ul><li>foo</li></ul>{}bar",
+ "<ul><li>foo</li></ul><ul><li>bar</li></ul>",
[true],
{"forwarddelete":[false,false,"",false,false,""]}],
["<ul><li>foo</ul><p>{}<br></p><ul><li>bar</ul>",
@@ -2304,7 +2304,7 @@ var browserTests = [
{"forwarddelete":[false,false,"",false,false,""]}],
["<ol><li>foo</ol>{}<br><ul><li>bar</ul>",
[["forwarddelete",""]],
- "<ol><li>foo</li></ol>{}bar",
+ "<ol><li>foo</li></ol><ul><li>bar</li></ul>",
[true],
{"forwarddelete":[false,false,"",false,false,""]}],
["<ol><li>foo</ol><p>{}<br></p><ul><li>bar</ul>",
@@ -2314,7 +2314,7 @@ var browserTests = [
{"forwarddelete":[false,false,"",false,false,""]}],
["<ul><li>foo</ul>{}<br><ol><li>bar</ol>",
[["forwarddelete",""]],
- "<ul><li>foo</li></ul>{}bar",
+ "<ul><li>foo</li></ul><ol><li>bar</li></ol>",
[true],
{"forwarddelete":[false,false,"",false,false,""]}],
["<ul><li>foo</ul><p>{}<br></p><ol><li>bar</ol>",
diff --git a/testing/web-platform/tests/editing/edit-context/WEB_FEATURES.yml b/testing/web-platform/tests/editing/edit-context/WEB_FEATURES.yml
new file mode 100644
index 0000000000..284280066a
--- /dev/null
+++ b/testing/web-platform/tests/editing/edit-context/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: edit-context
+ files: "**"
diff --git a/testing/web-platform/tests/editing/include/editor-test-utils.js b/testing/web-platform/tests/editing/include/editor-test-utils.js
index d0d50d22a6..b180f3343f 100644
--- a/testing/web-platform/tests/editing/include/editor-test-utils.js
+++ b/testing/web-platform/tests/editing/include/editor-test-utils.js
@@ -424,4 +424,79 @@ class EditorTestUtils {
);
}
}
+
+ static getRangeArrayDescription(arrayOfRanges) {
+ if (arrayOfRanges === null) {
+ return "null";
+ }
+ if (arrayOfRanges === undefined) {
+ return "undefined";
+ }
+ if (!Array.isArray(arrayOfRanges)) {
+ return "Unknown Object";
+ }
+ if (arrayOfRanges.length === 0) {
+ return "[]";
+ }
+ let result = "";
+ for (let range of arrayOfRanges) {
+ if (result === "") {
+ result = "[";
+ } else {
+ result += ",";
+ }
+ result += `{${EditorTestUtils.getRangeDescription(range)}}`;
+ }
+ result += "]";
+ return result;
+ }
+
+ static getNodeDescription(node) {
+ if (!node) {
+ return "null";
+ }
+ switch (node.nodeType) {
+ case Node.TEXT_NODE:
+ case Node.COMMENT_NODE:
+ case Node.CDATA_SECTION_NODE:
+ return `${node.nodeName} "${node.data.replaceAll("\n", "\\\\n")}"`;
+ case Node.ELEMENT_NODE:
+ return `<${node.nodeName.toLowerCase()}${
+ node.hasAttribute("id") ? ` id="${node.getAttribute("id")}"` : ""
+ }${
+ node.hasAttribute("class") ? ` class="${node.getAttribute("class")}"` : ""
+ }${
+ node.hasAttribute("contenteditable")
+ ? ` contenteditable="${node.getAttribute("contenteditable")}"`
+ : ""
+ }${
+ node.inert ? ` inert` : ""
+ }${
+ node.hidden ? ` hidden` : ""
+ }${
+ node.readonly ? ` readonly` : ""
+ }${
+ node.disabled ? ` disabled` : ""
+ }>`;
+ default:
+ return `${node.nodeName}`;
+ }
+ }
+
+ static getRangeDescription(range) {
+ if (range === null) {
+ return "null";
+ }
+ if (range === undefined) {
+ return "undefined";
+ }
+ return range.startContainer == range.endContainer &&
+ range.startOffset == range.endOffset
+ ? `(${EditorTestUtils.getNodeDescription(range.startContainer)}, ${range.startOffset})`
+ : `(${EditorTestUtils.getNodeDescription(range.startContainer)}, ${
+ range.startOffset
+ }) - (${EditorTestUtils.getNodeDescription(range.endContainer)}, ${range.endOffset})`;
+ }
+
+
}
diff --git a/testing/web-platform/tests/editing/other/delete-without-unwrapping-first-line-of-child-block.html b/testing/web-platform/tests/editing/other/delete-without-unwrapping-first-line-of-child-block.html
new file mode 100644
index 0000000000..99f8f05888
--- /dev/null
+++ b/testing/web-platform/tests/editing/other/delete-without-unwrapping-first-line-of-child-block.html
@@ -0,0 +1,1020 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<meta name="variant" content="?method=BackspaceKey&lineBreak=br">
+<meta name="variant" content="?method=DeleteKey&lineBreak=br">
+<meta name="variant" content="?method=deleteCommand&lineBreak=br">
+<meta name="variant" content="?method=forwardDeleteCommand&lineBreak=br">
+<meta name="variant" content="?method=BackspaceKey&lineBreak=preformat">
+<meta name="variant" content="?method=DeleteKey&lineBreak=preformat">
+<meta name="variant" content="?method=deleteCommand&lineBreak=preformat">
+<meta name="variant" content="?method=forwardDeleteCommand&lineBreak=preformat">
+<title>Tests for deleting preceding lines of right child block if range ends at start of the right child</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="../include/editor-test-utils.js"></script>
+<script>
+"use strict";
+
+/**
+ * Browsers delete only preceding lines (and selected content in the child
+ * block) when the deleting range starts from a line and ends in a child block
+ * without unwrapping the (new) first line of the child block at end. Note that
+ * this is a special handling for the above case, i.e., if the range starts from
+ * a middle of a preceding line of the child block, the first line of the child
+ * block should be unwrapped and merged into the preceding line. This is also
+ * applied when the range is directly replaced with new content like typing a
+ * character. Finally, selection should be collapsed at start of the child
+ * block and new content should be inserted at start of the child block.
+ *
+ * This file also tests getTargetRanges() of `beforeinput` of at deletion and
+ * replacing the selection directly. In the former case, if the range ends at
+ * start of the child block, browsers do not touch the child block. Therefore,
+ * the target ranges should the a range deleting the preceding lines, i.e.,
+ * should be end at the child block. When the range is replaced directly, the
+ * content will be inserted at start of the child block, and also when the range
+ * selects some content in the child block, browsers touch the child block.
+ * Therefore, the target range should end at the next insertion point.
+ */
+
+const searchParams = new URLSearchParams(document.location.search);
+const testUserInput = searchParams.get("method") == "BackspaceKey" || searchParams.get("method") == "DeleteKey";
+const testBackward = searchParams.get("method") == "BackspaceKey" || searchParams.get("method") == "deleteCommand";
+const deleteMethod =
+ testUserInput
+ ? testBackward ? "Backspace" : "Delete"
+ : `document.execCommand("${testBackward ? "delete" : "forwarddelete"}")`;
+const insertTextMethod = testUserInput ? "Typing \"X\"" : "document.execCommand(\"insertText\", false, \"X\")";
+const lineBreak = searchParams.get("lineBreak") == "br" ? "<br>" : "\n";
+const lineBreakIsBR = lineBreak == "<br>";
+
+function run(editorUtils) {
+ if (testUserInput) {
+ return testBackward ? editorUtils.sendBackspaceKey() : editorUtils.sendDeleteKey();
+ }
+ editorUtils.document.execCommand(testBackward ? "delete" : "forwardDelete");
+}
+
+function typeCharacter(editorUtils, ch) {
+ if (testUserInput) {
+ return editorUtils.sendKey(ch);
+ }
+ document.execCommand("insertText", false, ch);
+}
+
+async function runDeleteTest(
+ runningTest,
+ testUtils,
+ initialInnerHTML,
+ expectedAfterDeletion,
+ whatShouldHappenAfterDeletion,
+ expectedAfterDeletionAndInsertion,
+ whatShouldHappenAfterDeletionAndInsertion,
+ expectedTargetRangesAtDeletion,
+ whatGetTargetRangesShouldReturn
+) {
+ let targetRanges = [];
+ if (testUserInput) {
+ testUtils.editingHost.addEventListener(
+ "beforeinput",
+ event => targetRanges = event.getTargetRanges(),
+ {once: true}
+ );
+ }
+ await run(testUtils);
+ (Array.isArray(expectedAfterDeletion) ? assert_in_array : assert_equals)(
+ testUtils.editingHost.innerHTML,
+ expectedAfterDeletion,
+ `${runningTest.name} ${whatShouldHappenAfterDeletion}`
+ );
+ if (testUserInput) {
+ test(() => {
+ const arrayOfStringifiedExpectedTargetRanges = (() => {
+ let arrayOfTargetRanges = [];
+ for (const expectedTargetRanges of expectedTargetRangesAtDeletion) {
+ arrayOfTargetRanges.push(
+ EditorTestUtils.getRangeArrayDescription(expectedTargetRanges)
+ );
+ }
+ return arrayOfTargetRanges;
+ })();
+ assert_in_array(
+ EditorTestUtils.getRangeArrayDescription(targetRanges),
+ arrayOfStringifiedExpectedTargetRanges
+ );
+ }, `getTargetRanges() for ${runningTest.name} ${whatGetTargetRangesShouldReturn}`);
+ }
+ await typeCharacter(testUtils, "X");
+ (Array.isArray(expectedAfterDeletionAndInsertion) ? assert_in_array : assert_equals)(
+ testUtils.editingHost.innerHTML,
+ expectedAfterDeletionAndInsertion,
+ `${insertTextMethod} after ${runningTest.name} ${whatShouldHappenAfterDeletionAndInsertion}`
+ );
+}
+
+async function runReplacingTest(
+ runningTest,
+ testUtils,
+ initialInnerHTML,
+ expectedAfterReplacing,
+ whatShouldHappenAfterReplacing,
+ expectedTargetRangesAtReplace,
+ whatGetTargetRangesShouldReturn
+) {
+ let targetRanges = [];
+ if (testUserInput) {
+ testUtils.editingHost.addEventListener(
+ "beforeinput",
+ event => targetRanges = event.getTargetRanges(),
+ {once: true}
+ );
+ }
+ await typeCharacter(testUtils, "X");
+ (Array.isArray(expectedAfterReplacing) ? assert_in_array : assert_equals)(
+ testUtils.editingHost.innerHTML,
+ expectedAfterReplacing,
+ `${runningTest.name} ${whatShouldHappenAfterReplacing}`
+ );
+ if (testUserInput) {
+ test(() => {
+ const arrayOfStringifiedExpectedTargetRanges = (() => {
+ let arrayOfTargetRanges = [];
+ for (const expectedTargetRanges of expectedTargetRangesAtReplace) {
+ arrayOfTargetRanges.push(
+ EditorTestUtils.getRangeArrayDescription(expectedTargetRanges)
+ );
+ }
+ return arrayOfTargetRanges;
+ })();
+ assert_in_array(
+ EditorTestUtils.getRangeArrayDescription(targetRanges),
+ arrayOfStringifiedExpectedTargetRanges
+ );
+ }, `getTargetRanges() for ${runningTest.name} ${whatGetTargetRangesShouldReturn}`);
+ }
+}
+
+addEventListener("load", () => {
+ const editingHost = document.querySelector("div[contenteditable]");
+ const selStart = lineBreakIsBR ? "{" : "[";
+ const selCollapsed = lineBreakIsBR ? "{}" : "[]";
+ editingHost.style.whiteSpace = lineBreakIsBR ? "normal" : "pre";
+ const testUtils = new EditorTestUtils(editingHost);
+ (() => {
+ const initialInnerHTML =
+ `abc${lineBreak}${selStart}${lineBreak}<div id="child">]def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ [
+ `abc${lineBreak}<div id="child">def<br>ghi</div>`,
+ `abc<div id="child">def<br>ghi</div>`,
+ ],
+ "should delete only the preceding empty line of the child <div>",
+ [
+ `abc${lineBreak}<div id="child">Xdef<br>ghi</div>`,
+ `abc<div id="child">Xdef<br>ghi</div>`,
+ ],
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // abc<br>{<br>}<div>
+ [{ startContainer: editingHost, startOffset: 2, endContainer: editingHost, endOffset: 3 }],
+ // abc{<br><br>}<div>
+ [{ startContainer: editingHost, startOffset: 1, endContainer: editingHost, endOffset: 3 }],
+ // abc[<br><br>}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost, endOffset: 3 }],
+ ]
+ : [
+ // abc\n[\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: editingHost, endOffset: 1 }],
+ // abc[\n\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost, endOffset: 1 }],
+ // abc\n[\n]<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: editingHost.firstChild, endOffset: "abc\n\n".length }],
+ // abc[\n\n]<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost.firstChild, endOffset: "abc\n\n".length }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runReplacingTest(
+ t, testUtils, initialInnerHTML,
+ [
+ `abc${lineBreak}<div id="child">Xdef<br>ghi</div>`,
+ `abc<div id="child">Xdef<br>ghi</div>`,
+ ],
+ "should not unwrap the first line of the child <div>",
+ lineBreakIsBR
+ ? [
+ // abc<br>{<br><div>]def
+ [{ startContainer: editingHost, startOffset: 2, endContainer: firstTextInChildDiv, endOffset: 0 }],
+ // abc{<br><br><div>]def
+ [{ startContainer: editingHost, startOffset: 1, endContainer: firstTextInChildDiv, endOffset: 0 }],
+ // abc[<br><br><div>]def
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: firstTextInChildDiv, endOffset: 0 }],
+ ]
+ : [
+ // abc\n[\n<div>]def
+ [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: firstTextInChildDiv, endOffset: 0 }],
+ // abc[\n\n<div>]def
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: firstTextInChildDiv, endOffset: 0 }],
+ ],
+ "should return a range ending in the child <div>"
+ );
+ }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `${lineBreak}[abc${lineBreak}<div id="child">]def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ `${lineBreak}<div id="child">def<br>ghi</div>`,
+ "should delete only the preceding empty line of the child <div>",
+ `${lineBreak}<div id="child">Xdef<br>ghi</div>`,
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // <br>[abc<br>}<div>
+ [{ startContainer: editingHost.firstChild.nextSibling, startOffset: 0, endContainer: editingHost, endOffset: 3 }],
+ ]
+ : [
+ // \n[abc\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost, endOffset: 1 }],
+ // \n[abc\n]<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost.firstChild, endOffset: "\nabc\n".length }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runReplacingTest(
+ t, testUtils, initialInnerHTML,
+ `${lineBreak}<div id="child">Xdef<br>ghi</div>`,
+ "should not unwrap the first line of the child <div>",
+ lineBreakIsBR
+ ? [
+ // <br>[abc<br><div>]def
+ [{ startContainer: editingHost.firstChild.nextSibling, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 0 }],
+ ]
+ : [
+ // \n[abc\n<div>]def
+ [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: firstTextInChildDiv, endOffset: 0 }],
+ ],
+ "should return a range ending in the child <div>"
+ );
+ }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `${lineBreak}${selStart}${lineBreak}<div id="child">]def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ `${lineBreak}<div id="child">def<br>ghi</div>`,
+ "should delete only the preceding empty line of the child <div>",
+ `${lineBreak}<div id="child">Xdef<br>ghi</div>`,
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // <br>{<br>}<div>
+ [{ startContainer: editingHost, startOffset: 1, endContainer: editingHost, endOffset: 2 }],
+ ]
+ : [
+ // \n[\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost, endOffset: 1 }],
+ // \n[\n]<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost.firstChild, endOffset: "\n\n".length }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runReplacingTest(
+ t, testUtils, initialInnerHTML,
+ `${lineBreak}<div id="child">Xdef<br>ghi</div>`,
+ "should not unwrap the first line of the child <div>",
+ lineBreakIsBR
+ ? [
+ // <br>{<br><div>]def
+ [{ startContainer: editingHost, startOffset: 1, endContainer: firstTextInChildDiv, endOffset: 0 }],
+ ]
+ : [
+ // \n[\n<div>]def
+ [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: firstTextInChildDiv, endOffset: 0 }],
+ ],
+ "should return a range ending in the child <div>"
+ );
+ }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `${selStart}${lineBreak}${lineBreak}<div id="child">]def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ `<div id="child">def<br>ghi</div>`,
+ "should delete only the preceding empty line of the child <div>",
+ `<div id="child">Xdef<br>ghi</div>`,
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // {<br><br>}<div>
+ [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 2 }],
+ ]
+ : [
+ // [\n\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost, endOffset: 1 }],
+ // [\n\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost.firstChild, endOffset: "\n\n".length }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runReplacingTest(
+ t, testUtils, initialInnerHTML,
+ `<div id="child">Xdef<br>ghi</div>`,
+ "should not unwrap the first line of the child <div>",
+ lineBreakIsBR
+ ? [
+ // {<br><br><div>]def
+ [{ startContainer: editingHost, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 0 }],
+ ]
+ : [
+ // [\n\n<div>]def
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 0 }],
+ ],
+ "should return a range ending in the child <div>"
+ );
+ }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `[abc${lineBreak}${lineBreak}<div id="child">]def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ `<div id="child">def<br>ghi</div>`,
+ "should delete only the preceding empty line of the child <div>",
+ `<div id="child">Xdef<br>ghi</div>`,
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // [abc<br><br>}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost, endOffset: 3 }],
+ ]
+ : [
+ // {abc\n\n}<div>
+ [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 1 }],
+ // [abc\n\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost, endOffset: 1 }],
+ // [abc\n\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost.firstChild, endOffset: "abc\n\n".length }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runReplacingTest(
+ t, testUtils, initialInnerHTML,
+ `<div id="child">Xdef<br>ghi</div>`,
+ "should not unwrap the first line of the child <div>",
+ lineBreakIsBR
+ ? [
+ // [abc<br><div>]def
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 0 }],
+ ]
+ : [
+ // [abc\n<div>]def
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 0 }],
+ ],
+ "should return a range ending in the child <div>"
+ );
+ }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `abc${lineBreak}${selStart}${lineBreak}<div id="child">d]ef<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ [
+ `abc${lineBreak}<div id="child">ef<br>ghi</div>`,
+ `abc<div id="child">ef<br>ghi</div>`,
+ ],
+ "should delete only the preceding empty line of the child <div> and selected text in the <div>",
+ [
+ `abc${lineBreak}<div id="child">Xef<br>ghi</div>`,
+ `abc<div id="child">Xef<br>ghi</div>`,
+ ],
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // abc<br>{<br><div>d]ef
+ [{ startContainer: editingHost, startOffset: 2, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ // abc{<br><br><div>d]ef
+ [{ startContainer: editingHost, startOffset: 1, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ // abc[<br><br><div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ]
+ : [
+ // abc\n[\n}<div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ // abc[\n\n}<div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ],
+ "should return a range ends at start of the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runReplacingTest(
+ t, testUtils, initialInnerHTML,
+ [
+ `abc${lineBreak}<div id="child">Xef<br>ghi</div>`,
+ `abc<div id="child">Xef<br>ghi</div>`,
+ ],
+ "should not unwrap the first line of the child <div>",
+ lineBreakIsBR
+ ? [
+ // abc<br>{<br><div>d]ef
+ [{ startContainer: editingHost, startOffset: 2, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ // abc{<br><br><div>d]ef
+ [{ startContainer: editingHost, startOffset: 1, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ // abc[<br><br><div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ]
+ : [
+ // abc\n[\n<div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ // abc[\n\n<div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ],
+ "should return a range ends at start of the child <div>"
+ );
+ }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `${lineBreak}[abc${lineBreak}<div id="child">d]ef<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ `${lineBreak}<div id="child">ef<br>ghi</div>`,
+ "should delete only the preceding empty line of the child <div> and the selected content in the <div>",
+ `${lineBreak}<div id="child">Xef<br>ghi</div>`,
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // <br>[abc<br><div>d]ef
+ [{ startContainer: editingHost.firstChild.nextSibling, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ]
+ : [
+ // \n[abc\n<div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ],
+ "should return a range ends at start of the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runReplacingTest(
+ t, testUtils, initialInnerHTML,
+ `${lineBreak}<div id="child">Xef<br>ghi</div>`,
+ "should not unwrap the first line of the child <div>",
+ lineBreakIsBR
+ ? [
+ // <br>[abc<br><div>d]ef
+ [{ startContainer: editingHost.firstChild.nextSibling, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ]
+ : [
+ // \n[abc\n<div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ],
+ "should return a range ends at start of the child <div>"
+ );
+ }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `${lineBreak}${selStart}${lineBreak}<div id="child">d]ef<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ `${lineBreak}<div id="child">ef<br>ghi</div>`,
+ "should delete only the preceding empty line of the child <div> and selected content in the <div>",
+ `${lineBreak}<div id="child">Xef<br>ghi</div>`,
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // <br>{<br><div>d]ef
+ [{ startContainer: editingHost, startOffset: 1, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ]
+ : [
+ // \n[\n<div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ],
+ "should return a range ends at start of the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runReplacingTest(
+ t, testUtils, initialInnerHTML,
+ `${lineBreak}<div id="child">Xef<br>ghi</div>`,
+ "should not unwrap the first line of the child <div>",
+ lineBreakIsBR
+ ? [
+ // <br>{<br><div>d]ef
+ [{ startContainer: editingHost, startOffset: 1, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ]
+ : [
+ // \n[\n<div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ],
+ "should return a range ends at start of the child <div>"
+ );
+ }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `${selStart}${lineBreak}${lineBreak}<div id="child">d]ef<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ `<div id="child">ef<br>ghi</div>`,
+ "should delete only the preceding empty line of the child <div> and selected content in the <div>",
+ `<div id="child">Xef<br>ghi</div>`,
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // {<br><br><div>d]ef
+ [{ startContainer: editingHost, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ]
+ : [
+ // [\n\n<div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ],
+ "should return a range ends at start of the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runReplacingTest(
+ t, testUtils, initialInnerHTML,
+ `<div id="child">Xef<br>ghi</div>`,
+ "should not unwrap the first line of the child <div>",
+ lineBreakIsBR
+ ? [
+ // {<br><br><div>d]ef
+ [{ startContainer: editingHost, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ]
+ : [
+ // [\n\n<div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ],
+ "should return a range ends at start of the child <div>"
+ );
+ }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `[abc${lineBreak}${lineBreak}<div id="child">d]ef<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ `<div id="child">ef<br>ghi</div>`,
+ "should delete only the preceding empty line of the child <div> and selected content in the <div>",
+ `<div id="child">Xef<br>ghi</div>`,
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // [abc<br><br><div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ]
+ : [
+ // [abc\n\n}<div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ],
+ "should return a range ends at start of the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const firstTextInChildDiv = editingHost.querySelector("div").firstChild;
+ await runReplacingTest(
+ t, testUtils, initialInnerHTML,
+ `<div id="child">Xef<br>ghi</div>`,
+ "should not unwrap the first line of the child <div>",
+ lineBreakIsBR
+ ? [
+ // [abc<br><div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ]
+ : [
+ // [abc\n<div>d]ef
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }],
+ ],
+ "should return a range ends at start of the child <div>"
+ );
+ }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`);
+ })();
+
+ (function test_BackspaceForCollapsedSelection() {
+ if (!testBackward) {
+ return;
+ }
+ (() => {
+ const initialInnerHTML =
+ `abc${lineBreak}${lineBreak}<div id="child">[]def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ [
+ `abc${lineBreak}<div id="child">def<br>ghi</div>`,
+ `abc<div id="child">def<br>ghi</div>`,
+ ],
+ "should delete only the preceding empty line of the child <div>",
+ [
+ `abc${lineBreak}<div id="child">Xdef<br>ghi</div>`,
+ `abc<div id="child">Xdef<br>ghi</div>`,
+ ],
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // abc<br>{<br>}<div>
+ [{ startContainer: editingHost, startOffset: 2, endContainer: editingHost, endOffset: 3 }],
+ // abc{<br><br>}<div>
+ [{ startContainer: editingHost, startOffset: 1, endContainer: editingHost, endOffset: 3 }],
+ // abc[<br><br>}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost, endOffset: 3 }],
+ ]
+ : [
+ // abc\n[\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: editingHost, endOffset: 1 }],
+ // abc[\n\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost, endOffset: 1 }],
+ // abc\n[\n]<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: editingHost.firstChild, endOffset: "abc\n\n".length }],
+ // abc[\n\n]<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost.firstChild, endOffset: "abc\n\n".length }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `${lineBreak}${lineBreak}<div id="child">[]def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ `${lineBreak}<div id="child">def<br>ghi</div>`,
+ "should delete only the preceding empty line of the child <div>",
+ `${lineBreak}<div id="child">Xdef<br>ghi</div>`,
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // <br>{<br>}<div>
+ [{ startContainer: editingHost, startOffset: 1, endContainer: editingHost, endOffset: 2 }],
+ ]
+ : [
+ // \n[\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost, endOffset: 1 }],
+ // \n[\n]<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost.firstChild, endOffset: "\n\n".length }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `${lineBreak}<div id="child">[]def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ `<div id="child">def<br>ghi</div>`,
+ "should delete only the preceding empty line of the child <div>",
+ `<div id="child">Xdef<br>ghi</div>`,
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // {<br>}<div>
+ [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 1 }],
+ ]
+ : [
+ // {\n}<div>
+ [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 1 }],
+ // [\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost, endOffset: 1 }],
+ // [\n]<div>
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost.firstChild, endOffset: "\n".length }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `<b>abc${lineBreak}${lineBreak}</b></b><div id="child">[]def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const b = editingHost.querySelector("b");
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ [
+ `<b>abc${lineBreak}</b><div id="child">def<br>ghi</div>`,
+ `<b>abc</b><div id="child">def<br>ghi</div>`,
+ ],
+ "should delete only the preceding empty line of the child <div> (<b> should stay)",
+ [
+ `<b>abc${lineBreak}</b><div id="child">Xdef<br>ghi</div>`,
+ `<b>abc</b><div id="child">Xdef<br>ghi</div>`,
+ `<b>abc${lineBreak}</b><div id="child"><b>X</b>def<br>ghi</div>`,
+ `<b>abc</b><div id="child"><b>X</b>def<br>ghi</div>`,
+ ],
+ "should insert text into the child <div> with or without <b>",
+ lineBreakIsBR
+ ? [
+ // <b>abc<br>{<br>}</b><div>
+ [{ startContainer: b, startOffset: 2, endContainer: b, endOffset: 3 }],
+ // <b>abc{<br><br>}</b><div>
+ [{ startContainer: b, startOffset: 1, endContainer: b, endOffset: 3 }],
+ // <b>abc[<br><br>}</b><div>
+ [{ startContainer: b.firstChild, startOffset: "abc".length, endContainer: b, endOffset: 3 }],
+ ]
+ : [
+ // <b>abc\n[\n}</b><div>
+ [{ startContainer: b.firstChild, startOffset: "abc\n".length, endContainer: b, endOffset: 1 }],
+ // <b>abc[\n\n}</b><div>
+ [{ startContainer: b.firstChild, startOffset: "abc".length, endContainer: b, endOffset: 1 }],
+ // <b>abc\n[\n]</b><div>
+ [{ startContainer: b.firstChild, startOffset: "abc\n".length, endContainer: b.firstChild, endOffset: "abc\n\n".length }],
+ // <b>abc[\n\n]</b><div>
+ [{ startContainer: b.firstChild, startOffset: "abc".length, endContainer: b.firstChild, endOffset: "abc\n\n".length }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `<b>${lineBreak}</b><div id="child">[]def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ `<div id="child">def<br>ghi</div>`,
+ "should delete only the preceding empty line (including the <b>) of the child <div>",
+ [
+ `<div id="child">Xdef<br>ghi</div>`,
+ `<div id="child"><b>X</b>def<br>ghi</div>`,
+ ],
+ "should insert text into the child <div> with or without <b>",
+ [
+ // {<b><br></b>}<div> or {<b>\n</b>}<div>
+ [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 1 }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+ })();
+ })();
+
+ (function test_ForwardDeleteForCollapsedSelection() {
+ if (testBackward) {
+ return;
+ }
+ (() => {
+ const initialInnerHTML =
+ `abc${lineBreak}${selCollapsed}${lineBreak}<div id="child">def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ [
+ `abc${lineBreak}<div id="child">def<br>ghi</div>`,
+ `abc<div id="child">def<br>ghi</div>`,
+ ],
+ "should delete only the preceding empty line of the child <div>",
+ [
+ `abc${lineBreak}<div id="child">Xdef<br>ghi</div>`,
+ `abc<div id="child">Xdef<br>ghi</div>`,
+ ],
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // abc<br>{<br>}<div>
+ [{ startContainer: editingHost, startOffset: 2, endContainer: editingHost, endOffset: 3 }],
+ // abc{<br><br>}<div>
+ [{ startContainer: editingHost, startOffset: 1, endContainer: editingHost, endOffset: 3 }],
+ // abc[<br><br>}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost, endOffset: 3 }],
+ ]
+ : [
+ // abc\n[\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: editingHost, endOffset: 1 }],
+ // abc[\n\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost, endOffset: 1 }],
+ // abc\n[\n]<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: editingHost.firstChild, endOffset: "abc\n\n".length }],
+ // abc[\n\n]<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost.firstChild, endOffset: "abc\n\n".length }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `${lineBreak}${selCollapsed}${lineBreak}<div id="child">def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ `${lineBreak}<div id="child">def<br>ghi</div>`,
+ "should delete only the preceding empty line of the child <div>",
+ `${lineBreak}<div id="child">Xdef<br>ghi</div>`,
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // <br>{<br>}<div>
+ [{ startContainer: editingHost, startOffset: 1, endContainer: editingHost, endOffset: 2 }],
+ ]
+ : [
+ // \n[\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost, endOffset: 1 }],
+ // \n[\n]<div>
+ [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost.firstChild, endOffset: "\n\n".length }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `${selCollapsed}${lineBreak}<div id="child">def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ `<div id="child">def<br>ghi</div>`,
+ "should delete only the preceding empty line of the child <div>",
+ `<div id="child">Xdef<br>ghi</div>`,
+ "should insert text into the child <div>",
+ lineBreakIsBR
+ ? [
+ // {<br>}<div>
+ [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 1 }],
+ ]
+ : [
+ // {\n}<div>
+ [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 1 }],
+ // [\n}<div>
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost, endOffset: 1 }],
+ // [\n]<div>
+ [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost.firstChild, endOffset: "\n".length }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `<b>abc${lineBreak}${selCollapsed}${lineBreak}</b></b><div id="child">def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ const b = editingHost.querySelector("b");
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ [
+ `<b>abc${lineBreak}</b><div id="child">def<br>ghi</div>`,
+ `<b>abc</b><div id="child">def<br>ghi</div>`,
+ ],
+ "should delete only the preceding empty line of the child <div> (<b> should stay)",
+ [
+ `<b>abc${lineBreak}</b><div id="child">Xdef<br>ghi</div>`,
+ `<b>abc</b><div id="child">Xdef<br>ghi</div>`,
+ `<b>abc${lineBreak}</b><div id="child"><b>X</b>def<br>ghi</div>`,
+ `<b>abc</b><div id="child"><b>X</b>def<br>ghi</div>`,
+ ],
+ "should insert text into the child <div> with or without <b>",
+ lineBreakIsBR
+ ? [
+ // <b>abc<br>{<br>}</b><div>
+ [{ startContainer: b, startOffset: 2, endContainer: b, endOffset: 3 }],
+ // <b>abc{<br><br>}</b><div>
+ [{ startContainer: b, startOffset: 1, endContainer: b, endOffset: 3 }],
+ // <b>abc[<br><br>}</b><div>
+ [{ startContainer: b.firstChild, startOffset: "abc".length, endContainer: b, endOffset: 3 }],
+ ]
+ : [
+ // <b>abc\n[\n}</b><div>
+ [{ startContainer: b.firstChild, startOffset: "abc\n".length, endContainer: b, endOffset: 1 }],
+ // <b>abc[\n\n}</b><div>
+ [{ startContainer: b.firstChild, startOffset: "abc".length, endContainer: b, endOffset: 1 }],
+ // <b>abc\n[\n]</b><div>
+ [{ startContainer: b.firstChild, startOffset: "abc\n".length, endContainer: b.firstChild, endOffset: "abc\n\n".length }],
+ // <b>abc[\n\n]</b><div>
+ [{ startContainer: b.firstChild, startOffset: "abc".length, endContainer: b.firstChild, endOffset: "abc\n\n".length }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+ })();
+
+ (() => {
+ const initialInnerHTML =
+ `<b>${selCollapsed}${lineBreak}</b><div id="child">def<br>ghi</div>`;
+ promise_test(async t => {
+ testUtils.setupEditingHost(initialInnerHTML);
+ await runDeleteTest(
+ t, testUtils, initialInnerHTML,
+ `<div id="child">def<br>ghi</div>`,
+ "should delete only the preceding empty line (including the <b>) of the child <div>",
+ [
+ `<div id="child">Xdef<br>ghi</div>`,
+ `<div id="child"><b>X</b>def<br>ghi</div>`,
+ ],
+ "should insert text into the child <div> with or without <b>",
+ [
+ // {<b><br></b>}<div> or {<b>\n</b>}<div>
+ [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 1 }],
+ ],
+ "should return a range before the child <div>"
+ );
+ }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`);
+ })();
+ })();
+}, {once: true});
+</script>
+</head>
+<body><div contenteditable></div></body>
+</html>
diff --git a/testing/web-platform/tests/encoding/encodeInto.any.js b/testing/web-platform/tests/encoding/encodeInto.any.js
index 69d7089006..9ea36d23d0 100644
--- a/testing/web-platform/tests/encoding/encodeInto.any.js
+++ b/testing/web-platform/tests/encoding/encodeInto.any.js
@@ -129,6 +129,7 @@
"Uint8ClampedArray",
"BigInt64Array",
"BigUint64Array",
+ "Float16Array",
"Float32Array",
"Float64Array"].forEach(type => {
["ArrayBuffer", "SharedArrayBuffer"].forEach((arrayBufferOrSharedArrayBuffer) => {
diff --git a/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-content-initiated.https.html b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-content-initiated.https.html
new file mode 100644
index 0000000000..9c1d47d050
--- /dev/null
+++ b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-content-initiated.https.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<title>Test window.fence.reportEvent from cross-origin subframes.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/automatic-beacon-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<script>
+promise_test(async(t) => {
+ const fencedframe = await attachFencedFrameContext({
+ generator_api: 'fledge',
+ headers: [[
+ 'Allow-Cross-Origin-Event-Reporting', 'true'
+ ]],
+ register_beacon: true,
+ origin: get_host_info().HTTPS_ORIGIN
+ });
+ // Perform a cross-origin navigation. Since the navigation is
+ // content-initiated, the fenced frame reporting metadata will persist.
+ await navigateFrameContext(fencedframe, {
+ origin: get_host_info().HTTPS_REMOTE_ORIGIN,
+ });
+ await fencedframe.execute(() => {
+ // This page will call reportEvent() twice. Once for a destination enum
+ // event (i.e. an event with an eventType/eventData specified), and once for
+ // a destination URL event (i.e. an event with a destinationURL specified).
+ const destination_enum_event = {
+ eventType: "click",
+ eventData: "enum",
+ destination: ["buyer"],
+ crossOriginExposed: true
+ }
+ window.fence.reportEvent(destination_enum_event);
+
+ const destination_url = new URL(BEACON_URL + "?type=url",
+ get_host_info().HTTPS_ORIGIN);
+ const destination_url_event = {
+ destinationURL: destination_url,
+ crossOriginExposed: true
+ }
+ window.fence.reportEvent(destination_url_event);
+ });
+ await nextBeacon("click", "enum");
+ await nextBeacon("url", "<No data>");
+}, 'window.fence.reportEvent from a content-initiated cross-origin navigation');
+</script>
+</body>
diff --git a/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-nested-urn-iframe.https.html b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-nested-urn-iframe.https.html
new file mode 100644
index 0000000000..7d0544a5ad
--- /dev/null
+++ b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-nested-urn-iframe.https.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<title>Test window.fence.reportEvent from nested cross-origin subframes.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/automatic-beacon-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<script>
+promise_test(async(t) => {
+ // This test creates the following frame setup:
+ // ┌(test)─────────────────┐
+ // │┌(a.com)───[iframe]───┐│
+ // ││┌(b.com)──[iframe]──┐││
+ // │││┌(b.com)─[iframe]─┐│││
+ // ││││reportEvent(); ││││
+ // │││└─────────────────┘│││
+ // ││└───────────────────┘││
+ // │└─────────────────────┘│
+ // └───────────────────────┘
+ const fencedframe = await attachIFrameContext({
+ generator_api: 'fledge',
+ headers: [[
+ 'Allow-Cross-Origin-Event-Reporting', 'true'
+ ]],
+ register_beacon: true
+ });
+ await fencedframe.execute(async () => {
+ const iframe = await attachIFrameContext({
+ origin: get_host_info().HTTPS_REMOTE_ORIGIN,
+ });
+ await iframe.execute(async () => {
+ const nested_iframe = await attachIFrameContext({
+ origin: get_host_info().HTTPS_REMOTE_ORIGIN,
+ });
+ await nested_iframe.execute(() => {
+ const destination_url = new URL(BEACON_URL + "?type=url",
+ get_host_info().HTTPS_ORIGIN);
+ window.fence.reportEvent({
+ eventType: "click",
+ eventData: "enum",
+ destination: ["buyer"],
+ crossOriginExposed: true
+ });
+ window.fence.reportEvent({
+ destinationURL: destination_url,
+ crossOriginExposed: true
+ });
+ });
+ });
+ });
+ // Check that both the destination enum and destination URL events were
+ // reported.
+ await nextBeacon("click", "enum");
+ await nextBeacon("url", "<No data>");
+}, 'window.fence.reportEvent from a nested cross-origin subframe of a URN ' +
+ 'iframe');
+</script>
+</body>
diff --git a/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-nested.https.html b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-nested.https.html
new file mode 100644
index 0000000000..4d1262f4b1
--- /dev/null
+++ b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-nested.https.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<title>Test window.fence.reportEvent from nested cross-origin subframes.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/automatic-beacon-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<script>
+promise_test(async(t) => {
+ // This test creates the following frame setup:
+ // ┌(test)──────────────────────┐
+ // │┌(a.com)───[fencedframe]───┐│
+ // ││┌(b.com)──[iframe]───────┐││
+ // │││┌(b.com)─[iframe]──────┐│││
+ // ││││reportEvent(); ││││
+ // │││└──────────────────────┘│││
+ // ││└────────────────────────┘││
+ // │└──────────────────────────┘│
+ // └────────────────────────────┘
+ const fencedframe = await attachFencedFrameContext({
+ generator_api: 'fledge',
+ headers: [[
+ 'Allow-Cross-Origin-Event-Reporting', 'true'
+ ]],
+ register_beacon: true
+ });
+ await fencedframe.execute(async () => {
+ const iframe = await attachIFrameContext({
+ origin: get_host_info().HTTPS_REMOTE_ORIGIN,
+ });
+ await iframe.execute(async () => {
+ const nested_iframe = await attachIFrameContext({
+ origin: get_host_info().HTTPS_REMOTE_ORIGIN,
+ });
+ await nested_iframe.execute(() => {
+ const destination_url = new URL(BEACON_URL + "?type=url",
+ get_host_info().HTTPS_ORIGIN);
+ window.fence.reportEvent({
+ eventType: "click",
+ eventData: "enum",
+ destination: ["buyer"],
+ crossOriginExposed: true
+ });
+ window.fence.reportEvent({
+ destinationURL: destination_url,
+ crossOriginExposed: true
+ });
+ });
+ });
+ });
+ // Check that both the destination enum and destination URL events were
+ // reported.
+ await nextBeacon("click", "enum");
+ await nextBeacon("url", "<No data>");
+}, 'window.fence.reportEvent from a nested cross-origin subframe');
+</script>
+</body>
diff --git a/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-no-embedder-opt-in.https.html b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-no-embedder-opt-in.https.html
new file mode 100644
index 0000000000..d8fa5133cd
--- /dev/null
+++ b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-no-embedder-opt-in.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Test window.fence.reportEvent from cross-origin subframes.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/automatic-beacon-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<script>
+promise_test(async(t) => {
+ const fencedframe = await attachFencedFrameContext({
+ generator_api: 'fledge',
+ headers: [[
+ 'Allow-Cross-Origin-Event-Reporting', 'false'
+ ]],
+ register_beacon: true
+ });
+ // Perform a cross-origin navigation. Since the navigation is
+ // content-initiated, the fenced frame reporting metadata will persist.
+ await navigateFrameContext(fencedframe, {
+ origin: get_host_info().HTTPS_REMOTE_ORIGIN,
+ });
+ await fencedframe.execute(() => {
+ // This page will call reportEvent() twice. Once for a destination enum
+ // event (i.e. an event with an eventType/eventData specified), and once for
+ // a destination URL event (i.e. an event with a destinationURL specified).
+ const destination_enum_event = {
+ eventType: "click",
+ eventData: "enum",
+ destination: ["buyer"],
+ crossOriginExposed: true
+ }
+ window.fence.reportEvent(destination_enum_event);
+
+ const destination_url = new URL(BEACON_URL + "?type=url",
+ get_host_info().HTTPS_ORIGIN);
+ const destination_url_event = {
+ destinationURL: destination_url,
+ crossOriginExposed: true
+ }
+ window.fence.reportEvent(destination_url_event);
+ });
+ await verifyBeaconData("click", "enum", false, t);
+ await verifyBeaconData("url", "<No data>", false, t);
+}, 'Cross-origin window.fence.reportEvent without embedder opt-in');
+</script>
+</body>
diff --git a/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-no-subframe-opt-in.https.html b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-no-subframe-opt-in.https.html
new file mode 100644
index 0000000000..2b054c1837
--- /dev/null
+++ b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-no-subframe-opt-in.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<title>Test window.fence.reportEvent from cross-origin subframes.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/automatic-beacon-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<script>
+promise_test(async(t) => {
+ const fencedframe = await attachFencedFrameContext({
+ generator_api: 'fledge',
+ headers: [[
+ 'Allow-Cross-Origin-Event-Reporting', 'true'
+ ]],
+ register_beacon: true
+ });
+ await fencedframe.execute(async () => {
+ const iframe = await attachIFrameContext({
+ origin: get_host_info().HTTPS_REMOTE_ORIGIN,
+ });
+ await iframe.execute(() => {
+ const destination_url = new URL(BEACON_URL + "?type=url",
+ get_host_info().HTTPS_ORIGIN);
+ window.fence.reportEvent({
+ eventType: "click",
+ eventData: "enum",
+ destination: ["buyer"],
+ crossOriginExposed: false
+ });
+ window.fence.reportEvent({
+ destinationURL: destination_url,
+ crossOriginExposed: false
+ });
+ });
+ });
+ // Check that both the destination enum and destination URL events were
+ // reported.
+ await verifyBeaconData("click", "enum", false, t);
+ await verifyBeaconData("url", "<No data>", false, t);
+}, 'Cross-origin window.fence.reportEvent without subframe opt-in');
+</script>
+</body>
diff --git a/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe-content-initiated.https.html b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe-content-initiated.https.html
new file mode 100644
index 0000000000..21c9ea1a43
--- /dev/null
+++ b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe-content-initiated.https.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<title>Test window.fence.reportEvent from cross-origin subframes.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/automatic-beacon-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<script>
+promise_test(async(t) => {
+ const iframe = await attachIFrameContext({
+ generator_api: 'fledge',
+ headers: [[
+ 'Allow-Cross-Origin-Event-Reporting', 'true'
+ ]],
+ register_beacon: true,
+ origin: get_host_info().HTTPS_ORIGIN
+ });
+ // Perform a cross-origin navigation. Since the navigation is
+ // content-initiated, the fenced frame reporting metadata will persist.
+ await navigateFrameContext(iframe, {
+ origin: get_host_info().HTTPS_REMOTE_ORIGIN,
+ });
+ await iframe.execute(() => {
+ // This page will call reportEvent() twice. Once for a destination enum
+ // event (i.e. an event with an eventType/eventData specified), and once for
+ // a destination URL event (i.e. an event with a destinationURL specified).
+ const destination_enum_event = {
+ eventType: "click",
+ eventData: "enum",
+ destination: ["buyer"],
+ crossOriginExposed: true
+ }
+ window.fence.reportEvent(destination_enum_event);
+
+ const destination_url = new URL(BEACON_URL + "?type=url",
+ get_host_info().HTTPS_ORIGIN);
+ const destination_url_event = {
+ destinationURL: destination_url,
+ crossOriginExposed: true
+ }
+ window.fence.reportEvent(destination_url_event);
+ });
+ await nextBeacon("click", "enum");
+ await nextBeacon("url", "<No data>");
+}, 'window.fence.reportEvent from a content-initiated cross-origin navigation');
+</script>
+</body>
diff --git a/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe-no-embedder-opt-in.https.html b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe-no-embedder-opt-in.https.html
new file mode 100644
index 0000000000..5d368fe7f1
--- /dev/null
+++ b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe-no-embedder-opt-in.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Test window.fence.reportEvent from cross-origin subframes.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/automatic-beacon-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<script>
+promise_test(async(t) => {
+ const iframe = await attachIFrameContext({
+ generator_api: 'fledge',
+ headers: [[
+ 'Allow-Cross-Origin-Event-Reporting', 'false'
+ ]],
+ register_beacon: true
+ });
+ // Perform a cross-origin navigation. Since the navigation is
+ // content-initiated, the fenced frame reporting metadata will persist.
+ await navigateFrameContext(iframe, {
+ origin: get_host_info().HTTPS_REMOTE_ORIGIN,
+ });
+ await iframe.execute(() => {
+ // This page will call reportEvent() twice. Once for a destination enum
+ // event (i.e. an event with an eventType/eventData specified), and once for
+ // a destination URL event (i.e. an event with a destinationURL specified).
+ const destination_enum_event = {
+ eventType: "click",
+ eventData: "enum",
+ destination: ["buyer"],
+ crossOriginExposed: true
+ }
+ window.fence.reportEvent(destination_enum_event);
+
+ const destination_url = new URL(BEACON_URL + "?type=url",
+ get_host_info().HTTPS_ORIGIN);
+ const destination_url_event = {
+ destinationURL: destination_url,
+ crossOriginExposed: true
+ }
+ window.fence.reportEvent(destination_url_event);
+ });
+ await verifyBeaconData("click", "enum", false, t);
+ await verifyBeaconData("url", "<No data>", false, t);
+}, 'Cross-origin window.fence.reportEvent without embedder opt-in');
+</script>
+</body>
diff --git a/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe-no-subframe-opt-in.https.html b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe-no-subframe-opt-in.https.html
new file mode 100644
index 0000000000..df22749a9e
--- /dev/null
+++ b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe-no-subframe-opt-in.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<title>Test window.fence.reportEvent from cross-origin subframes.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/automatic-beacon-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<script>
+promise_test(async(t) => {
+ const outer_iframe = await attachIFrameContext({
+ generator_api: 'fledge',
+ headers: [[
+ 'Allow-Cross-Origin-Event-Reporting', 'true'
+ ]],
+ register_beacon: true
+ });
+ await outer_iframe.execute(async () => {
+ const inner_iframe = await attachIFrameContext({
+ origin: get_host_info().HTTPS_REMOTE_ORIGIN,
+ });
+ await inner_iframe.execute(() => {
+ const destination_url = new URL(BEACON_URL + "?type=url",
+ get_host_info().HTTPS_ORIGIN);
+ window.fence.reportEvent({
+ eventType: "click",
+ eventData: "enum",
+ destination: ["buyer"],
+ crossOriginExposed: false
+ });
+ window.fence.reportEvent({
+ destinationURL: destination_url,
+ crossOriginExposed: false
+ });
+ });
+ });
+ // Check that both the destination enum and destination URL events were
+ // reported.
+ await verifyBeaconData("click", "enum", false, t);
+ await verifyBeaconData("url", "<No data>", false, t);
+}, 'Cross-origin window.fence.reportEvent without subframe opt-in');
+</script>
+</body>
diff --git a/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe.https.html b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe.https.html
new file mode 100644
index 0000000000..b37fec812d
--- /dev/null
+++ b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin-urn-iframe.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<title>Test window.fence.reportEvent from cross-origin subframes.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/automatic-beacon-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<script>
+promise_test(async(t) => {
+ const outer_iframe = await attachIFrameContext({
+ generator_api: 'fledge',
+ headers: [[
+ 'Allow-Cross-Origin-Event-Reporting', 'true'
+ ]],
+ register_beacon: true
+ });
+ await outer_iframe.execute(async () => {
+ const inner_iframe = await attachIFrameContext({
+ origin: get_host_info().HTTPS_REMOTE_ORIGIN,
+ });
+ await inner_iframe.execute(() => {
+ const destination_url = new URL(BEACON_URL + "?type=url",
+ get_host_info().HTTPS_ORIGIN);
+ window.fence.reportEvent({
+ eventType: "click",
+ eventData: "enum",
+ destination: ["buyer"],
+ crossOriginExposed: true
+ });
+ window.fence.reportEvent({
+ destinationURL: destination_url,
+ crossOriginExposed: true
+ });
+ });
+ });
+ // Check that both the destination enum and destination URL events were
+ // reported.
+ await nextBeacon("click", "enum");
+ await nextBeacon("url", "<No data>");
+}, 'window.fence.reportEvent from a cross-origin iframe');
+</script>
+</body>
diff --git a/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin.https.html b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin.https.html
new file mode 100644
index 0000000000..df7ae776ab
--- /dev/null
+++ b/testing/web-platform/tests/fenced-frame/fence-report-event-cross-origin.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<title>Test window.fence.reportEvent from cross-origin subframes.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/automatic-beacon-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<script>
+promise_test(async(t) => {
+ const fencedframe = await attachFencedFrameContext({
+ generator_api: 'fledge',
+ headers: [[
+ 'Allow-Cross-Origin-Event-Reporting', 'true'
+ ]],
+ register_beacon: true
+ });
+ await fencedframe.execute(async () => {
+ const iframe = await attachIFrameContext({
+ origin: get_host_info().HTTPS_REMOTE_ORIGIN,
+ });
+ await iframe.execute(() => {
+ const destination_url = new URL(BEACON_URL + "?type=url",
+ get_host_info().HTTPS_ORIGIN);
+ window.fence.reportEvent({
+ eventType: "click",
+ eventData: "enum",
+ destination: ["buyer"],
+ crossOriginExposed: true
+ });
+ window.fence.reportEvent({
+ destinationURL: destination_url,
+ crossOriginExposed: true
+ });
+ });
+ });
+ // Check that both the destination enum and destination URL events were
+ // reported.
+ await nextBeacon("click", "enum");
+ await nextBeacon("url", "<No data>");
+}, 'window.fence.reportEvent from a cross-origin subframe');
+</script>
+</body>
diff --git a/testing/web-platform/tests/fenced-frame/fence-report-event-sub-fencedframe.https.html b/testing/web-platform/tests/fenced-frame/fence-report-event-sub-fencedframe.https.html
new file mode 100644
index 0000000000..0b3231ca92
--- /dev/null
+++ b/testing/web-platform/tests/fenced-frame/fence-report-event-sub-fencedframe.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<title>Test window.fence.reportEvent from nested fenced frames.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/automatic-beacon-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<script>
+promise_test(async(t) => {
+ const fencedframe = await attachFencedFrameContext({
+ generator_api: 'fledge',
+ headers: [[
+ 'Allow-Cross-Origin-Event-Reporting', 'true'
+ ]],
+ register_beacon: true
+ });
+ await fencedframe.execute(async () => {
+ const fencedframe = await attachFencedFrameContext({
+ generator_api: 'sharedstorage'
+ });
+ await fencedframe.execute(() => {
+ const destination_url = new URL(BEACON_URL + "?type=url",
+ get_host_info().HTTPS_ORIGIN);
+ window.fence.reportEvent({
+ eventType: "click",
+ eventData: "enum",
+ destination: ["buyer"],
+ crossOriginExposed: true
+ });
+ window.fence.reportEvent({
+ destinationURL: destination_url,
+ crossOriginExposed: true
+ });
+ });
+ });
+ // Check that both the destination enum and destination URL events were
+ // reported.
+ await verifyBeaconData("click", "enum", false, t);
+ await verifyBeaconData("url", "<No data>", false, t);
+}, 'window.fence.reportEvent should not work in a nested fenced frame');
+</script>
+</body>
diff --git a/testing/web-platform/tests/fenced-frame/resources/utils.js b/testing/web-platform/tests/fenced-frame/resources/utils.js
index 462bda37fc..4914802518 100644
--- a/testing/web-platform/tests/fenced-frame/resources/utils.js
+++ b/testing/web-platform/tests/fenced-frame/resources/utils.js
@@ -162,8 +162,8 @@ async function generateURNFromFledgeRawURL(
// @param {boolean} [ad_with_size = false] - Determines whether the auction is
// run with ad sizes specified.
// @param {boolean} [register_beacon = false] - If true, FLEDGE logic will
-// register reporting beacons
-// after completion.
+// register reporting beacons after
+// completion.
async function generateURNFromFledge(
href, keylist, nested_urls = [], resolve_to_config = false,
ad_with_size = false, requested_size = null, register_beacon = false) {
@@ -347,6 +347,19 @@ function attachFrameContext(
num_components);
}
+// Performs a content-initiated navigation of a frame proxy. This navigated page
+// uses a new urn:uuid as its communication channel to prevent potential clashes
+// with the currently loaded document.
+async function navigateFrameContext(frame_proxy, {headers = [], origin = ''}) {
+ const [uuid, url] = generateRemoteContextURL(headers, origin);
+ frame_proxy.execute((url) => {
+ window.executor.suspend(() => {
+ window.location = url;
+ });
+ }, [url])
+ frame_proxy.context_id = uuid;
+}
+
function replaceFrameContext(frame_proxy, {
generator_api = '',
resolve_to_config = false,
diff --git a/testing/web-platform/tests/fenced-frame/setting-null-config-navigates-to-about-blank.https.html b/testing/web-platform/tests/fenced-frame/setting-null-config-navigates-to-about-blank.https.html
index 2595fd64c9..c8322dab19 100644
--- a/testing/web-platform/tests/fenced-frame/setting-null-config-navigates-to-about-blank.https.html
+++ b/testing/web-platform/tests/fenced-frame/setting-null-config-navigates-to-about-blank.https.html
@@ -1,4 +1,5 @@
<!DOCTYPE html>
+<meta name=timeout content=long>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/utils.js"></script>
@@ -11,13 +12,13 @@
<script>
promise_test(async (t) => {
- var frame_context = attachFencedFrameContext();
+ var frame_context = await attachFencedFrameContext();
// Ensure remote context responds.
let alive_indicator = await Promise.race([
frame_context.execute(() => 'alive'),
new Promise((resolve, reject) => t.step_timeout(
- () => reject('timed_out'), 3000))
+ () => reject('timed_out'), 1500))
]);
assert_equals(alive_indicator, 'alive');
@@ -28,14 +29,20 @@
// removed.
frame_context.element.config = null;
+ let fenced_frame_alive_promise = async (resolve) => {
+ await frame_context.execute(() => {});
+ resolve('alive');
+ };
+
// This call should not succeed, because we should have navigated to
// about:blank. Note that because the code has been deleted as described
// above, we can't actually inspect the URL to determine it is
// about:blank; we have to use our timeout as a proxy.
let timeout_indicator = await Promise.any([
- frame_context.execute(() => 'alive'),
new Promise(resolve => t.step_timeout(
- () => resolve('timed_out'), 3000))
+ () => fenced_frame_alive_promise(resolve), 500)),
+ new Promise(resolve => t.step_timeout(
+ () => resolve('timed_out'), 1500))
]);
assert_equals(timeout_indicator, 'timed_out');
}, "Test that a fenced frame with a config explicitly set to null navigates to about:blank");
diff --git a/testing/web-platform/tests/fetch/api/basic/request-headers.any.js b/testing/web-platform/tests/fetch/api/basic/request-headers.any.js
index ac54256e4c..8d2ad31e70 100644
--- a/testing/web-platform/tests/fetch/api/basic/request-headers.any.js
+++ b/testing/web-platform/tests/fetch/api/basic/request-headers.any.js
@@ -54,6 +54,7 @@ requestHeaders("Fetch with POST with Blob body", url, "POST", new Blob(["Test"])
requestHeaders("Fetch with POST with ArrayBuffer body", url, "POST", new ArrayBuffer(4), location.origin, "4");
requestHeaders("Fetch with POST with Uint8Array body", url, "POST", new Uint8Array(4), location.origin, "4");
requestHeaders("Fetch with POST with Int8Array body", url, "POST", new Int8Array(4), location.origin, "4");
+requestHeaders("Fetch with POST with Float16Array body", url, "POST", new Float16Array(1), location.origin, "2");
requestHeaders("Fetch with POST with Float32Array body", url, "POST", new Float32Array(1), location.origin, "4");
requestHeaders("Fetch with POST with Float64Array body", url, "POST", new Float64Array(1), location.origin, "8");
requestHeaders("Fetch with POST with DataView body", url, "POST", new DataView(new ArrayBuffer(8), 0, 4), location.origin, "4");
diff --git a/testing/web-platform/tests/fetch/api/basic/request-upload.any.js b/testing/web-platform/tests/fetch/api/basic/request-upload.any.js
index 9168aa1154..0c4813bb53 100644
--- a/testing/web-platform/tests/fetch/api/basic/request-upload.any.js
+++ b/testing/web-platform/tests/fetch/api/basic/request-upload.any.js
@@ -60,6 +60,10 @@ testUpload("Fetch with POST with Int8Array body", url,
"POST",
() => new Int8Array(4),
"\0\0\0\0");
+testUpload("Fetch with POST with Float16Array body", url,
+ "POST",
+ () => new Float16Array(2),
+ "\0\0\0\0");
testUpload("Fetch with POST with Float32Array body", url,
"POST",
() => new Float32Array(1),
diff --git a/testing/web-platform/tests/fetch/api/crashtests/huge-fetch.any.js b/testing/web-platform/tests/fetch/api/crashtests/huge-fetch.any.js
new file mode 100644
index 0000000000..1b09925d85
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/crashtests/huge-fetch.any.js
@@ -0,0 +1,16 @@
+// META: global=window,worker
+
+'use strict';
+
+promise_test(async t => {
+ const response = await fetch('../resources/huge-response.py');
+ const reader = response.body.getReader();
+ // Read one chunk just to show willing.
+ const { value, done } = await reader.read();
+ assert_false(done, 'there should be some data');
+ assert_greater_than(value.byteLength, 0, 'the chunk should be non-empty');
+ // Wait 2 seconds to give it a chance to crash.
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ // If we get here without crashing we passed the test.
+ reader.cancel();
+}, 'fetching a huge cacheable file but not reading it should not crash');
diff --git a/testing/web-platform/tests/fetch/api/request/request-bad-port.any.js b/testing/web-platform/tests/fetch/api/request/request-bad-port.any.js
index 5c29823eaa..915063bab5 100644
--- a/testing/web-platform/tests/fetch/api/request/request-bad-port.any.js
+++ b/testing/web-platform/tests/fetch/api/request/request-bad-port.any.js
@@ -89,6 +89,6 @@ var BLOCKED_PORTS_LIST = [
BLOCKED_PORTS_LIST.map(function(a){
promise_test(function(t){
- return promise_rejects_js(t, TypeError, fetch("http://example.com:" + a))
+ return promise_rejects_js(t, TypeError, fetch(`${location.origin}:${a}`))
}, 'Request on bad port ' + a + ' should throw TypeError.');
});
diff --git a/testing/web-platform/tests/fetch/api/resources/huge-response.py b/testing/web-platform/tests/fetch/api/resources/huge-response.py
new file mode 100644
index 0000000000..16a60078e5
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/huge-response.py
@@ -0,0 +1,22 @@
+# A Python script that generates a huge response. Implemented as a script to
+# avoid needing to add a huge file to the repository.
+
+TOTAL_SIZE = 8 * 1024 * 1024 * 1024 # 8 GB
+CHUNK_SIZE = 1024 * 1024 # 1 MB
+
+assert TOTAL_SIZE % CHUNK_SIZE == 0
+
+
+def main(request, response):
+ response.headers.set(b"Content-type", b"text/plain")
+ response.headers.set(b"Content-Length", str(TOTAL_SIZE).encode())
+ response.headers.set(b"Cache-Control", b"max-age=86400")
+ response.write_status_headers()
+
+ chunk = bytes(CHUNK_SIZE)
+ total_sent = 0
+
+ while total_sent < TOTAL_SIZE:
+ if not response.writer.write(chunk):
+ break
+ total_sent += CHUNK_SIZE
diff --git a/testing/web-platform/tests/fetch/api/response/response-clone.any.js b/testing/web-platform/tests/fetch/api/response/response-clone.any.js
index f5cda75149..c0c844948d 100644
--- a/testing/web-platform/tests/fetch/api/response/response-clone.any.js
+++ b/testing/web-platform/tests/fetch/api/response/response-clone.any.js
@@ -135,6 +135,7 @@ testReadableStreamClone(new Uint16Array(arrayBuffer, 2), "Uint16Array");
testReadableStreamClone(new Uint32Array(arrayBuffer), "Uint32Array");
testReadableStreamClone(typeof BigInt64Array === "function" ? new BigInt64Array(arrayBuffer) : undefined, "BigInt64Array");
testReadableStreamClone(typeof BigUint64Array === "function" ? new BigUint64Array(arrayBuffer) : undefined, "BigUint64Array");
+testReadableStreamClone(typeof Float16Array === "function" ? new Float16Array(arrayBuffer) : undefined, "Float16Array");
testReadableStreamClone(new Float32Array(arrayBuffer), "Float32Array");
testReadableStreamClone(new Float64Array(arrayBuffer), "Float64Array");
testReadableStreamClone(new DataView(arrayBuffer, 2, 8), "DataView");
diff --git a/testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data-cache.tentative.https.html b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data-cache.tentative.https.html
new file mode 100644
index 0000000000..c8bcf7fdf1
--- /dev/null
+++ b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data-cache.tentative.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8">
+<meta name="timeout" content="long"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/compression-dictionary-util.js"></script>
+</head>
+<body>
+<script>
+
+compression_dictionary_promise_test(async (t) => {
+ const dict = await (await fetch(kRegisterDictionaryPath)).text();
+ // Wait until `available-dictionary` header is available.
+ assert_equals(
+ await waitUntilAvailableDictionaryHeader(t, {}),
+ kDefaultDictionaryHashBase64);
+ // Clear site data.
+ assert_equals(await clearSiteData(/*directive=*/'cache'), 'OK');
+ // Check if `available-dictionary` header is not available.
+ assert_equals(
+ await waitUntilAvailableDictionaryHeader(t, {max_retry: 0}),
+ '"available-dictionary" header is not available');
+}, 'Clear-Site-Data with "cache" directive must unregister dictionary');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data-cookies.tentative.https.html b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data-cookies.tentative.https.html
new file mode 100644
index 0000000000..aa1673e88c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data-cookies.tentative.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8">
+<meta name="timeout" content="long"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/compression-dictionary-util.js"></script>
+</head>
+<body>
+<script>
+
+compression_dictionary_promise_test(async (t) => {
+ const dict = await (await fetch(kRegisterDictionaryPath)).text();
+ // Wait until `available-dictionary` header is available.
+ assert_equals(
+ await waitUntilAvailableDictionaryHeader(t, {}),
+ kDefaultDictionaryHashBase64);
+ // Clear site data.
+ assert_equals(await clearSiteData(/*directive=*/'cookies'), 'OK');
+ // Check if `available-dictionary` header is not available.
+ assert_equals(
+ await waitUntilAvailableDictionaryHeader(t, {max_retry: 0}),
+ '"available-dictionary" header is not available');
+}, 'Clear-Site-Data with "cookies" directive must unregister dictionary');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data-storage.tentative.https.html b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data-storage.tentative.https.html
new file mode 100644
index 0000000000..22747eb656
--- /dev/null
+++ b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data-storage.tentative.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8">
+<meta name="timeout" content="long"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/compression-dictionary-util.js"></script>
+</head>
+<body>
+<script>
+
+compression_dictionary_promise_test(async (t) => {
+ const dict = await (await fetch(kRegisterDictionaryPath)).text();
+ // Wait until `available-dictionary` header is available.
+ assert_equals(
+ await waitUntilAvailableDictionaryHeader(t, {}),
+ kDefaultDictionaryHashBase64);
+ // Clear site data.
+ assert_equals(await clearSiteData(/*directive=*/'storage'), 'OK');
+ // Check if `available-dictionary` header is not available.
+ assert_equals(
+ await waitUntilAvailableDictionaryHeader(t, {max_retry: 0}),
+ kDefaultDictionaryHashBase64);
+}, 'Clear-Site-Data with "storage" directive must not unregister dictionary');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data.tentative.https.html b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data.tentative.https.html
deleted file mode 100644
index b583834831..0000000000
--- a/testing/web-platform/tests/fetch/compression-dictionary/dictionary-clear-site-data.tentative.https.html
+++ /dev/null
@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<head>
-<meta charset="utf-8">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="./resources/compression-dictionary-util.js"></script>
-</head>
-<body>
-<script>
-
-compression_dictionary_promise_test(async (t) => {
- const dict = await (await fetch(kRegisterDictionaryPath)).text();
- // Wait until `available-dictionary` header is available.
- assert_equals(
- await waitUntilAvailableDictionaryHeader(t, {}),
- kDefaultDictionaryHashBase64);
- // Clear site data.
- assert_equals(await clearSiteData(/*directive=*/'cache'), 'OK');
- // Check if `available-dictionary` header is not available.
- assert_equals(
- await waitUntilAvailableDictionaryHeader(t, {max_retry: 0}),
- '"available-dictionary" header is not available');
-}, 'Clear-Site-Data with "cache" directive must unregister dictionary');
-
-compression_dictionary_promise_test(async (t) => {
- const dict = await (await fetch(kRegisterDictionaryPath)).text();
- // Wait until `available-dictionary` header is available.
- assert_equals(
- await waitUntilAvailableDictionaryHeader(t, {}),
- kDefaultDictionaryHashBase64);
- // Clear site data.
- assert_equals(await clearSiteData(/*directive=*/'cookies'), 'OK');
- // Check if `available-dictionary` header is not available.
- assert_equals(
- await waitUntilAvailableDictionaryHeader(t, {max_retry: 0}),
- '"available-dictionary" header is not available');
-}, 'Clear-Site-Data with "cookies" directive must unregister dictionary');
-
-compression_dictionary_promise_test(async (t) => {
- const dict = await (await fetch(kRegisterDictionaryPath)).text();
- // Wait until `available-dictionary` header is available.
- assert_equals(
- await waitUntilAvailableDictionaryHeader(t, {}),
- kDefaultDictionaryHashBase64);
- // Clear site data.
- assert_equals(await clearSiteData(/*directive=*/'storage'), 'OK');
- // Check if `available-dictionary` header is not available.
- assert_equals(
- await waitUntilAvailableDictionaryHeader(t, {max_retry: 0}),
- kDefaultDictionaryHashBase64);
-}, 'Clear-Site-Data with "storage" directive must not unregister dictionary');
-
-</script>
-</body>
diff --git a/testing/web-platform/tests/fetch/compression-dictionary/dictionary-decompression.tentative.https.html b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-decompression.tentative.https.html
index cd20625816..c7b3b7c3a5 100644
--- a/testing/web-platform/tests/fetch/compression-dictionary/dictionary-decompression.tentative.https.html
+++ b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-decompression.tentative.https.html
@@ -1,6 +1,7 @@
<!DOCTYPE html>
<head>
<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>
diff --git a/testing/web-platform/tests/fetch/compression-dictionary/dictionary-fetch-with-link-element.tentative.https.html b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-fetch-with-link-element.tentative.https.html
index 71a9b1c050..23a271d481 100644
--- a/testing/web-platform/tests/fetch/compression-dictionary/dictionary-fetch-with-link-element.tentative.https.html
+++ b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-fetch-with-link-element.tentative.https.html
@@ -1,6 +1,7 @@
<!DOCTYPE html>
<head>
<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>
diff --git a/testing/web-platform/tests/fetch/compression-dictionary/dictionary-fetch-with-link-header.tentative.https.html b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-fetch-with-link-header.tentative.https.html
index a3ffd8ba74..6f6a792ade 100644
--- a/testing/web-platform/tests/fetch/compression-dictionary/dictionary-fetch-with-link-header.tentative.https.html
+++ b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-fetch-with-link-header.tentative.https.html
@@ -1,6 +1,7 @@
<!DOCTYPE html>
<head>
<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>
diff --git a/testing/web-platform/tests/fetch/compression-dictionary/dictionary-registration.tentative.https.html b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-registration.tentative.https.html
index 7921b12946..f0782aff3b 100644
--- a/testing/web-platform/tests/fetch/compression-dictionary/dictionary-registration.tentative.https.html
+++ b/testing/web-platform/tests/fetch/compression-dictionary/dictionary-registration.tentative.https.html
@@ -1,6 +1,7 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8">
+<meta name="timeout" content="long"/>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="./resources/compression-dictionary-util.js"></script>
diff --git a/testing/web-platform/tests/fetch/compression-dictionary/resources/compression-dictionary-util.js b/testing/web-platform/tests/fetch/compression-dictionary/resources/compression-dictionary-util.js
index 46d95041d8..7d86f594a8 100644
--- a/testing/web-platform/tests/fetch/compression-dictionary/resources/compression-dictionary-util.js
+++ b/testing/web-platform/tests/fetch/compression-dictionary/resources/compression-dictionary-util.js
@@ -6,8 +6,8 @@ const kRegisterDictionaryPath = './resources/register-dictionary.py';
const kCompressedDataPath = './resources/compressed-data.py';
const kExpectedCompressedData =
`This is compressed test data using a test dictionary`;
-const kCheckAvailableDictionaryHeaderMaxRetry = 5;
-const kCheckAvailableDictionaryHeaderRetryTimeout = 100;
+const kCheckAvailableDictionaryHeaderMaxRetry = 10;
+const kCheckAvailableDictionaryHeaderRetryTimeout = 200;
const kCheckPreviousRequestHeadersMaxRetry = 5;
const kCheckPreviousRequestHeadersRetryTimeout = 250;
diff --git a/testing/web-platform/tests/fetch/metadata/WEB_FEATURES.yml b/testing/web-platform/tests/fetch/metadata/WEB_FEATURES.yml
new file mode 100644
index 0000000000..fb48eaa311
--- /dev/null
+++ b/testing/web-platform/tests/fetch/metadata/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: fetch-metadata
+ files: "**"
diff --git a/testing/web-platform/tests/fetch/metadata/generated/appcache-manifest.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/appcache-manifest.https.sub.html
deleted file mode 100644
index cf322fd34b..0000000000
--- a/testing/web-platform/tests/fetch/metadata/generated/appcache-manifest.https.sub.html
+++ /dev/null
@@ -1,341 +0,0 @@
-<!DOCTYPE html>
-<!--
-This test was procedurally generated. Please do not modify it directly.
-Sources:
-- fetch/metadata/tools/fetch-metadata.conf.yml
-- fetch/metadata/tools/templates/appcache-manifest.sub.https.html
--->
-<html lang="en">
- <meta charset="utf-8">
- <title>HTTP headers on request for Appcache manifest</title>
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
- <script src="/fetch/metadata/resources/helper.sub.js"></script>
- <body>
- <script>
- 'use strict';
-
- function induceRequest(url) {
- const iframe = document.createElement('iframe');
- iframe.src =
- '/fetch/metadata/resources/appcache-iframe.sub.html?manifest=' + encodeURIComponent(url);
-
- return new Promise((resolve) => {
- addEventListener('message', function onMessage(event) {
- if (event.source !== iframe.contentWindow) {
- return;
- }
- removeEventListener('message', onMessage);
- resolve(event.data);
- });
-
- document.body.appendChild(iframe);
- })
- .then((message) => {
- if (message !== 'okay') {
- throw message;
- }
- })
- .then(() => iframe.remove());
- }
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsOrigin']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - Same origin');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsCrossSite']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - Cross-site');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsSameSite']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['same-site']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - Same site');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpOrigin']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_not_own_property(headers, 'sec-fetch-site');
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - HTTPS downgrade (header not sent)');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpOrigin', 'httpsOrigin']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - HTTPS upgrade');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - HTTPS downgrade-upgrade');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['same-site']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - Cross-Site -> Same Origin');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - Cross-Site -> Same-Site');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - Cross-Site -> Cross-Site');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - Same-Origin -> Same Origin');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['same-site']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - Same-Origin -> Same-Site');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - Same-Origin -> Cross-Site');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['same-site']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - Same-Site -> Same Origin');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['same-site']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - Same-Site -> Same-Site');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-site');
- assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-site - Same-Site -> Cross-Site');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, []))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-mode');
- assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-mode');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, []))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_own_property(headers, 'sec-fetch-dest');
- assert_array_equals(headers['sec-fetch-dest'], ['empty']);
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-dest');
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, []))
- .then(() => retrieve(key))
- .then((headers) => {
- assert_not_own_property(headers, 'sec-fetch-user');
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, 'sec-fetch-user');
- </script>
- </body>
-</html>
diff --git a/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html b/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html
index 69ac7682a5..65b1837c63 100644
--- a/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html
+++ b/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html
@@ -40,36 +40,6 @@ Sources:
const key = '{{uuid()}}';
const url = makeRequestURL(
key,
- ['httpSameSite'],
- { mime: 'application/javascript', body: 'postMessage("")' }
- );
-
- return induceRequest(url)
- .then(() => retrieve(key))
- .then((headers) => {
- assert_not_own_property(headers, 'sec-fetch-site');
- });
- }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no options');
-
- promise_test(() => {
- const key = '{{uuid()}}';
- const url = makeRequestURL(
- key,
- ['httpCrossSite'],
- { mime: 'application/javascript', body: 'postMessage("")' }
- );
-
- return induceRequest(url)
- .then(() => retrieve(key))
- .then((headers) => {
- assert_not_own_property(headers, 'sec-fetch-site');
- });
- }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no options');
-
- promise_test(() => {
- const key = '{{uuid()}}';
- const url = makeRequestURL(
- key,
['httpOrigin'],
{ mime: 'application/javascript', body: 'postMessage("")' }
);
@@ -85,36 +55,6 @@ Sources:
const key = '{{uuid()}}';
const url = makeRequestURL(
key,
- ['httpSameSite'],
- { mime: 'application/javascript', body: 'postMessage("")' }
- );
-
- return induceRequest(url)
- .then(() => retrieve(key))
- .then((headers) => {
- assert_not_own_property(headers, 'sec-fetch-mode');
- });
- }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no options');
-
- promise_test(() => {
- const key = '{{uuid()}}';
- const url = makeRequestURL(
- key,
- ['httpCrossSite'],
- { mime: 'application/javascript', body: 'postMessage("")' }
- );
-
- return induceRequest(url)
- .then(() => retrieve(key))
- .then((headers) => {
- assert_not_own_property(headers, 'sec-fetch-mode');
- });
- }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no options');
-
- promise_test(() => {
- const key = '{{uuid()}}';
- const url = makeRequestURL(
- key,
['httpOrigin'],
{ mime: 'application/javascript', body: 'postMessage("")' }
);
@@ -130,36 +70,6 @@ Sources:
const key = '{{uuid()}}';
const url = makeRequestURL(
key,
- ['httpSameSite'],
- { mime: 'application/javascript', body: 'postMessage("")' }
- );
-
- return induceRequest(url)
- .then(() => retrieve(key))
- .then((headers) => {
- assert_not_own_property(headers, 'sec-fetch-dest');
- });
- }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no options');
-
- promise_test(() => {
- const key = '{{uuid()}}';
- const url = makeRequestURL(
- key,
- ['httpCrossSite'],
- { mime: 'application/javascript', body: 'postMessage("")' }
- );
-
- return induceRequest(url)
- .then(() => retrieve(key))
- .then((headers) => {
- assert_not_own_property(headers, 'sec-fetch-dest');
- });
- }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no options');
-
- promise_test(() => {
- const key = '{{uuid()}}';
- const url = makeRequestURL(
- key,
['httpOrigin'],
{ mime: 'application/javascript', body: 'postMessage("")' }
);
@@ -170,35 +80,5 @@ Sources:
assert_not_own_property(headers, 'sec-fetch-user');
});
}, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no options');
-
- promise_test(() => {
- const key = '{{uuid()}}';
- const url = makeRequestURL(
- key,
- ['httpSameSite'],
- { mime: 'application/javascript', body: 'postMessage("")' }
- );
-
- return induceRequest(url)
- .then(() => retrieve(key))
- .then((headers) => {
- assert_not_own_property(headers, 'sec-fetch-user');
- });
- }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no options');
-
- promise_test(() => {
- const key = '{{uuid()}}';
- const url = makeRequestURL(
- key,
- ['httpCrossSite'],
- { mime: 'application/javascript', body: 'postMessage("")' }
- );
-
- return induceRequest(url)
- .then(() => retrieve(key))
- .then((headers) => {
- assert_not_own_property(headers, 'sec-fetch-user');
- });
- }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no options');
</script>
</html>
diff --git a/testing/web-platform/tests/fetch/metadata/tools/fetch-metadata.conf.yml b/testing/web-platform/tests/fetch/metadata/tools/fetch-metadata.conf.yml
index b277bcb7b5..b96bd2fd7b 100644
--- a/testing/web-platform/tests/fetch/metadata/tools/fetch-metadata.conf.yml
+++ b/testing/web-platform/tests/fetch/metadata/tools/fetch-metadata.conf.yml
@@ -43,10 +43,8 @@ cases:
origins: [httpCrossSite]
description: Not sent to non-trustworthy cross-site destination
template_axes:
- # Unused
- appcache-manifest.sub.https.html: []
- # The `audioWorklet` interface is only available in secure contexts
- # https://webaudio.github.io/web-audio-api/#BaseAudioContext
+ # The `AudioWorklet` interface is only available in secure contexts
+ # https://webaudio.github.io/web-audio-api/#AudioWorklet
audioworklet.https.sub.html: []
# Service workers are only available in secure context
fetch-via-serviceworker.https.sub.html: []
@@ -91,6 +89,62 @@ cases:
svg-image.sub.html: [{}]
window-history.sub.html: [{}]
worker-dedicated-importscripts.sub.html: [{}]
+ # `new Worker()` only makes same-origin requests, therefore we split it
+ # out into the next block.
+ worker-dedicated-constructor.sub.html: []
+
+ - all_subtests:
+ expected: NULL
+ filename_flags: []
+ common_axis:
+ - headerName: sec-fetch-site
+ origins: [httpOrigin]
+ description: Not sent to non-trustworthy same-origin destination
+ - headerName: sec-fetch-mode
+ origins: [httpOrigin]
+ description: Not sent to non-trustworthy same-origin destination
+ - headerName: sec-fetch-dest
+ origins: [httpOrigin]
+ description: Not sent to non-trustworthy same-origin destination
+ - headerName: sec-fetch-user
+ origins: [httpOrigin]
+ description: Not sent to non-trustworthy same-origin destination
+ template_axes:
+ # All the templates in this block are unused with the exception of
+ # `worker-dedicated-constructor`
+ audioworklet.https.sub.html: []
+ fetch-via-serviceworker.https.sub.html: []
+ serviceworker.https.sub.html: []
+ css-images.sub.html: []
+ css-font-face.sub.html: []
+ element-a.sub.html: []
+ element-area.sub.html: []
+ element-audio.sub.html: []
+ element-embed.sub.html: []
+ element-frame.sub.html: []
+ element-iframe.sub.html: []
+ element-img.sub.html: []
+ element-img-environment-change.sub.html: []
+ element-input-image.sub.html: []
+ element-link-icon.sub.html: []
+ element-link-prefetch.optional.sub.html: []
+ element-meta-refresh.optional.sub.html: []
+ element-picture.sub.html: []
+ element-script.sub.html: []
+ element-video.sub.html: []
+ element-video-poster.sub.html: []
+ fetch.sub.html: []
+ form-submission.sub.html: []
+ header-link.sub.html: []
+ header-refresh.optional.sub.html: []
+ window-location.sub.html: []
+ script-module-import-dynamic.sub.html: []
+ script-module-import-static.sub.html: []
+ svg-image.sub.html: []
+ window-history.sub.html: []
+ worker-dedicated-importscripts.sub.html: []
+ # `new Worker()` only makes same-origin requests, so we populate its
+ # generated tests here.
worker-dedicated-constructor.sub.html: [{}]
# Sec-Fetch-Site - direct requests
@@ -117,7 +171,6 @@ cases:
# https://html.spec.whatwg.org/#fetch-a-single-module-script
worker-dedicated-constructor.sub.html: []
- appcache-manifest.sub.https.html: [{}]
audioworklet.https.sub.html: [{}]
css-images.sub.html:
- filename_flags: [tentative]
@@ -176,8 +229,8 @@ cases:
expected: cross-site
template_axes:
# Unused
- # The `audioWorklet` interface is only available in secure contexts
- # https://webaudio.github.io/web-audio-api/#BaseAudioContext
+ # The `AudioWorklet` interface is only available in secure contexts
+ # https://webaudio.github.io/web-audio-api/#AudioWorklet
audioworklet.https.sub.html: []
# Service workers are only available in secure context
fetch-via-serviceworker.https.sub.html: []
@@ -196,7 +249,6 @@ cases:
# https://html.spec.whatwg.org/#fetch-a-single-module-script
worker-dedicated-constructor.sub.html: []
- appcache-manifest.sub.https.html: [{}]
css-images.sub.html:
- filename_flags: [tentative]
css-font-face.sub.html:
@@ -289,7 +341,6 @@ cases:
# https://html.spec.whatwg.org/#fetch-a-single-module-script
worker-dedicated-constructor.sub.html: []
- appcache-manifest.sub.https.html: [{}]
audioworklet.https.sub.html: [{}]
css-images.sub.html:
- filename_flags: [tentative]
@@ -377,7 +428,6 @@ cases:
worker-dedicated-constructor.sub.html: []
worker-dedicated-importscripts.sub.html: []
# Avoid duplicate subtest for 'sec-fetch-site - HTTPS downgrade-upgrade'
- appcache-manifest.sub.https.html: []
css-images.sub.html:
- filename_flags: [tentative]
element-a.sub.html: [{}]
@@ -409,8 +459,6 @@ cases:
filename_flags: [https]
origins: []
template_axes:
- appcache-manifest.sub.https.html:
- - expected: no-cors
audioworklet.https.sub.html:
# https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script
- expected: cors
@@ -586,8 +634,6 @@ cases:
filename_flags: [https]
origins: []
template_axes:
- appcache-manifest.sub.https.html:
- - expected: empty
audioworklet.https.sub.html:
# https://github.com/WebAudio/web-audio-api/issues/2203
- expected: audioworklet
@@ -709,8 +755,6 @@ cases:
filename_flags: [https]
origins: []
template_axes:
- appcache-manifest.sub.https.html:
- - expected: NULL
audioworklet.https.sub.html:
- expected: NULL
css-images.sub.html:
diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html b/testing/web-platform/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html
deleted file mode 100644
index 0dfc084f2e..0000000000
--- a/testing/web-platform/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html
+++ /dev/null
@@ -1,63 +0,0 @@
-<!DOCTYPE html>
-<!--
-[%provenance%]
--->
-<html lang="en">
- <meta charset="utf-8">
- <title>HTTP headers on request for Appcache manifest</title>
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
- <script src="/fetch/metadata/resources/helper.sub.js"></script>
- <body>
- <script>
- 'use strict';
-
- function induceRequest(url) {
- const iframe = document.createElement('iframe');
- iframe.src =
- '/fetch/metadata/resources/appcache-iframe.sub.html?manifest=' + encodeURIComponent(url);
-
- return new Promise((resolve) => {
- addEventListener('message', function onMessage(event) {
- if (event.source !== iframe.contentWindow) {
- return;
- }
- removeEventListener('message', onMessage);
- resolve(event.data);
- });
-
- document.body.appendChild(iframe);
- })
- .then((message) => {
- if (message !== 'okay') {
- throw message;
- }
- })
- .then(() => iframe.remove());
- }
-
- {%- for subtest in subtests %}
-
- async_test((t) => {
- const key = '{{uuid()}}';
- assert_implements_optional(
- !!window.applicationCache, 'Application Cache supported.'
- );
-
- induceRequest(makeRequestURL(key, [% subtest.origins %]))
- .then(() => retrieve(key))
- .then((headers) => {
- {%- if subtest.expected == none %}
- assert_not_own_property(headers, '[%subtest.headerName%]');
- {%- else %}
- assert_own_property(headers, '[%subtest.headerName%]');
- assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
- {%- endif %}
- })
- .then(() => t.done(), t.step_func((error) => { throw error; }));
- }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
-
- {%- endfor %}
- </script>
- </body>
-</html>
diff --git a/testing/web-platform/tests/fledge/tentative/TODO b/testing/web-platform/tests/fledge/tentative/TODO
index 6fd378c035..8760e59d21 100644
--- a/testing/web-platform/tests/fledge/tentative/TODO
+++ b/testing/web-platform/tests/fledge/tentative/TODO
@@ -79,7 +79,13 @@ Need tests for (likely not a complete list):
origins, and between generateBid() and reportWin().
* Test Content-Type headers allowed in responess for script/wasm/JSON fetches.
* Test WASM support, updating createBiddingWasmHelperURL().
-
+* Remaining interest group updates.
+ * Check that an update with one valid field and one invalid one fails.
+ * Test that an update works if owner and/or name match those in the interest group.
+ * Test updating the update URL and bidding script URL so they are all the same origin (requires updating test fixture to handle multiple updates).
+ * Test when Ads is null.
+ * Test updating a cross origin interest group.
+ * Test fields that are updatable but do not make it to 'generateBid'.
If possible:
* Aggregate reporting.
* Join/leave permission delegation via .well-known files
diff --git a/testing/web-platform/tests/fledge/tentative/auction-config-passed-to-worklets.https.window.js b/testing/web-platform/tests/fledge/tentative/auction-config-passed-to-worklets.https.window.js
index 9b12d077ba..7780957739 100644
--- a/testing/web-platform/tests/fledge/tentative/auction-config-passed-to-worklets.https.window.js
+++ b/testing/web-platform/tests/fledge/tentative/auction-config-passed-to-worklets.https.window.js
@@ -81,6 +81,18 @@ makeTest({
});
makeTest({
+ name: 'AuctionConfig.deprecatedRenderURLReplacements with brackets.',
+ fieldName: 'deprecatedRenderURLReplacements',
+ fieldValue: {'${EXAMPLE_MACRO}': 'SSP'},
+});
+
+makeTest({
+ name: 'AuctionConfig.deprecatedRenderURLReplacements with percents.',
+ fieldName: 'deprecatedRenderURLReplacements',
+ fieldValue: {'%%EXAMPLE_MACRO%%': 'SSP'},
+});
+
+makeTest({
name: 'AuctionConfig.seller is URL.',
fieldName: 'seller',
fieldValue: OTHER_ORIGIN1,
diff --git a/testing/web-platform/tests/fledge/tentative/auction-config.https.window.js b/testing/web-platform/tests/fledge/tentative/auction-config.https.window.js
index 5fa4fa252f..057b4d7f78 100644
--- a/testing/web-platform/tests/fledge/tentative/auction-config.https.window.js
+++ b/testing/web-platform/tests/fledge/tentative/auction-config.https.window.js
@@ -12,7 +12,10 @@
// META: variant=?31-35
// META: variant=?36-40
// META: variant=?40-45
-// META: variant=?46-last
+// META: variant=?46-50
+// META: variant=?51-55
+// META: variant=?56-60
+// META: variant=?61-last
"use strict;"
@@ -110,6 +113,62 @@ const EXPECT_PROMISE_ERROR = auctionResult => {
}
makeTest({
+ name: 'deprecatedRenderURLReplacements without end bracket is invalid.',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {deprecatedRenderURLReplacements: {'${No_End_Bracket': 'SSP'}}
+});
+
+makeTest({
+ name: 'deprecatedRenderURLReplacements without percents and brackets.',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {deprecatedRenderURLReplacements: {'No_Wrapper': 'SSP'}}
+});
+
+makeTest({
+ name: 'deprecatedRenderURLReplacements without dollar sign.',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {deprecatedRenderURLReplacements: {'{No_Dollar_Sign}': 'SSP'}}
+});
+
+makeTest({
+ name: 'deprecatedRenderURLReplacements without start bracket is invalid.',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {deprecatedRenderURLReplacements: {'$No_Start_Bracket}': 'SSP'}}
+});
+
+makeTest({
+ name: 'deprecatedRenderURLReplacements mix and match is invalid.',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {deprecatedRenderURLReplacements: {'${Bracket_And_Percent%%': 'SSP'}}
+});
+
+makeTest({
+ name: 'deprecatedRenderURLReplacements missing start percent is invalid.',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {deprecatedRenderURLReplacements: {'%Missing_Start_Percents%%': 'SSP'}}
+});
+
+makeTest({
+ name: 'deprecatedRenderURLReplacements single percents is invalid.',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {deprecatedRenderURLReplacements: {'%Single_Percents%': 'SSP'}}
+});
+
+makeTest({
+ name: 'deprecatedRenderURLReplacements without end percents is invalid.',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {deprecatedRenderURLReplacements: {'%%No_End_Percents': 'SSP'}}
+});
+
+makeTest({
name: 'no buyers => no winners',
expect: EXPECT_NO_WINNER,
auctionConfigOverrides: {interestGroupBuyers: []},
@@ -445,6 +504,60 @@ makeTest({
{width: '200furlongs', height: '200'}]}
});
+makeTest({
+ name: 'sellerRealTimeReportingConfig has default local reporting type',
+ expect: EXPECT_WINNER,
+ auctionConfigOverrides: {sellerRealTimeReportingConfig:
+ {type: 'default-local-reporting'}}
+});
+
+makeTest({
+ name: 'sellerRealTimeReportingConfig has no type',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {sellerRealTimeReportingConfig:
+ {notType: 'default-local-reporting'}}
+});
+
+makeTest({
+ name: 'sellerRealTimeReportingConfig has unknown type',
+ expect: EXPECT_WINNER,
+ auctionConfigOverrides: {sellerRealTimeReportingConfig: {type: 'unknown type'}}
+});
+
+makeTest({
+ name: 'perBuyerRealTimeReportingConfig',
+ expect: EXPECT_WINNER,
+ auctionConfigOverrides: {perBuyerRealTimeReportingConfig:
+ {"https://example.com": {type: 'default-local-reporting'}}}
+});
+
+makeTest({
+ name: 'perBuyerRealTimeReportingConfig has no entry',
+ expect: EXPECT_WINNER,
+ auctionConfigOverrides: {perBuyerRealTimeReportingConfig: {}}
+});
+
+makeTest({
+ name: 'perBuyerRealTimeReportingConfig has invalid buyer',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {perBuyerRealTimeReportingConfig:
+ {"http://example.com": {type: 'default-local-reporting'}}}
+});
+
+makeTest({
+ name: 'perBuyerRealTimeReportingConfig has no type',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {perBuyerRealTimeReportingConfig:
+ {"https://example.com": {notType: 'default-local-reporting'}}}
+});
+
+makeTest({
+ name: 'perBuyerRealTimeReportingConfig has unknown type',
+ expect: EXPECT_WINNER,
+ auctionConfigOverrides: {perBuyerRealTimeReportingConfig:
+ {"https://example.com": {type: 'unknown type'}}}
+});
+
subsetTest(promise_test, async test => {
const uuid = generateUuid(test);
diff --git a/testing/web-platform/tests/fledge/tentative/component-ads.https.window.js b/testing/web-platform/tests/fledge/tentative/component-ads.https.window.js
index 6b22585d57..8493025429 100644
--- a/testing/web-platform/tests/fledge/tentative/component-ads.https.window.js
+++ b/testing/web-platform/tests/fledge/tentative/component-ads.https.window.js
@@ -44,13 +44,25 @@ function createComponentAdRenderURL(uuid, id) {
//
// If "adMetadata" is true, metadata is added to each component ad. Only integer metadata
// is used, relying on renderURL tests to cover other types of renderURL metadata.
+//
+// If "deprecatedRenderURLReplacements" is passed, the matches and replacements will be
+// used in the trackingURLs and the object will be passed into the auctionConfig, to
+// replace matching macros within the renderURLs.
async function runComponentAdLoadingTest(test, uuid, numComponentAdsInInterestGroup,
- componentAdsInBid, componentAdsToLoad,
- adMetadata = false) {
+ componentAdsInBid, componentAdsToLoad,
+ adMetadata = false, deprecatedRenderURLReplacements = null) {
let interestGroupAdComponents = [];
+ // These are used within the URLs for deprecatedRenderURLReplacement tests.
+ const renderURLReplacementsStrings = createStringBeforeAndAfterReplacements(deprecatedRenderURLReplacements);
+ const beforeReplacementsString= renderURLReplacementsStrings.beforeReplacements;
+ const afterReplacementsString = renderURLReplacementsStrings.afterReplacements;
+
for (let i = 0; i < numComponentAdsInInterestGroup; ++i) {
- const componentRenderURL = createComponentAdRenderURL(uuid, i);
- let adComponent = {renderURL: componentRenderURL};
+ let componentRenderURL = createComponentAdRenderURL(uuid, i);
+ if (deprecatedRenderURLReplacements !== null) {
+ componentRenderURL = createTrackerURL(window.location.origin, uuid, 'track_get', beforeReplacementsString);
+ }
+ let adComponent = { renderURL: componentRenderURL };
if (adMetadata)
adComponent.metadata = i;
interestGroupAdComponents.push(adComponent);
@@ -74,7 +86,7 @@ async function runComponentAdLoadingTest(test, uuid, numComponentAdsInInterestGr
eventData: status,
destination: ["buyer"]});`);
- let bid = {bid:1, render: renderURL};
+ let bid = {bid:1, render:renderURL};
if (componentAdsInBid) {
bid.adComponents = [];
for (let index of componentAdsInBid) {
@@ -89,8 +101,13 @@ async function runComponentAdLoadingTest(test, uuid, numComponentAdsInInterestGr
// to "expectedTrackerURLs".
if (componentAdsToLoad && bid.adComponents) {
for (let index of componentAdsToLoad) {
+ let expectedURL = createComponentAdTrackerURL(uuid, componentAdsInBid[index]);
+ if (deprecatedRenderURLReplacements != null) {
+ expectedURL = createTrackerURL(window.location.origin, uuid, 'track_get',
+ afterReplacementsString);
+ }
if (index < componentAdsInBid.length)
- expectedTrackerURLs.push(createComponentAdTrackerURL(uuid, componentAdsInBid[index]));
+ expectedTrackerURLs.push(expectedURL);
}
}
@@ -127,11 +144,13 @@ async function runComponentAdLoadingTest(test, uuid, numComponentAdsInInterestGr
test, uuid,
{decisionLogicURL: createDecisionScriptURL(
uuid,
- { scoreAd:
+ { scoreAd:
`if (JSON.stringify(browserSignals.adComponents) !==
'${JSON.stringify(bid.adComponents)}') {
throw "Unexpected adComponents: " + JSON.stringify(browserSignals.adComponents);
- }`})});
+ }`}),
+ deprecatedRenderURLReplacements: deprecatedRenderURLReplacements
+ });
}
await waitForObservedRequests(uuid, expectedTrackerURLs);
@@ -447,3 +466,27 @@ subsetTest(promise_test, async test => {
}, 'Reports not sent from component ad.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/1,
+ /*componentAdsInBid=*/[0], /*componentAdsToLoad=*/[0], false, { '%%EXAMPLE-MACRO%%': 'SSP' });
+}, 'component ad with render url replacements with percents.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/1,
+ /*componentAdsInBid=*/[0], /*componentAdsToLoad=*/[0], false, { '${EXAMPLE-MACRO}': 'SSP' });
+}, 'component ad with render url replacements with brackets.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/1,
+ /*componentAdsInBid=*/[0], /*componentAdsToLoad=*/[0], false, { '${EXAMPLE-MACRO-1}': 'SSP-1', '%%EXAMPLE-MACRO-2%%': 'SSP-2' });
+}, 'component ad with render url replacements with multiple replacements.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/3,
+ /*componentAdsInBid=*/[0,1,2], /*componentAdsToLoad=*/[0,1,2], false, { '${EXAMPLE-MACRO-1}': 'SSP-1', '%%EXAMPLE-MACRO-2%%': 'SSP-2' });
+}, 'component ad with render url replacements with multiple replacements, and multiple component ads.');
diff --git a/testing/web-platform/tests/fledge/tentative/component-auction.https.window.js b/testing/web-platform/tests/fledge/tentative/component-auction.https.window.js
index 015c20a5c2..bf804e6857 100644
--- a/testing/web-platform/tests/fledge/tentative/component-auction.https.window.js
+++ b/testing/web-platform/tests/fledge/tentative/component-auction.https.window.js
@@ -11,18 +11,21 @@
"use strict";
// Creates an AuctionConfig with a single component auction.
-function createComponentAuctionConfig(uuid) {
+function createComponentAuctionConfig(uuid, auctionConfigOverrides = {},
+ deprecatedRenderURLReplacements = {}) {
let componentAuctionConfig = {
seller: window.location.origin,
decisionLogicURL: createDecisionScriptURL(uuid),
- interestGroupBuyers: [window.location.origin]
+ interestGroupBuyers: [window.location.origin],
+ deprecatedRenderURLReplacements: deprecatedRenderURLReplacements
};
return {
seller: window.location.origin,
decisionLogicURL: createDecisionScriptURL(uuid),
interestGroupBuyers: [],
- componentAuctions: [componentAuctionConfig]
+ componentAuctions: [componentAuctionConfig],
+ ...auctionConfigOverrides
};
}
@@ -717,3 +720,118 @@ subsetTest(promise_test, async test => {
uuid,
[bidderReportURL1, seller1ReportURL, bidderReportURL2, seller2ReportURL]);
}, `Component auction prevWinsMs and numBids updating in one component seller's auction, read in another's.`);
+
+
+const makeDeprecatedRenderURLReplacementTest = ({
+ name,
+ deprecatedRenderURLReplacements,
+}) => {
+ subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let bidderReportURL = createBidderReportURL(uuid);
+ let componentSellerReportURL = createSellerReportURL(uuid, /*id=*/"component");
+ let topLevelSellerReportURL = createSellerReportURL(uuid, /*id=*/"top");
+
+ // These are used within the URLs for deprecatedRenderURLReplacement tests.
+ const renderURLReplacementsStrings = createStringBeforeAndAfterReplacements(deprecatedRenderURLReplacements);
+ const beforeReplacementsString = renderURLReplacementsStrings.beforeReplacements;
+ const afterReplacementsString = renderURLReplacementsStrings.afterReplacements;
+ const renderURLBeforeReplacements = createTrackerURL(window.location.origin, uuid, 'track_get', beforeReplacementsString);
+ const renderURLAfterReplacements = createTrackerURL(window.location.origin, uuid, 'track_get', afterReplacementsString);
+
+ await joinInterestGroup(
+ test, uuid,
+ {
+ ads: [{ renderURL: renderURLBeforeReplacements }],
+ biddingLogicURL: createBiddingScriptURL(
+ {
+ allowComponentAuction: true,
+ bid: 5,
+ reportWin:
+ `if (browserSignals.bid !== 5)
+ throw "Unexpected bid: " + browserSignals.bid;
+ sendReportTo("${bidderReportURL}");`
+ })
+ });
+
+ let auctionConfig = createComponentAuctionConfig(uuid, {}, deprecatedRenderURLReplacements);
+
+ auctionConfig.componentAuctions[0].decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ {
+ scoreAd:
+ `if (bid !== 5)
+ throw "Unexpected component bid: " + bid`,
+ reportResult:
+ `if (browserSignals.bid !== 5)
+ throw "Unexpected component bid: " + browserSignals.bid;
+ if (browserSignals.modifiedBid !== undefined)
+ throw "Unexpected component modifiedBid: " + browserSignals.modifiedBid;
+ sendReportTo("${componentSellerReportURL}");`
+ });
+
+ auctionConfig.decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ {
+ scoreAd:
+ `if (bid !== 5)
+ throw "Unexpected top-level bid: " + bid`,
+ reportResult:
+ `if (browserSignals.bid !== 5)
+ throw "Unexpected top-level bid: " + browserSignals.bid;
+ if (browserSignals.modifiedBid !== undefined)
+ throw "Unexpected top-level modifiedBid: " + browserSignals.modifiedBid;
+ sendReportTo("${topLevelSellerReportURL}");`
+ });
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfig);
+ await waitForObservedRequests(
+ uuid,
+ [bidderReportURL, componentSellerReportURL, topLevelSellerReportURL, renderURLAfterReplacements]);
+ }, name);
+};
+
+makeDeprecatedRenderURLReplacementTest({
+ name: 'Replacements with brackets.',
+ deprecatedRenderURLReplacements: { '${EXAMPLE-MACRO}': 'SSP' }
+});
+
+makeDeprecatedRenderURLReplacementTest({
+ name: 'Replacements with percents.',
+ deprecatedRenderURLReplacements: { '%%EXAMPLE-MACRO%%': 'SSP' }
+});
+
+makeDeprecatedRenderURLReplacementTest({
+ name: 'Replacements with multiple replacements.',
+ deprecatedRenderURLReplacements: { '${EXAMPLE-MACRO1}': 'SSP1', '%%EXAMPLE-MACRO2%%': 'SSP2' }
+});
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let deprecatedRenderURLReplacements = { '${EXAMPLE-MACRO1}': 'SSP1', '%%EXAMPLE-MACRO2%%': 'SSP2' };
+ const renderURLReplacementsStrings = createStringBeforeAndAfterReplacements(deprecatedRenderURLReplacements);
+ let beforeReplacementsString = renderURLReplacementsStrings.beforeReplacements;
+
+ await joinInterestGroup(
+ test, uuid,
+ {
+ ads: [{ renderURL: createTrackerURL(window.location.origin, uuid, 'track_get', beforeReplacementsString) }],
+ biddingLogicURL: createBiddingScriptURL({allowComponentAuction: true})
+ });
+ let auctionConfigOverride = {deprecatedRenderURLReplacements: deprecatedRenderURLReplacements }
+ let auctionConfig = createComponentAuctionConfig(uuid,/*auctionConfigOverride=*/auctionConfigOverride,
+ /*deprecatedRenderURLReplacements=*/deprecatedRenderURLReplacements);
+
+ auctionConfig.componentAuctions[0].decisionLogicURL = createDecisionScriptURL(uuid);
+
+ try {
+ await runBasicFledgeAuction(test, uuid, auctionConfig);
+ } catch (exception) {
+ assert_true(exception instanceof TypeError, "did not get expected error: " + exception);
+ return;
+ }
+ throw 'Exception unexpectedly not thrown.'
+}, "deprecatedRenderURLReplacements cause error if passed in top level auction and component auction.");
diff --git a/testing/web-platform/tests/fledge/tentative/deprecated-render-url-replacements.https.window.js b/testing/web-platform/tests/fledge/tentative/deprecated-render-url-replacements.https.window.js
new file mode 100644
index 0000000000..4f8bc1cc7f
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/deprecated-render-url-replacements.https.window.js
@@ -0,0 +1,196 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-5
+// META: variant=?6-10
+// META: variant=?11-15
+// META: variant=?16-last
+
+
+"use strict;"
+
+// This test ensures proper handling of deprecatedRenderURLReplacements within auctionConfigOverrides.
+// It validates that these replacements are correctly applied to the winning bid's renderURL by
+// injecting a URL with matching macros into an interest group and ensuring that a new url with
+// the replacements in it, is tracked and observed.
+const makeTest = ({
+ // Test name
+ name,
+ // Overrides to the interest group.
+ interestGroupOverrides = {},
+ // Overrides to the auction config.
+ auctionConfigOverrides = {},
+ // This is what goes into the renderURL and is expected to be replaced.
+ beforeReplacements,
+ // This is what's expected when 'beforeReplacements' is replaced.
+ afterReplacements,
+}) => {
+ subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let urlBeforeReplacements = createTrackerURL(window.location.origin, uuid, 'track_get', beforeReplacements);
+ let urlAfterReplacements = createTrackerURL(window.location.origin, uuid, 'track_get', afterReplacements);
+ interestGroupOverrides.ads = [{ renderURL: urlBeforeReplacements }];
+ await joinInterestGroup(test, uuid, interestGroupOverrides);
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
+ await waitForObservedRequests(
+ uuid,
+ [urlAfterReplacements, createSellerReportURL(uuid), createBidderReportURL(uuid)]);
+ }, name);
+};
+
+makeTest({
+ name: 'Replacements with brackets.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '${EXAMPLE-MACRO}': 'SSP' }
+ },
+ beforeReplacements: "${EXAMPLE-MACRO}",
+ afterReplacements: 'SSP',
+
+});
+
+makeTest({
+ name: 'Replacements with percents.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '%%EXAMPLE-MACRO%%': 'SSP' }
+ },
+ beforeReplacements: "%%EXAMPLE-MACRO%%",
+ afterReplacements: 'SSP',
+});
+
+makeTest({
+ name: 'Multiple replacements within a URL.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '${EXAMPLE-MACRO1}': 'SSP1', '%%EXAMPLE-MACRO2%%': 'SSP2' }
+ },
+ beforeReplacements: "${EXAMPLE-MACRO1}/%%EXAMPLE-MACRO2%%",
+ afterReplacements: 'SSP1/SSP2',
+});
+
+makeTest({
+ name: 'Recursive and reduce size with brackets.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '${1}': '1' }
+ },
+ beforeReplacements: "${${${1}}}",
+ afterReplacements: "${${1}}"
+});
+
+makeTest({
+ name: 'Recursive and increase size with brackets.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '${1}': '${${1}}' }
+ },
+ beforeReplacements: "${1}",
+ afterReplacements: "${${1}}"
+});
+
+makeTest({
+ name: 'Replacements use a single pass with brackets.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '${1}': '${2}', '${2}': '${1}' }
+ },
+ beforeReplacements: "${1}${2}",
+ afterReplacements: "${2}${1}"
+});
+
+makeTest({
+ name: 'Multiple instances of same substitution string with brackets.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '${1}': '${2}' }
+ },
+ beforeReplacements: "{${1}${1}}",
+ afterReplacements: "{${2}${2}}"
+});
+
+makeTest({
+ name: 'Mismatched replacement with brackets.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '${2}': '${1}' }
+ },
+ beforeReplacements: "${1}",
+ afterReplacements: "${1}"
+});
+
+makeTest({
+ name: 'Recursive and reduce size with percents.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '%%1%%': '1' }
+ },
+ beforeReplacements: "%%%%1%%%%",
+ afterReplacements: "%%1%%"
+});
+
+makeTest({
+ name: 'Recursive and increase size with percents.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '%%1%%': '%%%%1%%%%' }
+ },
+ beforeReplacements: "%%1%%",
+ afterReplacements: "%%%%1%%%%"
+});
+
+makeTest({
+ name: 'Replacements use a single pass with percents.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '%%1%%': '%%2%%', '%%2%%': '%%1%%' }
+ },
+ beforeReplacements: "%%1%%%%2%%",
+ afterReplacements: "%%2%%%%1%%"
+});
+
+makeTest({
+ name: 'Multiple instances of same substitution string with percents.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '%%1%%': '%%2%%' }
+ },
+ beforeReplacements: "%%1%%%%1%%",
+ afterReplacements: "%%2%%%%2%%"
+});
+
+makeTest({
+ name: 'Mismatched replacement with percents.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '%%2%%': '%%1%%' }
+ },
+ beforeReplacements: "%%1%%",
+ afterReplacements: "%%1%%"
+});
+
+makeTest({
+ name: 'Case sensativity.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '%%foo%%': '%%bar%%' }
+ },
+ beforeReplacements: "%%FOO%%%%foo%%",
+ afterReplacements: "%%FOO%%%%bar%%"
+});
+
+makeTest({
+ name: 'Super macro, a macro with a macro inside it basically, with percents.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '%%%%foo%%%%': 'foo' }
+ },
+ beforeReplacements: "%%%%foo%%%%",
+ afterReplacements: "foo"
+});
+
+makeTest({
+ name: 'Super macro, with brackets.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '${${foo}}': 'foo' }
+ },
+ beforeReplacements: "${${foo}}",
+ afterReplacements: "foo"
+});
+
+makeTest({
+ name: 'Super macro, with both.',
+ auctionConfigOverrides: {
+ deprecatedRenderURLReplacements: { '${%%foo%%}': 'foo', '%%${bar}%%':'bar' }
+ },
+ beforeReplacements: "${%%foo%%}%%${bar}%%",
+ afterReplacements: "foobar"
+});
diff --git a/testing/web-platform/tests/fledge/tentative/interest-group-update.https.window.js b/testing/web-platform/tests/fledge/tentative/interest-group-update.https.window.js
new file mode 100644
index 0000000000..59b3736b09
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/interest-group-update.https.window.js
@@ -0,0 +1,406 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-4
+// META: variant=?5-9
+// META: variant=?10-14
+// META: variant=?15-19
+// META: variant=?20-last
+
+"use strict;"
+
+// This test repeatedly runs auctions to verify an update. A modified bidding script
+// continuously throws errors until it detects the expected change in the interest group
+// field. This update then stops the auction cycle.
+const makeTestForUpdate = ({
+ // Test name
+ name,
+ // fieldname that is getting updated
+ interestGroupFieldName,
+ // This is used to check if update has happened.
+ expectedValue,
+ // This is used to create the update response, by default it will always send
+ // back the `expectedValue`. Extra steps to make a deep copy.
+ responseOverride = expectedValue,
+ // Overrides to the interest group.
+ interestGroupOverrides = {},
+ // Overrides to the auction config.
+ auctionConfigOverrides = {},
+}) => {
+ subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ extraBiddingLogic = ``;
+
+ let replacePlaceholders = (ads) => ads.forEach(element => {
+ element.renderURL = element.renderURL.replace(`UUID-PLACEHOLDER`, uuid);
+ });
+
+ // Testing 'ads' requires some additional setup due to it's reliance
+ // on createRenderURL, specifically the bidding script used checks to make
+ // sure the `uuid` is the correct one for the test. We use a renderURL
+ // with a placeholder 'UUID-PLACEHOLDER' and make sure to replace it
+ // before moving on to the test.
+ if (interestGroupFieldName === `ads`) {
+ if (interestGroupFieldName in interestGroupOverrides) {
+ replacePlaceholders(interestGroupOverrides[interestGroupFieldName]);
+ }
+ replacePlaceholders(responseOverride);
+ replacePlaceholders(expectedValue);
+ }
+ // When checking the render URL, both the deprecated 'renderUrl' and the updated 'renderURL' might exist
+ // in the interest group simultaneously, so this test deletes the 'renderUrl' to ensure a
+ // clean comparison with deepEquals.
+ if (interestGroupFieldName === `ads` || interestGroupFieldName === `adComponents`) {
+ extraBiddingLogic = `
+ interestGroup.${interestGroupFieldName}.forEach(element => {
+ delete element.renderUrl;
+ });`
+ }
+
+ let expectedValueJSON = JSON.stringify(expectedValue);
+ // When the update has not yet been seen, throw an error which will cause the
+ // auction not to have a result.
+ interestGroupOverrides.biddingLogicURL = createBiddingScriptURL({
+ generateBid: `
+ ${extraBiddingLogic}
+ if (!deepEquals(interestGroup.${interestGroupFieldName}, ${expectedValueJSON})) {
+ throw '${interestGroupFieldName} is ' +
+ JSON.stringify(interestGroup.${interestGroupFieldName}) +
+ ' instead of ' + '${expectedValueJSON}';
+ }`
+ });
+
+ let responseBody = {};
+ responseBody[interestGroupFieldName] = responseOverride;
+ let updateParams = {
+ body: JSON.stringify(responseBody),
+ uuid: uuid
+ };
+ interestGroupOverrides.updateURL = createUpdateURL(updateParams);
+ await joinInterestGroup(test, uuid, interestGroupOverrides);
+
+ // Run an auction until there's a winner, which means update occurred.
+ let auctionResult = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides);
+ expectNoWinner(auctionResult);
+ while (!auctionResult) {
+ auctionResult = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides);
+ }
+ }, name);
+};
+
+// In order to test the update process does not update certain fields, this test uses two interest groups:
+
+// * `failedUpdateGroup`: Receives an invalid update, and will continue to throw errors until the update
+// occurs (which shouldn't happen). This group will have a high bid to ensure if
+// there was ever a tie, it would win.
+// * `successUpdateGroup`: A hard-coded interest group that receives a update and will signal the change
+// by throwing an error.
+
+// By tracking render URLs, this test guarantees that only the URL associated with the correct update
+// (`goodUpdateRenderURL`) is used, and the incorrect URL (`badUpdateRenderURL`) isn't. The test runs
+// auctions repeatedly until the update in `successUpdateGroup` stops an auction from producing a winner.
+// It then will run one final auction. If there's still no winner, it can infer that `failedUpdateGroup`
+// would have received the update if it were propagating correctly.
+
+// If there was a bug in the implementation, a possible case can occur and manifest as a flaky test.
+// In this scenerio with the current structure of the Protected Audience API, the `successUpdateGroup`
+// updates, and so does the `failedUpdateGroup`, but the `failedUpdateGroup` update happens significantly
+// after the `successUpdateGroup`'s update. In an effort to combat this, after the while loop we run
+// another auction to ensure there is no winner (both cases should throw), but depending how slow the
+// update takes, this flaky issue still can **possibly** occur.
+const makeTestForNoUpdate = ({
+ // Test name
+ name,
+ // fieldname that is should not be getting updated
+ interestGroupFieldName,
+ // this is used to create the update response and check if it did not happen.
+ responseOverride,
+ // Overrides to the auction config.
+ auctionConfigOverrides = {},
+ // Overrides to the interest group.
+ failedUpdateGroup = {},
+}) => {
+ subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ // successUpdateGroup
+
+ // These are used in `successUpdateGroup` in order to get a proper update.
+ let successUpdateGroup = {};
+ let successUpdateField = `userBiddingSignals`;
+ let successUpdateFieldExpectedValue = { 'test': 20 };
+
+ const goodUpdateRenderURL = createTrackerURL(window.location.origin, uuid, 'track_get', 'good_update');
+ successUpdateGroup.ads = [{ 'renderURL': goodUpdateRenderURL }];
+ successUpdateGroup.biddingLogicURL = createBiddingScriptURL({
+ generateBid: `
+ if (deepEquals(interestGroup.${successUpdateField}, ${JSON.stringify(successUpdateFieldExpectedValue)})){
+ throw '${successUpdateField} has updated and is ' +
+ '${JSON.stringify(successUpdateFieldExpectedValue)}.'
+ }`,
+ bid: 5
+ });
+
+ let successResponseBody = {};
+ successResponseBody[successUpdateField] = successUpdateFieldExpectedValue;
+ let successUpdateParams = {
+ body: JSON.stringify(successResponseBody),
+ uuid: uuid
+ };
+ successUpdateGroup.updateURL = createUpdateURL(successUpdateParams);
+ await joinInterestGroup(test, uuid, successUpdateGroup);
+ ///////////////////////// successUpdateGroup
+
+ // failedUpdateGroup
+ const badUpdateRenderURL = createTrackerURL(window.location.origin, uuid, `track_get`, `bad_update`);
+ // Name needed so we don't have two IGs with same name.
+ failedUpdateGroup.name = failedUpdateGroup.name ? failedUpdateGroup.name : `IG name`
+ failedUpdateGroup.ads = [{ 'renderURL': badUpdateRenderURL }];
+ failedUpdateGroup.biddingLogicURL = createBiddingScriptURL({
+ generateBid: `
+ if (!deepEquals(interestGroup.${interestGroupFieldName}, ${JSON.stringify(responseOverride)})){
+ throw '${interestGroupFieldName} is as expected: '+
+ JSON.stringify(interestGroup.${interestGroupFieldName});
+ }`,
+ bid: 1000
+ });
+ let failedResponseBody = {};
+ failedResponseBody[interestGroupFieldName] = responseOverride;
+
+ let failedUpdateParams = {
+ body: JSON.stringify(failedResponseBody),
+ uuid: uuid
+ };
+
+ failedUpdateGroup.updateURL = createUpdateURL(failedUpdateParams);
+ await joinInterestGroup(test, uuid, failedUpdateGroup);
+ ///////////////////////// failedUpdateGroup
+
+ // First result should be not be null, `successUpdateGroup` throws when update is detected so until then,
+ // run and observe the requests to ensure only `goodUpdateRenderURL` is fetched.
+ let auctionResult = await runBasicFledgeTestExpectingWinner(test, uuid, auctionConfigOverrides);
+ while (auctionResult) {
+ createAndNavigateFencedFrame(test, auctionResult);
+ await waitForObservedRequests(
+ uuid,
+ [goodUpdateRenderURL, createSellerReportURL(uuid)]);
+ await fetch(createCleanupURL(uuid));
+ auctionResult = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides);
+ }
+ // Re-run to ensure null because:
+ // `successUpdateGroup` should be throwing since update occurred.
+ // `failedUpdateGroup` should be throwing since update did not occur.
+ await runBasicFledgeTestExpectingNoWinner(test, uuid, auctionConfigOverrides);
+ }, name);
+};
+
+// Helper to eliminate rewriting a long call to createRenderURL().
+// Only thing to change would be signalParams to differentiate between URLs.
+const createTempRenderURL = (signalsParams = null) => {
+ return createRenderURL(/*uuid=*/`UUID-PLACEHOLDER`,/*script=*/ null,/*signalParams=*/ signalsParams,/*origin=*/ null);
+};
+
+makeTestForUpdate({
+ name: 'userBiddingSignals update overwrites everything in the field.',
+ interestGroupFieldName: 'userBiddingSignals',
+ expectedValue: { 'test': 20 },
+ interestGroupOverrides: {
+ userBiddingSignals: { 'test': 10, 'extra_value': true },
+ }
+});
+
+makeTestForUpdate({
+ name: 'userBiddingSignals updated multi-type',
+ interestGroupFieldName: 'userBiddingSignals',
+ expectedValue: { 'test': 20, 5: [1, [false, false, true], 3, 'Hello'] },
+ interestGroupOverrides: {
+ userBiddingSignals: { 'test': 10 },
+ }
+});
+
+makeTestForUpdate({
+ name: 'userBiddingSignals updated to non object',
+ interestGroupFieldName: 'userBiddingSignals',
+ expectedValue: 5,
+ interestGroupOverrides: {
+ userBiddingSignals: { 'test': 10 },
+ }
+});
+
+makeTestForUpdate({
+ name: 'userBiddingSignals updated to null',
+ interestGroupFieldName: 'userBiddingSignals',
+ expectedValue: null,
+ interestGroupOverrides: {
+ userBiddingSignals: { 'test': 10 },
+ }
+});
+
+makeTestForUpdate({
+ name: 'trustedBiddingSignalsKeys updated correctly',
+ interestGroupFieldName: 'trustedBiddingSignalsKeys',
+ expectedValue: ['new_key', 'old_key'],
+ interestGroupOverrides: {
+ trustedBiddingSignalsKeys: ['old_key'],
+ }
+});
+
+makeTestForUpdate({
+ name: 'trustedBiddingSignalsKeys updated to empty array.',
+ interestGroupFieldName: 'trustedBiddingSignalsKeys',
+ expectedValue: [],
+ interestGroupOverrides: {
+ trustedBiddingSignalsKeys: ['old_key'],
+ }
+});
+
+
+makeTestForUpdate({
+ name: 'trustedBiddingSignalsSlotSizeMode updated to slot-size',
+ interestGroupFieldName: 'trustedBiddingSignalsSlotSizeMode',
+ expectedValue: 'slot-size',
+ interestGroupOverrides: {
+ trustedBiddingSignalsKeys: ['key'],
+ trustedBiddingSignalsSlotSizeMode: 'none',
+ }
+});
+
+makeTestForUpdate({
+ name: 'trustedBiddingSignalsSlotSizeMode updated to all-slots-requested-sizes',
+ interestGroupFieldName: 'trustedBiddingSignalsSlotSizeMode',
+ expectedValue: 'all-slots-requested-sizes',
+ interestGroupOverrides: {
+ trustedBiddingSignalsKeys: ['key'],
+ trustedBiddingSignalsSlotSizeMode: 'slot-size',
+ }
+});
+
+makeTestForUpdate({
+ name: 'trustedBiddingSignalsSlotSizeMode updated to none',
+ interestGroupFieldName: 'trustedBiddingSignalsSlotSizeMode',
+ expectedValue: 'none',
+ interestGroupOverrides: {
+ trustedBiddingSignalsKeys: ['key'],
+ trustedBiddingSignalsSlotSizeMode: 'slot-size',
+ }
+});
+
+makeTestForUpdate({
+ name: 'trustedBiddingSignalsSlotSizeMode updated to unknown, defaults to none',
+ interestGroupFieldName: 'trustedBiddingSignalsSlotSizeMode',
+ expectedValue: 'none',
+ responseOverride: 'unknown-type',
+ interestGroupOverrides: {
+ trustedBiddingSignalsKeys: ['key'],
+ trustedBiddingSignalsSlotSizeMode: 'slot-size',
+ }
+});
+
+makeTestForUpdate({
+ name: 'ads updated from 2 ads to 1.',
+ interestGroupFieldName: 'ads',
+ expectedValue: [
+ { renderURL: createTempRenderURL('new_url1'), metadata: 'test1-new' },
+ ],
+ interestGroupOverrides: {
+ ads: [{ renderURL: createTempRenderURL() },
+ { renderURL: createTempRenderURL() }]
+ }
+});
+
+makeTestForUpdate({
+ name: 'ads updated from 1 ad to 2.',
+ interestGroupFieldName: 'ads',
+ expectedValue: [{ renderURL: createTempRenderURL('new_url1'), metadata: 'test1-new' },
+ { renderURL: createTempRenderURL('new_url2'), metadata: 'test2-new' }],
+ interestGroupOverrides: {
+ ads: [{ renderURL: createTempRenderURL() }]
+ }
+});
+
+makeTestForUpdate({
+ name: 'adComponents updated from 1 adComponent to 2.',
+ interestGroupFieldName: 'adComponents',
+ expectedValue: [{ renderURL: createTempRenderURL('new_url1'), metadata: 'test1-new' },
+ { renderURL: createTempRenderURL('new_url2'), metadata: 'test2' }],
+ interestGroupOverrides: {
+ adComponents: [{ renderURL: createTempRenderURL(), metadata: 'test1' }]
+ },
+});
+
+makeTestForUpdate({
+ name: 'adComponents updated from 2 adComponents to 1.',
+ interestGroupFieldName: 'adComponents',
+ expectedValue: [{ renderURL: createTempRenderURL('new_url1'), metadata: 'test1-new' }],
+ interestGroupOverrides: {
+ adComponents: [{ renderURL: createTempRenderURL() },
+ { renderURL: createTempRenderURL() }]
+ },
+});
+
+makeTestForUpdate({
+ name: 'executionMode updated to frozen context',
+ interestGroupFieldName: 'executionMode',
+ expectedValue: 'frozen-context',
+ interestGroupOverrides: {
+ executionMode: 'compatibility',
+ }
+});
+
+makeTestForUpdate({
+ name: 'executionMode updated to compatibility',
+ interestGroupFieldName: 'executionMode',
+ expectedValue: 'compatibility',
+ interestGroupOverrides: {
+ executionMode: 'frozen-context',
+ }
+});
+
+makeTestForUpdate({
+ name: 'executionMode updated to group by origin',
+ interestGroupFieldName: 'executionMode',
+ expectedValue: 'group-by-origin',
+ interestGroupOverrides: {
+ executionMode: 'compatibility',
+ }
+});
+
+makeTestForNoUpdate({
+ name: 'executionMode updated with invalid input',
+ interestGroupFieldName: 'executionMode',
+ responseOverride: 'unknown-type',
+});
+
+makeTestForNoUpdate({
+ name: 'owner cannot be updated.',
+ interestGroupFieldName: 'owner',
+ responseOverride: OTHER_ORIGIN1,
+ auctionConfigOverrides: {
+ interestGroupBuyers: [OTHER_ORIGIN1, window.location.origin]
+ }
+});
+
+makeTestForNoUpdate({
+ name: 'name cannot be updated.',
+ interestGroupFieldName: 'name',
+ responseOverride: 'new_name',
+ failedUpdateGroup: { name: 'name2' },
+});
+
+makeTestForNoUpdate({
+ name: 'executionMode not updated when unknown type.',
+ interestGroupFieldName: 'executionMode',
+ responseOverride: 'unknown-type',
+ failedUpdateGroup: { executionMode: 'compatibility' },
+});
+
+makeTestForNoUpdate({
+ name: 'trustedBiddingSignalsKeys not updated when bad value.',
+ interestGroupFieldName: 'trustedBiddingSignalsKeys',
+ responseOverride: 5,
+ failedUpdateGroup: {
+ trustedBiddingSignalsKeys: ['key'],
+ },
+});
+
diff --git a/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js b/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js
index 7be02e34ff..a7d0f63830 100644
--- a/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js
+++ b/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js
@@ -77,6 +77,15 @@ function createDirectFromSellerSignalsURL(origin = window.location.origin) {
return url.toString();
}
+function createUpdateURL(params = {}) {
+ let origin = window.location.origin;
+ let url = new URL(`${origin}${RESOURCE_PATH}update-url.py`);
+ url.searchParams.append('body', params.body);
+ url.searchParams.append('uuid', params.uuid);
+
+ return url.toString();
+}
+
// Generates a UUID and registers a cleanup method with the test fixture to
// request a URL from the request tracking script that clears all data
// associated with the generated uuid when requested.
@@ -142,19 +151,21 @@ async function waitForObservedRequests(uuid, expectedRequests, filter) {
trackedRequests = trackedRequests.filter(filter);
}
- // If expected number of requests have been observed, compare with list of
- // all expected requests and exit.
- if (trackedRequests.length >= expectedRequests.length) {
- assert_array_equals(trackedRequests, expectedRequests);
- break;
- }
-
// If fewer than total number of expected requests have been observed,
// compare what's been received so far, to have a greater chance to fail
// rather than hang on error.
for (const trackedRequest of trackedRequests) {
assert_in_array(trackedRequest, expectedRequests);
}
+
+ // If expected number of requests have been observed, compare with list of
+ // all expected requests and exit. This check was previously before the for loop,
+ // but was swapped in order to avoid flakiness with failing tests and their
+ // respective *-expected.txt.
+ if (trackedRequests.length >= expectedRequests.length) {
+ assert_array_equals(trackedRequests, expectedRequests);
+ break;
+ }
}
}
@@ -830,3 +841,21 @@ let additionalBidHelper = function() {
fetchAdditionalBids: fetchAdditionalBids
};
}();
+
+
+// DeprecatedRenderURLReplacements helper function.
+// Returns an object containing sample strings both before and after the
+// replacements in 'replacements' have been applied by
+// deprecatedRenderURLReplacements. All substitution strings will appear
+// only once in the output strings.
+function createStringBeforeAndAfterReplacements(deprecatedRenderURLReplacements) {
+ let beforeReplacements = '';
+ let afterReplacements = '';
+ if(deprecatedRenderURLReplacements){
+ for (const [match, replacement] of Object.entries(deprecatedRenderURLReplacements)) {
+ beforeReplacements += match + "/";
+ afterReplacements += replacement + "/";
+ }
+ }
+ return { beforeReplacements, afterReplacements };
+}
diff --git a/testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py b/testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py
index f9ca9031f1..955a7c0bdf 100644
--- a/testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py
+++ b/testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py
@@ -1,3 +1,4 @@
+import collections
import json
from urllib.parse import unquote_plus
@@ -47,7 +48,8 @@ def main(request, response):
response.status = (200, b"OK")
# The JSON representation of this is used as the response body. This does
- # not currently include a "perInterestGroupData" object.
+ # not currently include a "perInterestGroupData" object except for
+ # updateIfOlderThanMs.
responseBody = {"keys": {}}
# Set when certain special keys are observed, used in place of the JSON
@@ -117,6 +119,22 @@ def main(request, response):
if "data-version" in interestGroupNames:
dataVersion = "4"
+ per_interest_group_data = collections.defaultdict(dict)
+ for name in interestGroupNames:
+ if name == "use-update-if-older-than-ms":
+ # One hour in milliseconds.
+ per_interest_group_data[name]["updateIfOlderThanMs"] = 3_600_000
+ elif name == "use-update-if-older-than-ms-small":
+ # A value less than the minimum of 10 minutes.
+ per_interest_group_data[name]["updateIfOlderThanMs"] = 1
+ elif name == "use-update-if-older-than-ms-zero":
+ per_interest_group_data[name]["updateIfOlderThanMs"] = 0
+ elif name == "use-update-if-older-than-ms-negative":
+ per_interest_group_data[name]["updateIfOlderThanMs"] = -1
+
+ if per_interest_group_data:
+ responseBody["perInterestGroupData"] = dict(per_interest_group_data)
+
if contentType:
response.headers.set("Content-Type", contentType)
if adAuctionAllowed:
diff --git a/testing/web-platform/tests/fledge/tentative/resources/update-url.py b/testing/web-platform/tests/fledge/tentative/resources/update-url.py
new file mode 100644
index 0000000000..7de89e0f8f
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/update-url.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ response.status = (200, b"OK")
+ response.headers.set(b"Ad-Auction-Allowed", b"true")
+ response.headers.set(b"Content-Type", b"application/json")
+ body = request.GET.first(b"body", None)
+ return body \ No newline at end of file
diff --git a/testing/web-platform/tests/fledge/tentative/score-ad-browser-signals.https.window.js b/testing/web-platform/tests/fledge/tentative/score-ad-browser-signals.https.window.js
new file mode 100644
index 0000000000..f20412cfc7
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/score-ad-browser-signals.https.window.js
@@ -0,0 +1,57 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+
+"use strict;"
+
+// These tests focus on the browserSignals argument passed to scoreAd().
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let biddingLogicURL = createBiddingScriptURL(
+ {
+ generateBid:
+ `
+ return {
+ bid: 1,
+ render: { url: interestGroup.ads[0].renderURL,
+ width: '100sw',
+ height: '50px' }
+ };
+ `
+ });
+
+ let decisionLogicURL = createDecisionScriptURL(uuid,
+ {
+ scoreAd:
+ `
+ if (!browserSignals.hasOwnProperty('renderSize')) {
+ throw 'Missing renderSize member in browserSignals.';
+ }
+ if (browserSignals.renderSize.width !== '100sw' ||
+ browserSignals.renderSize.height !== '50px') {
+ throw 'Incorrect renderSize width or height.';
+ }
+ `
+ }
+ );
+
+ await joinGroupAndRunBasicFledgeTestExpectingWinner(
+ test,
+ {
+ uuid: uuid,
+ interestGroupOverrides: {
+ name: uuid,
+ biddingLogicURL: biddingLogicURL,
+ ads: [{ renderURL: createRenderURL(uuid), sizeGroup: 'group1' }],
+ adSizes: { 'size1': { width: '100sw', height: '50px' } },
+ sizeGroups: { 'group1': ['size1'] }
+ },
+ auctionConfigOverrides: {
+ decisionLogicURL: decisionLogicURL
+ }
+ });
+}, 'ScoreAd browserSignals renderSize test.');
diff --git a/testing/web-platform/tests/fledge/tentative/trusted-bidding-signals.https.window.js b/testing/web-platform/tests/fledge/tentative/trusted-bidding-signals.https.window.js
index d0b9a82086..905abf8381 100644
--- a/testing/web-platform/tests/fledge/tentative/trusted-bidding-signals.https.window.js
+++ b/testing/web-platform/tests/fledge/tentative/trusted-bidding-signals.https.window.js
@@ -17,7 +17,8 @@
// META: variant=?56-60
// META: variant=?61-65
// META: variant=?66-70
-// META: variant=?71-last
+// META: variant=?71-75
+// META: variant=?76-last
"use strict";
@@ -938,4 +939,44 @@ subsetTest(promise_test, async test => {
]
);
runBasicFledgeTestExpectingWinner(test, uuid);
-}, 'Trusted bidding signals splits the request if the combined URL length exceeds the limit of small value.'); \ No newline at end of file
+}, 'Trusted bidding signals splits the request if the combined URL length exceeds the limit of small value.');
+
+/////////////////////////////////////////////////////////////////////////////
+// updateIfOlderThanMs tests
+//
+// NOTE: Due to the lack of mock time in wpt, these test just exercise the code
+// paths and ensure that no crash occurs -- they don't otherwise verify
+// behavior.
+/////////////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ 'true',
+ { name: 'use-update-if-older-than-ms',
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has updateIfOlderThanMs > 10 min.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ 'true',
+ { name: 'use-update-if-older-than-ms-small',
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has updateIfOlderThanMs == 1 ms.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ 'true',
+ { name: 'use-update-if-older-than-ms-zero',
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has updateIfOlderThanMs == 0 ms.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ 'true',
+ { name: 'use-update-if-older-than-ms-negative',
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has updateIfOlderThanMs == -1 ms.');
diff --git a/testing/web-platform/tests/focus/cross-origin-ancestor-activeelement-after-child-lose-focus.sub.html b/testing/web-platform/tests/focus/cross-origin-ancestor-activeelement-after-child-lose-focus.sub.html
index 35844bc91f..1b84b43f1c 100644
--- a/testing/web-platform/tests/focus/cross-origin-ancestor-activeelement-after-child-lose-focus.sub.html
+++ b/testing/web-platform/tests/focus/cross-origin-ancestor-activeelement-after-child-lose-focus.sub.html
@@ -20,36 +20,32 @@ function runTest() {
const innerIFrame = outerIFrame.contentDocument.createElement("iframe");
- window.onmessage = function() {
+ window.onmessage = function(event) {
+ if (event.data != "ready") {
+ return;
+ }
+
+ // We receive an message when the innerIFrame is ready and its input is focused.
+ // outerIframe is the ancestor of inner iframe, so the activeElement of
+ // it should be the inner iframe.
+ assert_equals(outerIFrame.contentDocument.activeElement, innerIFrame,
+ "The activeElement of the outer iframe should be the inner iframe");
+
+ // Now we focus the input in the top level
+ document.querySelector("input").focus();
+
// Wait for a bit to let whatever the code that might change the focus to run
window.requestAnimationFrame(function() {
window.requestAnimationFrame(function() {
window.requestAnimationFrame(function() {
-
- // We receive an message when the innerIFrame is ready and its input is focused.
- // outerIframe is the ancestor of inner iframe, so the activeElement of
- // it should be the inner iframe.
- assert_equals(outerIFrame.contentDocument.activeElement, innerIFrame,
- "The activeElement of the outer iframe should be the inner iframe");
-
- // Now we focus the input in the top level
- document.querySelector("input").focus();
-
- // Wait for a bit to let whatever the code that might change the focus to run
- window.requestAnimationFrame(function() {
- window.requestAnimationFrame(function() {
- window.requestAnimationFrame(function() {
- // Since inner iframe lost its focus, the activeElement of outer iframe
- // should be cleared as well, hence <body> should be focused.
- assert_equals(outerIFrame.contentDocument.activeElement, outerIFrame.contentDocument.body,
- "The activeElement of the outer iframe should be reverted back to <body>");
- assert_equals(document.activeElement, document.querySelector("input"),
- "The activeElement of the top-level document should the input");
- done();
- });
- });
- });
- });
+ // Since inner iframe lost its focus, the activeElement of outer iframe
+ // should be cleared as well, hence <body> should be focused.
+ assert_equals(outerIFrame.contentDocument.activeElement, outerIFrame.contentDocument.body,
+ "The activeElement of the outer iframe should be reverted back to <body>");
+ assert_equals(document.activeElement, document.querySelector("input"),
+ "The activeElement of the top-level document should the input");
+ done();
+ })
});
});
}
diff --git a/testing/web-platform/tests/focus/support/cross-origin-ancestor-activeelement-after-child-lose-focus-helper.html b/testing/web-platform/tests/focus/support/cross-origin-ancestor-activeelement-after-child-lose-focus-helper.html
index 83d39d5c70..fe96614517 100644
--- a/testing/web-platform/tests/focus/support/cross-origin-ancestor-activeelement-after-child-lose-focus-helper.html
+++ b/testing/web-platform/tests/focus/support/cross-origin-ancestor-activeelement-after-child-lose-focus-helper.html
@@ -2,8 +2,10 @@
<body>
<input />
<script>
- document.querySelector("input").focus();
- window.parent.parent.postMessage("ready", '*');
+ window.onload = function() {
+ document.querySelector("input").focus();
+ window.parent.parent.postMessage("ready", '*');
+ }
</script>
</body>
</html>
diff --git a/testing/web-platform/tests/fonts/math/stretchy.woff b/testing/web-platform/tests/fonts/math/stretchy.woff
index eb67181e1e..ac7d908f6e 100644
--- a/testing/web-platform/tests/fonts/math/stretchy.woff
+++ b/testing/web-platform/tests/fonts/math/stretchy.woff
Binary files differ
diff --git a/testing/web-platform/tests/fonts/noto/cjk/NotoSansCJKjp-Regular-subset-halt-min.otf b/testing/web-platform/tests/fonts/noto/cjk/NotoSansCJKjp-Regular-subset-halt-min.otf
new file mode 100644
index 0000000000..1ec5a7dbea
--- /dev/null
+++ b/testing/web-platform/tests/fonts/noto/cjk/NotoSansCJKjp-Regular-subset-halt-min.otf
Binary files differ
diff --git a/testing/web-platform/tests/fonts/noto/cjk/README.md b/testing/web-platform/tests/fonts/noto/cjk/README.md
index a14fa64b42..c053ea8359 100644
--- a/testing/web-platform/tests/fonts/noto/cjk/README.md
+++ b/testing/web-platform/tests/fonts/noto/cjk/README.md
@@ -4,26 +4,16 @@ Fonts in this directory help testing the `text-spacing-trim` property.
## NotoSansCJKjp-Regular-subset-halt.otf
-This font is generated by the following command:
-```
-pyftsubset NotoSansCJKjp-Regular.otf \
- --unicodes=20-7E,2018-201F,56FD,6C34,3000-301F,30FB,FF01-FF1F,FF5B-FF65 \
- --layout-features+=halt,fwid,hwid,palt,pwid,vhal,vpal \
- --output-file=NotoSansCJKjp-Regular-subset-halt.otf
-```
-where `pyftsubset` comes from https://github.com/fonttools/fonttools
+Please see `subset.sh` to generate.
## NotoSansCJKjp-Regular-subset-chws.otf
This font has `chws` and `vchw` in addition to the font above.
-This font is generated by the following command:
-```
-pyftsubset NotoSansCJKjp-Regular.otf \
- --unicodes=20-7E,2018-201F,56FD,6C34,3000-301F,30FB,FF01-FF1F,FF5B-FF65 \
- --layout-features+=halt,fwid,hwid,palt,pwid,vhal,vpal,chws,vchw \
- --output-file=NotoSansCJKjp-Regular-subset-chws.otf
-```
Note there are two variants of Noto CJK; one with `chws` and one without.
The input font for this command must be the one with `chws`, processed by the
[chws_tool](https://github.com/googlefonts/chws_tool).
+
+## NotoSansCJKjp-Regular-subset-halt-min.otf
+
+This font is to test web fonts scenario where not all glyphs are available.
diff --git a/testing/web-platform/tests/fonts/noto/cjk/subset.sh b/testing/web-platform/tests/fonts/noto/cjk/subset.sh
new file mode 100755
index 0000000000..c348406676
--- /dev/null
+++ b/testing/web-platform/tests/fonts/noto/cjk/subset.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+#
+# Produces subset font files for testing the `text-spacing-trim` property.
+#
+# Requires `pyftsubset` from <https://github.com/fonttools/fonttools>.
+#
+range="--unicodes=20-7E,2018-201F,56FD,6C34,3000-301F,30FB,FF01-FF1F,FF5B-FF65"
+subrange="--unicodes=20-7E,56FD,FF08-FF09"
+features="--layout-features+=halt,fwid,hwid,palt,pwid,vhal,vpal"
+features_chws="$features,chws,vchw"
+for path in "$@"; do
+ filename="$(basename -- "$path")"
+ stem="${filename%.*}"
+ output_halt="$stem-subset-halt.otf"
+ output_chws="$stem-subset-chws.otf"
+ output_min="$stem-subset-halt-min.otf"
+ (set -x;
+ pyftsubset "$path" $range $features --output-file="$output_halt"
+ pyftsubset "$path" $range $features_chws --output-file="$output_chws"
+ pyftsubset "$output_halt" $subrange $features --output-file="$output_min"
+ )
+done
diff --git a/testing/web-platform/tests/gamepad/gamepad-dual-rumble-effect-manual.https.html b/testing/web-platform/tests/gamepad/gamepad-dual-rumble-effect-manual.https.html
new file mode 100644
index 0000000000..4a1c5ba8c5
--- /dev/null
+++ b/testing/web-platform/tests/gamepad/gamepad-dual-rumble-effect-manual.https.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <p>
+ This test requires a gamepad device to be connected. Please interact with
+ the gamepad for it to be recognized. The "Dual rumble!" button will be
+ enabled after that.
+ </p>
+ <p>
+ After pressing the "Dual rumble!" button below, you should expect all the
+ "dual-rumble" compatible gamepads to vibrate for one second.
+ </p>
+ <p>
+ Please press the "Confirm effect has played" button to conclude
+ the test.
+ </p>
+ <button id="play_dual_rumble_button" disabled>No dual-rumble gamepads detected</button>
+ <button id="confirm_effect_button" disabled>Confirm effect has played</button>
+ <script>
+ async_test(t => {
+ let connectedDualRumbleGamepads = {};
+ playEffectButton = document.getElementById('play_dual_rumble_button');
+
+ function isDualRumbleSupported(gamepad) {
+ return gamepad.vibrationActuator.effects.includes('dual-rumble');
+ }
+
+ window.addEventListener('gamepadconnected', (e) => {
+ if (!e.gamepad || !e.gamepad.vibrationActuator || !e.gamepad.vibrationActuator.effects) {
+ return;
+ }
+
+ if (isDualRumbleSupported(e.gamepad)) {
+ connectedDualRumbleGamepads[e.gamepad.index] = e.gamepad;
+
+ if (playEffectButton.disabled) {
+ playEffectButton.disabled = false;
+ playEffectButton.innerText = 'Dual rumble!'
+ }
+ }
+ });
+
+ window.addEventListener('gamepaddisconnected', (e) => {
+ delete connectedDualRumbleGamepads[e.gamepad.index];
+
+ let anyDualRumbleGamepad = false;
+ for (let index in connectedDualRumbleGamepads){
+ const gamepad = connectedDualRumbleGamepads[index];
+ if (!gamepad || !gamepad.vibrationActuator || !gamepad.vibrationActuator.effects) {
+ continue;
+ }
+
+ if (isDualRumbleSupported(gamepad)){
+ anyDualRumbleGamepad = true;
+ break;
+ }
+ }
+
+ if (!anyDualRumbleGamepad && !playEffectButton.disabled) {
+ playEffectButton.disabled = true;
+ playEffectButton.innerText = "No dual-rumble gamepads detected";
+ }
+ });
+
+ playEffectButton.addEventListener("click", () => {
+ let gamepads = navigator.getGamepads();
+ for (const gamepad of gamepads) {
+ if (gamepad && isDualRumbleSupported(gamepad)) {
+ gamepad.vibrationActuator.playEffect("dual-rumble", {
+ duration: 1000,
+ strongMagnitude: 1.0,
+ weakMagnitude: 1.0,
+ });
+ }
+ }
+
+ const confirmButton = document.getElementById("confirm_effect_button");
+ if (confirmButton.disabled) {
+ confirmButton.disabled = false;
+ }
+ confirmButton.addEventListener('click', () => {
+ t.done();
+ });
+ });
+ }, "Gamepads with dual-rumble capabilities should have the body's motors activated.");
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/gamepad/gamepad-trigger-rumble-effect-manual.https.html b/testing/web-platform/tests/gamepad/gamepad-trigger-rumble-effect-manual.https.html
new file mode 100644
index 0000000000..f436a60aac
--- /dev/null
+++ b/testing/web-platform/tests/gamepad/gamepad-trigger-rumble-effect-manual.https.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <p>
+ This test requires a gamepad device to be connected. Please interact with
+ the gamepad for it to be recognized. The "Trigger rumble!" button will be
+ enabled after that.
+ </p>
+ <p>
+ After pressing the "Trigger rumble!" button below, you should expect to
+ feel a localized vibration in both triggers of all connected
+ trigger-rumble compatible gamepads for one second.
+ </p>
+ <p>
+ Please press the "Confirm effect has played" button to conclude
+ the test.
+ </p>
+ <button id="play_trigger_rumble_button" disabled>No trigger-rumble gamepads detected</button>
+ <button id="confirm_effect_button" disabled>Confirm effect has played</button>
+ <script>
+ async_test(t => {
+ let connectedTriggerRumbleGamepads = {};
+ playEffectButton = document.getElementById('play_trigger_rumble_button');
+
+ function isTriggerRumbleSupported(gamepad) {
+ return gamepad.vibrationActuator.effects.includes('trigger-rumble');
+ }
+
+ window.addEventListener('gamepadconnected', (e) => {
+ if (!e.gamepad || !e.gamepad.vibrationActuator || !e.gamepad.vibrationActuator.effects) {
+ return;
+ }
+
+ if (isTriggerRumbleSupported(e.gamepad)) {
+ connectedTriggerRumbleGamepads[e.gamepad.index] = e.gamepad;
+
+ if (playEffectButton.disabled) {
+ playEffectButton.disabled = false;
+ playEffectButton.innerText = 'Trigger rumble!'
+ }
+ }
+ });
+
+ window.addEventListener('gamepaddisconnected', (e) => {
+ delete connectedTriggerRumbleGamepads[e.gamepad.index];
+
+ let anyTriggerRumbleGamepad = false;
+ for (let index in connectedTriggerRumbleGamepads){
+ const gamepad = connectedTriggerRumbleGamepads[index];
+ if (!gamepad || !gamepad.vibrationActuator || !gamepad.vibrationActuator.effects) {
+ continue;
+ }
+
+ if (isTriggerRumbleSupported(gamepad)){
+ anyTriggerRumbleGamepad = true;
+ break;
+ }
+ }
+
+ if (!anyTriggerRumbleGamepad && !playEffectButton.disabled) {
+ playEffectButton.disabled = true;
+ playEffectButton.innerText = "No trigger-rumble gamepads detected";
+ }
+ });
+
+ playEffectButton.addEventListener("click", () => {
+ let gamepads = navigator.getGamepads();
+ for (const gamepad of gamepads) {
+ if (gamepad && isTriggerRumbleSupported(gamepad)) {
+ gamepad.vibrationActuator.playEffect("trigger-rumble", {
+ duration: 1000,
+ leftTrigger: 1.0,
+ rightTrigger: 1.0,
+ });
+ }
+ }
+
+ const confirmButton = document.getElementById("confirm_effect_button");
+ if (confirmButton.disabled) {
+ confirmButton.disabled = false;
+ }
+ confirmButton.addEventListener('click', () => {
+ t.done();
+ });
+ });
+ }, "Gamepads with trigger-rumble capabilities should have the triggers' motors activated.");
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/geolocation-API/disabled-by-permissions-policy.https.sub.html b/testing/web-platform/tests/geolocation-API/disabled-by-permissions-policy.https.sub.html
index 6bd3e3500b..d3d94e1d3d 100644
--- a/testing/web-platform/tests/geolocation-API/disabled-by-permissions-policy.https.sub.html
+++ b/testing/web-platform/tests/geolocation-API/disabled-by-permissions-policy.https.sub.html
@@ -12,14 +12,13 @@
const same_origin_src =
"/permissions-policy/resources/permissions-policy-geolocation.html";
const cross_origin_src =
- "https://{{hosts[][]}}:{{ports[https][0]}}" + same_origin_src;
+ "https://{{hosts[alt][]}}:{{ports[https][0]}}" + same_origin_src;
- promise_test(async (t) => {
- await test_driver.set_permission(
- { name: "geolocation" },
- "granted"
- );
+ promise_setup(async () => {
+ await test_driver.set_permission({ name: "geolocation" }, "granted");
+ });
+ promise_test(async (test) => {
const posError = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(reject, resolve);
});
@@ -49,22 +48,25 @@
);
}, "Permissions-Policy header geolocation=() disallows the top-level document.");
- async_test((t) => {
- test_feature_availability(
- "geolocation",
- t,
- same_origin_src,
- expect_feature_unavailable_default
- );
+ promise_test(async (test) => {
+ await test_feature_availability({
+ feature_description: "Geolocation API",
+ test,
+ src: same_origin_src,
+ expect_feature_available: expect_feature_unavailable_default,
+ is_promise_test: true,
+ });
}, "Permissions-Policy header geolocation=() disallows same-origin iframes.");
- async_test((t) => {
- test_feature_availability(
- "geolocation",
- t,
- cross_origin_src,
- expect_feature_unavailable_default
- );
+ promise_test(async (test) => {
+ await test_feature_availability({
+ feature_description: "Geolocation API",
+ test,
+ src: cross_origin_src,
+ expect_feature_available: expect_feature_unavailable_default,
+ feature_name: "geolocation",
+ is_promise_test: true,
+ });
}, "Permissions-Policy header geolocation=() disallows cross-origin iframes.");
</script>
</body>
diff --git a/testing/web-platform/tests/geolocation-API/enabled-by-permission-policy-attribute-redirect-on-load.https.sub.html b/testing/web-platform/tests/geolocation-API/enabled-by-permission-policy-attribute-redirect-on-load.https.sub.html
index 864fb5e761..d25afa52bb 100644
--- a/testing/web-platform/tests/geolocation-API/enabled-by-permission-policy-attribute-redirect-on-load.https.sub.html
+++ b/testing/web-platform/tests/geolocation-API/enabled-by-permission-policy-attribute-redirect-on-load.https.sub.html
@@ -1,36 +1,36 @@
<!DOCTYPE html>
<body>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/permissions-policy/resources/permissions-policy.js"></script>
-<script>
- "use strict";
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/permissions-policy/resources/permissions-policy.js"></script>
+ <script>
+ "use strict";
- const relative_path = "/permissions-policy/resources/permissions-policy-geolocation.html";
- const base_src = "/permissions-policy/resources/redirect-on-load.html#";
- const same_origin_src = base_src + relative_path;
- const cross_origin_src =
- `${base_src}https://{{hosts[][]}}:{{ports[https][0]}}${relative_path}`;
+ const relative_path =
+ "/permissions-policy/resources/permissions-policy-geolocation.html";
+ const base_src = "/permissions-policy/resources/redirect-on-load.html#";
+ const same_origin_src = base_src + relative_path;
+ const cross_origin_src = `${base_src}https://{{hosts[alt][]}}:{{ports[https][0]}}${relative_path}`;
- async_test(t => {
- test_feature_availability(
- 'geolocation',
- t,
- same_origin_src,
- expect_feature_available_default,
- "geolocation"
- );
- }, 'Permissions-Policy allow="geolocation" allows same-origin relocation');
+ promise_test(async (test) => {
+ await test_feature_availability({
+ feature_description: "Geolocation API",
+ test,
+ src: same_origin_src,
+ expect_feature_available: expect_feature_available_default,
+ is_promise_test: true,
+ });
+ }, 'Permissions-Policy allow="geolocation" allows same-origin redirection');
- async_test(t => {
- test_feature_availability(
- 'geolocation',
- t,
- cross_origin_src,
- expect_feature_available_default,
- "geolocation"
- );
- }, 'Permissions-Policy allow="geolocation" allows cross-origin relocation');
-
-</script>
+ promise_test(async (test) => {
+ await test_feature_availability({
+ feature_description: "Geolocation API",
+ test,
+ src: cross_origin_src,
+ expect_feature_available: expect_feature_available_default,
+ is_promise_test: true,
+ feature_name: "geolocation",
+ });
+ }, 'Permissions-Policy allow="geolocation" allows cross-origin redirection');
+ </script>
</body>
diff --git a/testing/web-platform/tests/geolocation-API/enabled-by-permission-policy-attribute.https.sub.html b/testing/web-platform/tests/geolocation-API/enabled-by-permission-policy-attribute.https.sub.html
index 018409b829..50b8475b81 100644
--- a/testing/web-platform/tests/geolocation-API/enabled-by-permission-policy-attribute.https.sub.html
+++ b/testing/web-platform/tests/geolocation-API/enabled-by-permission-policy-attribute.https.sub.html
@@ -9,7 +9,7 @@
const same_origin_src =
"/permissions-policy/resources/permissions-policy-geolocation.html";
const cross_origin_src =
- "https://{{hosts[][]}}:{{ports[https][0]}}" + same_origin_src;
+ "https://{{hosts[alt][]}}:{{ports[https][0]}}" + same_origin_src;
async_test(t => {
test_feature_availability(
diff --git a/testing/web-platform/tests/geolocation-API/enabled-by-permissions-policy.https.sub.html b/testing/web-platform/tests/geolocation-API/enabled-by-permissions-policy.https.sub.html
index 007f79ab9c..332e4cea16 100644
--- a/testing/web-platform/tests/geolocation-API/enabled-by-permissions-policy.https.sub.html
+++ b/testing/web-platform/tests/geolocation-API/enabled-by-permissions-policy.https.sub.html
@@ -10,36 +10,42 @@
const same_origin_src =
"/permissions-policy/resources/permissions-policy-geolocation.html";
const cross_origin_src =
- "https://{{hosts[][]}}:{{ports[https][0]}}" + same_origin_src;
+ "https://{{hosts[alt][]}}:{{ports[https][0]}}" + same_origin_src;
- promise_test(async (t) => {
+ promise_setup(async () => {
await test_driver.set_permission({ name: "geolocation" }, "granted");
- const result = await new Promise((resolve, reject) => {
+ });
+
+ promise_test(async (test) => {
+ const position = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject);
});
assert_true(
- result instanceof GeolocationPosition,
+ position instanceof GeolocationPosition,
"Expected a GeolocationPosition"
);
}, "Permissions-Policy header geolocation=* allows the top-level document.");
- async_test((t) => {
- test_feature_availability(
- "geolocation",
- t,
- same_origin_src,
- expect_feature_available_default
- );
+ promise_test(async (test) => {
+ await test_feature_availability({
+ feature_description: "Geolocation API",
+ test,
+ src: same_origin_src,
+ expect_feature_available: expect_feature_available_default,
+ is_promise_test: true,
+ });
}, "Permissions-Policy header geolocation=* allows same-origin iframes.");
- async_test((t) => {
- test_feature_availability(
- "geolocation",
- t,
- cross_origin_src,
- expect_feature_available_default
- );
+ promise_test(async (test) => {
+ await test_feature_availability({
+ feature_description: "Geolocation API",
+ test,
+ src: cross_origin_src,
+ expect_feature_available: expect_feature_available_default,
+ feature_name: "geolocation",
+ is_promise_test: true,
+ });
}, "Permissions-Policy header geolocation=* allows cross-origin iframes.");
</script>
</body>
diff --git a/testing/web-platform/tests/geolocation-API/enabled-on-self-origin-by-permissions-policy.https.sub.html b/testing/web-platform/tests/geolocation-API/enabled-on-self-origin-by-permissions-policy.https.sub.html
index d879c1c543..5940888b35 100644
--- a/testing/web-platform/tests/geolocation-API/enabled-on-self-origin-by-permissions-policy.https.sub.html
+++ b/testing/web-platform/tests/geolocation-API/enabled-on-self-origin-by-permissions-policy.https.sub.html
@@ -12,31 +12,42 @@
const same_origin_src =
"/permissions-policy/resources/permissions-policy-geolocation.html";
const cross_origin_src =
- "https://{{hosts[][]}}:{{ports[https][0]}}" + same_origin_src;
+ "https://{{hosts[alt][]}}:{{ports[https][0]}}" + same_origin_src;
- promise_test(async (t) => {
+
+ promise_setup(async () => {
await test_driver.set_permission({ name: "geolocation" }, "granted");
- await new Promise((resolve, reject) => {
+ });
+
+ promise_test(async (t) => {
+ const position = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject);
});
+ assert_true(
+ position instanceof GeolocationPosition,
+ "Expected a GeolocationPosition"
+ );
}, "Permissions-Policy header geolocation=(self) allows the top-level document.");
- async_test((t) => {
- test_feature_availability(
- "geolocation",
- t,
- same_origin_src,
- expect_feature_available_default
- );
+ promise_test(async (test) => {
+ await test_feature_availability({
+ feature_description: "Geolocation API",
+ test,
+ src: same_origin_src,
+ expect_feature_available: expect_feature_available_default,
+ is_promise_test: true,
+ });
}, "Permissions-Policy header geolocation=(self) allows same-origin iframes.");
- async_test((t) => {
- test_feature_availability(
- "geolocation",
- t,
- cross_origin_src,
- expect_feature_unavailable_default
- );
+ promise_test(async (test) => {
+ await test_feature_availability({
+ feature_description: "Geolocation API",
+ test,
+ src: cross_origin_src,
+ expect_feature_available: expect_feature_unavailable_default,
+ feature_name: "geolocation",
+ is_promise_test: true,
+ });
}, "Permissions-Policy header geolocation=(self) disallows cross-origin iframes.");
</script>
</body>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position-inline-nearest.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position-inline-nearest.html
new file mode 100644
index 0000000000..4aab0aa5e5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position-inline-nearest.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html style="writing-mode: vertical-lr;">
+<head>
+<meta charset="UTF-8">
+<title>Fragment Navigation: inline start position should not scroll out of content range</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#scroll-to-the-fragment-identifier">
+<link rel="author" href="mailto:mrobinson@igalia.com" title="Martin Robinson">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+ <!-- When scrolling to this fragment the viewport inline position should not
+ change because, it is already fully enclosed by the viewport and page width. -->
+ <div id="test1" style="position: absolute; top: 5000px; left: 100px; height: 100px; width: 100px;"></div>
+<script>
+
+var t = async_test("ScrollToFragment");
+t.step(() => {
+ location.hash = "test1";
+ setTimeout(t.step_func(() => {
+ assert_true(window.scrollY > 0);
+ assert_true(window.scrollY < 5000);
+ assert_equals(window.scrollX, 0);
+ t.done();
+ }));
+});
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-1.html b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-1.html
deleted file mode 100644
index 59d66c383c..0000000000
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-1.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.fillStyle.parse.hsl-clamp-1</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
-<body class="show_output">
-
-<h1>2d.fillStyle.parse.hsl-clamp-1</h1>
-<p class="desc"></p>
-
-<p class="notes">
-<p class="output">Actual output:</p>
-<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
-<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsl-clamp-1.png" class="output expected" id="expected" alt="">
-<ul id="d"></ul>
-<script>
-var t = async_test("");
-_addTest(function(canvas, ctx) {
-
- ctx.fillStyle = '#f00';
- ctx.fillStyle = 'hsl(120, 200%, 50%)';
- ctx.fillRect(0, 0, 100, 50);
- _assertPixel(canvas, 50,25, 0,255,0,255);
-
-});
-</script>
-
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-3.html b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-3.html
deleted file mode 100644
index 56f3a0a8b5..0000000000
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-3.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.fillStyle.parse.hsl-clamp-3</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
-<body class="show_output">
-
-<h1>2d.fillStyle.parse.hsl-clamp-3</h1>
-<p class="desc"></p>
-
-<p class="notes">
-<p class="output">Actual output:</p>
-<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
-<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsl-clamp-3.png" class="output expected" id="expected" alt="">
-<ul id="d"></ul>
-<script>
-var t = async_test("");
-_addTest(function(canvas, ctx) {
-
- ctx.fillStyle = '#f00';
- ctx.fillStyle = 'hsl(120, 100%, 200%)';
- ctx.fillRect(0, 0, 100, 50);
- _assertPixel(canvas, 50,25, 255,255,255,255);
-
-});
-</script>
-
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-3.png b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-3.png
deleted file mode 100644
index bf48767a88..0000000000
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-3.png
+++ /dev/null
Binary files differ
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-4.html b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-4.html
deleted file mode 100644
index af9d11e678..0000000000
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-4.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.fillStyle.parse.hsl-clamp-4</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
-<body class="show_output">
-
-<h1>2d.fillStyle.parse.hsl-clamp-4</h1>
-<p class="desc"></p>
-
-<p class="notes">
-<p class="output">Actual output:</p>
-<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
-<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsl-clamp-4.png" class="output expected" id="expected" alt="">
-<ul id="d"></ul>
-<script>
-var t = async_test("");
-_addTest(function(canvas, ctx) {
-
- ctx.fillStyle = '#f00';
- ctx.fillStyle = 'hsl(120, 100%, -200%)';
- ctx.fillRect(0, 0, 100, 50);
- _assertPixel(canvas, 50,25, 0,0,0,255);
-
-});
-</script>
-
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-4.png b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-4.png
deleted file mode 100644
index d638d03386..0000000000
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-4.png
+++ /dev/null
Binary files differ
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-2.html b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html
index 1a1939e47a..65440c6228 100644
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-2.html
+++ b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html
@@ -1,19 +1,19 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.fillStyle.parse.hsl-clamp-2</title>
+<title>Canvas test: 2d.fillStyle.parse.hsl-clamp-negative-saturation</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/html/canvas/resources/canvas-tests.js"></script>
<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
<body class="show_output">
-<h1>2d.fillStyle.parse.hsl-clamp-2</h1>
+<h1>2d.fillStyle.parse.hsl-clamp-negative-saturation</h1>
<p class="desc"></p>
<p class="notes">
<p class="output">Actual output:</p>
<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
-<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsl-clamp-2.png" class="output expected" id="expected" alt="">
+<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsl-clamp-negative-saturation.png" class="output expected" id="expected" alt="">
<ul id="d"></ul>
<script>
var t = async_test("");
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-2.png b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.png
index 88fd827985..88fd827985 100644
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-2.png
+++ b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.png
Binary files differ
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-1.html b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-1.html
deleted file mode 100644
index 2acac26e1a..0000000000
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-1.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-1</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
-<body class="show_output">
-
-<h1>2d.fillStyle.parse.hsla-clamp-1</h1>
-<p class="desc"></p>
-
-<p class="notes">
-<p class="output">Actual output:</p>
-<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
-<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-1.png" class="output expected" id="expected" alt="">
-<ul id="d"></ul>
-<script>
-var t = async_test("");
-_addTest(function(canvas, ctx) {
-
- ctx.fillStyle = '#f00';
- ctx.fillStyle = 'hsla(120, 200%, 50%, 1)';
- ctx.fillRect(0, 0, 100, 50);
- _assertPixel(canvas, 50,25, 0,255,0,255);
-
-});
-</script>
-
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-1.png b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-1.png
deleted file mode 100644
index 2733836c99..0000000000
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-1.png
+++ /dev/null
Binary files differ
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-3.html b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-3.html
deleted file mode 100644
index 4bc134aec5..0000000000
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-3.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-3</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
-<body class="show_output">
-
-<h1>2d.fillStyle.parse.hsla-clamp-3</h1>
-<p class="desc"></p>
-
-<p class="notes">
-<p class="output">Actual output:</p>
-<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
-<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-3.png" class="output expected" id="expected" alt="">
-<ul id="d"></ul>
-<script>
-var t = async_test("");
-_addTest(function(canvas, ctx) {
-
- ctx.fillStyle = '#f00';
- ctx.fillStyle = 'hsla(120, 100%, 200%, 1)';
- ctx.fillRect(0, 0, 100, 50);
- _assertPixel(canvas, 50,25, 255,255,255,255);
-
-});
-</script>
-
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-3.png b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-3.png
deleted file mode 100644
index bf48767a88..0000000000
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-3.png
+++ /dev/null
Binary files differ
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-4.html b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-4.html
deleted file mode 100644
index f8b2382755..0000000000
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-4.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-4</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
-<body class="show_output">
-
-<h1>2d.fillStyle.parse.hsla-clamp-4</h1>
-<p class="desc"></p>
-
-<p class="notes">
-<p class="output">Actual output:</p>
-<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
-<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-4.png" class="output expected" id="expected" alt="">
-<ul id="d"></ul>
-<script>
-var t = async_test("");
-_addTest(function(canvas, ctx) {
-
- ctx.fillStyle = '#f00';
- ctx.fillStyle = 'hsla(120, 100%, -200%, 1)';
- ctx.fillRect(0, 0, 100, 50);
- _assertPixel(canvas, 50,25, 0,0,0,255);
-
-});
-</script>
-
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-4.png b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-4.png
deleted file mode 100644
index d638d03386..0000000000
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-4.png
+++ /dev/null
Binary files differ
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-5.png b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-5.png
deleted file mode 100644
index 2733836c99..0000000000
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-5.png
+++ /dev/null
Binary files differ
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-5.html b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html
index 9c5e2258b9..e5dc98d4e3 100644
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-5.html
+++ b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html
@@ -1,19 +1,19 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-5</title>
+<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-alpha-1</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/html/canvas/resources/canvas-tests.js"></script>
<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
<body class="show_output">
-<h1>2d.fillStyle.parse.hsla-clamp-5</h1>
+<h1>2d.fillStyle.parse.hsla-clamp-alpha-1</h1>
<p class="desc"></p>
<p class="notes">
<p class="output">Actual output:</p>
<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
-<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-5.png" class="output expected" id="expected" alt="">
+<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-alpha-1.png" class="output expected" id="expected" alt="">
<ul id="d"></ul>
<script>
var t = async_test("");
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-1.png b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.png
index 2733836c99..2733836c99 100644
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-1.png
+++ b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.png
Binary files differ
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-6.html b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html
index 153515eedd..26139a562e 100644
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-6.html
+++ b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html
@@ -1,19 +1,19 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-6</title>
+<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-alpha-2</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/html/canvas/resources/canvas-tests.js"></script>
<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
<body class="show_output">
-<h1>2d.fillStyle.parse.hsla-clamp-6</h1>
+<h1>2d.fillStyle.parse.hsla-clamp-alpha-2</h1>
<p class="desc"></p>
<p class="notes">
<p class="output">Actual output:</p>
<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
-<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-6.png" class="output expected" id="expected" alt="">
+<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-alpha-2.png" class="output expected" id="expected" alt="">
<ul id="d"></ul>
<script>
var t = async_test("");
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-6.png b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.png
index eeedd0ff05..eeedd0ff05 100644
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-6.png
+++ b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.png
Binary files differ
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-2.html b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html
index 0f32fb5474..2d9b9d3bdf 100644
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-2.html
+++ b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html
@@ -1,19 +1,19 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-2</title>
+<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-negative-saturation</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/html/canvas/resources/canvas-tests.js"></script>
<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
<body class="show_output">
-<h1>2d.fillStyle.parse.hsla-clamp-2</h1>
+<h1>2d.fillStyle.parse.hsla-clamp-negative-saturation</h1>
<p class="desc"></p>
<p class="notes">
<p class="output">Actual output:</p>
<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
-<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-2.png" class="output expected" id="expected" alt="">
+<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-negative-saturation.png" class="output expected" id="expected" alt="">
<ul id="d"></ul>
<script>
var t = async_test("");
diff --git a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-2.png b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.png
index 88fd827985..88fd827985 100644
--- a/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-2.png
+++ b/testing/web-platform/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.png
Binary files differ
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative-expected.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative-expected.html
deleted file mode 100644
index dac31c97f1..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="4 4" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative.html
deleted file mode 100644
index f4c8c1033a..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative.html
+++ /dev/null
@@ -1,20 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [4, 4],
- });
- ctx.fillRect(25, 25, 50, 50);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative-expected.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative-expected.html
deleted file mode 100644
index 88d0cb2de2..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="4 1" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative.html
deleted file mode 100644
index b3efcb8c3e..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative.html
+++ /dev/null
@@ -1,20 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [4, 1],
- });
- ctx.fillRect(25, 25, 50, 50);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative-expected.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative-expected.html
deleted file mode 100644
index 744983d4ae..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="1 4" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative.html
deleted file mode 100644
index d5cc6e0058..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative.html
+++ /dev/null
@@ -1,20 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [1, 4],
- });
- ctx.fillRect(25, 25, 50, 50);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative-expected.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative-expected.html
new file mode 100644
index 0000000000..c1ca0ab46f
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative-expected.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.tentative</title>
+<h1 style="font-size: 20px;">2d.filter.canvasFilterObject.gaussianBlur.tentative</h1>
+<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(5, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>x-only</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur0" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="4 0" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur0)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>mostly-x</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur1" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="4 1" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur1)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>isotropic</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur2" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="4 4" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur2)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>mostly-y</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur3" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="1 4" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur3)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>y-only</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur4" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="0 4" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur4)" />
+ </svg>
+ </div>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative.html
new file mode 100644
index 0000000000..498418c292
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.tentative-expected.html">
+<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.tentative</title>
+<h1 style="font-size: 20px;">2d.filter.canvasFilterObject.gaussianBlur.tentative</h1>
+<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(5, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>x-only</div>
+ <canvas id="canvas0" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [4, 0],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+ </script>
+</span>
+
+<span>
+ <div>mostly-x</div>
+ <canvas id="canvas1" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [4, 1],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+ </script>
+</span>
+
+<span>
+ <div>isotropic</div>
+ <canvas id="canvas2" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas2");
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [4, 4],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+ </script>
+</span>
+
+<span>
+ <div>mostly-y</div>
+ <canvas id="canvas3" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas3");
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [1, 4],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+ </script>
+</span>
+
+<span>
+ <div>y-only</div>
+ <canvas id="canvas4" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas4");
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [0, 4],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative-expected.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative-expected.html
deleted file mode 100644
index e611113e42..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="4 0" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative.html
deleted file mode 100644
index 4e8576fe74..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative.html
+++ /dev/null
@@ -1,20 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [4, 0],
- });
- ctx.fillRect(25, 25, 50, 50);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative-expected.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative-expected.html
deleted file mode 100644
index c6d915cb07..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="0 4" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative.html
deleted file mode 100644
index ec0a2353cf..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative.html
+++ /dev/null
@@ -1,20 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [0, 4],
- });
- ctx.fillRect(25, 25, 50, 50);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur-expected.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur-expected.html
new file mode 100644
index 0000000000..f24e9d0dba
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur-expected.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.filter.layers.gaussianBlur</title>
+<h1 style="font-size: 20px;">2d.filter.layers.gaussianBlur</h1>
+<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(5, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>x-only</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur0" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="4 0" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur0)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>mostly-x</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur1" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="4 1" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur1)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>isotropic</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur2" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="4 4" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur2)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>mostly-y</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur3" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="1 4" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur3)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>y-only</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur4" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="0 4" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur4)" />
+ </svg>
+ </div>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.html
new file mode 100644
index 0000000000..19db679076
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.filter.layers.gaussianBlur-expected.html">
+<title>Canvas test: 2d.filter.layers.gaussianBlur</title>
+<h1 style="font-size: 20px;">2d.filter.layers.gaussianBlur</h1>
+<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(5, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>x-only</div>
+ <canvas id="canvas0" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [4, 0],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>mostly-x</div>
+ <canvas id="canvas1" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [4, 1],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>isotropic</div>
+ <canvas id="canvas2" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas2");
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [4, 4],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>mostly-y</div>
+ <canvas id="canvas3" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas3");
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [1, 4],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>y-only</div>
+ <canvas id="canvas4" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas4");
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [0, 4],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.isotropic-expected.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.isotropic-expected.html
deleted file mode 100644
index 4f93754862..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.isotropic-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.layers.gaussianBlur.isotropic</title>
-<h1>2d.filter.layers.gaussianBlur.isotropic</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="4 4" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.isotropic.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.isotropic.html
deleted file mode 100644
index a2cc098896..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.isotropic.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.layers.gaussianBlur.isotropic-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.isotropic</title>
-<h1>2d.filter.layers.gaussianBlur.isotropic</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [4, 4],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-x-expected.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-x-expected.html
deleted file mode 100644
index 255270c192..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-x-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.layers.gaussianBlur.mostly-x</title>
-<h1>2d.filter.layers.gaussianBlur.mostly-x</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="4 1" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-x.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-x.html
deleted file mode 100644
index 0090e0e71e..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-x.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.layers.gaussianBlur.mostly-x-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.mostly-x</title>
-<h1>2d.filter.layers.gaussianBlur.mostly-x</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [4, 1],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-y-expected.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-y-expected.html
deleted file mode 100644
index 76a46b1533..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-y-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.layers.gaussianBlur.mostly-y</title>
-<h1>2d.filter.layers.gaussianBlur.mostly-y</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="1 4" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-y.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-y.html
deleted file mode 100644
index 5c481cbe25..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.mostly-y.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.layers.gaussianBlur.mostly-y-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.mostly-y</title>
-<h1>2d.filter.layers.gaussianBlur.mostly-y</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [1, 4],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.x-only-expected.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.x-only-expected.html
deleted file mode 100644
index 26741f9847..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.x-only-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.layers.gaussianBlur.x-only</title>
-<h1>2d.filter.layers.gaussianBlur.x-only</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="4 0" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.x-only.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.x-only.html
deleted file mode 100644
index f7940eb921..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.x-only.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.layers.gaussianBlur.x-only-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.x-only</title>
-<h1>2d.filter.layers.gaussianBlur.x-only</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [4, 0],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.y-only-expected.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.y-only-expected.html
deleted file mode 100644
index d00eec6b57..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.y-only-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.layers.gaussianBlur.y-only</title>
-<h1>2d.filter.layers.gaussianBlur.y-only</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="0 4" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.y-only.html b/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.y-only.html
deleted file mode 100644
index 59421a1ff3..0000000000
--- a/testing/web-platform/tests/html/canvas/element/filters/2d.filter.layers.gaussianBlur.y-only.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.layers.gaussianBlur.y-only-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.y-only</title>
-<h1>2d.filter.layers.gaussianBlur.y-only</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [0, 4],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.beginLayer-options.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.beginLayer-options.html
index 658d7e0991..573d300055 100644
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.beginLayer-options.html
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.beginLayer-options.html
@@ -16,8 +16,9 @@
<ul id="d"></ul>
<script>
-var t = async_test("Checks beginLayer works for different option parameter values");
-_addTest(function(canvas, ctx) {
+test(t => {
+ var canvas = document.getElementById('c');
+ var ctx = canvas.getContext('2d');
ctx.beginLayer(); ctx.endLayer();
ctx.beginLayer(null); ctx.endLayer();
@@ -45,6 +46,6 @@ _addTest(function(canvas, ctx) {
ctx.beginLayer({filter: true}); ctx.endLayer();
ctx.beginLayer({filter: false}); ctx.endLayer();
-});
+}, "Checks beginLayer works for different option parameter values");
</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.ctm.getTransform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.ctm.getTransform.html
index 7a69c59527..d669b3c522 100644
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.ctm.getTransform.html
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.ctm.getTransform.html
@@ -16,8 +16,9 @@
<ul id="d"></ul>
<script>
-var t = async_test("Tests getTransform inside layers.");
-_addTest(function(canvas, ctx) {
+test(t => {
+ var canvas = document.getElementById('c');
+ var ctx = canvas.getContext('2d');
ctx.translate(10, 20);
ctx.beginLayer();
@@ -26,6 +27,6 @@ _addTest(function(canvas, ctx) {
assert_array_equals([m.a, m.b, m.c, m.d, m.e, m.f], [2, 0, 0, 3, 10, 20]);
ctx.endLayer();
-});
+}, "Tests getTransform inside layers.");
</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.exceptions-are-no-op.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.exceptions-are-no-op.html
index 7ab2080fca..facffd74e9 100644
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.exceptions-are-no-op.html
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.exceptions-are-no-op.html
@@ -16,8 +16,9 @@
<ul id="d"></ul>
<script>
-var t = async_test("Checks that the context state is left unchanged if beginLayer throws.");
-_addTest(function(canvas, ctx) {
+test(t => {
+ var canvas = document.getElementById('c');
+ var ctx = canvas.getContext('2d');
// Get `beginLayer` to throw while parsing the filter.
assert_throws_js(TypeError,
@@ -26,6 +27,6 @@ _addTest(function(canvas, ctx) {
// `beginLayer` shouldn't have opened the layer, so `endLayer` should throw.
assert_throws_dom("InvalidStateError", () => ctx.endLayer());
-});
+}, "Checks that the context state is left unchanged if beginLayer throws.");
</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha-expected.html
deleted file mode 100644
index 0666e3098a..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha-expected.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.alpha</title>
-<h1>2d.layer.global-states.alpha</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending-expected.html
deleted file mode 100644
index 8a45027588..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending-expected.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.alpha.blending</title>
-<h1>2d.layer.global-states.alpha.blending</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending.html
deleted file mode 100644
index 8e15a2b936..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.alpha.blending-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.blending</title>
-<h1>2d.layer.global-states.alpha.blending</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending.shadow-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending.shadow-expected.html
deleted file mode 100644
index f7b633b35f..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending.shadow-expected.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.alpha.blending.shadow</title>
-<h1>2d.layer.global-states.alpha.blending.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending.shadow.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending.shadow.html
deleted file mode 100644
index c8c6d433bc..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.blending.shadow.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.alpha.blending.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.blending.shadow</title>
-<h1>2d.layer.global-states.alpha.blending.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite-expected.html
deleted file mode 100644
index 951049e638..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite-expected.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.alpha.composite</title>
-<h1>2d.layer.global-states.alpha.composite</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite.html
deleted file mode 100644
index 1ac6a2cbfe..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.alpha.composite-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.composite</title>
-<h1>2d.layer.global-states.alpha.composite</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite.shadow-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite.shadow-expected.html
deleted file mode 100644
index 0ae93871f5..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite.shadow-expected.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.alpha.composite.shadow</title>
-<h1>2d.layer.global-states.alpha.composite.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite.shadow.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite.shadow.html
deleted file mode 100644
index 92b8a0d7a7..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.composite.shadow.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.alpha.composite.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.composite.shadow</title>
-<h1>2d.layer.global-states.alpha.composite.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.html
deleted file mode 100644
index 829796acbf..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.alpha-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha</title>
-<h1>2d.layer.global-states.alpha</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.shadow-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.shadow-expected.html
deleted file mode 100644
index 6f764c5001..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.shadow-expected.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.alpha.shadow</title>
-<h1>2d.layer.global-states.alpha.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.5;
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.shadow.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.shadow.html
deleted file mode 100644
index a325302b3b..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.alpha.shadow.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.alpha.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.shadow</title>
-<h1>2d.layer.global-states.alpha.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.5;
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending-expected.html
deleted file mode 100644
index 33fdf46a28..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending-expected.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.blending</title>
-<h1>2d.layer.global-states.blending</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.html
deleted file mode 100644
index 7d4d9ae4b5..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.blending-expected.html">
-<title>Canvas test: 2d.layer.global-states.blending</title>
-<h1>2d.layer.global-states.blending</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..c56f13f2fd
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.no-transform-expected.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.blending.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.no-transform.html
new file mode 100644
index 0000000000..91decadfe2
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.no-transform.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.blending.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.blending.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..e5f8ba0db4
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.rotation-expected.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.blending.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.rotation.html
new file mode 100644
index 0000000000..d6b28315f2
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.no-shadow.rotation.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.blending.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.blending.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow-expected.html
deleted file mode 100644
index 6f969074f9..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow-expected.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.blending.shadow</title>
-<h1>2d.layer.global-states.blending.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.html
deleted file mode 100644
index 51926d76d8..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.blending.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.blending.shadow</title>
-<h1>2d.layer.global-states.blending.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..debbd430c4
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.no-transform-expected.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.blending.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.no-transform.html
new file mode 100644
index 0000000000..e0b8e45b51
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.no-transform.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.blending.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.blending.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.rotation-expected.html
new file mode 100644
index 0000000000..75a55e591b
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.rotation-expected.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.blending.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.rotation.html
new file mode 100644
index 0000000000..f3891369c9
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.blending.shadow.rotation.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.blending.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.blending.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite-expected.html
deleted file mode 100644
index ed7669c4cf..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite-expected.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.composite</title>
-<h1>2d.layer.global-states.composite</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.html
deleted file mode 100644
index 898d149924..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.composite-expected.html">
-<title>Canvas test: 2d.layer.global-states.composite</title>
-<h1>2d.layer.global-states.composite</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..cf87559582
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.no-transform-expected.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.composite.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.no-transform.html
new file mode 100644
index 0000000000..195905e16b
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.no-transform.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.composite.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.composite.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..2f9bb208fb
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.rotation-expected.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.composite.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.rotation.html
new file mode 100644
index 0000000000..905fd7637b
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.no-shadow.rotation.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.composite.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.composite.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow-expected.html
deleted file mode 100644
index b687c27f47..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow-expected.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.composite.shadow</title>
-<h1>2d.layer.global-states.composite.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.html
deleted file mode 100644
index c563a57b76..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.composite.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.composite.shadow</title>
-<h1>2d.layer.global-states.composite.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..2b4436806a
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.no-transform-expected.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.composite.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.no-transform.html
new file mode 100644
index 0000000000..df008cf12e
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.no-transform.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.composite.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.composite.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.rotation-expected.html
new file mode 100644
index 0000000000..da144975a5
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.rotation-expected.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.composite.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.rotation.html
new file mode 100644
index 0000000000..99e1d95443
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.composite.shadow.rotation.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.composite.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.composite.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..489d432282
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.no-transform-expected.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.copy.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.no-transform.html
new file mode 100644
index 0000000000..1720f20589
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.no-transform.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.copy.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.copy.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..63913ffb05
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.rotation-expected.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.copy.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.rotation.html
new file mode 100644
index 0000000000..87cd91bb12
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.no-shadow.rotation.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.copy.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.copy.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..021581f892
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.no-transform-expected.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.copy.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.no-transform.html
new file mode 100644
index 0000000000..e8d01065c1
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.no-transform.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.copy.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.copy.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.rotation-expected.html
new file mode 100644
index 0000000000..dd9a5c2a00
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.rotation-expected.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.copy.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.rotation.html
new file mode 100644
index 0000000000..5c7fa379f1
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.copy.shadow.rotation.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.copy.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.copy.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha-expected.html
deleted file mode 100644
index f304700feb..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha-expected.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.alpha</title>
-<h1>2d.layer.global-states.filter.alpha</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending-expected.html
deleted file mode 100644
index 7c91ce4229..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending-expected.html
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.alpha.blending</title>
-<h1>2d.layer.global-states.filter.alpha.blending</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending.html
deleted file mode 100644
index 98ea67e9e9..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.alpha.blending-expected.html">
-<meta name=fuzzy content="maxDifference=0-1; totalPixels=0-2453">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.blending</title>
-<h1>2d.layer.global-states.filter.alpha.blending</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending.shadow-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending.shadow-expected.html
deleted file mode 100644
index 62942ffeae..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending.shadow-expected.html
+++ /dev/null
@@ -1,55 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.alpha.blending.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.blending.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending.shadow.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending.shadow.html
deleted file mode 100644
index ccadfb624b..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.blending.shadow.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.alpha.blending.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.blending.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.blending.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite-expected.html
deleted file mode 100644
index 8e0d98648e..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite-expected.html
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.alpha.composite</title>
-<h1>2d.layer.global-states.filter.alpha.composite</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite.html
deleted file mode 100644
index 29041d4933..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.alpha.composite-expected.html">
-<meta name=fuzzy content="maxDifference=0-1; totalPixels=0-5204">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.composite</title>
-<h1>2d.layer.global-states.filter.alpha.composite</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite.shadow-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite.shadow-expected.html
deleted file mode 100644
index a649972546..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite.shadow-expected.html
+++ /dev/null
@@ -1,55 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.alpha.composite.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.composite.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite.shadow.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite.shadow.html
deleted file mode 100644
index b2907f02aa..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.composite.shadow.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.alpha.composite.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.composite.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.composite.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.html
deleted file mode 100644
index 85718cffba..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.alpha-expected.html">
-<meta name=fuzzy content="maxDifference=0-2; totalPixels=0-6766">
-<title>Canvas test: 2d.layer.global-states.filter.alpha</title>
-<h1>2d.layer.global-states.filter.alpha</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.shadow-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.shadow-expected.html
deleted file mode 100644
index 169baee29b..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.shadow-expected.html
+++ /dev/null
@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.alpha.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.5;
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.shadow.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.shadow.html
deleted file mode 100644
index aaeb167ccf..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.alpha.shadow.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.alpha.shadow-expected.html">
-<meta name=fuzzy content="maxDifference=0-2; totalPixels=0-6311">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.5;
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending-expected.html
deleted file mode 100644
index f81dcf72dc..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending-expected.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.blending</title>
-<h1>2d.layer.global-states.filter.blending</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.html
deleted file mode 100644
index 31628812c2..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.blending-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.blending</title>
-<h1>2d.layer.global-states.filter.blending</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..482ab25a85
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform-expected.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.blending.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform.html
new file mode 100644
index 0000000000..188d5ea98b
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.blending.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.blending.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..3af6b863ed
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.rotation-expected.html
@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.blending.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.rotation.html
new file mode 100644
index 0000000000..849a0c997e
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.no-shadow.rotation.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.blending.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.blending.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow-expected.html
deleted file mode 100644
index 91f3725f8e..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow-expected.html
+++ /dev/null
@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.blending.shadow</title>
-<h1>2d.layer.global-states.filter.blending.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.html
deleted file mode 100644
index e54cf06d0d..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.blending.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.blending.shadow</title>
-<h1>2d.layer.global-states.filter.blending.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..d530ef9d19
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.no-transform-expected.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.blending.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.no-transform.html
new file mode 100644
index 0000000000..34ded8fef5
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.no-transform.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.blending.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.blending.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.rotation-expected.html
new file mode 100644
index 0000000000..80705c36fd
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.rotation-expected.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.blending.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.rotation.html
new file mode 100644
index 0000000000..78407dd459
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.blending.shadow.rotation.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.blending.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.blending.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite-expected.html
deleted file mode 100644
index 97e85a1593..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite-expected.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.composite</title>
-<h1>2d.layer.global-states.filter.composite</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.html
deleted file mode 100644
index d7e365422f..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.composite-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.composite</title>
-<h1>2d.layer.global-states.filter.composite</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..2f513bff0a
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform-expected.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.composite.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform.html
new file mode 100644
index 0000000000..e3c36d3c0e
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.composite.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.composite.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..242973300f
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.rotation-expected.html
@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.composite.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.rotation.html
new file mode 100644
index 0000000000..466513864e
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.no-shadow.rotation.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.composite.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.composite.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow-expected.html
deleted file mode 100644
index 4716bb2760..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow-expected.html
+++ /dev/null
@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.composite.shadow</title>
-<h1>2d.layer.global-states.filter.composite.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.html
deleted file mode 100644
index e5c7698634..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.composite.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.composite.shadow</title>
-<h1>2d.layer.global-states.filter.composite.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..c8926e5e15
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.no-transform-expected.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.composite.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.no-transform.html
new file mode 100644
index 0000000000..bc7cfd314e
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.no-transform.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.composite.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.composite.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.rotation-expected.html
new file mode 100644
index 0000000000..e70fe3e92d
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.rotation-expected.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.composite.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.rotation.html
new file mode 100644
index 0000000000..f304e9c8e8
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.composite.shadow.rotation.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.composite.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.composite.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..21aa241aca
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform-expected.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.copy.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform.html
new file mode 100644
index 0000000000..ac5c7303a8
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.copy.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.copy.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..b2b46ee039
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.rotation-expected.html
@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.copy.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.rotation.html
new file mode 100644
index 0000000000..3158a2fff3
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.no-shadow.rotation.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.copy.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.copy.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..d11326d5d6
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.no-transform-expected.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.copy.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.no-transform.html
new file mode 100644
index 0000000000..839ab13add
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.no-transform.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.copy.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.copy.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.rotation-expected.html
new file mode 100644
index 0000000000..3ac098c344
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.rotation-expected.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.copy.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.rotation.html
new file mode 100644
index 0000000000..7c090165f5
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.copy.shadow.rotation.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.copy.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.copy.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..a445ae2c4a
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform-expected.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform.html
new file mode 100644
index 0000000000..d2b2d806c4
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..610c601b1c
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation-expected.html
@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation.html
new file mode 100644
index 0000000000..afc4ebbd40
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.no-composite-op.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..6bd4501584
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform-expected.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform.html
new file mode 100644
index 0000000000..93caac12fc
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.no-composite-op.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation-expected.html
new file mode 100644
index 0000000000..b60a5526d9
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation-expected.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation.html
new file mode 100644
index 0000000000..6849a2f40d
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.no-composite-op.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-global-states-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-global-states-expected.html
deleted file mode 100644
index e56fe0b360..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-global-states-expected.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.no-global-states</title>
-<h1>2d.layer.global-states.filter.no-global-states</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- // No global states.
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-global-states.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-global-states.html
deleted file mode 100644
index 68f4d5004a..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.no-global-states.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.no-global-states-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.no-global-states</title>
-<h1>2d.layer.global-states.filter.no-global-states</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- // No global states.
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.shadow-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.shadow-expected.html
deleted file mode 100644
index 13ba2dd4cd..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.shadow-expected.html
+++ /dev/null
@@ -1,53 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.shadow</title>
-<h1>2d.layer.global-states.filter.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.shadow.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.shadow.html
deleted file mode 100644
index 9efcd9d4f7..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.filter.shadow.html
+++ /dev/null
@@ -1,40 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.shadow</title>
-<h1>2d.layer.global-states.filter.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..d1a799707f
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform-expected.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.no-composite-op.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform.html
new file mode 100644
index 0000000000..f0fd2d19e3
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.no-composite-op.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.no-composite-op.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..cc91a67faf
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation-expected.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.no-composite-op.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation.html
new file mode 100644
index 0000000000..7ab850023e
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.no-composite-op.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.no-composite-op.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..02b239116c
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.no-transform-expected.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.no-composite-op.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.no-transform.html
new file mode 100644
index 0000000000..9da1936c5d
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.no-transform.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.no-composite-op.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.no-composite-op.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.rotation-expected.html
new file mode 100644
index 0000000000..06f6a2dff3
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.rotation-expected.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.no-composite-op.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.rotation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.rotation.html
new file mode 100644
index 0000000000..00ace7c54d
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-composite-op.shadow.rotation.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.no-composite-op.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.no-composite-op.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-global-states-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-global-states-expected.html
deleted file mode 100644
index b91a2ae8b5..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-global-states-expected.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.no-global-states</title>
-<h1>2d.layer.global-states.no-global-states</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- // No global states.
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-global-states.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-global-states.html
deleted file mode 100644
index d561be2341..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.no-global-states.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.no-global-states-expected.html">
-<title>Canvas test: 2d.layer.global-states.no-global-states</title>
-<h1>2d.layer.global-states.no-global-states</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- // No global states.
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.shadow-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.shadow-expected.html
deleted file mode 100644
index 835e9d420a..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.shadow-expected.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.shadow</title>
-<h1>2d.layer.global-states.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.shadow.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.shadow.html
deleted file mode 100644
index 209316164c..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.global-states.shadow.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.shadow</title>
-<h1>2d.layer.global-states.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.globalCompositeOperation-expected.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.globalCompositeOperation-expected.html
new file mode 100644
index 0000000000..02a8915c0b
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.globalCompositeOperation-expected.html
@@ -0,0 +1,910 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.globalCompositeOperation</title>
+<h1 style="font-size: 20px;">2d.layer.globalCompositeOperation</h1>
+<p class="desc">Checks that layers work with all globalCompositeOperation values.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(7, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>source-over</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-over';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>source-in</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>source-atop</div>
+ <canvas id="canvas2" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas2");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-atop';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>destination-over</div>
+ <canvas id="canvas3" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas3");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-over';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>destination-in</div>
+ <canvas id="canvas4" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas4");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-in';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>destination-out</div>
+ <canvas id="canvas5" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas5");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-out';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>destination-atop</div>
+ <canvas id="canvas6" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas6");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-atop';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>lighter</div>
+ <canvas id="canvas7" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas7");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'lighter';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>copy</div>
+ <canvas id="canvas8" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas8");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>xor</div>
+ <canvas id="canvas9" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas9");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'xor';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>multiply</div>
+ <canvas id="canvas10" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas10");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>screen</div>
+ <canvas id="canvas11" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas11");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'screen';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>overlay</div>
+ <canvas id="canvas12" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas12");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'overlay';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>darken</div>
+ <canvas id="canvas13" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas13");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'darken';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>lighten</div>
+ <canvas id="canvas14" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas14");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'lighten';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>color-dodge</div>
+ <canvas id="canvas15" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas15");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color-dodge';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>color-burn</div>
+ <canvas id="canvas16" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas16");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color-burn';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>hard-light</div>
+ <canvas id="canvas17" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas17");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'hard-light';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>soft-light</div>
+ <canvas id="canvas18" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas18");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'soft-light';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>difference</div>
+ <canvas id="canvas19" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas19");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'difference';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>exclusion</div>
+ <canvas id="canvas20" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas20");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'exclusion';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>hue</div>
+ <canvas id="canvas21" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas21");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'hue';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>saturation</div>
+ <canvas id="canvas22" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas22");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'saturation';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>color</div>
+ <canvas id="canvas23" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas23");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>luminosity</div>
+ <canvas id="canvas24" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas24");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'luminosity';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.globalCompositeOperation.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.globalCompositeOperation.html
new file mode 100644
index 0000000000..32809d852c
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.globalCompositeOperation.html
@@ -0,0 +1,861 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.globalCompositeOperation-expected.html">
+<title>Canvas test: 2d.layer.globalCompositeOperation</title>
+<h1 style="font-size: 20px;">2d.layer.globalCompositeOperation</h1>
+<p class="desc">Checks that layers work with all globalCompositeOperation values.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(7, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>source-over</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-over';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>source-in</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>source-atop</div>
+ <canvas id="canvas2" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas2");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-atop';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>destination-over</div>
+ <canvas id="canvas3" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas3");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-over';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>destination-in</div>
+ <canvas id="canvas4" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas4");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-in';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>destination-out</div>
+ <canvas id="canvas5" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas5");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-out';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>destination-atop</div>
+ <canvas id="canvas6" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas6");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-atop';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>lighter</div>
+ <canvas id="canvas7" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas7");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'lighter';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>copy</div>
+ <canvas id="canvas8" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas8");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>xor</div>
+ <canvas id="canvas9" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas9");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'xor';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>multiply</div>
+ <canvas id="canvas10" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas10");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>screen</div>
+ <canvas id="canvas11" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas11");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'screen';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>overlay</div>
+ <canvas id="canvas12" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas12");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'overlay';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>darken</div>
+ <canvas id="canvas13" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas13");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'darken';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>lighten</div>
+ <canvas id="canvas14" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas14");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'lighten';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>color-dodge</div>
+ <canvas id="canvas15" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas15");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color-dodge';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>color-burn</div>
+ <canvas id="canvas16" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas16");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color-burn';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>hard-light</div>
+ <canvas id="canvas17" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas17");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'hard-light';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>soft-light</div>
+ <canvas id="canvas18" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas18");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'soft-light';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>difference</div>
+ <canvas id="canvas19" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas19");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'difference';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>exclusion</div>
+ <canvas id="canvas20" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas20");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'exclusion';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>hue</div>
+ <canvas id="canvas21" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas21");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'hue';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>saturation</div>
+ <canvas id="canvas22" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas22");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'saturation';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>color</div>
+ <canvas id="canvas23" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas23");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+<span>
+ <div>luminosity</div>
+ <canvas id="canvas24" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas24");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'luminosity';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.html
index 74e05e1e48..1544bbcb82 100644
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.html
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.html
@@ -16,8 +16,9 @@
<ul id="d"></ul>
<script>
-var t = async_test("Raises exception on beginLayer() + reset() + endLayer().");
-_addTest(function(canvas, ctx) {
+test(t => {
+ var canvas = document.getElementById('c');
+ var ctx = canvas.getContext('2d');
assert_throws_dom("INVALID_STATE_ERR", function() {
ctx.beginLayer();
@@ -25,6 +26,6 @@ _addTest(function(canvas, ctx) {
ctx.endLayer();
});
-});
+}, "Raises exception on beginLayer() + reset() + endLayer().");
</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-restore.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-restore.html
index 1979cb6c73..3d33fbf7fb 100644
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-restore.html
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-restore.html
@@ -16,14 +16,15 @@
<ul id="d"></ul>
<script>
-var t = async_test("Raises exception on beginLayer() + restore().");
-_addTest(function(canvas, ctx) {
+test(t => {
+ var canvas = document.getElementById('c');
+ var ctx = canvas.getContext('2d');
assert_throws_dom("INVALID_STATE_ERR", function() {
ctx.beginLayer();
ctx.restore();
});
-});
+}, "Raises exception on beginLayer() + restore().");
</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.html
index c635ac75b9..e48f806f32 100644
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.html
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.html
@@ -16,8 +16,9 @@
<ul id="d"></ul>
<script>
-var t = async_test("Raises exception on beginLayer() + save() + endLayer().");
-_addTest(function(canvas, ctx) {
+test(t => {
+ var canvas = document.getElementById('c');
+ var ctx = canvas.getContext('2d');
assert_throws_dom("INVALID_STATE_ERR", function() {
ctx.beginLayer();
@@ -25,6 +26,6 @@ _addTest(function(canvas, ctx) {
ctx.endLayer();
});
-});
+}, "Raises exception on beginLayer() + save() + endLayer().");
</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.endLayer.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.endLayer.html
index c39a352d65..2950de37bc 100644
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.endLayer.html
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.endLayer.html
@@ -16,13 +16,14 @@
<ul id="d"></ul>
<script>
-var t = async_test("Raises exception on lone endLayer calls.");
-_addTest(function(canvas, ctx) {
+test(t => {
+ var canvas = document.getElementById('c');
+ var ctx = canvas.getContext('2d');
assert_throws_dom("INVALID_STATE_ERR", function() {
ctx.endLayer();
});
-});
+}, "Raises exception on lone endLayer calls.");
</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.save-beginLayer-restore.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.save-beginLayer-restore.html
index e2d4d56589..ff21610074 100644
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.save-beginLayer-restore.html
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.save-beginLayer-restore.html
@@ -16,8 +16,9 @@
<ul id="d"></ul>
<script>
-var t = async_test("Raises exception on save() + beginLayer() + restore().");
-_addTest(function(canvas, ctx) {
+test(t => {
+ var canvas = document.getElementById('c');
+ var ctx = canvas.getContext('2d');
assert_throws_dom("INVALID_STATE_ERR", function() {
ctx.save();
@@ -25,6 +26,6 @@ _addTest(function(canvas, ctx) {
ctx.restore();
});
-});
+}, "Raises exception on save() + beginLayer() + restore().");
</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.save-endLayer.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.save-endLayer.html
index f4308e1191..5c6da4b5bc 100644
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.save-endLayer.html
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.invalid-calls.save-endLayer.html
@@ -16,14 +16,15 @@
<ul id="d"></ul>
<script>
-var t = async_test("Raises exception on save() + endLayer().");
-_addTest(function(canvas, ctx) {
+test(t => {
+ var canvas = document.getElementById('c');
+ var ctx = canvas.getContext('2d');
assert_throws_dom("INVALID_STATE_ERR", function() {
ctx.save();
ctx.endLayer();
});
-});
+}, "Raises exception on save() + endLayer().");
</script>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations-with-promises.createImageBitmap.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations-with-promises.createImageBitmap.html
deleted file mode 100644
index f1204aa61b..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations-with-promises.createImageBitmap.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.malformed-operations-with-promises.createImageBitmap</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
-<body class="show_output">
-
-<h1>2d.layer.malformed-operations-with-promises.createImageBitmap</h1>
-<p class="desc">Check that exceptions are thrown for operations that are malformed while layers are open.</p>
-
-
-<p class="output">Actual output:</p>
-<canvas id="c" class="output" width="200" height="200"><p class="fallback">FAIL (fallback content)</p></canvas>
-
-<ul id="d"></ul>
-<script>
-promise_test(async t => {
-
- var canvas = document.getElementById('c');
- var ctx = canvas.getContext('2d');
-
- // Shouldn't throw on its own.
- await createImageBitmap(canvas);
- // Make sure the exception isn't caused by calling the function twice.
- await createImageBitmap(canvas);
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- await promise_rejects_dom(t, 'InvalidStateError', createImageBitmap(canvas));
-
-}, "Check that exceptions are thrown for operations that are malformed while layers are open.");
-</script>
-
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations-with-promises.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations-with-promises.html
new file mode 100644
index 0000000000..8e81bffdfb
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations-with-promises.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.malformed-operations-with-promises</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/html/canvas/resources/canvas-tests.js"></script>
+<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
+
+<script>
+
+promise_test(async t => {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ await createImageBitmap(canvas);
+ // Make sure the exception isn't caused by calling the function twice.
+ await createImageBitmap(canvas);
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ await promise_rejects_dom(t, 'InvalidStateError',
+ createImageBitmap(canvas));
+}, "Throws if createImageBitmap is called while layers are open.");
+
+promise_test(async t => {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ await new Promise(resolve => canvas.toBlob(resolve));
+ // Make sure the exception isn't caused by calling the function twice.
+ await new Promise(resolve => canvas.toBlob(resolve));
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ await promise_rejects_dom(t, 'InvalidStateError',
+ new Promise(resolve => canvas.toBlob(resolve)));
+}, "Throws if toBlob is called while layers are open.");
+
+</script>
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations-with-promises.toBlob.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations-with-promises.toBlob.html
deleted file mode 100644
index 6c69bb3784..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations-with-promises.toBlob.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.malformed-operations-with-promises.toBlob</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
-<body class="show_output">
-
-<h1>2d.layer.malformed-operations-with-promises.toBlob</h1>
-<p class="desc">Check that exceptions are thrown for operations that are malformed while layers are open.</p>
-
-
-<p class="output">Actual output:</p>
-<canvas id="c" class="output" width="200" height="200"><p class="fallback">FAIL (fallback content)</p></canvas>
-
-<ul id="d"></ul>
-<script>
-promise_test(async t => {
-
- var canvas = document.getElementById('c');
- var ctx = canvas.getContext('2d');
-
- // Shouldn't throw on its own.
- await new Promise(resolve => canvas.toBlob(resolve));
- // Make sure the exception isn't caused by calling the function twice.
- await new Promise(resolve => canvas.toBlob(resolve));
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- await promise_rejects_dom(t, 'InvalidStateError', new Promise(resolve => canvas.toBlob(resolve)));
-
-}, "Check that exceptions are thrown for operations that are malformed while layers are open.");
-</script>
-
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.createPattern.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.createPattern.html
deleted file mode 100644
index f927b96524..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.createPattern.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.malformed-operations.createPattern</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
-<body class="show_output">
-
-<h1>2d.layer.malformed-operations.createPattern</h1>
-<p class="desc">Check that exceptions are thrown for operations that are malformed while layers are open.</p>
-
-
-<p class="output">Actual output:</p>
-<canvas id="c" class="output" width="200" height="200"><p class="fallback">FAIL (fallback content)</p></canvas>
-
-<ul id="d"></ul>
-<script>
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-_addTest(function(canvas, ctx) {
-
- // Shouldn't throw on its own.
- ctx.createPattern(canvas, 'repeat');
- // Make sure the exception isn't caused by calling the function twice.
- ctx.createPattern(canvas, 'repeat');
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => ctx.createPattern(canvas, 'repeat'));
-
-});
-</script>
-
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.drawImage.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.drawImage.html
deleted file mode 100644
index 8bcc89d38e..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.drawImage.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.malformed-operations.drawImage</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
-<body class="show_output">
-
-<h1>2d.layer.malformed-operations.drawImage</h1>
-<p class="desc">Check that exceptions are thrown for operations that are malformed while layers are open.</p>
-
-
-<p class="output">Actual output:</p>
-<canvas id="c" class="output" width="200" height="200"><p class="fallback">FAIL (fallback content)</p></canvas>
-
-<ul id="d"></ul>
-<script>
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-_addTest(function(canvas, ctx) {
-
- const canvas2 = new OffscreenCanvas(200, 200);
- const ctx2 = canvas2.getContext('2d');
- // Shouldn't throw on its own.
- ctx2.drawImage(canvas, 0, 0);
- // Make sure the exception isn't caused by calling the function twice.
- ctx2.drawImage(canvas, 0, 0);
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => ctx2.drawImage(canvas, 0, 0));
-
-});
-</script>
-
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.getImageData.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.getImageData.html
deleted file mode 100644
index 5dc3fcc017..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.getImageData.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.malformed-operations.getImageData</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
-<body class="show_output">
-
-<h1>2d.layer.malformed-operations.getImageData</h1>
-<p class="desc">Check that exceptions are thrown for operations that are malformed while layers are open.</p>
-
-
-<p class="output">Actual output:</p>
-<canvas id="c" class="output" width="200" height="200"><p class="fallback">FAIL (fallback content)</p></canvas>
-
-<ul id="d"></ul>
-<script>
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-_addTest(function(canvas, ctx) {
-
- // Shouldn't throw on its own.
- ctx.getImageData(0, 0, 200, 200);
- // Make sure the exception isn't caused by calling the function twice.
- ctx.getImageData(0, 0, 200, 200);
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => ctx.getImageData(0, 0, 200, 200));
-
-});
-</script>
-
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.html
new file mode 100644
index 0000000000..cf6e7a80db
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.malformed-operations</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/html/canvas/resources/canvas-tests.js"></script>
+<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
+
+<script>
+
+test(t => {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ ctx.createPattern(canvas, 'repeat');
+ // Make sure the exception isn't caused by calling the function twice.
+ ctx.createPattern(canvas, 'repeat');
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => ctx.createPattern(canvas, 'repeat'));
+}, "Throws if createPattern is called while layers are open.");
+
+test(t => {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+
+ const canvas2 = new OffscreenCanvas(200, 200);
+ const ctx2 = canvas2.getContext('2d');
+ // Shouldn't throw on its own.
+ ctx2.drawImage(canvas, 0, 0);
+ // Make sure the exception isn't caused by calling the function twice.
+ ctx2.drawImage(canvas, 0, 0);
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => ctx2.drawImage(canvas, 0, 0));
+}, "Throws if drawImage is called while layers are open.");
+
+test(t => {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ ctx.getImageData(0, 0, 200, 200);
+ // Make sure the exception isn't caused by calling the function twice.
+ ctx.getImageData(0, 0, 200, 200);
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => ctx.getImageData(0, 0, 200, 200));
+}, "Throws if getImageData is called while layers are open.");
+
+test(t => {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+
+ const canvas2 = new OffscreenCanvas(200, 200);
+ const ctx2 = canvas2.getContext('2d')
+ const data = ctx2.getImageData(0, 0, 1, 1);
+ // Shouldn't throw on its own.
+ ctx.putImageData(data, 0, 0);
+ // Make sure the exception isn't caused by calling the function twice.
+ ctx.putImageData(data, 0, 0);
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => ctx.putImageData(data, 0, 0));
+}, "Throws if putImageData is called while layers are open.");
+
+test(t => {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ canvas.toDataURL();
+ // Make sure the exception isn't caused by calling the function twice.
+ canvas.toDataURL();
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => canvas.toDataURL());
+}, "Throws if toDataURL is called while layers are open.");
+
+</script>
+</div>
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.putImageData.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.putImageData.html
deleted file mode 100644
index fd4fc262c2..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.putImageData.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.malformed-operations.putImageData</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
-<body class="show_output">
-
-<h1>2d.layer.malformed-operations.putImageData</h1>
-<p class="desc">Check that exceptions are thrown for operations that are malformed while layers are open.</p>
-
-
-<p class="output">Actual output:</p>
-<canvas id="c" class="output" width="200" height="200"><p class="fallback">FAIL (fallback content)</p></canvas>
-
-<ul id="d"></ul>
-<script>
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-_addTest(function(canvas, ctx) {
-
- const canvas2 = new OffscreenCanvas(200, 200);
- const ctx2 = canvas2.getContext('2d')
- const data = ctx2.getImageData(0, 0, 1, 1);
- // Shouldn't throw on its own.
- ctx.putImageData(data, 0, 0);
- // Make sure the exception isn't caused by calling the function twice.
- ctx.putImageData(data, 0, 0);
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => ctx.putImageData(data, 0, 0));
-
-});
-</script>
-
diff --git a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.toDataURL.html b/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.toDataURL.html
deleted file mode 100644
index c9bb4f3875..0000000000
--- a/testing/web-platform/tests/html/canvas/element/layers/2d.layer.malformed-operations.toDataURL.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.malformed-operations.toDataURL</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
-<body class="show_output">
-
-<h1>2d.layer.malformed-operations.toDataURL</h1>
-<p class="desc">Check that exceptions are thrown for operations that are malformed while layers are open.</p>
-
-
-<p class="output">Actual output:</p>
-<canvas id="c" class="output" width="200" height="200"><p class="fallback">FAIL (fallback content)</p></canvas>
-
-<ul id="d"></ul>
-<script>
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-_addTest(function(canvas, ctx) {
-
- // Shouldn't throw on its own.
- canvas.toDataURL();
- // Make sure the exception isn't caused by calling the function twice.
- canvas.toDataURL();
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => canvas.toDataURL());
-
-});
-</script>
-
diff --git a/testing/web-platform/tests/html/canvas/element/manual/filters/svg-filter-lh-rlh-expected.html b/testing/web-platform/tests/html/canvas/element/manual/filters/svg-filter-lh-rlh-expected.html
new file mode 100644
index 0000000000..d183f6940d
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/manual/filters/svg-filter-lh-rlh-expected.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="test-wait">
+<title>HTML Canvas reference: SVG filter using CSS font-relative line-height units</title>
+<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" />
+<style>
+:root {
+ font: 20px Ahem;
+}
+
+div {
+ display: inline-block;
+ width: 100px;
+ height: 100px;
+ background: green;
+}
+
+.r {
+ background: red;
+}
+</style>
+<div><div class="r" style="width: 48px;"></div></div><br>
+<div><div class="r" style="width: 24px;"></div></div>
diff --git a/testing/web-platform/tests/html/canvas/element/manual/filters/svg-filter-lh-rlh.html b/testing/web-platform/tests/html/canvas/element/manual/filters/svg-filter-lh-rlh.html
new file mode 100644
index 0000000000..b61193fb9f
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/manual/filters/svg-filter-lh-rlh.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html class="test-wait">
+<title>HTML Canvas test: SVG filter using CSS font-relative line-height units</title>
+<link rel="match" href="svg-filter-lh-rlh-expected.html"/>
+<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" />
+<style>
+:root {
+ font: 20px Ahem;
+}
+</style>
+<svg width="0" height="0">
+ <!-- https://html.spec.whatwg.org/multipage/canvas.html#working-with-externally-defined-svg-filters -->
+ <use href="./svg-filter.svg#filter-lh" />
+ <use href="./svg-filter.svg#filter-rlh" />
+</svg
+><canvas id="canvas-lh" width="100" height="100"></canvas><br
+><canvas id="canvas-rlh" width="100" height="100"></canvas>
+<script>
+function use_filter(canvas, filter_id) {
+ const ctx = canvas.getContext("2d");
+ ctx.font = "40px Ahem";
+ ctx.filter = `url(./svg-filter.svg#${filter_id})`;
+ ctx.fillStyle = "red";
+ ctx.fillRect(0, 0, 100, 100);
+}
+window.addEventListener("load", async () => {
+ use_filter(document.getElementById("canvas-lh"), "filter-lh");
+ use_filter(document.getElementById("canvas-rlh"), "filter-rlh");
+ document.documentElement.classList.remove('test-wait');
+});
+</script>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/element/manual/filters/svg-filter.svg b/testing/web-platform/tests/html/canvas/element/manual/filters/svg-filter.svg
new file mode 100644
index 0000000000..a783a3eb15
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/manual/filters/svg-filter.svg
@@ -0,0 +1,10 @@
+<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
+ <filter id="filter-lh">
+ <feFlood result="floodFill" x="1lh" y="0" width="100" height="100" flood-color="green" />
+ <feBlend in="SourceGraphic" in2="floodFill" mode="color-dodge" />
+ </filter>
+ <filter id="filter-rlh">
+ <feFlood result="floodFill" x="1rlh" y="0" width="100" height="100" flood-color="green" />
+ <feBlend in="SourceGraphic" in2="floodFill" mode="color-dodge" />
+ </filter>
+</svg>
diff --git a/testing/web-platform/tests/html/canvas/element/text/WEB_FEATURES.yml b/testing/web-platform/tests/html/canvas/element/text/WEB_FEATURES.yml
new file mode 100644
index 0000000000..1d9e4bab82
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/element/text/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: canvas-text-baselines
+ files:
+ - 2d.text.measure.baselines.*
diff --git a/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html
new file mode 100644
index 0000000000..69277677b9
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>OffscreenCanvas test: 2d.fillStyle.parse.hsl-clamp-negative-saturation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/html/canvas/resources/canvas-tests.js"></script>
+
+<h1>2d.fillStyle.parse.hsl-clamp-negative-saturation</h1>
+<p class="desc"></p>
+
+<p class="notes">
+<script>
+var t = async_test("");
+var t_pass = t.done.bind(t);
+var t_fail = t.step_func(function(reason) {
+ throw reason;
+});
+t.step(function() {
+
+ var canvas = new OffscreenCanvas(100, 50);
+ var ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = '#f00';
+ ctx.fillStyle = 'hsl(120, -200%, 49.9%)';
+ ctx.fillRect(0, 0, 100, 50);
+ _assertPixel(canvas, 50,25, 127,127,127,255);
+ t.done();
+
+});
+</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.worker.js b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.worker.js
new file mode 100644
index 0000000000..7f1e91ddb7
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.worker.js
@@ -0,0 +1,25 @@
+// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
+// OffscreenCanvas test in a worker:2d.fillStyle.parse.hsl-clamp-negative-saturation
+// Description:
+// Note:<p class="notes">
+
+importScripts("/resources/testharness.js");
+importScripts("/html/canvas/resources/canvas-tests.js");
+
+var t = async_test("");
+var t_pass = t.done.bind(t);
+var t_fail = t.step_func(function(reason) {
+ throw reason;
+});
+t.step(function() {
+
+ var canvas = new OffscreenCanvas(100, 50);
+ var ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = '#f00';
+ ctx.fillStyle = 'hsl(120, -200%, 49.9%)';
+ ctx.fillRect(0, 0, 100, 50);
+ _assertPixel(canvas, 50,25, 127,127,127,255);
+ t.done();
+});
+done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html
new file mode 100644
index 0000000000..5d73d34c9c
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>OffscreenCanvas test: 2d.fillStyle.parse.hsla-clamp-alpha-1</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/html/canvas/resources/canvas-tests.js"></script>
+
+<h1>2d.fillStyle.parse.hsla-clamp-alpha-1</h1>
+<p class="desc"></p>
+
+<p class="notes">
+<script>
+var t = async_test("");
+var t_pass = t.done.bind(t);
+var t_fail = t.step_func(function(reason) {
+ throw reason;
+});
+t.step(function() {
+
+ var canvas = new OffscreenCanvas(100, 50);
+ var ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = '#f00';
+ ctx.fillStyle = 'hsla(120, 100%, 50%, 2)';
+ ctx.fillRect(0, 0, 100, 50);
+ _assertPixel(canvas, 50,25, 0,255,0,255);
+ t.done();
+
+});
+</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.worker.js b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.worker.js
new file mode 100644
index 0000000000..7acb76d80b
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.worker.js
@@ -0,0 +1,25 @@
+// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
+// OffscreenCanvas test in a worker:2d.fillStyle.parse.hsla-clamp-alpha-1
+// Description:
+// Note:<p class="notes">
+
+importScripts("/resources/testharness.js");
+importScripts("/html/canvas/resources/canvas-tests.js");
+
+var t = async_test("");
+var t_pass = t.done.bind(t);
+var t_fail = t.step_func(function(reason) {
+ throw reason;
+});
+t.step(function() {
+
+ var canvas = new OffscreenCanvas(100, 50);
+ var ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = '#f00';
+ ctx.fillStyle = 'hsla(120, 100%, 50%, 2)';
+ ctx.fillRect(0, 0, 100, 50);
+ _assertPixel(canvas, 50,25, 0,255,0,255);
+ t.done();
+});
+done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html
new file mode 100644
index 0000000000..eaf7a6af89
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>OffscreenCanvas test: 2d.fillStyle.parse.hsla-clamp-alpha-2</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/html/canvas/resources/canvas-tests.js"></script>
+
+<h1>2d.fillStyle.parse.hsla-clamp-alpha-2</h1>
+<p class="desc"></p>
+
+<p class="notes">
+<script>
+var t = async_test("");
+var t_pass = t.done.bind(t);
+var t_fail = t.step_func(function(reason) {
+ throw reason;
+});
+t.step(function() {
+
+ var canvas = new OffscreenCanvas(100, 50);
+ var ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = '#f00';
+ ctx.fillStyle = 'hsla(120, 100%, 0%, -2)';
+ ctx.fillRect(0, 0, 100, 50);
+ _assertPixel(canvas, 50,25, 0,0,0,0);
+ t.done();
+
+});
+</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.worker.js b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.worker.js
new file mode 100644
index 0000000000..540b3ea15f
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.worker.js
@@ -0,0 +1,25 @@
+// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
+// OffscreenCanvas test in a worker:2d.fillStyle.parse.hsla-clamp-alpha-2
+// Description:
+// Note:<p class="notes">
+
+importScripts("/resources/testharness.js");
+importScripts("/html/canvas/resources/canvas-tests.js");
+
+var t = async_test("");
+var t_pass = t.done.bind(t);
+var t_fail = t.step_func(function(reason) {
+ throw reason;
+});
+t.step(function() {
+
+ var canvas = new OffscreenCanvas(100, 50);
+ var ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = '#f00';
+ ctx.fillStyle = 'hsla(120, 100%, 0%, -2)';
+ ctx.fillRect(0, 0, 100, 50);
+ _assertPixel(canvas, 50,25, 0,0,0,0);
+ t.done();
+});
+done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html
new file mode 100644
index 0000000000..04749fb4a7
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>OffscreenCanvas test: 2d.fillStyle.parse.hsla-clamp-negative-saturation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/html/canvas/resources/canvas-tests.js"></script>
+
+<h1>2d.fillStyle.parse.hsla-clamp-negative-saturation</h1>
+<p class="desc"></p>
+
+<p class="notes">
+<script>
+var t = async_test("");
+var t_pass = t.done.bind(t);
+var t_fail = t.step_func(function(reason) {
+ throw reason;
+});
+t.step(function() {
+
+ var canvas = new OffscreenCanvas(100, 50);
+ var ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = '#f00';
+ ctx.fillStyle = 'hsla(120, -200%, 49.9%, 1)';
+ ctx.fillRect(0, 0, 100, 50);
+ _assertPixel(canvas, 50,25, 127,127,127,255);
+ t.done();
+
+});
+</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.worker.js b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.worker.js
new file mode 100644
index 0000000000..f5fe6d4296
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.worker.js
@@ -0,0 +1,25 @@
+// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
+// OffscreenCanvas test in a worker:2d.fillStyle.parse.hsla-clamp-negative-saturation
+// Description:
+// Note:<p class="notes">
+
+importScripts("/resources/testharness.js");
+importScripts("/html/canvas/resources/canvas-tests.js");
+
+var t = async_test("");
+var t_pass = t.done.bind(t);
+var t_fail = t.step_func(function(reason) {
+ throw reason;
+});
+t.step(function() {
+
+ var canvas = new OffscreenCanvas(100, 50);
+ var ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = '#f00';
+ ctx.fillStyle = 'hsla(120, -200%, 49.9%, 1)';
+ ctx.fillRect(0, 0, 100, 50);
+ _assertPixel(canvas, 50,25, 127,127,127,255);
+ t.done();
+});
+done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative-expected.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative-expected.html
deleted file mode 100644
index dac31c97f1..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="4 4" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative.html
deleted file mode 100644
index 801e6fdb8c..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [4, 4],
- });
- ctx.fillRect(25, 25, 50, 50);
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative.w.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative.w.html
deleted file mode 100644
index 6c7c7f7649..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative.w.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.isotropic.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [4, 4],
- });
- ctx.fillRect(25, 25, 50, 50);
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative-expected.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative-expected.html
deleted file mode 100644
index 88d0cb2de2..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="4 1" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative.html
deleted file mode 100644
index ba986d8e7b..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [4, 1],
- });
- ctx.fillRect(25, 25, 50, 50);
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative.w.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative.w.html
deleted file mode 100644
index 86fe086327..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative.w.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.mostly-x.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [4, 1],
- });
- ctx.fillRect(25, 25, 50, 50);
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative-expected.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative-expected.html
deleted file mode 100644
index 744983d4ae..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="1 4" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative.html
deleted file mode 100644
index 0265cfa6c4..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [1, 4],
- });
- ctx.fillRect(25, 25, 50, 50);
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative.w.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative.w.html
deleted file mode 100644
index c22b320857..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative.w.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.mostly-y.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [1, 4],
- });
- ctx.fillRect(25, 25, 50, 50);
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative-expected.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative-expected.html
new file mode 100644
index 0000000000..c1ca0ab46f
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative-expected.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.tentative</title>
+<h1 style="font-size: 20px;">2d.filter.canvasFilterObject.gaussianBlur.tentative</h1>
+<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(5, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>x-only</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur0" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="4 0" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur0)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>mostly-x</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur1" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="4 1" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur1)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>isotropic</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur2" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="4 4" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur2)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>mostly-y</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur3" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="1 4" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur3)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>y-only</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur4" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="0 4" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur4)" />
+ </svg>
+ </div>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative.html
new file mode 100644
index 0000000000..8e7ea3f727
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.tentative-expected.html">
+<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.tentative</title>
+<h1 style="font-size: 20px;">2d.filter.canvasFilterObject.gaussianBlur.tentative</h1>
+<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(5, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>x-only</div>
+ <canvas id="canvas0" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [4, 0],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>mostly-x</div>
+ <canvas id="canvas1" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [4, 1],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>isotropic</div>
+ <canvas id="canvas2" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [4, 4],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+
+ const outputCanvas = document.getElementById("canvas2");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>mostly-y</div>
+ <canvas id="canvas3" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [1, 4],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+
+ const outputCanvas = document.getElementById("canvas3");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>y-only</div>
+ <canvas id="canvas4" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [0, 4],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+
+ const outputCanvas = document.getElementById("canvas4");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative.w.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative.w.html
new file mode 100644
index 0000000000..71626ac46e
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.tentative.w.html
@@ -0,0 +1,194 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.tentative-expected.html">
+<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.tentative</title>
+<h1 style="font-size: 20px;">2d.filter.canvasFilterObject.gaussianBlur.tentative</h1>
+<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
+<script>pending_tests = 5;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(5, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>x-only</div>
+ <canvas id="canvas0" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [4, 0],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>mostly-x</div>
+ <canvas id="canvas1" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [4, 1],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>isotropic</div>
+ <canvas id="canvas2" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker2" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [4, 4],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker2').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas2');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>mostly-y</div>
+ <canvas id="canvas3" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker3" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [1, 4],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker3').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas3');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>y-only</div>
+ <canvas id="canvas4" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker4" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.filter = new CanvasFilter({
+ name: 'gaussianBlur',
+ stdDeviation: [0, 4],
+ });
+ ctx.fillRect(25, 25, 50, 50);
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker4').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas4');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative-expected.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative-expected.html
deleted file mode 100644
index e611113e42..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="4 0" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative.html
deleted file mode 100644
index 3ed8e9ddf9..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [4, 0],
- });
- ctx.fillRect(25, 25, 50, 50);
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative.w.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative.w.html
deleted file mode 100644
index 35cbc1b365..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative.w.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.x-only.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [4, 0],
- });
- ctx.fillRect(25, 25, 50, 50);
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative-expected.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative-expected.html
deleted file mode 100644
index c6d915cb07..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="0 4" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative.html
deleted file mode 100644
index f563ad9d77..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [0, 4],
- });
- ctx.fillRect(25, 25, 50, 50);
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative.w.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative.w.html
deleted file mode 100644
index 171a41caa8..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative.w.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative-expected.html">
-<title>Canvas test: 2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative</title>
-<h1>2d.filter.canvasFilterObject.gaussianBlur.y-only.tentative</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.filter = new CanvasFilter({
- name: 'gaussianBlur',
- stdDeviation: [0, 4],
- });
- ctx.fillRect(25, 25, 50, 50);
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur-expected.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur-expected.html
new file mode 100644
index 0000000000..f24e9d0dba
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur-expected.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.filter.layers.gaussianBlur</title>
+<h1 style="font-size: 20px;">2d.filter.layers.gaussianBlur</h1>
+<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(5, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>x-only</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur0" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="4 0" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur0)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>mostly-x</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur1" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="4 1" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur1)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>isotropic</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur2" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="4 4" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur2)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>mostly-y</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur3" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="1 4" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur3)" />
+ </svg>
+ </div>
+</span>
+
+<span>
+ <div>y-only</div>
+ <div style="width: 100px; height: 100px; outline: 1px solid">
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="100" height="100"
+ color-interpolation-filters="sRGB">
+ <filter id="blur4" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="0 4" />
+ </filter>
+ <rect x="25" y="25" width="50" height="50"
+ fill="teal" filter="url(#blur4)" />
+ </svg>
+ </div>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.html
new file mode 100644
index 0000000000..a51fe32008
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.filter.layers.gaussianBlur-expected.html">
+<title>Canvas test: 2d.filter.layers.gaussianBlur</title>
+<h1 style="font-size: 20px;">2d.filter.layers.gaussianBlur</h1>
+<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(5, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>x-only</div>
+ <canvas id="canvas0" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [4, 0],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>mostly-x</div>
+ <canvas id="canvas1" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [4, 1],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>isotropic</div>
+ <canvas id="canvas2" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [4, 4],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas2");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>mostly-y</div>
+ <canvas id="canvas3" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [1, 4],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas3");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>y-only</div>
+ <canvas id="canvas4" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [0, 4],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas4");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.isotropic-expected.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.isotropic-expected.html
deleted file mode 100644
index 4f93754862..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.isotropic-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.layers.gaussianBlur.isotropic</title>
-<h1>2d.filter.layers.gaussianBlur.isotropic</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="4 4" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.isotropic.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.isotropic.html
deleted file mode 100644
index 50a98df1be..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.isotropic.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.layers.gaussianBlur.isotropic-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.isotropic</title>
-<h1>2d.filter.layers.gaussianBlur.isotropic</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [4, 4],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.isotropic.w.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.isotropic.w.html
deleted file mode 100644
index a68b8e78a2..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.isotropic.w.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.filter.layers.gaussianBlur.isotropic-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.isotropic</title>
-<h1>2d.filter.layers.gaussianBlur.isotropic</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [4, 4],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-x-expected.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-x-expected.html
deleted file mode 100644
index 255270c192..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-x-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.layers.gaussianBlur.mostly-x</title>
-<h1>2d.filter.layers.gaussianBlur.mostly-x</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="4 1" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-x.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-x.html
deleted file mode 100644
index efc634796c..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-x.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.layers.gaussianBlur.mostly-x-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.mostly-x</title>
-<h1>2d.filter.layers.gaussianBlur.mostly-x</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [4, 1],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-x.w.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-x.w.html
deleted file mode 100644
index 7d20d78503..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-x.w.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.filter.layers.gaussianBlur.mostly-x-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.mostly-x</title>
-<h1>2d.filter.layers.gaussianBlur.mostly-x</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [4, 1],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-y-expected.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-y-expected.html
deleted file mode 100644
index 76a46b1533..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-y-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.layers.gaussianBlur.mostly-y</title>
-<h1>2d.filter.layers.gaussianBlur.mostly-y</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="1 4" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-y.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-y.html
deleted file mode 100644
index bdc6e66fe5..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-y.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.layers.gaussianBlur.mostly-y-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.mostly-y</title>
-<h1>2d.filter.layers.gaussianBlur.mostly-y</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [1, 4],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-y.w.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-y.w.html
deleted file mode 100644
index dfd6438b19..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.mostly-y.w.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.filter.layers.gaussianBlur.mostly-y-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.mostly-y</title>
-<h1>2d.filter.layers.gaussianBlur.mostly-y</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [1, 4],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.w.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.w.html
new file mode 100644
index 0000000000..10ea8baa10
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.w.html
@@ -0,0 +1,199 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.filter.layers.gaussianBlur-expected.html">
+<title>Canvas test: 2d.filter.layers.gaussianBlur</title>
+<h1 style="font-size: 20px;">2d.filter.layers.gaussianBlur</h1>
+<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
+<script>pending_tests = 5;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(5, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>x-only</div>
+ <canvas id="canvas0" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [4, 0],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>mostly-x</div>
+ <canvas id="canvas1" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [4, 1],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>isotropic</div>
+ <canvas id="canvas2" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker2" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [4, 4],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker2').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas2');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>mostly-y</div>
+ <canvas id="canvas3" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker3" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [1, 4],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker3').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas3');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>y-only</div>
+ <canvas id="canvas4" width="100" height="100" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker4" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(100, 100);
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = 'teal';
+ ctx.beginLayer({filter: {
+ name: 'gaussianBlur',
+ stdDeviation: [0, 4],
+ }});
+ ctx.fillRect(25, 25, 50, 50);
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker4').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas4');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.x-only-expected.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.x-only-expected.html
deleted file mode 100644
index 26741f9847..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.x-only-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.layers.gaussianBlur.x-only</title>
-<h1>2d.filter.layers.gaussianBlur.x-only</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="4 0" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.x-only.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.x-only.html
deleted file mode 100644
index 0d42acb8b5..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.x-only.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.layers.gaussianBlur.x-only-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.x-only</title>
-<h1>2d.filter.layers.gaussianBlur.x-only</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [4, 0],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.x-only.w.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.x-only.w.html
deleted file mode 100644
index b235c7ad38..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.x-only.w.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.filter.layers.gaussianBlur.x-only-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.x-only</title>
-<h1>2d.filter.layers.gaussianBlur.x-only</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [4, 0],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.y-only-expected.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.y-only-expected.html
deleted file mode 100644
index d00eec6b57..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.y-only-expected.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.filter.layers.gaussianBlur.y-only</title>
-<h1>2d.filter.layers.gaussianBlur.y-only</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-
-<svg xmlns="http://www.w3.org/2000/svg"
- width="100" height="100"
- color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
- <feGaussianBlur stdDeviation="0 4" />
- </filter>
- <rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
-</svg>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.y-only.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.y-only.html
deleted file mode 100644
index c9bc85d699..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.y-only.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.filter.layers.gaussianBlur.y-only-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.y-only</title>
-<h1>2d.filter.layers.gaussianBlur.y-only</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [0, 4],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.y-only.w.html b/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.y-only.w.html
deleted file mode 100644
index 5deb96c255..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/filters/2d.filter.layers.gaussianBlur.y-only.w.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.filter.layers.gaussianBlur.y-only-expected.html">
-<title>Canvas test: 2d.filter.layers.gaussianBlur.y-only</title>
-<h1>2d.filter.layers.gaussianBlur.y-only</h1>
-<p class="desc">Test CanvasFilter() with gaussianBlur.</p>
-<canvas id="canvas" width="100" height="100">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(100, 100);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'teal';
- ctx.beginLayer({filter: {
- name: 'gaussianBlur',
- stdDeviation: [0, 4],
- }});
- ctx.fillRect(25, 25, 50, 50);
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.beginLayer-options.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.beginLayer-options.html
index 4fb042a1d8..354a03a134 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.beginLayer-options.html
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.beginLayer-options.html
@@ -10,13 +10,7 @@
<script>
-var t = async_test("Checks beginLayer works for different option parameter values");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -45,7 +39,6 @@ t.step(function() {
ctx.beginLayer({filter: 1}); ctx.endLayer();
ctx.beginLayer({filter: true}); ctx.endLayer();
ctx.beginLayer({filter: false}); ctx.endLayer();
- t.done();
-});
+}, "Checks beginLayer works for different option parameter values");
</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.beginLayer-options.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.beginLayer-options.worker.js
index cafbc83f3e..492ac3fcea 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.beginLayer-options.worker.js
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.beginLayer-options.worker.js
@@ -6,13 +6,7 @@
importScripts("/resources/testharness.js");
importScripts("/html/canvas/resources/canvas-tests.js");
-var t = async_test("Checks beginLayer works for different option parameter values");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -41,6 +35,5 @@ t.step(function() {
ctx.beginLayer({filter: 1}); ctx.endLayer();
ctx.beginLayer({filter: true}); ctx.endLayer();
ctx.beginLayer({filter: false}); ctx.endLayer();
- t.done();
-});
+}, "Checks beginLayer works for different option parameter values");
done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.ctm.getTransform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.ctm.getTransform.html
index b2306d95ac..919de40d2e 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.ctm.getTransform.html
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.ctm.getTransform.html
@@ -10,13 +10,7 @@
<script>
-var t = async_test("Tests getTransform inside layers.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -26,7 +20,6 @@ t.step(function() {
const m = ctx.getTransform();
assert_array_equals([m.a, m.b, m.c, m.d, m.e, m.f], [2, 0, 0, 3, 10, 20]);
ctx.endLayer();
- t.done();
-});
+}, "Tests getTransform inside layers.");
</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.ctm.getTransform.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.ctm.getTransform.worker.js
index 54b1fee5d0..2d6e6ef8a2 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.ctm.getTransform.worker.js
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.ctm.getTransform.worker.js
@@ -6,13 +6,7 @@
importScripts("/resources/testharness.js");
importScripts("/html/canvas/resources/canvas-tests.js");
-var t = async_test("Tests getTransform inside layers.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -22,6 +16,5 @@ t.step(function() {
const m = ctx.getTransform();
assert_array_equals([m.a, m.b, m.c, m.d, m.e, m.f], [2, 0, 0, 3, 10, 20]);
ctx.endLayer();
- t.done();
-});
+}, "Tests getTransform inside layers.");
done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.exceptions-are-no-op.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.exceptions-are-no-op.html
index a047c539cf..29b316b256 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.exceptions-are-no-op.html
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.exceptions-are-no-op.html
@@ -10,13 +10,7 @@
<script>
-var t = async_test("Checks that the context state is left unchanged if beginLayer throws.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -26,7 +20,6 @@ t.step(function() {
values: 'foo'}}));
// `beginLayer` shouldn't have opened the layer, so `endLayer` should throw.
assert_throws_dom("InvalidStateError", () => ctx.endLayer());
- t.done();
-});
+}, "Checks that the context state is left unchanged if beginLayer throws.");
</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.exceptions-are-no-op.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.exceptions-are-no-op.worker.js
index bd1e376084..6e253b26d5 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.exceptions-are-no-op.worker.js
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.exceptions-are-no-op.worker.js
@@ -6,13 +6,7 @@
importScripts("/resources/testharness.js");
importScripts("/html/canvas/resources/canvas-tests.js");
-var t = async_test("Checks that the context state is left unchanged if beginLayer throws.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -22,6 +16,5 @@ t.step(function() {
values: 'foo'}}));
// `beginLayer` shouldn't have opened the layer, so `endLayer` should throw.
assert_throws_dom("InvalidStateError", () => ctx.endLayer());
- t.done();
-});
+}, "Checks that the context state is left unchanged if beginLayer throws.");
done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha-expected.html
deleted file mode 100644
index 0666e3098a..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha-expected.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.alpha</title>
-<h1>2d.layer.global-states.alpha</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending-expected.html
deleted file mode 100644
index 8a45027588..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending-expected.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.alpha.blending</title>
-<h1>2d.layer.global-states.alpha.blending</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.html
deleted file mode 100644
index 71414b4b37..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.alpha.blending-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.blending</title>
-<h1>2d.layer.global-states.alpha.blending</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.shadow-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.shadow-expected.html
deleted file mode 100644
index f7b633b35f..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.shadow-expected.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.alpha.blending.shadow</title>
-<h1>2d.layer.global-states.alpha.blending.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.shadow.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.shadow.html
deleted file mode 100644
index ed2d2d70af..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.shadow.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.alpha.blending.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.blending.shadow</title>
-<h1>2d.layer.global-states.alpha.blending.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.shadow.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.shadow.w.html
deleted file mode 100644
index 1ff3ad8385..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.shadow.w.html
+++ /dev/null
@@ -1,56 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.alpha.blending.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.blending.shadow</title>
-<h1>2d.layer.global-states.alpha.blending.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.w.html
deleted file mode 100644
index 618480c813..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.blending.w.html
+++ /dev/null
@@ -1,52 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.alpha.blending-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.blending</title>
-<h1>2d.layer.global-states.alpha.blending</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite-expected.html
deleted file mode 100644
index 951049e638..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite-expected.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.alpha.composite</title>
-<h1>2d.layer.global-states.alpha.composite</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.html
deleted file mode 100644
index 94fed5752d..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.alpha.composite-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.composite</title>
-<h1>2d.layer.global-states.alpha.composite</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.shadow-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.shadow-expected.html
deleted file mode 100644
index 0ae93871f5..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.shadow-expected.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.alpha.composite.shadow</title>
-<h1>2d.layer.global-states.alpha.composite.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.shadow.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.shadow.html
deleted file mode 100644
index eb579cdcce..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.shadow.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.alpha.composite.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.composite.shadow</title>
-<h1>2d.layer.global-states.alpha.composite.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.shadow.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.shadow.w.html
deleted file mode 100644
index 60e36f4b97..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.shadow.w.html
+++ /dev/null
@@ -1,56 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.alpha.composite.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.composite.shadow</title>
-<h1>2d.layer.global-states.alpha.composite.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.w.html
deleted file mode 100644
index d7d2b7a21e..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.composite.w.html
+++ /dev/null
@@ -1,52 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.alpha.composite-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.composite</title>
-<h1>2d.layer.global-states.alpha.composite</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.html
deleted file mode 100644
index 63a264e681..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.alpha-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha</title>
-<h1>2d.layer.global-states.alpha</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.shadow-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.shadow-expected.html
deleted file mode 100644
index 6f764c5001..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.shadow-expected.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.alpha.shadow</title>
-<h1>2d.layer.global-states.alpha.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.5;
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.shadow.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.shadow.html
deleted file mode 100644
index 65a66c977d..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.shadow.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.alpha.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.shadow</title>
-<h1>2d.layer.global-states.alpha.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.5;
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.shadow.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.shadow.w.html
deleted file mode 100644
index f404601e3d..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.shadow.w.html
+++ /dev/null
@@ -1,55 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.alpha.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha.shadow</title>
-<h1>2d.layer.global-states.alpha.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.5;
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.w.html
deleted file mode 100644
index 694f31e208..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.alpha.w.html
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.alpha-expected.html">
-<title>Canvas test: 2d.layer.global-states.alpha</title>
-<h1>2d.layer.global-states.alpha</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending-expected.html
deleted file mode 100644
index 33fdf46a28..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending-expected.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.blending</title>
-<h1>2d.layer.global-states.blending</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.html
deleted file mode 100644
index 6a36bb4ba1..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.blending-expected.html">
-<title>Canvas test: 2d.layer.global-states.blending</title>
-<h1>2d.layer.global-states.blending</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..c56f13f2fd
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.no-transform-expected.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.blending.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.no-transform.html
new file mode 100644
index 0000000000..1f8736e0e4
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.no-transform.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.blending.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.blending.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.no-transform.w.html
new file mode 100644
index 0000000000..2bd46eee66
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.no-transform.w.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.blending.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.blending.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..e5f8ba0db4
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.rotation-expected.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.blending.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.rotation.html
new file mode 100644
index 0000000000..0470777988
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.rotation.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.blending.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.blending.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.rotation.w.html
new file mode 100644
index 0000000000..4d33b3a638
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.no-shadow.rotation.w.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.blending.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.blending.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow-expected.html
deleted file mode 100644
index 6f969074f9..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow-expected.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.blending.shadow</title>
-<h1>2d.layer.global-states.blending.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.html
deleted file mode 100644
index 2e91f3d2d1..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.blending.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.blending.shadow</title>
-<h1>2d.layer.global-states.blending.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..debbd430c4
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.no-transform-expected.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.blending.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.no-transform.html
new file mode 100644
index 0000000000..c6f0239c39
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.no-transform.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.blending.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.blending.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.no-transform.w.html
new file mode 100644
index 0000000000..0be246ebfb
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.no-transform.w.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.blending.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.blending.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.rotation-expected.html
new file mode 100644
index 0000000000..75a55e591b
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.rotation-expected.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.blending.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.rotation.html
new file mode 100644
index 0000000000..aac9d86e66
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.rotation.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.blending.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.blending.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.rotation.w.html
new file mode 100644
index 0000000000..86067b7299
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.rotation.w.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.blending.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.blending.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.blending.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.w.html
deleted file mode 100644
index d8e20d0479..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.shadow.w.html
+++ /dev/null
@@ -1,55 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.blending.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.blending.shadow</title>
-<h1>2d.layer.global-states.blending.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.w.html
deleted file mode 100644
index 8964e97713..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.blending.w.html
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.blending-expected.html">
-<title>Canvas test: 2d.layer.global-states.blending</title>
-<h1>2d.layer.global-states.blending</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite-expected.html
deleted file mode 100644
index ed7669c4cf..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite-expected.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.composite</title>
-<h1>2d.layer.global-states.composite</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.html
deleted file mode 100644
index 84fb4b3d95..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.composite-expected.html">
-<title>Canvas test: 2d.layer.global-states.composite</title>
-<h1>2d.layer.global-states.composite</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..cf87559582
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.no-transform-expected.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.composite.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.no-transform.html
new file mode 100644
index 0000000000..69dc916d7d
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.no-transform.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.composite.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.composite.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.no-transform.w.html
new file mode 100644
index 0000000000..aa358b57bc
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.no-transform.w.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.composite.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.composite.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..2f9bb208fb
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.rotation-expected.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.composite.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.rotation.html
new file mode 100644
index 0000000000..d0d08f9835
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.rotation.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.composite.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.composite.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.rotation.w.html
new file mode 100644
index 0000000000..5e2cd0783d
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.no-shadow.rotation.w.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.composite.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.composite.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow-expected.html
deleted file mode 100644
index b687c27f47..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow-expected.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.composite.shadow</title>
-<h1>2d.layer.global-states.composite.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.html
deleted file mode 100644
index 1e3ab4d6a0..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.composite.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.composite.shadow</title>
-<h1>2d.layer.global-states.composite.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..2b4436806a
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.no-transform-expected.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.composite.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.no-transform.html
new file mode 100644
index 0000000000..8b79eba128
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.no-transform.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.composite.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.composite.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.no-transform.w.html
new file mode 100644
index 0000000000..47f2df391f
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.no-transform.w.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.composite.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.composite.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.rotation-expected.html
new file mode 100644
index 0000000000..da144975a5
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.rotation-expected.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.composite.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.rotation.html
new file mode 100644
index 0000000000..632d390986
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.rotation.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.composite.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.composite.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.rotation.w.html
new file mode 100644
index 0000000000..dde0d8bec0
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.rotation.w.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.composite.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.composite.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.composite.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.w.html
deleted file mode 100644
index 7dfb70148b..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.shadow.w.html
+++ /dev/null
@@ -1,55 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.composite.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.composite.shadow</title>
-<h1>2d.layer.global-states.composite.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.w.html
deleted file mode 100644
index b695871fcd..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.composite.w.html
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.composite-expected.html">
-<title>Canvas test: 2d.layer.global-states.composite</title>
-<h1>2d.layer.global-states.composite</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..489d432282
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.no-transform-expected.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.copy.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.no-transform.html
new file mode 100644
index 0000000000..70b659f52b
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.no-transform.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.copy.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.copy.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.no-transform.w.html
new file mode 100644
index 0000000000..1db7f17a94
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.no-transform.w.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.copy.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.copy.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..63913ffb05
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.rotation-expected.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.copy.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.rotation.html
new file mode 100644
index 0000000000..2151535015
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.rotation.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.copy.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.copy.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.rotation.w.html
new file mode 100644
index 0000000000..9c0cac99b9
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.no-shadow.rotation.w.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.copy.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.copy.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..021581f892
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.no-transform-expected.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.copy.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.no-transform.html
new file mode 100644
index 0000000000..42231fa61e
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.no-transform.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.copy.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.copy.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.no-transform.w.html
new file mode 100644
index 0000000000..20dc667ef8
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.no-transform.w.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.copy.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.copy.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.rotation-expected.html
new file mode 100644
index 0000000000..dd9a5c2a00
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.rotation-expected.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.copy.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.rotation.html
new file mode 100644
index 0000000000..77158a9f96
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.rotation.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.copy.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.copy.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.rotation.w.html
new file mode 100644
index 0000000000..def6bc51e2
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.copy.shadow.rotation.w.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.copy.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.copy.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.copy.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha-expected.html
deleted file mode 100644
index f304700feb..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha-expected.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.alpha</title>
-<h1>2d.layer.global-states.filter.alpha</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending-expected.html
deleted file mode 100644
index 7c91ce4229..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending-expected.html
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.alpha.blending</title>
-<h1>2d.layer.global-states.filter.alpha.blending</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.html
deleted file mode 100644
index 0e48cb49f7..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.alpha.blending-expected.html">
-<meta name=fuzzy content="maxDifference=0-1; totalPixels=0-2453">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.blending</title>
-<h1>2d.layer.global-states.filter.alpha.blending</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.shadow-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.shadow-expected.html
deleted file mode 100644
index 62942ffeae..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.shadow-expected.html
+++ /dev/null
@@ -1,55 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.alpha.blending.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.blending.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.shadow.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.shadow.html
deleted file mode 100644
index 62d98d967c..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.shadow.html
+++ /dev/null
@@ -1,45 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.alpha.blending.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.blending.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.blending.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.shadow.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.shadow.w.html
deleted file mode 100644
index e81efd6b8d..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.shadow.w.html
+++ /dev/null
@@ -1,59 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.filter.alpha.blending.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.blending.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.blending.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.w.html
deleted file mode 100644
index 3887ed4485..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.blending.w.html
+++ /dev/null
@@ -1,56 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.filter.alpha.blending-expected.html">
-<meta name=fuzzy content="maxDifference=0-1; totalPixels=0-2453">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.blending</title>
-<h1>2d.layer.global-states.filter.alpha.blending</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite-expected.html
deleted file mode 100644
index 8e0d98648e..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite-expected.html
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.alpha.composite</title>
-<h1>2d.layer.global-states.filter.alpha.composite</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.html
deleted file mode 100644
index 1a9bc8b733..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.alpha.composite-expected.html">
-<meta name=fuzzy content="maxDifference=0-1; totalPixels=0-5204">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.composite</title>
-<h1>2d.layer.global-states.filter.alpha.composite</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.shadow-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.shadow-expected.html
deleted file mode 100644
index a649972546..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.shadow-expected.html
+++ /dev/null
@@ -1,55 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.alpha.composite.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.composite.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.shadow.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.shadow.html
deleted file mode 100644
index d067ff2f5e..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.shadow.html
+++ /dev/null
@@ -1,45 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.alpha.composite.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.composite.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.composite.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.shadow.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.shadow.w.html
deleted file mode 100644
index 39abc78b17..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.shadow.w.html
+++ /dev/null
@@ -1,59 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.filter.alpha.composite.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.composite.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.composite.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.w.html
deleted file mode 100644
index 5c90fe95aa..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.composite.w.html
+++ /dev/null
@@ -1,56 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.filter.alpha.composite-expected.html">
-<meta name=fuzzy content="maxDifference=0-1; totalPixels=0-5204">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.composite</title>
-<h1>2d.layer.global-states.filter.alpha.composite</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.html
deleted file mode 100644
index f64e8925f0..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.alpha-expected.html">
-<meta name=fuzzy content="maxDifference=0-2; totalPixels=0-6766">
-<title>Canvas test: 2d.layer.global-states.filter.alpha</title>
-<h1>2d.layer.global-states.filter.alpha</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.shadow-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.shadow-expected.html
deleted file mode 100644
index 169baee29b..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.shadow-expected.html
+++ /dev/null
@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.alpha.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.5;
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.shadow.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.shadow.html
deleted file mode 100644
index 5e8911ee17..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.shadow.html
+++ /dev/null
@@ -1,45 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.alpha.shadow-expected.html">
-<meta name=fuzzy content="maxDifference=0-2; totalPixels=0-6311">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.5;
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.shadow.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.shadow.w.html
deleted file mode 100644
index b3be7e1ac8..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.shadow.w.html
+++ /dev/null
@@ -1,59 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.filter.alpha.shadow-expected.html">
-<meta name=fuzzy content="maxDifference=0-2; totalPixels=0-6311">
-<title>Canvas test: 2d.layer.global-states.filter.alpha.shadow</title>
-<h1>2d.layer.global-states.filter.alpha.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.5;
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.w.html
deleted file mode 100644
index 21e55f856c..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.alpha.w.html
+++ /dev/null
@@ -1,55 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.filter.alpha-expected.html">
-<meta name=fuzzy content="maxDifference=0-2; totalPixels=0-6766">
-<title>Canvas test: 2d.layer.global-states.filter.alpha</title>
-<h1>2d.layer.global-states.filter.alpha</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalAlpha = 0.6;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending-expected.html
deleted file mode 100644
index f81dcf72dc..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending-expected.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.blending</title>
-<h1>2d.layer.global-states.filter.blending</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.html
deleted file mode 100644
index ce2b046798..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.html
+++ /dev/null
@@ -1,40 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.blending-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.blending</title>
-<h1>2d.layer.global-states.filter.blending</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..482ab25a85
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform-expected.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.blending.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform.html
new file mode 100644
index 0000000000..8acbfa668f
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.blending.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.blending.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform.w.html
new file mode 100644
index 0000000000..93edfabdf3
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.no-transform.w.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.blending.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.blending.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..3af6b863ed
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.rotation-expected.html
@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.blending.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.rotation.html
new file mode 100644
index 0000000000..0a4a8c65d2
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.rotation.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.blending.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.blending.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.rotation.w.html
new file mode 100644
index 0000000000..7caaf1edee
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.no-shadow.rotation.w.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.blending.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.blending.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow-expected.html
deleted file mode 100644
index 91f3725f8e..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow-expected.html
+++ /dev/null
@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.blending.shadow</title>
-<h1>2d.layer.global-states.filter.blending.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.html
deleted file mode 100644
index d0d429bee3..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.html
+++ /dev/null
@@ -1,44 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.blending.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.blending.shadow</title>
-<h1>2d.layer.global-states.filter.blending.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..d530ef9d19
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.no-transform-expected.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.blending.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.no-transform.html
new file mode 100644
index 0000000000..1ee3254324
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.no-transform.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.blending.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.blending.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.no-transform.w.html
new file mode 100644
index 0000000000..3ec656c135
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.no-transform.w.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.blending.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.blending.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.rotation-expected.html
new file mode 100644
index 0000000000..80705c36fd
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.rotation-expected.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.blending.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.rotation.html
new file mode 100644
index 0000000000..62af08d2ac
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.rotation.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.blending.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.blending.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.rotation.w.html
new file mode 100644
index 0000000000..d5693cdcdc
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.rotation.w.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.blending.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.blending.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.blending.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.w.html
deleted file mode 100644
index ce432ea74d..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.shadow.w.html
+++ /dev/null
@@ -1,58 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.filter.blending.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.blending.shadow</title>
-<h1>2d.layer.global-states.filter.blending.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.w.html
deleted file mode 100644
index bb101cdc0b..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.blending.w.html
+++ /dev/null
@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.filter.blending-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.blending</title>
-<h1>2d.layer.global-states.filter.blending</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'multiply';
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite-expected.html
deleted file mode 100644
index 97e85a1593..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite-expected.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.composite</title>
-<h1>2d.layer.global-states.filter.composite</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.html
deleted file mode 100644
index 32052a1150..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.html
+++ /dev/null
@@ -1,40 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.composite-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.composite</title>
-<h1>2d.layer.global-states.filter.composite</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..2f513bff0a
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform-expected.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.composite.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform.html
new file mode 100644
index 0000000000..17394aa4c8
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.composite.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.composite.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform.w.html
new file mode 100644
index 0000000000..386ad43b0f
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.no-transform.w.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.composite.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.composite.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..242973300f
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.rotation-expected.html
@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.composite.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.rotation.html
new file mode 100644
index 0000000000..576283ea73
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.rotation.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.composite.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.composite.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.rotation.w.html
new file mode 100644
index 0000000000..6dd39d77e9
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.no-shadow.rotation.w.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.composite.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.composite.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow-expected.html
deleted file mode 100644
index 4716bb2760..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow-expected.html
+++ /dev/null
@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.composite.shadow</title>
-<h1>2d.layer.global-states.filter.composite.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.html
deleted file mode 100644
index b5e8b9f843..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.html
+++ /dev/null
@@ -1,44 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.composite.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.composite.shadow</title>
-<h1>2d.layer.global-states.filter.composite.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..c8926e5e15
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.no-transform-expected.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.composite.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.no-transform.html
new file mode 100644
index 0000000000..b574a819d6
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.no-transform.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.composite.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.composite.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.no-transform.w.html
new file mode 100644
index 0000000000..96ee1b027b
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.no-transform.w.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.composite.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.composite.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.rotation-expected.html
new file mode 100644
index 0000000000..e70fe3e92d
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.rotation-expected.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.composite.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.rotation.html
new file mode 100644
index 0000000000..b7b4312a35
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.rotation.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.composite.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.composite.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.rotation.w.html
new file mode 100644
index 0000000000..1ae9f00a85
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.rotation.w.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.composite.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.composite.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.composite.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.w.html
deleted file mode 100644
index 894089d88e..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.shadow.w.html
+++ /dev/null
@@ -1,58 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.filter.composite.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.composite.shadow</title>
-<h1>2d.layer.global-states.filter.composite.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.w.html
deleted file mode 100644
index 41ccdaf5c0..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.composite.w.html
+++ /dev/null
@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.filter.composite-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.composite</title>
-<h1>2d.layer.global-states.filter.composite</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.globalCompositeOperation = 'source-in';
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..21aa241aca
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform-expected.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.copy.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform.html
new file mode 100644
index 0000000000..f08ba940af
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.copy.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.copy.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform.w.html
new file mode 100644
index 0000000000..75fc90bae6
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.no-transform.w.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.copy.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.copy.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..b2b46ee039
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.rotation-expected.html
@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.copy.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.rotation.html
new file mode 100644
index 0000000000..3f12cf0c4e
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.rotation.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.copy.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.copy.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.rotation.w.html
new file mode 100644
index 0000000000..0a3ecee669
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.no-shadow.rotation.w.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.copy.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.copy.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..d11326d5d6
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.no-transform-expected.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.copy.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.no-transform.html
new file mode 100644
index 0000000000..b4996deb6f
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.no-transform.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.copy.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.copy.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.no-transform.w.html
new file mode 100644
index 0000000000..9c13a367b3
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.no-transform.w.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.copy.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.copy.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.rotation-expected.html
new file mode 100644
index 0000000000..3ac098c344
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.rotation-expected.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.copy.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.rotation.html
new file mode 100644
index 0000000000..eab5f8312b
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.rotation.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.copy.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.copy.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.rotation.w.html
new file mode 100644
index 0000000000..aebd6cc00d
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.copy.shadow.rotation.w.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.copy.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.copy.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.copy.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..a445ae2c4a
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform-expected.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform.html
new file mode 100644
index 0000000000..f86b0fd37d
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform.w.html
new file mode 100644
index 0000000000..ab0836af94
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform.w.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..610c601b1c
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation-expected.html
@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation.html
new file mode 100644
index 0000000000..5854af853c
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.no-composite-op.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation.w.html
new file mode 100644
index 0000000000..c08f1f9f3c
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.no-shadow.rotation.w.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.no-composite-op.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.no-shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..6bd4501584
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform-expected.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform.html
new file mode 100644
index 0000000000..f1dc725b83
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.no-composite-op.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform.w.html
new file mode 100644
index 0000000000..f191747805
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.no-transform.w.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.no-composite-op.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.shadow.no-transform</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation-expected.html
new file mode 100644
index 0000000000..b60a5526d9
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation-expected.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ const svg = `
+ <svg xmlns="http://www.w3.org/2000/svg"
+ width="90" height="90"
+ color-interpolation-filters="sRGB">
+ <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
+ <feComponentTransfer>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
+ </feComponentTransfer>
+ </filter>
+ <g filter="url(#filter)">
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
+ </g>
+ </svg>`;
+
+ const img = new Image();
+ img.width = 90;
+ img.height = 90;
+ img.onload = () => {
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation.html
new file mode 100644
index 0000000000..76258b326d
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.filter.no-composite-op.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation.w.html
new file mode 100644
index 0000000000..9a10fb569f
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-composite-op.shadow.rotation.w.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.filter.no-composite-op.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.filter.no-composite-op.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.filter.no-composite-op.shadow.rotation</h1>
+<p class="desc">Checks that layers with filters correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer({filter: [
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
+ {name: 'componentTransfer',
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-global-states-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-global-states-expected.html
deleted file mode 100644
index e56fe0b360..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-global-states-expected.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.no-global-states</title>
-<h1>2d.layer.global-states.filter.no-global-states</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- // No global states.
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-global-states.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-global-states.html
deleted file mode 100644
index 3effa3ee9d..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-global-states.html
+++ /dev/null
@@ -1,40 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.no-global-states-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.no-global-states</title>
-<h1>2d.layer.global-states.filter.no-global-states</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- // No global states.
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-global-states.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-global-states.w.html
deleted file mode 100644
index ec744d7ffe..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.no-global-states.w.html
+++ /dev/null
@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.filter.no-global-states-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.no-global-states</title>
-<h1>2d.layer.global-states.filter.no-global-states</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- // No global states.
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.shadow-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.shadow-expected.html
deleted file mode 100644
index 13ba2dd4cd..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.shadow-expected.html
+++ /dev/null
@@ -1,53 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.filter.shadow</title>
-<h1>2d.layer.global-states.filter.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- const svg = `
- <svg xmlns="http://www.w3.org/2000/svg"
- width="200" height="200"
- color-interpolation-filters="sRGB">
- <filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
- <feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
- </feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
- </filter>
- <g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
- </g>
- </svg>`;
-
- const img = new Image();
- img.width = 200;
- img.height = 200;
- img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.drawImage(img, 0, 0);
- };
- img.src = 'data:image/svg+xml;base64,' + btoa(svg);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.shadow.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.shadow.html
deleted file mode 100644
index 7bb0ef5e13..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.shadow.html
+++ /dev/null
@@ -1,43 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.filter.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.shadow</title>
-<h1>2d.layer.global-states.filter.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.shadow.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.shadow.w.html
deleted file mode 100644
index bc9bd48aad..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.filter.shadow.w.html
+++ /dev/null
@@ -1,57 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.filter.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.filter.shadow</title>
-<h1>2d.layer.global-states.filter.shadow</h1>
-<p class="desc">Checks that layers with filters correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
- {name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
-
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform-expected.html
new file mode 100644
index 0000000000..d1a799707f
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform-expected.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.no-composite-op.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform.html
new file mode 100644
index 0000000000..016f78a5d8
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.no-composite-op.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.no-composite-op.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform.w.html
new file mode 100644
index 0000000000..2a450624f1
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.no-transform.w.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.no-composite-op.no-shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.no-composite-op.no-shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.no-shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation-expected.html
new file mode 100644
index 0000000000..cc91a67faf
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation-expected.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.no-composite-op.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation.html
new file mode 100644
index 0000000000..12a1e64b42
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.no-composite-op.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.no-composite-op.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation.w.html
new file mode 100644
index 0000000000..a0f5f76099
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.no-shadow.rotation.w.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.no-composite-op.no-shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.no-composite-op.no-shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.no-shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ // No shadow.
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.no-transform-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.no-transform-expected.html
new file mode 100644
index 0000000000..02b239116c
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.no-transform-expected.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.no-composite-op.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.no-transform.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.no-transform.html
new file mode 100644
index 0000000000..99301fde09
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.no-transform.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.no-composite-op.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.no-composite-op.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.no-transform.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.no-transform.w.html
new file mode 100644
index 0000000000..46375f290c
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.no-transform.w.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.no-composite-op.shadow.no-transform-expected.html">
+<title>Canvas test: 2d.layer.global-states.no-composite-op.shadow.no-transform</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.shadow.no-transform</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ // No transform.
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.rotation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.rotation-expected.html
new file mode 100644
index 0000000000..06f6a2dff3
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.rotation-expected.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.global-states.no-composite-op.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.globalCompositeOperation = 'screen';
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
+ ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx2.fillRect(30, 5, 50, 40);
+
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.rotation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.rotation.html
new file mode 100644
index 0000000000..280cc1cceb
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.rotation.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.global-states.no-composite-op.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.no-composite-op.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.rotation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.rotation.w.html
new file mode 100644
index 0000000000..3192f79afa
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-composite-op.shadow.rotation.w.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.global-states.no-composite-op.shadow.rotation-expected.html">
+<title>Canvas test: 2d.layer.global-states.no-composite-op.shadow.rotation</title>
+<h1 style="font-size: 20px;">2d.layer.global-states.no-composite-op.shadow.rotation</h1>
+<p class="desc">Checks that layers correctly use global render states.</p>
+<script>pending_tests = 2;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(2, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>no-globalAlpha</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ // No globalAlpha.
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>globalAlpha</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
+
+ ctx.globalAlpha = 0.75;
+ // No globalCompositeOperation.
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+ ctx.shadowBlur = 3;
+
+ ctx.beginLayer();
+
+ // Enable compositing in the layer to validate that draw calls in the layer
+ // won't individually composite with the background.
+ ctx.globalCompositeOperation = 'screen';
+
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-global-states-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-global-states-expected.html
deleted file mode 100644
index b91a2ae8b5..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-global-states-expected.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.no-global-states</title>
-<h1>2d.layer.global-states.no-global-states</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- // No global states.
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-global-states.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-global-states.html
deleted file mode 100644
index c8a9815381..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-global-states.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.no-global-states-expected.html">
-<title>Canvas test: 2d.layer.global-states.no-global-states</title>
-<h1>2d.layer.global-states.no-global-states</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- // No global states.
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-global-states.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-global-states.w.html
deleted file mode 100644
index db03a3fd0c..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.no-global-states.w.html
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.no-global-states-expected.html">
-<title>Canvas test: 2d.layer.global-states.no-global-states</title>
-<h1>2d.layer.global-states.no-global-states</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- // No global states.
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.shadow-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.shadow-expected.html
deleted file mode 100644
index 835e9d420a..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.shadow-expected.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>Canvas test: 2d.layer.global-states.shadow</title>
-<h1>2d.layer.global-states.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = document.getElementById("canvas");
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
-
- ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
- ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
-
- ctx.drawImage(canvas2, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.shadow.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.shadow.html
deleted file mode 100644
index ad60e87fb1..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.shadow.html
+++ /dev/null
@@ -1,40 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<link rel="match" href="2d.layer.global-states.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.shadow</title>
-<h1>2d.layer.global-states.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script>
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const outputCanvas = document.getElementById("canvas");
- outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.shadow.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.shadow.w.html
deleted file mode 100644
index 1fc35fd33a..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.global-states.shadow.w.html
+++ /dev/null
@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<html class="reftest-wait">
-<link rel="match" href="2d.layer.global-states.shadow-expected.html">
-<title>Canvas test: 2d.layer.global-states.shadow</title>
-<h1>2d.layer.global-states.shadow</h1>
-<p class="desc">Checks that layers correctly use global render states.</p>
-<canvas id="canvas" width="200" height="200">
- <p class="fallback">FAIL (fallback content)</p>
-</canvas>
-<script id='myWorker' type='text/worker'>
- self.onmessage = function(e) {
- const canvas = new OffscreenCanvas(200, 200);
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
-
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
-
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
-
- ctx.beginLayer();
-
- // Enable compositing in the layer to validate that draw calls in the layer
- // won't individually composite with the background.
- ctx.globalCompositeOperation = 'screen';
-
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
-
- ctx.endLayer();
-
- const bitmap = canvas.transferToImageBitmap();
- self.postMessage(bitmap, bitmap);
- };
-</script>
-<script>
- const blob = new Blob([document.getElementById('myWorker').textContent]);
- const worker = new Worker(URL.createObjectURL(blob));
- worker.addEventListener('message', msg => {
- const outputCtx = document.getElementById("canvas").getContext('2d');
- outputCtx.drawImage(msg.data, 0, 0);
- document.documentElement.classList.remove("reftest-wait");
- });
- worker.postMessage(null);
-</script>
-</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.globalCompositeOperation-expected.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.globalCompositeOperation-expected.html
new file mode 100644
index 0000000000..02a8915c0b
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.globalCompositeOperation-expected.html
@@ -0,0 +1,910 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.globalCompositeOperation</title>
+<h1 style="font-size: 20px;">2d.layer.globalCompositeOperation</h1>
+<p class="desc">Checks that layers work with all globalCompositeOperation values.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(7, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>source-over</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas0");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-over';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>source-in</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas1");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>source-atop</div>
+ <canvas id="canvas2" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas2");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-atop';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>destination-over</div>
+ <canvas id="canvas3" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas3");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-over';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>destination-in</div>
+ <canvas id="canvas4" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas4");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-in';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>destination-out</div>
+ <canvas id="canvas5" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas5");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-out';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>destination-atop</div>
+ <canvas id="canvas6" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas6");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-atop';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>lighter</div>
+ <canvas id="canvas7" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas7");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'lighter';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>copy</div>
+ <canvas id="canvas8" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas8");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>xor</div>
+ <canvas id="canvas9" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas9");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'xor';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>multiply</div>
+ <canvas id="canvas10" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas10");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>screen</div>
+ <canvas id="canvas11" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas11");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'screen';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>overlay</div>
+ <canvas id="canvas12" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas12");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'overlay';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>darken</div>
+ <canvas id="canvas13" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas13");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'darken';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>lighten</div>
+ <canvas id="canvas14" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas14");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'lighten';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>color-dodge</div>
+ <canvas id="canvas15" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas15");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color-dodge';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>color-burn</div>
+ <canvas id="canvas16" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas16");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color-burn';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>hard-light</div>
+ <canvas id="canvas17" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas17");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'hard-light';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>soft-light</div>
+ <canvas id="canvas18" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas18");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'soft-light';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>difference</div>
+ <canvas id="canvas19" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas19");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'difference';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>exclusion</div>
+ <canvas id="canvas20" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas20");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'exclusion';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>hue</div>
+ <canvas id="canvas21" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas21");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'hue';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>saturation</div>
+ <canvas id="canvas22" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas22");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'saturation';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>color</div>
+ <canvas id="canvas23" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas23");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>luminosity</div>
+ <canvas id="canvas24" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas24");
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'luminosity';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.globalCompositeOperation.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.globalCompositeOperation.html
new file mode 100644
index 0000000000..ab9bc4270b
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.globalCompositeOperation.html
@@ -0,0 +1,961 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.globalCompositeOperation-expected.html">
+<title>Canvas test: 2d.layer.globalCompositeOperation</title>
+<h1 style="font-size: 20px;">2d.layer.globalCompositeOperation</h1>
+<p class="desc">Checks that layers work with all globalCompositeOperation values.</p>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(7, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>source-over</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-over';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas0");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>source-in</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas1");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>source-atop</div>
+ <canvas id="canvas2" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-atop';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas2");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>destination-over</div>
+ <canvas id="canvas3" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-over';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas3");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>destination-in</div>
+ <canvas id="canvas4" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-in';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas4");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>destination-out</div>
+ <canvas id="canvas5" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-out';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas5");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>destination-atop</div>
+ <canvas id="canvas6" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-atop';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas6");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>lighter</div>
+ <canvas id="canvas7" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'lighter';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas7");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>copy</div>
+ <canvas id="canvas8" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas8");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>xor</div>
+ <canvas id="canvas9" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'xor';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas9");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>multiply</div>
+ <canvas id="canvas10" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas10");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>screen</div>
+ <canvas id="canvas11" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'screen';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas11");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>overlay</div>
+ <canvas id="canvas12" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'overlay';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas12");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>darken</div>
+ <canvas id="canvas13" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'darken';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas13");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>lighten</div>
+ <canvas id="canvas14" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'lighten';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas14");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>color-dodge</div>
+ <canvas id="canvas15" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color-dodge';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas15");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>color-burn</div>
+ <canvas id="canvas16" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color-burn';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas16");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>hard-light</div>
+ <canvas id="canvas17" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'hard-light';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas17");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>soft-light</div>
+ <canvas id="canvas18" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'soft-light';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas18");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>difference</div>
+ <canvas id="canvas19" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'difference';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas19");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>exclusion</div>
+ <canvas id="canvas20" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'exclusion';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas20");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>hue</div>
+ <canvas id="canvas21" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'hue';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas21");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>saturation</div>
+ <canvas id="canvas22" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'saturation';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas22");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>color</div>
+ <canvas id="canvas23" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas23");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+<span>
+ <div>luminosity</div>
+ <canvas id="canvas24" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'luminosity';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const outputCanvas = document.getElementById("canvas24");
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(canvas, 0, 0);
+ </script>
+</span>
+
+</div>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.globalCompositeOperation.w.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.globalCompositeOperation.w.html
new file mode 100644
index 0000000000..9a403140fc
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.globalCompositeOperation.w.html
@@ -0,0 +1,1314 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.globalCompositeOperation-expected.html">
+<title>Canvas test: 2d.layer.globalCompositeOperation</title>
+<h1 style="font-size: 20px;">2d.layer.globalCompositeOperation</h1>
+<p class="desc">Checks that layers work with all globalCompositeOperation values.</p>
+<script>pending_tests = 25;</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat(7, max-content);
+ font-size: 13px; text-align: center;">
+<span>
+ <div>source-over</div>
+ <canvas id="canvas0" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker0" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-over';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker0').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas0');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>source-in</div>
+ <canvas id="canvas1" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker1" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker1').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas1');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>source-atop</div>
+ <canvas id="canvas2" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker2" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'source-atop';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker2').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas2');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>destination-over</div>
+ <canvas id="canvas3" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker3" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-over';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker3').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas3');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>destination-in</div>
+ <canvas id="canvas4" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker4" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-in';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker4').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas4');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>destination-out</div>
+ <canvas id="canvas5" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker5" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-out';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker5').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas5');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>destination-atop</div>
+ <canvas id="canvas6" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker6" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'destination-atop';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker6').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas6');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>lighter</div>
+ <canvas id="canvas7" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker7" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'lighter';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker7').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas7');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>copy</div>
+ <canvas id="canvas8" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker8" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'copy';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker8').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas8');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>xor</div>
+ <canvas id="canvas9" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker9" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'xor';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker9').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas9');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>multiply</div>
+ <canvas id="canvas10" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker10" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'multiply';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker10').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas10');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>screen</div>
+ <canvas id="canvas11" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker11" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'screen';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker11').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas11');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>overlay</div>
+ <canvas id="canvas12" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker12" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'overlay';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker12').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas12');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>darken</div>
+ <canvas id="canvas13" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker13" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'darken';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker13').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas13');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>lighten</div>
+ <canvas id="canvas14" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker14" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'lighten';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker14').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas14');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>color-dodge</div>
+ <canvas id="canvas15" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker15" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color-dodge';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker15').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas15');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>color-burn</div>
+ <canvas id="canvas16" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker16" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color-burn';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker16').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas16');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>hard-light</div>
+ <canvas id="canvas17" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker17" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'hard-light';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker17').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas17');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>soft-light</div>
+ <canvas id="canvas18" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker18" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'soft-light';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker18').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas18');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>difference</div>
+ <canvas id="canvas19" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker19" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'difference';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker19').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas19');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>exclusion</div>
+ <canvas id="canvas20" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker20" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'exclusion';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker20').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas20');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>hue</div>
+ <canvas id="canvas21" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker21" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'hue';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker21').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas21');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>saturation</div>
+ <canvas id="canvas22" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker22" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'saturation';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker22').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas22');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>color</div>
+ <canvas id="canvas23" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker23" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'color';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker23').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas23');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+<span>
+ <div>luminosity</div>
+ <canvas id="canvas24" width="90" height="90" style="outline: 1px solid">
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker24" type="text/worker">
+ self.onmessage = function(e) {
+ const canvas = new OffscreenCanvas(90, 90);
+ const ctx = canvas.getContext('2d');
+
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = 'luminosity';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker24').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas24');
+ const outputCtx = outputCanvas.getContext('2d');
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.html
index c0b11aa611..e588e48b5f 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.html
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.html
@@ -10,13 +10,7 @@
<script>
-var t = async_test("Raises exception on beginLayer() + reset() + endLayer().");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -25,7 +19,6 @@ t.step(function() {
ctx.reset();
ctx.endLayer();
});
- t.done();
-});
+}, "Raises exception on beginLayer() + reset() + endLayer().");
</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.worker.js
index 1c147d6f34..cab1b9d92d 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.worker.js
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-reset-endLayer.worker.js
@@ -6,13 +6,7 @@
importScripts("/resources/testharness.js");
importScripts("/html/canvas/resources/canvas-tests.js");
-var t = async_test("Raises exception on beginLayer() + reset() + endLayer().");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -21,6 +15,5 @@ t.step(function() {
ctx.reset();
ctx.endLayer();
});
- t.done();
-});
+}, "Raises exception on beginLayer() + reset() + endLayer().");
done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-restore.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-restore.html
index 022532b329..30a981f75a 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-restore.html
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-restore.html
@@ -10,13 +10,7 @@
<script>
-var t = async_test("Raises exception on beginLayer() + restore().");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -24,7 +18,6 @@ t.step(function() {
ctx.beginLayer();
ctx.restore();
});
- t.done();
-});
+}, "Raises exception on beginLayer() + restore().");
</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-restore.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-restore.worker.js
index 1aa86635e6..287f8eb004 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-restore.worker.js
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-restore.worker.js
@@ -6,13 +6,7 @@
importScripts("/resources/testharness.js");
importScripts("/html/canvas/resources/canvas-tests.js");
-var t = async_test("Raises exception on beginLayer() + restore().");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -20,6 +14,5 @@ t.step(function() {
ctx.beginLayer();
ctx.restore();
});
- t.done();
-});
+}, "Raises exception on beginLayer() + restore().");
done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.html
index 26dd0eee4b..04992b115e 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.html
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.html
@@ -10,13 +10,7 @@
<script>
-var t = async_test("Raises exception on beginLayer() + save() + endLayer().");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -25,7 +19,6 @@ t.step(function() {
ctx.save();
ctx.endLayer();
});
- t.done();
-});
+}, "Raises exception on beginLayer() + save() + endLayer().");
</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.worker.js
index 613921c67c..402bf5e0fd 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.worker.js
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.beginLayer-save-endLayer.worker.js
@@ -6,13 +6,7 @@
importScripts("/resources/testharness.js");
importScripts("/html/canvas/resources/canvas-tests.js");
-var t = async_test("Raises exception on beginLayer() + save() + endLayer().");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -21,6 +15,5 @@ t.step(function() {
ctx.save();
ctx.endLayer();
});
- t.done();
-});
+}, "Raises exception on beginLayer() + save() + endLayer().");
done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.endLayer.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.endLayer.html
index 440249980a..5b7f8a851e 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.endLayer.html
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.endLayer.html
@@ -10,20 +10,13 @@
<script>
-var t = async_test("Raises exception on lone endLayer calls.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
assert_throws_dom("INVALID_STATE_ERR", function() {
ctx.endLayer();
});
- t.done();
-});
+}, "Raises exception on lone endLayer calls.");
</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.endLayer.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.endLayer.worker.js
index b2ba231b9c..2229aa9628 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.endLayer.worker.js
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.endLayer.worker.js
@@ -6,19 +6,12 @@
importScripts("/resources/testharness.js");
importScripts("/html/canvas/resources/canvas-tests.js");
-var t = async_test("Raises exception on lone endLayer calls.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
assert_throws_dom("INVALID_STATE_ERR", function() {
ctx.endLayer();
});
- t.done();
-});
+}, "Raises exception on lone endLayer calls.");
done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-beginLayer-restore.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-beginLayer-restore.html
index c2b09961ac..2a6c9b1ccb 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-beginLayer-restore.html
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-beginLayer-restore.html
@@ -10,13 +10,7 @@
<script>
-var t = async_test("Raises exception on save() + beginLayer() + restore().");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -25,7 +19,6 @@ t.step(function() {
ctx.beginLayer();
ctx.restore();
});
- t.done();
-});
+}, "Raises exception on save() + beginLayer() + restore().");
</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-beginLayer-restore.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-beginLayer-restore.worker.js
index d155379fcb..711280a6de 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-beginLayer-restore.worker.js
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-beginLayer-restore.worker.js
@@ -6,13 +6,7 @@
importScripts("/resources/testharness.js");
importScripts("/html/canvas/resources/canvas-tests.js");
-var t = async_test("Raises exception on save() + beginLayer() + restore().");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -21,6 +15,5 @@ t.step(function() {
ctx.beginLayer();
ctx.restore();
});
- t.done();
-});
+}, "Raises exception on save() + beginLayer() + restore().");
done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-endLayer.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-endLayer.html
index 01b62d1e85..32ca134663 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-endLayer.html
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-endLayer.html
@@ -10,13 +10,7 @@
<script>
-var t = async_test("Raises exception on save() + endLayer().");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -24,7 +18,6 @@ t.step(function() {
ctx.save();
ctx.endLayer();
});
- t.done();
-});
+}, "Raises exception on save() + endLayer().");
</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-endLayer.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-endLayer.worker.js
index 353c1b00cd..af3667e50a 100644
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-endLayer.worker.js
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.invalid-calls.save-endLayer.worker.js
@@ -6,13 +6,7 @@
importScripts("/resources/testharness.js");
importScripts("/html/canvas/resources/canvas-tests.js");
-var t = async_test("Raises exception on save() + endLayer().");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
+test(t => {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
@@ -20,6 +14,5 @@ t.step(function() {
ctx.save();
ctx.endLayer();
});
- t.done();
-});
+}, "Raises exception on save() + endLayer().");
done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.convertToBlob.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.convertToBlob.html
deleted file mode 100644
index 0b3854c31d..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.convertToBlob.html
+++ /dev/null
@@ -1,27 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>OffscreenCanvas test: 2d.layer.malformed-operations-with-promises.convertToBlob</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-
-<h1>2d.layer.malformed-operations-with-promises.convertToBlob</h1>
-<p class="desc">Check that exceptions are thrown for operations that are malformed while layers are open.</p>
-
-
-<script>
-promise_test(async t => {
-
- var canvas = new OffscreenCanvas(200, 200);
- var ctx = canvas.getContext('2d');
-
- // Shouldn't throw on its own.
- await canvas.convertToBlob();
- // Make sure the exception isn't caused by calling the function twice.
- await canvas.convertToBlob();
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- await promise_rejects_dom(t, 'InvalidStateError', canvas.convertToBlob());
-
-}, "Check that exceptions are thrown for operations that are malformed while layers are open.");
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.convertToBlob.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.convertToBlob.worker.js
deleted file mode 100644
index 8361e19108..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.convertToBlob.worker.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
-// OffscreenCanvas test in a worker:2d.layer.malformed-operations-with-promises.convertToBlob
-// Description:Check that exceptions are thrown for operations that are malformed while layers are open.
-// Note:
-
-importScripts("/resources/testharness.js");
-importScripts("/html/canvas/resources/canvas-tests.js");
-
-promise_test(async t => {
- var canvas = new OffscreenCanvas(200, 200);
- var ctx = canvas.getContext('2d');
-
- // Shouldn't throw on its own.
- await canvas.convertToBlob();
- // Make sure the exception isn't caused by calling the function twice.
- await canvas.convertToBlob();
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- await promise_rejects_dom(t, 'InvalidStateError', canvas.convertToBlob());
-}, "Check that exceptions are thrown for operations that are malformed while layers are open.");
-done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.createImageBitmap.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.createImageBitmap.html
deleted file mode 100644
index 085554d9f5..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.createImageBitmap.html
+++ /dev/null
@@ -1,27 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>OffscreenCanvas test: 2d.layer.malformed-operations-with-promises.createImageBitmap</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-
-<h1>2d.layer.malformed-operations-with-promises.createImageBitmap</h1>
-<p class="desc">Check that exceptions are thrown for operations that are malformed while layers are open.</p>
-
-
-<script>
-promise_test(async t => {
-
- var canvas = new OffscreenCanvas(200, 200);
- var ctx = canvas.getContext('2d');
-
- // Shouldn't throw on its own.
- await createImageBitmap(canvas);
- // Make sure the exception isn't caused by calling the function twice.
- await createImageBitmap(canvas);
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- await promise_rejects_dom(t, 'InvalidStateError', createImageBitmap(canvas));
-
-}, "Check that exceptions are thrown for operations that are malformed while layers are open.");
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.createImageBitmap.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.createImageBitmap.worker.js
deleted file mode 100644
index d64f693864..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.createImageBitmap.worker.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
-// OffscreenCanvas test in a worker:2d.layer.malformed-operations-with-promises.createImageBitmap
-// Description:Check that exceptions are thrown for operations that are malformed while layers are open.
-// Note:
-
-importScripts("/resources/testharness.js");
-importScripts("/html/canvas/resources/canvas-tests.js");
-
-promise_test(async t => {
- var canvas = new OffscreenCanvas(200, 200);
- var ctx = canvas.getContext('2d');
-
- // Shouldn't throw on its own.
- await createImageBitmap(canvas);
- // Make sure the exception isn't caused by calling the function twice.
- await createImageBitmap(canvas);
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- await promise_rejects_dom(t, 'InvalidStateError', createImageBitmap(canvas));
-}, "Check that exceptions are thrown for operations that are malformed while layers are open.");
-done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.html
new file mode 100644
index 0000000000..7b8f9b0943
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>OffscreenCanvas test: 2d.layer.malformed-operations-with-promises</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/html/canvas/resources/canvas-tests.js"></script>
+
+<script>
+
+promise_test(async t => {
+ const canvas = new OffscreenCanvas(200, 200);
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ await canvas.convertToBlob();
+ // Make sure the exception isn't caused by calling the function twice.
+ await canvas.convertToBlob();
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ await promise_rejects_dom(t, 'InvalidStateError',
+ canvas.convertToBlob());
+}, "Throws if convertToBlob is called while layers are open.");
+
+promise_test(async t => {
+ const canvas = new OffscreenCanvas(200, 200);
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ await createImageBitmap(canvas);
+ // Make sure the exception isn't caused by calling the function twice.
+ await createImageBitmap(canvas);
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ await promise_rejects_dom(t, 'InvalidStateError',
+ createImageBitmap(canvas));
+}, "Throws if createImageBitmap is called while layers are open.");
+
+</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.worker.js
new file mode 100644
index 0000000000..693901b648
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations-with-promises.worker.js
@@ -0,0 +1,37 @@
+// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
+// OffscreenCanvas test in a worker:2d.layer.malformed-operations-with-promises
+// Description:
+// Note:
+
+importScripts("/resources/testharness.js");
+importScripts("/html/canvas/resources/canvas-tests.js");
+
+promise_test(async t => {
+ const canvas = new OffscreenCanvas(200, 200);
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ await canvas.convertToBlob();
+ // Make sure the exception isn't caused by calling the function twice.
+ await canvas.convertToBlob();
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ await promise_rejects_dom(t, 'InvalidStateError',
+ canvas.convertToBlob());
+}, "Throws if convertToBlob is called while layers are open.");
+
+promise_test(async t => {
+ const canvas = new OffscreenCanvas(200, 200);
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ await createImageBitmap(canvas);
+ // Make sure the exception isn't caused by calling the function twice.
+ await createImageBitmap(canvas);
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ await promise_rejects_dom(t, 'InvalidStateError',
+ createImageBitmap(canvas));
+}, "Throws if createImageBitmap is called while layers are open.");
+
+done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.createPattern.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.createPattern.html
deleted file mode 100644
index a206e64ceb..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.createPattern.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>OffscreenCanvas test: 2d.layer.malformed-operations.createPattern</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-
-<h1>2d.layer.malformed-operations.createPattern</h1>
-<p class="desc">Check that exceptions are thrown for operations that are malformed while layers are open.</p>
-
-
-<script>
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
- var canvas = new OffscreenCanvas(200, 200);
- var ctx = canvas.getContext('2d');
-
- // Shouldn't throw on its own.
- ctx.createPattern(canvas, 'repeat');
- // Make sure the exception isn't caused by calling the function twice.
- ctx.createPattern(canvas, 'repeat');
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => ctx.createPattern(canvas, 'repeat'));
- t.done();
-
-});
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.createPattern.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.createPattern.worker.js
deleted file mode 100644
index bcb42cba87..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.createPattern.worker.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
-// OffscreenCanvas test in a worker:2d.layer.malformed-operations.createPattern
-// Description:Check that exceptions are thrown for operations that are malformed while layers are open.
-// Note:
-
-importScripts("/resources/testharness.js");
-importScripts("/html/canvas/resources/canvas-tests.js");
-
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
- var canvas = new OffscreenCanvas(200, 200);
- var ctx = canvas.getContext('2d');
-
- // Shouldn't throw on its own.
- ctx.createPattern(canvas, 'repeat');
- // Make sure the exception isn't caused by calling the function twice.
- ctx.createPattern(canvas, 'repeat');
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => ctx.createPattern(canvas, 'repeat'));
- t.done();
-});
-done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.drawImage.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.drawImage.html
deleted file mode 100644
index e6a9872100..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.drawImage.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>OffscreenCanvas test: 2d.layer.malformed-operations.drawImage</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-
-<h1>2d.layer.malformed-operations.drawImage</h1>
-<p class="desc">Check that exceptions are thrown for operations that are malformed while layers are open.</p>
-
-
-<script>
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
- var canvas = new OffscreenCanvas(200, 200);
- var ctx = canvas.getContext('2d');
-
- const canvas2 = new OffscreenCanvas(200, 200);
- const ctx2 = canvas2.getContext('2d');
- // Shouldn't throw on its own.
- ctx2.drawImage(canvas, 0, 0);
- // Make sure the exception isn't caused by calling the function twice.
- ctx2.drawImage(canvas, 0, 0);
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => ctx2.drawImage(canvas, 0, 0));
- t.done();
-
-});
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.drawImage.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.drawImage.worker.js
deleted file mode 100644
index b66cdee62e..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.drawImage.worker.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
-// OffscreenCanvas test in a worker:2d.layer.malformed-operations.drawImage
-// Description:Check that exceptions are thrown for operations that are malformed while layers are open.
-// Note:
-
-importScripts("/resources/testharness.js");
-importScripts("/html/canvas/resources/canvas-tests.js");
-
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
- var canvas = new OffscreenCanvas(200, 200);
- var ctx = canvas.getContext('2d');
-
- const canvas2 = new OffscreenCanvas(200, 200);
- const ctx2 = canvas2.getContext('2d');
- // Shouldn't throw on its own.
- ctx2.drawImage(canvas, 0, 0);
- // Make sure the exception isn't caused by calling the function twice.
- ctx2.drawImage(canvas, 0, 0);
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => ctx2.drawImage(canvas, 0, 0));
- t.done();
-});
-done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.getImageData.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.getImageData.html
deleted file mode 100644
index 87bc8c6ede..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.getImageData.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>OffscreenCanvas test: 2d.layer.malformed-operations.getImageData</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-
-<h1>2d.layer.malformed-operations.getImageData</h1>
-<p class="desc">Check that exceptions are thrown for operations that are malformed while layers are open.</p>
-
-
-<script>
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
- var canvas = new OffscreenCanvas(200, 200);
- var ctx = canvas.getContext('2d');
-
- // Shouldn't throw on its own.
- ctx.getImageData(0, 0, 200, 200);
- // Make sure the exception isn't caused by calling the function twice.
- ctx.getImageData(0, 0, 200, 200);
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => ctx.getImageData(0, 0, 200, 200));
- t.done();
-
-});
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.getImageData.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.getImageData.worker.js
deleted file mode 100644
index 6a1a16fccb..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.getImageData.worker.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
-// OffscreenCanvas test in a worker:2d.layer.malformed-operations.getImageData
-// Description:Check that exceptions are thrown for operations that are malformed while layers are open.
-// Note:
-
-importScripts("/resources/testharness.js");
-importScripts("/html/canvas/resources/canvas-tests.js");
-
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
- var canvas = new OffscreenCanvas(200, 200);
- var ctx = canvas.getContext('2d');
-
- // Shouldn't throw on its own.
- ctx.getImageData(0, 0, 200, 200);
- // Make sure the exception isn't caused by calling the function twice.
- ctx.getImageData(0, 0, 200, 200);
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => ctx.getImageData(0, 0, 200, 200));
- t.done();
-});
-done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.html
new file mode 100644
index 0000000000..a810665faf
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>OffscreenCanvas test: 2d.layer.malformed-operations</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/html/canvas/resources/canvas-tests.js"></script>
+
+<script>
+
+test(t => {
+ const canvas = new OffscreenCanvas(200, 200);
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ ctx.createPattern(canvas, 'repeat');
+ // Make sure the exception isn't caused by calling the function twice.
+ ctx.createPattern(canvas, 'repeat');
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => ctx.createPattern(canvas, 'repeat'));
+}, "Throws if createPattern is called while layers are open.");
+
+test(t => {
+ const canvas = new OffscreenCanvas(200, 200);
+ const ctx = canvas.getContext('2d');
+
+ const canvas2 = new OffscreenCanvas(200, 200);
+ const ctx2 = canvas2.getContext('2d');
+ // Shouldn't throw on its own.
+ ctx2.drawImage(canvas, 0, 0);
+ // Make sure the exception isn't caused by calling the function twice.
+ ctx2.drawImage(canvas, 0, 0);
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => ctx2.drawImage(canvas, 0, 0));
+}, "Throws if drawImage is called while layers are open.");
+
+test(t => {
+ const canvas = new OffscreenCanvas(200, 200);
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ ctx.getImageData(0, 0, 200, 200);
+ // Make sure the exception isn't caused by calling the function twice.
+ ctx.getImageData(0, 0, 200, 200);
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => ctx.getImageData(0, 0, 200, 200));
+}, "Throws if getImageData is called while layers are open.");
+
+test(t => {
+ const canvas = new OffscreenCanvas(200, 200);
+ const ctx = canvas.getContext('2d');
+
+ const canvas2 = new OffscreenCanvas(200, 200);
+ const ctx2 = canvas2.getContext('2d')
+ const data = ctx2.getImageData(0, 0, 1, 1);
+ // Shouldn't throw on its own.
+ ctx.putImageData(data, 0, 0);
+ // Make sure the exception isn't caused by calling the function twice.
+ ctx.putImageData(data, 0, 0);
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => ctx.putImageData(data, 0, 0));
+}, "Throws if putImageData is called while layers are open.");
+
+test(t => {
+ const canvas = new OffscreenCanvas(200, 200);
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ canvas.transferToImageBitmap();
+ // Make sure the exception isn't caused by calling the function twice.
+ canvas.transferToImageBitmap();
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => canvas.transferToImageBitmap());
+}, "Throws if transferToImageBitmap is called while layers are open.");
+
+</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.putImageData.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.putImageData.html
deleted file mode 100644
index e8059076bb..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.putImageData.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>OffscreenCanvas test: 2d.layer.malformed-operations.putImageData</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-
-<h1>2d.layer.malformed-operations.putImageData</h1>
-<p class="desc">Check that exceptions are thrown for operations that are malformed while layers are open.</p>
-
-
-<script>
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
- var canvas = new OffscreenCanvas(200, 200);
- var ctx = canvas.getContext('2d');
-
- const canvas2 = new OffscreenCanvas(200, 200);
- const ctx2 = canvas2.getContext('2d')
- const data = ctx2.getImageData(0, 0, 1, 1);
- // Shouldn't throw on its own.
- ctx.putImageData(data, 0, 0);
- // Make sure the exception isn't caused by calling the function twice.
- ctx.putImageData(data, 0, 0);
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => ctx.putImageData(data, 0, 0));
- t.done();
-
-});
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.putImageData.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.putImageData.worker.js
deleted file mode 100644
index 8810c3a73c..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.putImageData.worker.js
+++ /dev/null
@@ -1,32 +0,0 @@
-// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
-// OffscreenCanvas test in a worker:2d.layer.malformed-operations.putImageData
-// Description:Check that exceptions are thrown for operations that are malformed while layers are open.
-// Note:
-
-importScripts("/resources/testharness.js");
-importScripts("/html/canvas/resources/canvas-tests.js");
-
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
- var canvas = new OffscreenCanvas(200, 200);
- var ctx = canvas.getContext('2d');
-
- const canvas2 = new OffscreenCanvas(200, 200);
- const ctx2 = canvas2.getContext('2d')
- const data = ctx2.getImageData(0, 0, 1, 1);
- // Shouldn't throw on its own.
- ctx.putImageData(data, 0, 0);
- // Make sure the exception isn't caused by calling the function twice.
- ctx.putImageData(data, 0, 0);
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => ctx.putImageData(data, 0, 0));
- t.done();
-});
-done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.transferToImageBitmap.html b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.transferToImageBitmap.html
deleted file mode 100644
index 79c216421f..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.transferToImageBitmap.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
-<title>OffscreenCanvas test: 2d.layer.malformed-operations.transferToImageBitmap</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/html/canvas/resources/canvas-tests.js"></script>
-
-<h1>2d.layer.malformed-operations.transferToImageBitmap</h1>
-<p class="desc">Check that exceptions are thrown for operations that are malformed while layers are open.</p>
-
-
-<script>
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
- var canvas = new OffscreenCanvas(200, 200);
- var ctx = canvas.getContext('2d');
-
- // Shouldn't throw on its own.
- canvas.transferToImageBitmap();
- // Make sure the exception isn't caused by calling the function twice.
- canvas.transferToImageBitmap();
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => canvas.transferToImageBitmap());
- t.done();
-
-});
-</script>
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.transferToImageBitmap.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.transferToImageBitmap.worker.js
deleted file mode 100644
index be0b43665a..0000000000
--- a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.transferToImageBitmap.worker.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
-// OffscreenCanvas test in a worker:2d.layer.malformed-operations.transferToImageBitmap
-// Description:Check that exceptions are thrown for operations that are malformed while layers are open.
-// Note:
-
-importScripts("/resources/testharness.js");
-importScripts("/html/canvas/resources/canvas-tests.js");
-
-var t = async_test("Check that exceptions are thrown for operations that are malformed while layers are open.");
-var t_pass = t.done.bind(t);
-var t_fail = t.step_func(function(reason) {
- throw reason;
-});
-t.step(function() {
-
- var canvas = new OffscreenCanvas(200, 200);
- var ctx = canvas.getContext('2d');
-
- // Shouldn't throw on its own.
- canvas.transferToImageBitmap();
- // Make sure the exception isn't caused by calling the function twice.
- canvas.transferToImageBitmap();
- // Calling again inside a layer should throw.
- ctx.beginLayer();
- assert_throws_dom("InvalidStateError",
- () => canvas.transferToImageBitmap());
- t.done();
-});
-done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.worker.js b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.worker.js
new file mode 100644
index 0000000000..5851fcfbc6
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/layers/2d.layer.malformed-operations.worker.js
@@ -0,0 +1,84 @@
+// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
+// OffscreenCanvas test in a worker:2d.layer.malformed-operations
+// Description:
+// Note:
+
+importScripts("/resources/testharness.js");
+importScripts("/html/canvas/resources/canvas-tests.js");
+
+test(t => {
+ const canvas = new OffscreenCanvas(200, 200);
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ ctx.createPattern(canvas, 'repeat');
+ // Make sure the exception isn't caused by calling the function twice.
+ ctx.createPattern(canvas, 'repeat');
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => ctx.createPattern(canvas, 'repeat'));
+}, "Throws if createPattern is called while layers are open.");
+
+test(t => {
+ const canvas = new OffscreenCanvas(200, 200);
+ const ctx = canvas.getContext('2d');
+
+ const canvas2 = new OffscreenCanvas(200, 200);
+ const ctx2 = canvas2.getContext('2d');
+ // Shouldn't throw on its own.
+ ctx2.drawImage(canvas, 0, 0);
+ // Make sure the exception isn't caused by calling the function twice.
+ ctx2.drawImage(canvas, 0, 0);
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => ctx2.drawImage(canvas, 0, 0));
+}, "Throws if drawImage is called while layers are open.");
+
+test(t => {
+ const canvas = new OffscreenCanvas(200, 200);
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ ctx.getImageData(0, 0, 200, 200);
+ // Make sure the exception isn't caused by calling the function twice.
+ ctx.getImageData(0, 0, 200, 200);
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => ctx.getImageData(0, 0, 200, 200));
+}, "Throws if getImageData is called while layers are open.");
+
+test(t => {
+ const canvas = new OffscreenCanvas(200, 200);
+ const ctx = canvas.getContext('2d');
+
+ const canvas2 = new OffscreenCanvas(200, 200);
+ const ctx2 = canvas2.getContext('2d')
+ const data = ctx2.getImageData(0, 0, 1, 1);
+ // Shouldn't throw on its own.
+ ctx.putImageData(data, 0, 0);
+ // Make sure the exception isn't caused by calling the function twice.
+ ctx.putImageData(data, 0, 0);
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => ctx.putImageData(data, 0, 0));
+}, "Throws if putImageData is called while layers are open.");
+
+test(t => {
+ const canvas = new OffscreenCanvas(200, 200);
+ const ctx = canvas.getContext('2d');
+
+ // Shouldn't throw on its own.
+ canvas.transferToImageBitmap();
+ // Make sure the exception isn't caused by calling the function twice.
+ canvas.transferToImageBitmap();
+ // Calling again inside a layer should throw.
+ ctx.beginLayer();
+ assert_throws_dom("InvalidStateError",
+ () => canvas.transferToImageBitmap());
+}, "Throws if transferToImageBitmap is called while layers are open.");
+
+done();
diff --git a/testing/web-platform/tests/html/canvas/offscreen/text/WEB_FEATURES.yml b/testing/web-platform/tests/html/canvas/offscreen/text/WEB_FEATURES.yml
new file mode 100644
index 0000000000..1d9e4bab82
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/offscreen/text/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: canvas-text-baselines
+ files:
+ - 2d.text.measure.baselines.*
diff --git a/testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py b/testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py
index 57077f6057..415090a14a 100644
--- a/testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py
+++ b/testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py
@@ -345,17 +345,43 @@ class _Variant():
'desc': '',
'size': [100, 50],
'variant_names': [],
+ 'grid_variant_names': [],
+ 'images': [],
+ 'svgimages': [],
}
params.update(test)
return _Variant(params)
- def _get_variant_name(self, jinja_env: jinja2.Environment) -> str:
- name = self.params['name']
+ def merge_params(self, params: _TestParams) -> '_Variant':
+ """Returns a new `_Variant` that merges `self.params` and `params`."""
+ new_params = {}
+ new_params.update(self.params)
+ new_params.update(params)
+ return _Variant(new_params)
+
+ def with_grid_variant_name(self, name: str) -> '_Variant':
+ """Addend a variant name to include in the grid element label."""
+ self._params.update({
+ 'variant_names': (self.params['variant_names'] + [name]),
+ 'grid_variant_names': (self.params['grid_variant_names'] + [name]),
+ })
+ return self
+
+ def with_file_variant_name(self, name: str) -> '_Variant':
+ """Addend a variant name to include in the generated file name."""
+ self._params.update({
+ 'variant_names': (self.params['variant_names'] + [name]),
+ })
if self.params.get('append_variants_to_name', True):
- name = '.'.join([name] + self.params['variant_names'])
+ self._params['name'] = self.params['name'] + '.' + name
+ return self
+
+ def _render_param(self, jinja_env: jinja2.Environment,
+ param_name: str) -> str:
+ """Get the specified variant parameter and render it with Jinja."""
+ value = self.params[param_name]
+ return jinja_env.from_string(value).render(self.params)
- name = jinja_env.from_string(name).render(self.params)
- return name
def _get_file_name(self) -> str:
file_name = self.params['name']
@@ -389,9 +415,12 @@ class _Variant():
return _TemplateType.HTML_REFERENCE
return _TemplateType.TESTHARNESS
- def finalize_params(self, jinja_env: jinja2.Environment) -> None:
+ def finalize_params(self, jinja_env: jinja2.Environment,
+ variant_id: int) -> None:
"""Finalize this variant by adding computed param fields."""
- self._params['name'] = self._get_variant_name(jinja_env)
+ self._params['id'] = variant_id
+ self._params['name'] = self._render_param(jinja_env, 'name')
+ self._params['desc'] = self._render_param(jinja_env, 'desc')
self._params['file_name'] = self._get_file_name()
self._params['canvas_types'] = self._get_canvas_types()
self._params['template_type'] = self._get_template_type()
@@ -461,103 +490,282 @@ class _Variant():
self._params['expected_img'] = f'{name}.png'
+
+class _VariantGrid:
+
+ def __init__(self, variants: List[_Variant], grid_width: int) -> None:
+ self._variants = variants
+ self._grid_width = grid_width
+
+ self._file_name = None
+ self._canvas_types = None
+ self._template_type = None
+ self._params = None
+
+ @property
+ def variants(self) -> List[_Variant]:
+ """Read only getter for the list of variant in this grid."""
+ return self._variants
+
+ @property
+ def file_name(self):
+ """File name to which this grid will be written."""
+ if self._file_name is None:
+ self._file_name = self._unique_param('file_name')
+ return self._file_name
+
+ @property
+ def canvas_types(self) -> FrozenSet[_CanvasType]:
+ """Returns the set of all _CanvasType used by this grid's variants."""
+ if self._canvas_types is None:
+ self._canvas_types = self._param_set('canvas_types')
+ return self._canvas_types
+
+ @property
+ def template_type(self) -> _TemplateType:
+ """Returns the type of Jinja template needed to render this grid."""
+ if self._template_type is None:
+ self._template_type = self._unique_param('template_type')
+ return self._template_type
+
+ @property
+ def params(self) -> _TestParams:
+ """Returns this grid's param dict, used to render Jinja templates."""
+ if self._params is None:
+ if len(self.variants) == 1:
+ self._params = dict(self.variants[0].params)
+ else:
+ self._params = self._get_grid_params()
+ return self._params
+
+ def finalize(self, jinja_env: jinja2.Environment):
+ """Finalize this grid's variants, adding computed params fields."""
+ for variant_id, variant in enumerate(self.variants):
+ variant.finalize_params(jinja_env, variant_id)
+
+ def add_dimension(self, variants: Mapping[str,
+ _TestParams]) -> '_VariantGrid':
+ """Adds a variant dimension to this variant grid.
+
+ If the grid currently has N variants, adding a dimension with M variants
+ results in a grid containing N*M variants. Of course, we can't display
+ more than 2 dimensions on a 2D screen, so adding dimensions beyond 2
+ repeats all previous dimensions down vertically, with the grid width
+ set to the number of variants of the first dimension (unless overridden
+ by setting `grid_width`). For instance, a 3D variant space with
+ dimensions 3 x 2 x 2 will result in this layout:
+ 000 100 200
+ 010 110 210
+
+ 001 101 201
+ 011 111 211
+ """
+ new_variants = [
+ old_variant.merge_params(params or {}).with_grid_variant_name(name)
+ for name, params in variants.items()
+ for old_variant in self.variants
+ ]
+ # The first dimension dictates the grid-width, unless it was specified
+ # beforehand via the test params.
+ new_grid_width = (self._grid_width
+ if self._grid_width > 1 else len(variants))
+ return _VariantGrid(variants=new_variants, grid_width=new_grid_width)
+
+ def merge_params(self, name: str, params: _TestParams) -> '_VariantGrid':
+ """Merges the specified `params` into every variant of this grid."""
+ return _VariantGrid(variants=[
+ variant.merge_params(params).with_file_variant_name(name)
+ for variant in self.variants
+ ],
+ grid_width=self._grid_width)
+
+ def _variants_for_canvas_type(
+ self, canvas_type: _CanvasType) -> List[_TestParams]:
+ """Returns the variants of this grid enabled for `canvas_type`."""
+ return [
+ v.params for v in self.variants
+ if canvas_type in v.params['canvas_types']
+ ]
+
+ def _unique_param(self, name: str) -> Any:
+ """Returns the value of the `name` param for this grid.
+
+ All the variants in this grid must agree on the same value for this
+ parameter, or else an exception is thrown."""
+ values = {variant.params.get(name) for variant in self.variants}
+ if len(values) != 1:
+ raise InvalidTestDefinitionError(
+ 'All variants in a variant grid must use the same value '
+ f'for property "{name}". Got these values: {values}. '
+ 'Consider specifying the property outside of grid '
+ 'variants dimensions (in the base test definition or in a '
+ 'file variant dimension)')
+ return values.pop()
+
+ def _param_set(self, name: str):
+ """Returns the set of all values this grid has for the `name` param.
+
+ The `name` parameter of each variant is expected to be a sequence.
+ These are all accumulated in a set and returned."""
+ return frozenset(sum([list(v.params[name]) for v in self.variants],
+ []))
+
+ def _get_grid_params(self) -> _TestParams:
+ """Returns the params dict needed to render this grid with Jinja."""
+ filter_variant = self._variants_for_canvas_type
+ grid_params = {
+ 'element_variants': filter_variant(_CanvasType.HTML_CANVAS),
+ 'offscreen_variants': filter_variant(_CanvasType.OFFSCREEN_CANVAS),
+ 'worker_variants': filter_variant(_CanvasType.WORKER),
+ 'grid_width': self._grid_width,
+ 'name': self._unique_param('name'),
+ 'test_type': self._unique_param('test_type'),
+ 'fuzzy': self._unique_param('fuzzy'),
+ 'timeout': self._unique_param('timeout'),
+ 'notes': self._unique_param('notes'),
+ 'images': self._param_set('images'),
+ 'svgimages': self._param_set('svgimages'),
+ }
+ if self.template_type in (_TemplateType.REFERENCE,
+ _TemplateType.HTML_REFERENCE):
+ grid_params['desc'] = self._unique_param('desc')
+ return grid_params
+
def _write_reference_test(self, jinja_env: jinja2.Environment,
output_files: _OutputPaths):
+ grid = '_grid' if len(self.variants) > 1 else ''
+
+ # If variants don't all use the same offscreen and worker canvas types,
+ # the offscreen and worker grids won't be identical. The worker test
+ # therefore can't reuse the offscreen reference file.
+ offscreen_types = {_CanvasType.OFFSCREEN_CANVAS, _CanvasType.WORKER}
+ needs_worker_reference = len({
+ variant.params['canvas_types'] & offscreen_types
+ for variant in self.variants
+ }) != 1
+
params = dict(self.params)
- if _CanvasType.HTML_CANVAS in params['canvas_types']:
- _render(jinja_env, 'reftest_element.html', params,
+ params['reference_file'] = f'{params["name"]}-expected.html'
+ if _CanvasType.HTML_CANVAS in self.canvas_types:
+ _render(jinja_env, f'reftest_element{grid}.html', params,
f'{output_files.element}.html')
- if _CanvasType.OFFSCREEN_CANVAS in params['canvas_types']:
- _render(jinja_env, 'reftest_offscreen.html', params,
+ if _CanvasType.OFFSCREEN_CANVAS in self.canvas_types:
+ _render(jinja_env, f'reftest_offscreen{grid}.html', params,
f'{output_files.offscreen}.html')
- if _CanvasType.WORKER in params['canvas_types']:
- _render(jinja_env, 'reftest_worker.html', params,
+ if _CanvasType.WORKER in self.canvas_types:
+ if needs_worker_reference:
+ params['reference_file'] = f'{params["name"]}.w-expected.html'
+ _render(jinja_env, f'reftest_worker{grid}.html', params,
f'{output_files.offscreen}.w.html')
params['is_test_reference'] = True
- is_html_ref = params['template_type'] == _TemplateType.HTML_REFERENCE
- ref_template = 'reftest.html' if is_html_ref else 'reftest_element.html'
- if _CanvasType.HTML_CANVAS in params['canvas_types']:
- _render(jinja_env, ref_template, params,
+ is_html_ref = self.template_type == _TemplateType.HTML_REFERENCE
+ ref_template_name = (f'reftest{grid}.html'
+ if is_html_ref else f'reftest_element{grid}.html')
+
+ if _CanvasType.HTML_CANVAS in self.canvas_types:
+ _render(jinja_env, ref_template_name, params,
f'{output_files.element}-expected.html')
- if {_CanvasType.OFFSCREEN_CANVAS, _CanvasType.WORKER
- } & params['canvas_types']:
- _render(jinja_env, ref_template, params,
+
+ if self.canvas_types & offscreen_types:
+ # We use the same template for all reference files, so we need to
+ # assign the variant definition to the variable expected by the
+ # template.
+ params['element_variants'] = params.get('offscreen_variants')
+ _render(jinja_env, ref_template_name, params,
f'{output_files.offscreen}-expected.html')
+ if needs_worker_reference:
+ params['element_variants'] = params.get('worker_variants')
+ _render(jinja_env, ref_template_name, params,
+ f'{output_files.offscreen}.w-expected.html')
def _write_testharness_test(self, jinja_env: jinja2.Environment,
output_files: _OutputPaths):
+ grid = '_grid' if len(self.variants) > 1 else ''
+
# Create test cases for canvas and offscreencanvas.
- if _CanvasType.HTML_CANVAS in self.params['canvas_types']:
- _render(jinja_env, 'testharness_element.html', self.params,
+ if _CanvasType.HTML_CANVAS in self.canvas_types:
+ _render(jinja_env, f'testharness_element{grid}.html', self.params,
f'{output_files.element}.html')
-
- if _CanvasType.OFFSCREEN_CANVAS in self.params['canvas_types']:
- _render(jinja_env, 'testharness_offscreen.html', self.params,
- f'{output_files.offscreen}.html')
-
- if _CanvasType.WORKER in self.params['canvas_types']:
- _render(jinja_env, 'testharness_worker.js', self.params,
+ if _CanvasType.OFFSCREEN_CANVAS in self.canvas_types:
+ _render(jinja_env, f'testharness_offscreen{grid}.html',
+ self.params, f'{output_files.offscreen}.html')
+ if _CanvasType.WORKER in self.canvas_types:
+ _render(jinja_env, f'testharness_worker{grid}.js', self.params,
f'{output_files.offscreen}.worker.js')
def generate_test(self, jinja_env: jinja2.Environment,
output_dirs: _OutputPaths) -> None:
"""Generate the test files to the specified output dirs."""
- output_files = output_dirs.sub_path(self.params['file_name'])
+ output_files = output_dirs.sub_path(self.file_name)
- if self.params['template_type'] in (_TemplateType.REFERENCE,
- _TemplateType.HTML_REFERENCE):
+ if self.template_type in (_TemplateType.REFERENCE,
+ _TemplateType.HTML_REFERENCE):
self._write_reference_test(jinja_env, output_files)
else:
self._write_testharness_test(jinja_env, output_files)
-def _recursive_expand_variant_matrix(original_test: _TestParams,
- variant_matrix: List[_TestParams],
- current_selection: List[Tuple[str, Any]],
- test_variants: List[_Variant]):
- if len(current_selection) == len(variant_matrix):
- # Selection for each variant is done, so add a new test to test_list.
- test = dict(original_test)
- variant_name_list = []
- for variant_name, variant_params in current_selection:
- test.update(variant_params)
- variant_name_list.append(variant_name)
- # Expose variant names as a list so they can be used from the yaml
- # files, which helps with better naming of tests.
- test.update({'variant_names': variant_name_list})
- test_variants.append(_Variant.create_with_defaults(test))
- else:
- # Continue the recursion with each possible selection for the current
- # variant.
- variant = variant_matrix[len(current_selection)]
- for variant_options in variant.items():
- current_selection.append(variant_options)
- _recursive_expand_variant_matrix(original_test, variant_matrix,
- current_selection, test_variants)
- current_selection.pop()
-
-
-def _get_variants(test: _TestParams) -> List[_Variant]:
- current_selection = []
- test_variants = []
- variants = test.get('variants', [])
+class _VariantLayout(str, enum.Enum):
+ SINGLE_FILE = 'single_file'
+ MULTI_FILES = 'multi_files'
+
+
+@dataclasses.dataclass
+class _VariantDimension:
+ variants: Mapping[str, _TestParams]
+ layout: _VariantLayout
+
+
+def _get_variant_dimensions(params: _TestParams) -> List[_VariantDimension]:
+ variants = params.get('variants', [])
if not isinstance(variants, list):
raise InvalidTestDefinitionError(
textwrap.dedent("""
Variants must be specified as a list of variant dimensions, e.g.:
- variants:
- - dimension1-variant1:
- param: ...
- dimension1-variant2:
- param: ...
- - dimension2-variant1:
- param: ...
- dimension2-variant2:
- param: ..."""))
- _recursive_expand_variant_matrix(test, variants, current_selection,
- test_variants)
- return test_variants
+ variants:
+ - dimension1-variant1:
+ param: ...
+ dimension1-variant2:
+ param: ...
+ - dimension2-variant1:
+ param: ...
+ dimension2-variant2:
+ param: ..."""))
+
+ variants_layout = params.get('variants_layout',
+ [_VariantLayout.MULTI_FILES] * len(variants))
+ if len(variants) != len(variants_layout):
+ raise InvalidTestDefinitionError(
+ 'variants and variants_layout must be lists of the same size')
+ invalid_layouts = [
+ l for l in variants_layout if l not in list(_VariantLayout)
+ ]
+ if invalid_layouts:
+ raise InvalidTestDefinitionError('Invalid variants_layout: ' +
+ ', '.join(invalid_layouts) +
+ '. Valid layouts are: ' +
+ ', '.join(_VariantLayout))
+
+ return [
+ _VariantDimension(z[0], z[1]) for z in zip(variants, variants_layout)
+ ]
+
+
+def _get_variant_grids(test: Mapping[str, Any]) -> List[_VariantGrid]:
+ base_variant = _Variant.create_with_defaults(test)
+ grid_width = base_variant.params.get('grid_width', 1)
+ grids = [_VariantGrid([base_variant], grid_width=grid_width)]
+ for dimension in _get_variant_dimensions(test):
+ variants = dimension.variants
+ if dimension.layout == _VariantLayout.MULTI_FILES:
+ grids = [
+ grid.merge_params(name, params)
+ for name, params in variants.items() for grid in grids
+ ]
+ else:
+ grids = [grid.add_dimension(variants) for grid in grids]
+ return grids
def _check_uniqueness(tested: DefaultDict[str, Set[_CanvasType]], name: str,
@@ -619,21 +827,30 @@ def generate_test_files(name_to_dir_file: str) -> None:
except FileExistsError:
pass # Ignore if it already exists,
- used_tests = collections.defaultdict(set)
+ used_filenames = collections.defaultdict(set)
+ used_variants = collections.defaultdict(set)
for test in tests:
print(test['name'])
- for variant in _get_variants(test):
- variant.finalize_params(jinja_env)
- if test['name'] != variant.params['name']:
- print(f' {variant.params["name"]}')
+ for grid in _get_variant_grids(test):
+
+ grid.finalize(jinja_env)
+ if test['name'] != grid.file_name:
+ print(f' {grid.file_name}')
- sub_dir = _get_test_sub_dir(variant.params['file_name'],
- name_to_sub_dir)
+ sub_dir = _get_test_sub_dir(grid.file_name, name_to_sub_dir)
output_sub_dirs = output_dirs.sub_path(sub_dir)
- _check_uniqueness(used_tests, variant.params['name'],
- variant.params['canvas_types'])
- variant.generate_expected_image(output_sub_dirs)
- variant.generate_test(jinja_env, output_sub_dirs)
+ _check_uniqueness(used_filenames, grid.file_name,
+ grid.canvas_types)
+ for variant in grid.variants:
+ _check_uniqueness(
+ used_variants,
+ '.'.join([grid.file_name] +
+ variant.params['grid_variant_names']),
+ grid.canvas_types)
+
+ for variant in grid.variants:
+ variant.generate_expected_image(output_sub_dirs)
+ grid.generate_test(jinja_env, output_sub_dirs)
print()
diff --git a/testing/web-platform/tests/html/canvas/tools/templates/reftest_element.html b/testing/web-platform/tests/html/canvas/tools/templates/reftest_element.html
index 6f7a8c8507..8f403f84f2 100644
--- a/testing/web-platform/tests/html/canvas/tools/templates/reftest_element.html
+++ b/testing/web-platform/tests/html/canvas/tools/templates/reftest_element.html
@@ -3,7 +3,7 @@
{% if test_type == 'promise' %}<html class="reftest-wait">
{% endif %}
{% if not is_test_reference %}
-<link rel="match" href="{{ name }}-expected.html">
+<link rel="match" href="{{ reference_file }}">
{% if fuzzy %}<meta name=fuzzy content="{{ fuzzy }}">
{% endif %}
{% endif %}
diff --git a/testing/web-platform/tests/html/canvas/tools/templates/reftest_element_grid.html b/testing/web-platform/tests/html/canvas/tools/templates/reftest_element_grid.html
new file mode 100644
index 0000000000..d1c90bd993
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/tools/templates/reftest_element_grid.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+{% if test_type == 'promise' %}<html class="reftest-wait">
+<script>pending_tests = {{ element_variants | length }};</script>
+{% endif %}
+{% if not is_test_reference %}
+<link rel="match" href="{{ reference_file }}">
+{% if fuzzy %}<meta name=fuzzy content="{{ fuzzy }}">
+{% endif %}
+{% endif %}
+{% if timeout %}<meta name="timeout" content="{{ timeout }}">
+{% endif %}
+<title>Canvas test: {{ name }}</title>
+<h1 style="font-size: 20px;">{{ name }}</h1>
+<p class="desc">{{ desc }}</p>
+{% if notes %}<p class="notes">{{ notes }}{% endif %}
+{% for image in images %}
+<img src="/images/{{ image }}" id="{{ image }}" class="resource">
+{% endfor -%}
+{% for svgimage in svgimages %}
+<svg><image xlink:href="/images/{{ svgimage }}" id="{{ svgimage
+ }}" class="resource"></svg>
+{% endfor %}
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat({{ grid_width }}, max-content);
+ font-size: 13px; text-align: center;">
+{% for variant in element_variants %}
+<span>
+ {% for variant_name in variant.grid_variant_names %}
+ <div>{{ variant_name }}</div>
+ {% endfor %}
+ <canvas id="canvas{{ variant.id
+ }}" width="{{ variant.size[0]
+ }}" height="{{ variant.size[1]
+ }}" style="outline: 1px solid"{{ variant.canvas }}>
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = document.getElementById("canvas{{ variant.id }}");
+ const ctx = canvas.getContext('2d'{%
+ if variant.attributes %}, {{ variant.attributes }}{% endif %});
+
+ {{ variant.reference | trim | indent(4) if is_test_reference else
+ variant.code_element | trim | indent(4) }}
+ {% if test_type == 'promise' %}
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove("reftest-wait");
+ }
+ {% endif %}
+ </script>
+</span>
+
+{% endfor %}
+</div>
+{% if test_type == 'promise' %}</html>{% endif %}
diff --git a/testing/web-platform/tests/html/canvas/tools/templates/reftest_grid.html b/testing/web-platform/tests/html/canvas/tools/templates/reftest_grid.html
new file mode 100644
index 0000000000..9fd42b7aa5
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/tools/templates/reftest_grid.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: {{ name }}</title>
+<h1 style="font-size: 20px;">{{ name }}</h1>
+<p class="desc">{{ desc }}</p>
+{% if notes %}<p class="notes">{{ notes }}{% endif %}
+{% for image in images %}
+<img src="/images/{{ image }}" id="{{ image }}" class="resource">
+{% endfor %}
+{% for svgimage in svgimages %}
+<svg><image xlink:href="/images/{{ svgimage
+ }}" id="{{ svgimage }}" class="resource"></svg>
+{% endfor %}
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat({{ grid_width }}, max-content);
+ font-size: 13px; text-align: center;">
+{% for variant in element_variants %}
+<span>
+ {% for variant_name in variant.grid_variant_names %}
+ <div>{{ variant_name }}</div>
+ {% endfor %}
+ <div style="width: {{ variant.size[0] }}px; height: {{ variant.size[1]
+ }}px; outline: 1px solid">
+ {{ variant.html_reference | trim | indent(4) }}
+ </div>
+</span>
+
+{% endfor %}
+</div>
diff --git a/testing/web-platform/tests/html/canvas/tools/templates/reftest_offscreen.html b/testing/web-platform/tests/html/canvas/tools/templates/reftest_offscreen.html
index abc840159f..2cd8e9750d 100644
--- a/testing/web-platform/tests/html/canvas/tools/templates/reftest_offscreen.html
+++ b/testing/web-platform/tests/html/canvas/tools/templates/reftest_offscreen.html
@@ -2,7 +2,7 @@
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
{% if test_type == 'promise' %}<html class="reftest-wait">
{% endif %}
-<link rel="match" href="{{ name }}-expected.html">
+<link rel="match" href="{{ reference_file }}">
{% if fuzzy %}<meta name=fuzzy content="{{ fuzzy }}">
{% endif %}
{% if timeout %}<meta name="timeout" content="{{ timeout }}">
diff --git a/testing/web-platform/tests/html/canvas/tools/templates/reftest_offscreen_grid.html b/testing/web-platform/tests/html/canvas/tools/templates/reftest_offscreen_grid.html
new file mode 100644
index 0000000000..d001260bea
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/tools/templates/reftest_offscreen_grid.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+{% if test_type == 'promise' %}<html class="reftest-wait">
+<script>pending_tests = {{ offscreen_variants | length }};</script>
+{% endif %}
+<link rel="match" href="{{ reference_file }}">
+{% if fuzzy %}<meta name=fuzzy content="{{ fuzzy }}">
+{% endif %}
+{% if timeout %}<meta name="timeout" content="{{ timeout }}">
+{% endif %}
+<title>Canvas test: {{ name }}</title>
+<h1 style="font-size: 20px;">{{ name }}</h1>
+<p class="desc">{{ desc }}</p>
+{% if notes %}<p class="notes">{{ notes }}{% endif %}
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat({{ grid_width }}, max-content);
+ font-size: 13px; text-align: center;">
+{% for variant in offscreen_variants %}
+<span>
+ {% for variant_name in variant.grid_variant_names %}
+ <div>{{ variant_name }}</div>
+ {% endfor %}
+ <canvas id="canvas{{ variant.id
+ }}" width="{{ variant.size[0]
+ }}" height="{{ variant.size[1]
+ }}" style="outline: 1px solid"{{ variant.canvas }}>
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script type="module">
+ const canvas = new OffscreenCanvas({{ variant.size[0] }}, {{
+ variant.size[1] }});
+ const ctx = canvas.getContext('2d'{%
+ if variant.attributes %}, {{ variant.attributes }}{% endif %});
+
+ {{ variant.code_offscreen | trim | indent(4) }}
+
+ const outputCanvas = document.getElementById("canvas{{ variant.id }}");
+ const outputCtx = outputCanvas.getContext('2d'{%
+ if variant.attributes %}, {{ variant.attributes }}{% endif %});
+ outputCtx.drawImage(canvas, 0, 0);
+{% if test_type == 'promise' %}
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove("reftest-wait");
+ }
+{% endif %}
+ </script>
+</span>
+
+{% endfor %}
+</div>
+{% if test_type == 'promise' %}</html>{% endif %}
diff --git a/testing/web-platform/tests/html/canvas/tools/templates/reftest_worker.html b/testing/web-platform/tests/html/canvas/tools/templates/reftest_worker.html
index 02281af5d1..50aa29d00d 100644
--- a/testing/web-platform/tests/html/canvas/tools/templates/reftest_worker.html
+++ b/testing/web-platform/tests/html/canvas/tools/templates/reftest_worker.html
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<html class="reftest-wait">
-<link rel="match" href="{{ name }}-expected.html">
+<link rel="match" href="{{ reference_file }}">
{% if fuzzy %}<meta name=fuzzy content="{{ fuzzy }}">
{% endif %}
{% if timeout %}<meta name="timeout" content="{{ timeout }}">
diff --git a/testing/web-platform/tests/html/canvas/tools/templates/reftest_worker_grid.html b/testing/web-platform/tests/html/canvas/tools/templates/reftest_worker_grid.html
new file mode 100644
index 0000000000..652dddffd8
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/tools/templates/reftest_worker_grid.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="{{ reference_file }}">
+{% if fuzzy %}<meta name=fuzzy content="{{ fuzzy }}">
+{% endif %}
+{% if timeout %}<meta name="timeout" content="{{ timeout }}">
+{% endif %}
+<title>Canvas test: {{ name }}</title>
+<h1 style="font-size: 20px;">{{ name }}</h1>
+<p class="desc">{{ desc }}</p>
+{% if notes %}<p class="notes">{{ notes }}{% endif %}
+<script>pending_tests = {{ worker_variants | length }};</script>
+
+<div style="display: grid; grid-gap: 4px;
+ grid-template-columns: repeat({{ grid_width }}, max-content);
+ font-size: 13px; text-align: center;">
+{% for variant in worker_variants %}
+<span>
+ {% for variant_name in variant.grid_variant_names %}
+ <div>{{ variant_name }}</div>
+ {% endfor %}
+ <canvas id="canvas{{ variant.id
+ }}" width="{{ variant.size[0]
+ }}" height="{{ variant.size[1]
+ }}" style="outline: 1px solid"{{ variant.canvas }}>
+ <p class="fallback">FAIL (fallback content)</p>
+ </canvas>
+ <script id="myWorker{{ variant.id }}" type="text/worker">
+ {% set async = 'async ' if test_type == 'promise' else '' %}
+ self.onmessage = {{async}}function(e) {
+ const canvas = new OffscreenCanvas({{
+ variant.size[0] }}, {{ variant.size[1] }});
+ const ctx = canvas.getContext('2d'{%
+ if variant.attributes %}, {{ variant.attributes }}{% endif %});
+
+ {{ variant.code_worker | trim | indent(6) }}
+
+ const bitmap = canvas.transferToImageBitmap();
+ self.postMessage(bitmap, bitmap);
+ };
+ </script>
+ <script type="module">
+ const blob = new Blob([document.getElementById('myWorker{{
+ variant.id }}').textContent]);
+ const worker = new Worker(URL.createObjectURL(blob));
+ worker.addEventListener('message', msg => {
+ const outputCanvas = document.getElementById('canvas{{ variant.id }}');
+ const outputCtx = outputCanvas.getContext('2d'{%
+ if variant.attributes %}, {{ variant.attributes }}{% endif %});
+ outputCtx.drawImage(msg.data, 0, 0);
+ if (--pending_tests == 0) {
+ document.documentElement.classList.remove('reftest-wait');
+ }
+ });
+ worker.postMessage(null);
+ </script>
+</span>
+
+{% endfor %}
+</div>
+</html>
diff --git a/testing/web-platform/tests/html/canvas/tools/templates/testharness_element_grid.html b/testing/web-platform/tests/html/canvas/tools/templates/testharness_element_grid.html
new file mode 100644
index 0000000000..b8f0ffe020
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/tools/templates/testharness_element_grid.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: {{ name }}</title>
+{% if timeout %}<meta name="timeout" content="{{ timeout }}">{% endif %}
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/html/canvas/resources/canvas-tests.js"></script>
+<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
+
+{% if fonts %}
+<style>
+{% for font in fonts %}
+ @font-face {
+ font-family: {{ font }};
+ src: url("/fonts/{{ font }}.ttf");
+ }
+{% endfor %}
+</style>
+{% if not font_unused_in_dom %}
+{% for font in fonts %}
+<span style="font-family: {{ font }};
+ position: absolute; visibility: hidden">A</span>
+{% endfor %}
+{% endif %}
+{% endif %}
+{% for image in images %}
+<img src="/images/{{ image }}" id="{{ image }}" class="resource">
+{% endfor %}
+{% for svgimage in svgimages %}
+<svg><image xlink:href="/images/{{ svgimage }}" id="{{ svgimage
+ }}" class="resource"></svg>
+{% endfor %}
+<script>
+{% for variant in element_variants %}
+
+{% if test_type == 'promise' %}
+promise_test(async t => {
+{% elif test_type == 'async' %}
+async_test(t => {
+{% else %}
+test(t => {
+{% endif %}
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d'{%
+ if attributes %}, {{ variant.attributes }}{% endif %});
+
+ {{ variant.code_element | trim | indent(2) }}
+}, "{{ variant.desc | double_quote_escape }}");
+{% endfor %}
+
+</script>
+</div>
diff --git a/testing/web-platform/tests/html/canvas/tools/templates/testharness_offscreen_grid.html b/testing/web-platform/tests/html/canvas/tools/templates/testharness_offscreen_grid.html
new file mode 100644
index 0000000000..6e5628036b
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/tools/templates/testharness_offscreen_grid.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>OffscreenCanvas test: {{ name }}</title>
+{% if timeout %}<meta name="timeout" content="{{ timeout }}">{% endif %}
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/html/canvas/resources/canvas-tests.js"></script>
+
+<script>
+{% for variant in offscreen_variants %}
+
+{% if test_type == 'promise' %}
+promise_test(async t => {
+{% elif test_type == 'async' %}
+async_test(t => {
+{% else %}
+test(t => {
+{% endif %}
+ const canvas = new OffscreenCanvas({{
+ variant.size[0] }}, {{ variant.size[1] }});
+ const ctx = canvas.getContext('2d'{%
+ if variant.attributes %}, {{ variant.attributes }}{% endif %});
+
+ {{ variant.code_offscreen | trim | indent(2)}}
+}, "{{ variant.desc | double_quote_escape }}");
+{% endfor %}
+
+</script>
diff --git a/testing/web-platform/tests/html/canvas/tools/templates/testharness_worker_grid.js b/testing/web-platform/tests/html/canvas/tools/templates/testharness_worker_grid.js
new file mode 100644
index 0000000000..53c3b69cb6
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/tools/templates/testharness_worker_grid.js
@@ -0,0 +1,27 @@
+{% if timeout %}// META: timeout={{ timeout }}{% endif %}
+// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
+// OffscreenCanvas test in a worker:{{ name }}
+// Description:{{ desc }}
+// Note:{% if notes %}<p class="notes">{{ notes }}{% endif +%}
+
+importScripts("/resources/testharness.js");
+importScripts("/html/canvas/resources/canvas-tests.js");
+{% for variant in worker_variants %}
+
+{% if test_type == 'promise' %}
+promise_test(async t => {
+{% elif test_type == 'async' %}
+async_test(t => {
+{% else %}
+test(t => {
+{% endif %}
+ const canvas = new OffscreenCanvas({{
+ variant.size[0] }}, {{ variant.size[1] }});
+ const ctx = canvas.getContext('2d'{%
+ if variant.attributes %}, {{ variant.attributes }}{% endif %});
+
+ {{ variant.code_worker | trim | indent(2)}}
+}, "{{ variant.desc | double_quote_escape }}");
+{% endfor %}
+
+done();
diff --git a/testing/web-platform/tests/html/canvas/tools/yaml-new/filters.yaml b/testing/web-platform/tests/html/canvas/tools/yaml-new/filters.yaml
index 1ce9d8ed74..9a738a37bd 100644
--- a/testing/web-platform/tests/html/canvas/tools/yaml-new/filters.yaml
+++ b/testing/web-platform/tests/html/canvas/tools/yaml-new/filters.yaml
@@ -617,8 +617,7 @@
tentative: .tentative
- name: >-
- 2d.filter.{{ variant_names[0] }}.gaussianBlur.{{ variant_names[1] }}{{
- tentative }}
+ 2d.filter.{{ variant_names[0] }}.gaussianBlur{{ tentative }}
desc: Test CanvasFilter() with gaussianBlur.
size: [100, 100]
code: |
@@ -633,13 +632,14 @@
<svg xmlns="http://www.w3.org/2000/svg"
width="{{ size[0] }}" height="{{ size[1] }}"
color-interpolation-filters="sRGB">
- <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
+ <filter id="blur{{ id }}" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="{{ blur_x }} {{blur_y}}" />
</filter>
<rect x="25" y="25" width="50" height="50"
- fill="teal" filter="url(#blur)" />
+ fill="teal" filter="url(#blur{{ id }})" />
</svg>
append_variants_to_name: false
+ variants_layout: [multi_files, single_file]
variants:
- layers:
filter_declaration: |-
diff --git a/testing/web-platform/tests/html/canvas/tools/yaml-new/layers.yaml b/testing/web-platform/tests/html/canvas/tools/yaml-new/layers.yaml
index d1e9a97043..e71155650b 100644
--- a/testing/web-platform/tests/html/canvas/tools/yaml-new/layers.yaml
+++ b/testing/web-platform/tests/html/canvas/tools/yaml-new/layers.yaml
@@ -1,14 +1,15 @@
- name: 2d.layer.global-states
desc: Checks that layers correctly use global render states.
- size: [200, 200]
+ size: [90, 90]
code: |
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
+ {{ transform_statement }}
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
- {{ render_states }}
+ {{ alpha_statement }}
+ {{ composite_op_statement }}
+ {{ shadow_statement }}
ctx.beginLayer();
@@ -16,118 +17,86 @@
// won't individually composite with the background.
ctx.globalCompositeOperation = 'screen';
- ctx.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
ctx.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
+ ctx.fillRect(30, 5, 50, 40);
ctx.endLayer();
reference: |
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
+ {{ transform_statement }}
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
- {{ render_states }}
+ {{ alpha_statement }}
+ {{ composite_op_statement }}
+ {{ shadow_statement }}
- canvas2 = document.createElement("canvas");
- ctx2 = canvas2.getContext("2d");
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
ctx2.globalCompositeOperation = 'screen';
- ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
- ctx2.fillRect(50, 50, 75, 50);
+ ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 40, 50);
ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
- ctx2.fillRect(70, 70, 75, 50);
+ ctx2.fillRect(30, 5, 50, 40);
ctx.drawImage(canvas2, 0, 0);
- variants:
- - &global-state-variants
- no-global-states:
- render_states: // No global states.
- alpha: &global-state-alpha
- render_states: ctx.globalAlpha = 0.6;
+ variants_layout: [single_file, multi_files, multi_files, multi_files]
+ variants: &global-state-variants
+ - no-globalAlpha:
+ alpha_statement: // No globalAlpha.
+ globalAlpha:
+ alpha_statement: ctx.globalAlpha = 0.75;
+ - no-composite-op:
+ composite_op_statement: // No globalCompositeOperation.
blending:
- render_states: ctx.globalCompositeOperation = 'multiply';
+ composite_op_statement: ctx.globalCompositeOperation = 'multiply';
composite:
- render_states: ctx.globalCompositeOperation = 'source-in';
+ composite_op_statement: ctx.globalCompositeOperation = 'source-in';
+ copy:
+ composite_op_statement: ctx.globalCompositeOperation = 'copy';
+ - no-shadow:
+ shadow_statement: // No shadow.
shadow:
- render_states: |-
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
- alpha.blending: &global-state-alpha-blending
- render_states: |-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
- alpha.composite: &global-state-alpha-composite
- render_states: |-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
- alpha.shadow: &global-state-alpha-shadow
- render_states: |-
- ctx.globalAlpha = 0.5;
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
- alpha.blending.shadow:
- render_states: |-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
- alpha.composite.shadow:
- render_states: |-
- ctx.globalAlpha = 0.6;
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
- blending.shadow:
- render_states: |-
- ctx.globalCompositeOperation = 'multiply';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
- ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
- ctx.shadowBlur = 3;
- composite.shadow:
- render_states: |-
- ctx.globalCompositeOperation = 'source-in';
- ctx.shadowOffsetX = -10;
- ctx.shadowOffsetY = 10;
+ shadow_statement: |-
+ ctx.shadowOffsetX = -7;
+ ctx.shadowOffsetY = 7;
ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
ctx.shadowBlur = 3;
+ - no-transform:
+ transform_statement: // No transform.
+ rotation:
+ transform_statement: |-
+ ctx.translate(50, 40);
+ ctx.rotate(Math.PI);
+ ctx.translate(-45, -45);
+
- name: 2d.layer.global-states.filter
desc: Checks that layers with filters correctly use global render states.
- size: [200, 200]
+ size: [90, 90]
code: |
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
+ {{ transform_statement }}
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
- {{ render_states }}
+ {{ alpha_statement }}
+ {{ composite_op_statement }}
+ {{ shadow_statement }}
ctx.beginLayer({filter: [
- {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0]},
+ {name: 'dropShadow',
+ dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
{name: 'componentTransfer',
- funcA: {type: "table", tableValues: [0, 0.7]}},
- {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]});
+ funcA: {type: "table", tableValues: [0, 0.8]}}]});
- ctx.fillStyle = 'rgba(200, 0, 0, 1)';
- ctx.fillRect(50, 50, 75, 50);
- ctx.fillStyle = 'rgba(0, 200, 0, 1)';
- ctx.fillRect(70, 70, 75, 50);
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)';
+ ctx.fillRect(10, 25, 40, 50);
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)';
+ ctx.fillRect(30, 5, 50, 40);
ctx.endLayer();
reference: |
@@ -136,20 +105,14 @@
width="{{ size[0] }}" height="{{ size[1] }}"
color-interpolation-filters="sRGB">
<filter id="filter" x="-100%" y="-100%" width="300%" height="300%">
- <feColorMatrix
- type="matrix"
- values="0.393 0.769 0.189 0 0
- 0.349 0.686 0.168 0 0
- 0.272 0.534 0.131 0 0
- 0 0 0 1 0" />
+ <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="#00f" />
<feComponentTransfer>
- <feFuncA type="table" tableValues="0 0.7"></feFuncA>
+ <feFuncA type="table" tableValues="0 0.8"></feFuncA>
</feComponentTransfer>
- <feDropShadow dx="5" dy="5" flood-color="#81e" />
</filter>
<g filter="url(#filter)">
- <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/>
- <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/>
+ <rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
+ <rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
</g>
</svg>`;
@@ -157,31 +120,100 @@
img.width = {{ size[0] }};
img.height = {{ size[1] }};
img.onload = () => {
- ctx.fillStyle = 'rgba(0, 0, 255, 1)';
+ {{ transform_statement | indent(2) }}
- var circle = new Path2D();
- circle.arc(90, 90, 45, 0, 2 * Math.PI);
- ctx.fill(circle);
+ ctx.fillStyle = 'rgba(128, 128, 128, 1)';
+ ctx.fillRect(20, 15, 50, 50);
- {{ render_states }}
+ {{ alpha_statement | indent(2) }}
+ {{ composite_op_statement | indent(2) }}
+ {{ shadow_statement | indent(2) }}
ctx.drawImage(img, 0, 0);
};
img.src = 'data:image/svg+xml;base64,' + btoa(svg);
+ variants_layout: [single_file, multi_files, multi_files, multi_files]
+ variants: *global-state-variants
+
+- name: 2d.layer.globalCompositeOperation
+ desc: Checks that layers work with all globalCompositeOperation values.
+ size: [90, 90]
+ code: |
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = '{{ variant_names[0] }}';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ ctx.beginLayer();
+
+ ctx.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx.fillRect(10, 25, 25, 20);
+ ctx.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx.fillRect(25, 10, 20, 25);
+
+ ctx.endLayer();
+ reference: |
+ ctx.translate(50, 50);
+ ctx.scale(2, 2);
+ ctx.rotate(Math.PI);
+ ctx.translate(-25, -25);
+
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
+ ctx.fillRect(15, 15, 25, 25);
+
+ ctx.globalAlpha = 0.75;
+ ctx.globalCompositeOperation = '{{ variant_names[0] }}';
+ ctx.shadowOffsetX = 7;
+ ctx.shadowOffsetY = 7;
+ ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
+
+ const canvas2 = document.createElement("canvas");
+ const ctx2 = canvas2.getContext("2d");
+
+ ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
+ ctx2.fillRect(10, 25, 25, 20);
+ ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
+ ctx2.fillRect(25, 10, 20, 25);
+
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(canvas2, 0, 0);
+ variants_layout: [single_file]
+ grid_width: 7
variants:
- - <<: *global-state-variants
- alpha:
- <<: *global-state-alpha
- fuzzy: maxDifference=0-2; totalPixels=0-6766
- alpha.blending:
- <<: *global-state-alpha-blending
- fuzzy: maxDifference=0-1; totalPixels=0-2453
- alpha.composite:
- <<: *global-state-alpha-composite
- fuzzy: maxDifference=0-1; totalPixels=0-5204
- alpha.shadow:
- <<: *global-state-alpha-shadow
- fuzzy: maxDifference=0-2; totalPixels=0-6311
+ - source-over:
+ source-in:
+ source-atop:
+ destination-over:
+ destination-in:
+ destination-out:
+ destination-atop:
+ lighter:
+ copy:
+ xor:
+ multiply:
+ screen:
+ overlay:
+ darken:
+ lighten:
+ color-dodge:
+ color-burn:
+ hard-light:
+ soft-light:
+ difference:
+ exclusion:
+ hue:
+ saturation:
+ color:
+ luminosity:
- name: 2d.layer.global-filter
desc: Tests that layers ignore the global context filter.
@@ -428,6 +460,7 @@
- name: 2d.layer.ctm.getTransform
desc: Tests getTransform inside layers.
+ test_type: sync
code: |
ctx.translate(10, 20);
ctx.beginLayer();
@@ -559,7 +592,7 @@
desc: Check that layers state stack is flushed and rebuilt on frame renders.
size: [200, 200]
canvas_types: ['HtmlCanvas']
- test_type: "promise"
+ test_type: promise
code: |
ctx.fillStyle = 'purple';
ctx.fillRect(60, 60, 75, 50);
@@ -599,10 +632,9 @@
ctx.fillRect(80, 40, 75, 50);
- name: 2d.layer.malformed-operations
- desc: >-
- Check that exceptions are thrown for operations that are malformed while
- layers are open.
+ desc: Throws if {{ variant_names[0] }} is called while layers are open.
size: [200, 200]
+ test_type: sync
code: |
{{ setup }}
// Shouldn't throw on its own.
@@ -613,6 +645,7 @@
ctx.beginLayer();
assert_throws_dom("InvalidStateError",
() => {{ operation }});
+ variants_layout: [single_file]
variants:
- createPattern:
operation: ctx.createPattern(canvas, 'repeat')
@@ -639,11 +672,9 @@
operation: canvas.transferToImageBitmap()
- name: 2d.layer.malformed-operations-with-promises
- desc: >-
- Check that exceptions are thrown for operations that are malformed while
- layers are open.
+ desc: Throws if {{ variant_names[0] }} is called while layers are open.
size: [200, 200]
- test_type: "promise"
+ test_type: promise
code: |
// Shouldn't throw on its own.
await {{ operation }};
@@ -651,7 +682,9 @@
await {{ operation }};
// Calling again inside a layer should throw.
ctx.beginLayer();
- await promise_rejects_dom(t, 'InvalidStateError', {{ operation }});
+ await promise_rejects_dom(t, 'InvalidStateError',
+ {{ operation }});
+ variants_layout: [single_file]
variants:
- convertToBlob:
canvas_types: ['OffscreenCanvas', 'Worker']
@@ -864,6 +897,7 @@
- name: 2d.layer.invalid-calls
desc: Raises exception on {{ variant_desc }}.
+ test_type: sync
code: |
assert_throws_dom("INVALID_STATE_ERR", function() {
{{ call_sequence | indent(2) }}
@@ -903,6 +937,7 @@
- name: 2d.layer.exceptions-are-no-op
desc: Checks that the context state is left unchanged if beginLayer throws.
+ test_type: sync
code: |
// Get `beginLayer` to throw while parsing the filter.
assert_throws_js(TypeError,
@@ -927,6 +962,7 @@
- name: 2d.layer.beginLayer-options
desc: Checks beginLayer works for different option parameter values
+ test_type: sync
code: |
ctx.beginLayer(); ctx.endLayer();
ctx.beginLayer(null); ctx.endLayer();
diff --git a/testing/web-platform/tests/html/canvas/tools/yaml/element/meta.yaml b/testing/web-platform/tests/html/canvas/tools/yaml/element/meta.yaml
index 5fd8b68498..12852e200a 100644
--- a/testing/web-platform/tests/html/canvas/tools/yaml/element/meta.yaml
+++ b/testing/web-platform/tests/html/canvas/tools/yaml/element/meta.yaml
@@ -390,18 +390,12 @@
('hsl-4', 'hsl(-360240, 100%, 50%)', 0,255,0,255, ""),
('hsl-5', 'hsl(120.0, 100.0%, 50.0%)', 0,255,0,255, ""),
('hsl-6', 'hsl(+120, +100%, +50%)', 0,255,0,255, ""),
- ('hsl-clamp-1', 'hsl(120, 200%, 50%)', 0,255,0,255, ""),
- ('hsl-clamp-2', 'hsl(120, -200%, 49.9%)', 127,127,127,255, ""),
- ('hsl-clamp-3', 'hsl(120, 100%, 200%)', 255,255,255,255, ""),
- ('hsl-clamp-4', 'hsl(120, 100%, -200%)', 0,0,0,255, ""),
+ ('hsl-clamp-negative-saturation', 'hsl(120, -200%, 49.9%)', 127,127,127,255, ""),
('hsla-1', 'hsla(120, 100%, 50%, 0.499)', 0,255,0,127, ""),
('hsla-2', 'hsla( 120.0 , 100.0% , 50.0% , 1 )', 0,255,0,255, ""),
- ('hsla-clamp-1', 'hsla(120, 200%, 50%, 1)', 0,255,0,255, ""),
- ('hsla-clamp-2', 'hsla(120, -200%, 49.9%, 1)', 127,127,127,255, ""),
- ('hsla-clamp-3', 'hsla(120, 100%, 200%, 1)', 255,255,255,255, ""),
- ('hsla-clamp-4', 'hsla(120, 100%, -200%, 1)', 0,0,0,255, ""),
- ('hsla-clamp-5', 'hsla(120, 100%, 50%, 2)', 0,255,0,255, ""),
- ('hsla-clamp-6', 'hsla(120, 100%, 0%, -2)', 0,0,0,0, ""),
+ ('hsla-clamp-negative-saturation', 'hsla(120, -200%, 49.9%, 1)', 127,127,127,255, ""),
+ ('hsla-clamp-alpha-1', 'hsla(120, 100%, 50%, 2)', 0,255,0,255, ""),
+ ('hsla-clamp-alpha-2', 'hsla(120, 100%, 0%, -2)', 0,0,0,0, ""),
('svg-1', 'gray', 128,128,128,255, ""),
('svg-2', 'grey', 128,128,128,255, ""),
# css-color-4 rgb() color function
diff --git a/testing/web-platform/tests/html/canvas/tools/yaml/offscreen/meta.yaml b/testing/web-platform/tests/html/canvas/tools/yaml/offscreen/meta.yaml
index 7b44fd9f26..b07898224d 100644
--- a/testing/web-platform/tests/html/canvas/tools/yaml/offscreen/meta.yaml
+++ b/testing/web-platform/tests/html/canvas/tools/yaml/offscreen/meta.yaml
@@ -346,18 +346,12 @@
('hsl-4', 'hsl(-360240, 100%, 50%)', 0,255,0,255, ""),
('hsl-5', 'hsl(120.0, 100.0%, 50.0%)', 0,255,0,255, ""),
('hsl-6', 'hsl(+120, +100%, +50%)', 0,255,0,255, ""),
- ('hsl-clamp-1', 'hsl(120, 200%, 50%)', 0,255,0,255, ""),
- ('hsl-clamp-2', 'hsl(120, -200%, 49.9%)', 127,127,127,255, ""),
- ('hsl-clamp-3', 'hsl(120, 100%, 200%)', 255,255,255,255, ""),
- ('hsl-clamp-4', 'hsl(120, 100%, -200%)', 0,0,0,255, ""),
+ ('hsl-clamp-negative-saturation', 'hsl(120, -200%, 49.9%)', 127,127,127,255, ""),
('hsla-1', 'hsla(120, 100%, 50%, 0.499)', 0,255,0,127, ""),
('hsla-2', 'hsla( 120.0 , 100.0% , 50.0% , 1 )', 0,255,0,255, ""),
- ('hsla-clamp-1', 'hsla(120, 200%, 50%, 1)', 0,255,0,255, ""),
- ('hsla-clamp-2', 'hsla(120, -200%, 49.9%, 1)', 127,127,127,255, ""),
- ('hsla-clamp-3', 'hsla(120, 100%, 200%, 1)', 255,255,255,255, ""),
- ('hsla-clamp-4', 'hsla(120, 100%, -200%, 1)', 0,0,0,255, ""),
- ('hsla-clamp-5', 'hsla(120, 100%, 50%, 2)', 0,255,0,255, ""),
- ('hsla-clamp-6', 'hsla(120, 100%, 0%, -2)', 0,0,0,0, ""),
+ ('hsla-clamp-negative-saturation', 'hsla(120, -200%, 49.9%, 1)', 127,127,127,255, ""),
+ ('hsla-clamp-alpha-1', 'hsla(120, 100%, 50%, 2)', 0,255,0,255, ""),
+ ('hsla-clamp-alpha-2', 'hsla(120, 100%, 0%, -2)', 0,0,0,0, ""),
('svg-1', 'gray', 128,128,128,255, ""),
('svg-2', 'grey', 128,128,128,255, ""),
# css-color-4 rgb() color function
diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/META.yml b/testing/web-platform/tests/html/cross-origin-embedder-policy/META.yml
index dc7010880b..066fcc2081 100644
--- a/testing/web-platform/tests/html/cross-origin-embedder-policy/META.yml
+++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/META.yml
@@ -1,7 +1,6 @@
spec: https://html.spec.whatwg.org/multipage/origin.html#coep
suggested_reviewers:
- mikewest
- - jugglinmike
- arturjanc
- lweichselbaum
- hemeryar
diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/META.yml b/testing/web-platform/tests/html/cross-origin-opener-policy/META.yml
index b9d578d22f..69c67da459 100644
--- a/testing/web-platform/tests/html/cross-origin-opener-policy/META.yml
+++ b/testing/web-platform/tests/html/cross-origin-opener-policy/META.yml
@@ -1,7 +1,6 @@
spec: https://html.spec.whatwg.org/multipage/origin.html#cross-origin-opener-policies
suggested_reviewers:
- mikewest
- - jugglinmike
- arturjanc
- lweichselbaum
- hemeryar
diff --git a/testing/web-platform/tests/html/dom/WEB_FEATURES.yml b/testing/web-platform/tests/html/dom/WEB_FEATURES.yml
new file mode 100644
index 0000000000..5c9e27d297
--- /dev/null
+++ b/testing/web-platform/tests/html/dom/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: aria-attribute-reflection
+ files:
+ - aria-attribute-reflection.html
diff --git a/testing/web-platform/tests/html/dom/aria-element-reflection.html b/testing/web-platform/tests/html/dom/aria-element-reflection.html
index bdf2450708..e04610171b 100644
--- a/testing/web-platform/tests/html/dom/aria-element-reflection.html
+++ b/testing/web-platform/tests/html/dom/aria-element-reflection.html
@@ -611,7 +611,7 @@
</script>
<div id="sameScopeContainer">
- <div id="labeledby" aria-labeledby="headingLabel1 headingLabel2">Misspelling</div>
+ <div id="labelledby" aria-labelledby="headingLabel1 headingLabel2">Misspelling</div>
<div id="headingLabel1">Wonderful</div>
<div id="headingLabel2">Fantastic</div>
@@ -626,7 +626,7 @@
const headingLabel2 = document.getElementById("headingLabel2")
shadowRoot.appendChild(headingElement);
- assert_array_equals(labeledby.ariaLabelledByElements, [headingLabel1, headingLabel2], "aria-labeled by is supported by IDL getter.");
+ assert_array_equals(labelledby.ariaLabelledByElements, [headingLabel1, headingLabel2], "aria-labelledby is supported by IDL getter.");
// Explicitly set elements are in a lighter shadow DOM, so that's ok.
headingElement.ariaLabelledByElements = [headingLabel1, headingLabel2];
diff --git a/testing/web-platform/tests/html/dom/elements/global-attributes/the-anchor-attribute-003.tentative.html b/testing/web-platform/tests/html/dom/elements/global-attributes/the-anchor-attribute-003.tentative.html
index ec2d8d5ead..e8c12c784f 100644
--- a/testing/web-platform/tests/html/dom/elements/global-attributes/the-anchor-attribute-003.tentative.html
+++ b/testing/web-platform/tests/html/dom/elements/global-attributes/the-anchor-attribute-003.tentative.html
@@ -37,8 +37,8 @@ body {
dialog::backdrop {
top: calc(anchor(top) - 10px);
right: calc(anchor(right) - 10px);
- bottom: calc(anchor(bottom) - 10px);
- left: calc(anchor(left) - 10px);
+ bottom: calc(anchor(bottom, 0px) - 10px);
+ left: calc(anchor(left, 0px) - 10px);
background: lime;
}
diff --git a/testing/web-platform/tests/html/dom/elements/global-attributes/the-anchor-attribute-xml.tentative.html b/testing/web-platform/tests/html/dom/elements/global-attributes/the-anchor-attribute-xml.tentative.html
new file mode 100644
index 0000000000..c5e6d81826
--- /dev/null
+++ b/testing/web-platform/tests/html/dom/elements/global-attributes/the-anchor-attribute-xml.tentative.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel="help" href="https://github.com/whatwg/html/pull/9144">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+test(() => {
+ const xmlDoc = document.implementation.createDocument(null, 'root', null);
+ assert_equals(xmlDoc.contentType, 'application/xml');
+ xmlDoc.documentElement.innerHTML = '<div id="target">target</div><div anchor="target">anchored</div>';
+ assert_equals(xmlDoc.documentElement.innerHTML,
+ '<div id="target">target</div><div anchor="target">anchored</div>');
+ const target = xmlDoc.documentElement.children[0];
+ const anchored = xmlDoc.documentElement.children[1];
+
+ assert_equals(xmlDoc.documentElement.children[1].anchorElement, null,
+ 'Setting the anchor attribute in XML should not set the anchorElement IDL.');
+
+ anchored.anchorElement = target;
+ assert_equals(xmlDoc.documentElement.children[1].anchorElement, null,
+ 'Setting element.anchorElement in an XML document should not set the anchorElement IDL.');
+});
+</script>
diff --git a/testing/web-platform/tests/html/dom/render-blocking/WEB_FEATURES.yml b/testing/web-platform/tests/html/dom/render-blocking/WEB_FEATURES.yml
new file mode 100644
index 0000000000..36ab4f3010
--- /dev/null
+++ b/testing/web-platform/tests/html/dom/render-blocking/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: blocking-render
+ files: "**"
diff --git a/testing/web-platform/tests/html/dom/usvstring-reflection.https.html b/testing/web-platform/tests/html/dom/usvstring-reflection.https.html
index 775cb49281..d8d830dc59 100644
--- a/testing/web-platform/tests/html/dom/usvstring-reflection.https.html
+++ b/testing/web-platform/tests/html/dom/usvstring-reflection.https.html
@@ -124,7 +124,7 @@ promise_test(t => {
const sendString = 'hello\uD999';
const receiveString = 'hello\uFFFD';
- return createDataChannelPair(t)
+ return createDataChannelPair(t, {})
.then(([channel1, channel2]) => {
channel1.send(sendString);
return awaitMessage(channel2)
diff --git a/testing/web-platform/tests/html/editing/dnd/events/drag-event-div-manual.html b/testing/web-platform/tests/html/editing/dnd/events/drag-event-div-manual.html
index 79c0c4332d..505b0049be 100644
--- a/testing/web-platform/tests/html/editing/dnd/events/drag-event-div-manual.html
+++ b/testing/web-platform/tests/html/editing/dnd/events/drag-event-div-manual.html
@@ -6,7 +6,6 @@
<link rel="author" title="Microsoft" href="http://www.microsoft.com/"/>
<link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/>
<meta name="assert" content="Fire drag event when dragging a div element"/>
- <script src="../resources/dragdrop_support.js" type="text/javascript"></script>
<script type="text/javascript">
var EVENT, TARGET;
@@ -14,11 +13,11 @@
{
if ((TARGET == evt.target) && (EVENT == evt.type))
{
- LogTestResult("PASS");
+ document.getElementById("test_result").firstChild.data = "PASS";
}
else
{
- LogTestResult("FAIL");
+ document.getElementById("test_result").firstChild.data = "FAIL";
}
}
@@ -27,7 +26,7 @@
window.onload = function()
{
TARGET = document.getElementById("target");
- AddEventListenersForElement(EVENT, DragEvent, false, TARGET);
+ TARGET.addEventListener(EVENT, DragEvent, false);
}
</script>
</head>
diff --git a/testing/web-platform/tests/html/editing/dnd/events/drag-event-manual.html b/testing/web-platform/tests/html/editing/dnd/events/drag-event-manual.html
index d278b864bb..d3f517ea1d 100644
--- a/testing/web-platform/tests/html/editing/dnd/events/drag-event-manual.html
+++ b/testing/web-platform/tests/html/editing/dnd/events/drag-event-manual.html
@@ -6,7 +6,6 @@
<link rel="author" title="Microsoft" href="http://www.microsoft.com/"/>
<link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/>
<meta name="assert" content="Fire drag event during the drag and drop processing"/>
- <script src="../resources/dragdrop_support.js" type="text/javascript"></script>
<script type="text/javascript">
var EVENT, TARGET;
@@ -14,11 +13,11 @@
{
if ((TARGET == evt.target) && (EVENT == evt.type))
{
- LogTestResult("PASS");
+ document.getElementById("test_result").firstChild.data = "PASS";
}
else
{
- LogTestResult("FAIL");
+ document.getElementById("test_result").firstChild.data = "FAIL";
}
}
@@ -27,7 +26,7 @@
window.onload = function()
{
TARGET = document.getElementById("target");
- AddEventListenersForElement(EVENT, DragEvent, false, TARGET);
+ TARGET.addEventListener(EVENT, DragEvent, false);
}
</script>
</head>
diff --git a/testing/web-platform/tests/html/editing/dnd/events/dragend-event-manual.html b/testing/web-platform/tests/html/editing/dnd/events/dragend-event-manual.html
index 8bfb1fb7b6..b4bb621e88 100644
--- a/testing/web-platform/tests/html/editing/dnd/events/dragend-event-manual.html
+++ b/testing/web-platform/tests/html/editing/dnd/events/dragend-event-manual.html
@@ -6,7 +6,6 @@
<link rel="author" title="Microsoft" href="http://www.microsoft.com/"/>
<link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/>
<meta name="assert" content="Fire dragend event during the drag and drop processing"/>
- <script src="../resources/dragdrop_support.js" type="text/javascript"></script>
<script type="text/javascript">
var EVENT, TARGET;
@@ -14,11 +13,11 @@
{
if ((TARGET == evt.target) && (EVENT == evt.type))
{
- LogTestResult("PASS");
+ document.getElementById("test_result").firstChild.data = "PASS";
}
else
{
- LogTestResult("FAIL");
+ document.getElementById("test_result").firstChild.data = "FAIL";
}
}
@@ -27,7 +26,7 @@
window.onload = function()
{
TARGET = document.getElementById("target");
- AddEventListenersForElement(EVENT, DragendEvent, false, TARGET);
+ TARGET.addEventListener(EVENT, DragendEvent, false);
}
</script>
</head>
diff --git a/testing/web-platform/tests/html/editing/dnd/events/dragenter-event-manual.html b/testing/web-platform/tests/html/editing/dnd/events/dragenter-event-manual.html
index e81b32949c..23b404b0b1 100644
--- a/testing/web-platform/tests/html/editing/dnd/events/dragenter-event-manual.html
+++ b/testing/web-platform/tests/html/editing/dnd/events/dragenter-event-manual.html
@@ -6,7 +6,6 @@
<link rel="author" title="Microsoft" href="http://www.microsoft.com/"/>
<link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/>
<meta name="assert" content="Fire dragenter event during the drag and drop processing"/>
- <script src="../resources/dragdrop_support.js" type="text/javascript"></script>
<script type="text/javascript">
var EVENT, TARGET;
@@ -14,11 +13,11 @@
{
if ((TARGET == evt.target) && (EVENT == evt.type))
{
- LogTestResult("PASS");
+ document.getElementById("test_result").firstChild.data = "PASS";
}
else
{
- LogTestResult("FAIL");
+ document.getElementById("test_result").firstChild.data = "FAIL";
}
}
@@ -27,7 +26,7 @@
window.onload = function()
{
TARGET = document.getElementById("target");
- AddEventListenersForElement(EVENT, DragenterEvent, false, TARGET);
+ TARGET.addEventListener(EVENT, DragenterEvent, false);
}
</script>
</head>
diff --git a/testing/web-platform/tests/html/editing/dnd/events/dragleave-event-manual.html b/testing/web-platform/tests/html/editing/dnd/events/dragleave-event-manual.html
index f6a405915f..a400fa3417 100644
--- a/testing/web-platform/tests/html/editing/dnd/events/dragleave-event-manual.html
+++ b/testing/web-platform/tests/html/editing/dnd/events/dragleave-event-manual.html
@@ -6,7 +6,6 @@
<link rel="author" title="Microsoft" href="http://www.microsoft.com/"/>
<link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/>
<meta name="assert" content="Fire dragleave event during the drag and drop processing"/>
- <script src="../resources/dragdrop_support.js" type="text/javascript"></script>
<script type="text/javascript">
var EVENT, TARGET;
@@ -14,11 +13,11 @@
{
if ((TARGET == evt.target) && (EVENT == evt.type))
{
- LogTestResult("PASS");
+ document.getElementById("test_result").firstChild.data = "PASS";
}
else
{
- LogTestResult("FAIL");
+ document.getElementById("test_result").firstChild.data = "FAIL";
}
}
@@ -27,7 +26,7 @@
window.onload = function()
{
TARGET = document.getElementById("target");
- AddEventListenersForElement(EVENT, DragleaveEvent, false, TARGET);
+ TARGET.addEventListener(EVENT, DragleaveEvent, false);
}
</script>
</head>
diff --git a/testing/web-platform/tests/html/editing/dnd/events/dragover-event-manual.html b/testing/web-platform/tests/html/editing/dnd/events/dragover-event-manual.html
index f8d99241d5..f37a33cff6 100644
--- a/testing/web-platform/tests/html/editing/dnd/events/dragover-event-manual.html
+++ b/testing/web-platform/tests/html/editing/dnd/events/dragover-event-manual.html
@@ -6,7 +6,6 @@
<link rel="author" title="Microsoft" href="http://www.microsoft.com/"/>
<link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/>
<meta name="assert" content="Fire dragover event during the drag and drop processing"/>
- <script src="../resources/dragdrop_support.js" type="text/javascript"></script>
<script type="text/javascript">
var EVENT, TARGET;
@@ -14,11 +13,11 @@
{
if ((TARGET == evt.target) && (EVENT == evt.type))
{
- LogTestResult("PASS");
+ document.getElementById("test_result").firstChild.data = "PASS";
}
else
{
- LogTestResult("FAIL");
+ document.getElementById("test_result").firstChild.data = "FAIL";
}
}
@@ -27,7 +26,7 @@
window.onload = function()
{
TARGET = document.getElementById("target");
- AddEventListenersForElement(EVENT, DragoverEvent, false, TARGET);
+ TARGET.addEventListener(EVENT, DragoverEvent, false);
}
</script>
</head>
diff --git a/testing/web-platform/tests/html/editing/dnd/events/dragstart-event-manual.html b/testing/web-platform/tests/html/editing/dnd/events/dragstart-event-manual.html
index 20786648da..9128401ffa 100644
--- a/testing/web-platform/tests/html/editing/dnd/events/dragstart-event-manual.html
+++ b/testing/web-platform/tests/html/editing/dnd/events/dragstart-event-manual.html
@@ -6,7 +6,6 @@
<link rel="author" title="Microsoft" href="http://www.microsoft.com/"/>
<link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/>
<meta name="assert" content="Fire dragstart event during the drag and drop processing"/>
- <script src="../resources/dragdrop_support.js" type="text/javascript"></script>
<script type="text/javascript">
var EVENT, TARGET;
@@ -14,11 +13,11 @@
{
if ((TARGET == evt.target) && (EVENT == evt.type))
{
- LogTestResult("PASS");
+ document.getElementById("test_result").firstChild.data = "PASS";
}
else
{
- LogTestResult("FAIL");
+ document.getElementById("test_result").firstChild.data = "FAIL";
}
}
@@ -27,7 +26,7 @@
window.onload = function()
{
TARGET = document.getElementById("target");
- AddEventListenersForElement(EVENT, DragstartEvent, false, TARGET);
+ TARGET.addEventListener(EVENT, DragstartEvent, false);
}
</script>
</head>
diff --git a/testing/web-platform/tests/html/editing/dnd/events/drop-event-manual.html b/testing/web-platform/tests/html/editing/dnd/events/drop-event-manual.html
index 2897bd5713..8393e38696 100644
--- a/testing/web-platform/tests/html/editing/dnd/events/drop-event-manual.html
+++ b/testing/web-platform/tests/html/editing/dnd/events/drop-event-manual.html
@@ -6,7 +6,6 @@
<link rel="author" title="Microsoft" href="http://www.microsoft.com/"/>
<link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/>
<meta name="assert" content="Fire drop event during the drag and drop processing"/>
- <script src="../resources/dragdrop_support.js" type="text/javascript"></script>
<script type="text/javascript">
var EVENT, TARGET;
@@ -14,11 +13,11 @@
{
if ((TARGET == evt.target) && (EVENT == evt.type))
{
- LogTestResult("PASS");
+ document.getElementById("test_result").firstChild.data = "PASS";
}
else
{
- LogTestResult("FAIL");
+ document.getElementById("test_result").firstChild.data = "FAIL";
}
}
@@ -27,7 +26,7 @@
window.onload = function()
{
TARGET = document.getElementById("target");
- AddEventListenersForElement(EVENT, DropEvent, false, TARGET);
+ TARGET.addEventListener(EVENT, DropEvent, false);
}
</script>
</head>
diff --git a/testing/web-platform/tests/html/editing/dnd/resources/dragdrop_support.js b/testing/web-platform/tests/html/editing/dnd/resources/dragdrop_support.js
deleted file mode 100644
index f5a1d6417f..0000000000
--- a/testing/web-platform/tests/html/editing/dnd/resources/dragdrop_support.js
+++ /dev/null
@@ -1,9 +0,0 @@
-function AddEventListenersForElement(evt, callback, capture, element)
-{
- element.addEventListener(evt, callback, capture);
-}
-
-function LogTestResult(result)
-{
- document.getElementById("test_result").firstChild.data = result;
-}
diff --git a/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/effectAllowed-manual.html b/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/effectAllowed-manual.html
index 08540b906a..61443da2c0 100644
--- a/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/effectAllowed-manual.html
+++ b/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/effectAllowed-manual.html
@@ -6,7 +6,6 @@
<link rel="author" title="Microsoft" href="http://www.microsoft.com/"/>
<link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#datatransfer"/>
<meta name="assert" content="Set a value to effectAllowed attribute"/>
- <script src="../resources/dragdrop_support.js" type="text/javascript"></script>
<script type="text/javascript">
var TARGETEVENT1, TARGETEVENT2, TARGET1, TARGET2;
@@ -23,11 +22,11 @@
{
if("move" == evt.dataTransfer.effectAllowed)
{
- LogTestResult("PASS");
+ document.getElementById("test_result").firstChild.data = "PASS";
}
else
{
- LogTestResult("FAIL");
+ document.getElementById("test_result").firstChild.data = "FAIL";
}
}
}
@@ -39,8 +38,8 @@
{
TARGET1 = document.getElementById("target1");
TARGET2 = document.getElementById("target2");
- AddEventListenersForElement(TARGETEVENT1, DragstartEvent, false, TARGET1);
- AddEventListenersForElement(TARGETEVENT2, DragenterEvent, false, TARGET2);
+ TARGET1.addEventListener(TARGETEVENT1, DragstartEvent, false);
+ TARGET2.addEventListener(TARGETEVENT2, DragenterEvent, false);
}
</script>
</head>
diff --git a/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/files-manual.html b/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/files-manual.html
index 7de0b4bbce..ffafb66db4 100644
--- a/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/files-manual.html
+++ b/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/files-manual.html
@@ -6,7 +6,6 @@
<link rel="author" title="Microsoft" href="http://www.microsoft.com/"/>
<link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#datatransfer"/>
<meta name="assert" content="files attribute returns a FileList"/>
- <script src="../resources/dragdrop_support.js" type="text/javascript"></script>
<script type="text/javascript">
var EVENT, TARGET;
@@ -17,16 +16,16 @@
var files = evt.dataTransfer.files;
if(('[object FileList]' == files))
{
- LogTestResult("PASS");
+ document.getElementById("test_result").firstChild.data = "PASS";
}
else
{
- LogTestResult("FAIL");
+ document.getElementById("test_result").firstChild.data = "FAIL";
}
}
else
{
- LogTestResult("FAIL");
+ document.getElementById("test_result").firstChild.data = "FAIL";
}
}
@@ -45,9 +44,9 @@
window.onload = function()
{
TARGET = document.getElementById("target");
- AddEventListenersForElement(EVENT, DropEvent, false, TARGET);
- AddEventListenersForElement("dragenter", DragenterEvent, false, TARGET);
- AddEventListenersForElement("dragover", DragoverEvent, false, TARGET);
+ TARGET.addEventListener(EVENT, DropEvent, false);
+ TARGET.addEventListener("dragenter", DragenterEvent, false);
+ TARGET.addEventListener("dragover", DragoverEvent, false);
}
</script>
</head>
diff --git a/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/setData-manual.html b/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/setData-manual.html
index f0f7cae600..1438e932ad 100644
--- a/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/setData-manual.html
+++ b/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/setData-manual.html
@@ -6,7 +6,6 @@
<link rel="author" title="Microsoft" href="http://www.microsoft.com/"/>
<link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#datatransfer"/>
<meta name="assert" content="Add an item to the drag data store item list whose data is the string given by setData method's second argument"/>
- <script src="../resources/dragdrop_support.js" type="text/javascript"></script>
<script type="text/javascript">
var TARGETEVENT1, TARGETEVENT2, TARGET1, TARGET2;
@@ -23,11 +22,11 @@
{
if("SetText" == evt.dataTransfer.getData("text"))
{
- LogTestResult("PASS");
+ document.getElementById("test_result").firstChild.data = "PASS";
}
else
{
- LogTestResult("FAIL");
+ document.getElementById("test_result").firstChild.data = "FAIL";
}
}
}
@@ -39,8 +38,8 @@
{
TARGET1 = document.getElementById("target1");
TARGET2 = document.getElementById("target2");
- AddEventListenersForElement(TARGETEVENT1, DragstartEvent, false, TARGET1);
- AddEventListenersForElement(TARGETEVENT2, DropEvent, false, TARGET2);
+ TARGET1.addEventListener(TARGETEVENT1, DragstartEvent, false);
+ TARGET2.addEventListener(TARGETEVENT2, DropEvent, false);
}
</script>
</head>
diff --git a/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/types-manual.html b/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/types-manual.html
index 1730c4bc73..3aa1404e29 100644
--- a/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/types-manual.html
+++ b/testing/web-platform/tests/html/editing/dnd/the-datatransfer-interface/types-manual.html
@@ -6,7 +6,6 @@
<link rel="author" title="Microsoft" href="http://www.microsoft.com/"/>
<link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#datatransfer"/>
<meta name="assert" content="types attribute returns a DOMStringList"/>
- <script src="../resources/dragdrop_support.js" type="text/javascript"></script>
<script type="text/javascript">
var EVENT, TARGET;
@@ -17,16 +16,16 @@
var types = evt.dataTransfer.types;
if(('[object DOMStringList]' == types))
{
- LogTestResult("PASS");
+ document.getElementById("test_result").firstChild.data = "PASS";
}
else
{
- LogTestResult("FAIL");
+ document.getElementById("test_result").firstChild.data = "FAIL";
}
}
else
{
- LogTestResult("FAIL");
+ document.getElementById("test_result").firstChild.data = "FAIL";
}
}
@@ -35,7 +34,7 @@
window.onload = function()
{
TARGET = document.getElementById("target");
- AddEventListenersForElement(EVENT, DropEvent, false, TARGET);
+ TARGET.addEventListener(EVENT, DropEvent, false);
}
</script>
</head>
diff --git a/testing/web-platform/tests/html/infrastructure/safe-passing-of-structured-data/shared-array-buffers/window-simple-success.https.html b/testing/web-platform/tests/html/infrastructure/safe-passing-of-structured-data/shared-array-buffers/window-simple-success.https.html
index c9b41d0a0d..5be96d37a4 100644
--- a/testing/web-platform/tests/html/infrastructure/safe-passing-of-structured-data/shared-array-buffers/window-simple-success.https.html
+++ b/testing/web-platform/tests/html/infrastructure/safe-passing-of-structured-data/shared-array-buffers/window-simple-success.https.html
@@ -23,6 +23,7 @@
"Uint32Array",
"BigInt64Array",
"BigUint64Array",
+ "Float16Array",
"Float32Array",
"Float64Array"
].forEach(type => {
diff --git a/testing/web-platform/tests/html/interaction/focus/WEB_FEATURES.yml b/testing/web-platform/tests/html/interaction/focus/WEB_FEATURES.yml
new file mode 100644
index 0000000000..210e2d3f99
--- /dev/null
+++ b/testing/web-platform/tests/html/interaction/focus/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: tabindex
+ files:
+ - tabindex-focus-flag.html
diff --git a/testing/web-platform/tests/html/interaction/focus/sequential-focus-navigation-and-the-tabindex-attribute/WEB_FEATURES.yml b/testing/web-platform/tests/html/interaction/focus/sequential-focus-navigation-and-the-tabindex-attribute/WEB_FEATURES.yml
new file mode 100644
index 0000000000..6d868044c9
--- /dev/null
+++ b/testing/web-platform/tests/html/interaction/focus/sequential-focus-navigation-and-the-tabindex-attribute/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: tabindex
+ files: "**"
diff --git a/testing/web-platform/tests/html/meta/refresh-time.html b/testing/web-platform/tests/html/meta/refresh-time.html
new file mode 100644
index 0000000000..7aef1266a2
--- /dev/null
+++ b/testing/web-platform/tests/html/meta/refresh-time.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test fractional values in meta http-equiv=refresh</title>
+<link rel="author" title="Psychpsyo" href="mailto:psychpsyo@gmail.com">
+<link rel="help" href="https://html.spec.whatwg.org/#pragma-directives">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+ async function sleep(ms, test) {
+ return new Promise(resolve => {test.step_timeout(resolve, ms)});
+ }
+
+ promise_test(async test => {
+ const frame = document.createElement("iframe");
+ document.body.appendChild(frame);
+ frame.src = "resources/refresh1.html";
+ await sleep(500, test);
+ assert_equals(frame.contentWindow.document.title, "refresh 1");
+ await sleep(1000, test);
+ assert_equals(frame.contentWindow.document.title, "got refreshed");
+ }, "Ensure that refresh is observed");
+
+ promise_test(async test => {
+ const frame = document.createElement("iframe");
+ document.body.appendChild(frame);
+ frame.src = "resources/refresh.99.html";
+ await sleep(500, test);
+ assert_equals(frame.contentWindow.document.title, "got refreshed");
+ }, "Ensure that fractions in refresh time are ignored");
+
+ promise_test(async test => {
+ const frame = document.createElement("iframe");
+ document.body.appendChild(frame);
+ frame.src = "resources/refresh1.99.html";
+ await sleep(500, test);
+ assert_equals(frame.contentWindow.document.title, "refresh 1.99");
+ await sleep(1000, test);
+ assert_equals(frame.contentWindow.document.title, "got refreshed");
+ }, "Ensure that non-fractional part in refresh time does not get discarded");
+
+ promise_test(async test => {
+ const frame = document.createElement("iframe");
+ document.body.appendChild(frame);
+ frame.src = "resources/refresh1dotdot5dot.html";
+ await sleep(500, test);
+ assert_equals(frame.contentWindow.document.title, "refresh 1..5.");
+ await sleep(750, test);
+ assert_equals(frame.contentWindow.document.title, "got refreshed");
+ }, "Ensure that multiple periods in refresh time just get ignored");
+</script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/meta/resources/gotRefreshed.html b/testing/web-platform/tests/html/meta/resources/gotRefreshed.html
new file mode 100644
index 0000000000..c894269593
--- /dev/null
+++ b/testing/web-platform/tests/html/meta/resources/gotRefreshed.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>got refreshed</title> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/meta/resources/refresh.99.html b/testing/web-platform/tests/html/meta/resources/refresh.99.html
new file mode 100644
index 0000000000..ca4e346277
--- /dev/null
+++ b/testing/web-platform/tests/html/meta/resources/refresh.99.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>refresh .99</title>
+
+<meta http-equiv="refresh" content=".99;url=gotRefreshed.html"> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/meta/resources/refresh1.99.html b/testing/web-platform/tests/html/meta/resources/refresh1.99.html
new file mode 100644
index 0000000000..76121cfd40
--- /dev/null
+++ b/testing/web-platform/tests/html/meta/resources/refresh1.99.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>refresh 1.99</title>
+
+<meta http-equiv="refresh" content="1.99;url=gotRefreshed.html"> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/meta/resources/refresh1.html b/testing/web-platform/tests/html/meta/resources/refresh1.html
new file mode 100644
index 0000000000..14819dc3db
--- /dev/null
+++ b/testing/web-platform/tests/html/meta/resources/refresh1.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>refresh 1</title>
+
+<meta http-equiv="refresh" content="1;url=gotRefreshed.html"> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/meta/resources/refresh1dotdot5dot.html b/testing/web-platform/tests/html/meta/resources/refresh1dotdot5dot.html
new file mode 100644
index 0000000000..085b9e9ba7
--- /dev/null
+++ b/testing/web-platform/tests/html/meta/resources/refresh1dotdot5dot.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>refresh 1..5.</title>
+
+<meta http-equiv="refresh" content="1..5.;url=gotRefreshed.html"> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/rendering/non-replaced-elements/flow-content-0/WEB_FEATURES.yml b/testing/web-platform/tests/html/rendering/non-replaced-elements/flow-content-0/WEB_FEATURES.yml
new file mode 100644
index 0000000000..831b257f72
--- /dev/null
+++ b/testing/web-platform/tests/html/rendering/non-replaced-elements/flow-content-0/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: search
+ files:
+ - search-*
diff --git a/testing/web-platform/tests/html/rendering/replaced-elements/the-select-element/select-1-block-size-001-ref-2.html b/testing/web-platform/tests/html/rendering/replaced-elements/the-select-element/select-1-block-size-001-ref-2.html
index 385c2a75d4..5246123186 100644
--- a/testing/web-platform/tests/html/rendering/replaced-elements/the-select-element/select-1-block-size-001-ref-2.html
+++ b/testing/web-platform/tests/html/rendering/replaced-elements/the-select-element/select-1-block-size-001-ref-2.html
@@ -9,7 +9,7 @@ div {
appearance: none;
background: black;
- color: black;
+ color: transparent;
line-height: 100px;
width: 100px;
diff --git a/testing/web-platform/tests/html/rendering/replaced-elements/the-select-element/select-1-block-size-001-ref.html b/testing/web-platform/tests/html/rendering/replaced-elements/the-select-element/select-1-block-size-001-ref.html
index 3834281dd8..3d30c2bd9d 100644
--- a/testing/web-platform/tests/html/rendering/replaced-elements/the-select-element/select-1-block-size-001-ref.html
+++ b/testing/web-platform/tests/html/rendering/replaced-elements/the-select-element/select-1-block-size-001-ref.html
@@ -10,7 +10,7 @@ button {
appearance: none;
background: black;
- color: black;
+ color: transparent;
line-height: 100px;
width: 100px;
diff --git a/testing/web-platform/tests/html/rendering/the-details-element/WEB_FEATURES.yml b/testing/web-platform/tests/html/rendering/the-details-element/WEB_FEATURES.yml
new file mode 100644
index 0000000000..769e741180
--- /dev/null
+++ b/testing/web-platform/tests/html/rendering/the-details-element/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: details
+ files: "**"
diff --git a/testing/web-platform/tests/html/rendering/widgets/input-password-background-suppresses-appearance-ref.html b/testing/web-platform/tests/html/rendering/widgets/input-password-background-suppresses-appearance-ref.html
new file mode 100644
index 0000000000..33e0e6ac16
--- /dev/null
+++ b/testing/web-platform/tests/html/rendering/widgets/input-password-background-suppresses-appearance-ref.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<meta charset="utf-8">
+<input type="password" style="appearance: none; background-color: blue">
diff --git a/testing/web-platform/tests/html/rendering/widgets/input-password-background-suppresses-appearance.html b/testing/web-platform/tests/html/rendering/widgets/input-password-background-suppresses-appearance.html
new file mode 100644
index 0000000000..3c85b100a1
--- /dev/null
+++ b/testing/web-platform/tests/html/rendering/widgets/input-password-background-suppresses-appearance.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<meta charset="utf-8">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1895561">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="match" href="input-password-background-suppresses-appearance-ref.html">
+<title>backgrounds disable native password input appearance</title>
+<input type="password" style="background-color: blue">
diff --git a/testing/web-platform/tests/html/semantics/embedded-content/media-elements/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/embedded-content/media-elements/WEB_FEATURES.yml
new file mode 100644
index 0000000000..5730fe4715
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/embedded-content/media-elements/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: preserves-pitch
+ files:
+ - preserves-pitch.html
diff --git a/testing/web-platform/tests/html/semantics/embedded-content/the-object-element/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/embedded-content/the-object-element/WEB_FEATURES.yml
new file mode 100644
index 0000000000..d3411c2d8d
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/embedded-content/the-object-element/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: constraint-validation
+ files:
+ - object-setcustomvalidity.html
diff --git a/testing/web-platform/tests/html/semantics/forms/attributes-common-to-form-controls/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/forms/attributes-common-to-form-controls/WEB_FEATURES.yml
new file mode 100644
index 0000000000..457c904005
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/attributes-common-to-form-controls/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: dirname
+ files:
+ - dirname-*
diff --git a/testing/web-platform/tests/html/semantics/forms/constraints/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/forms/constraints/WEB_FEATURES.yml
new file mode 100644
index 0000000000..93b4c5745f
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/constraints/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: constraint-validation
+ files: "**"
diff --git a/testing/web-platform/tests/html/semantics/forms/constraints/form-validation-validity-textarea-defaultValue.html b/testing/web-platform/tests/html/semantics/forms/constraints/form-validation-validity-textarea-defaultValue.html
new file mode 100644
index 0000000000..55276116ad
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/constraints/form-validation-validity-textarea-defaultValue.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>textarea validation behavior</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#suffering-from-being-too-short">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#the-constraint-validation-api">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<textarea id=t1 minlength=5 required></textarea>
+<textarea id=t2 minlength=5 required>a</textarea>
+<textarea id=t3 required>a</textarea>
+<textarea id=t4>a</textarea>
+<script>
+test(() => {
+ const emptyMinlength = document.getElementById('t1');
+ const nonEmptyMinlength = document.getElementById('t2');
+ const nonEmptyRequired = document.getElementById('t3');
+ const nonEmptyNonRequired = document.getElementById('t4');
+ assert_false(emptyMinlength.validity.valid,'Empty textareas with constraints will validate');
+ assert_true(nonEmptyMinlength.validity.valid,'Non-empty textareas with constraints will *not* validate');
+ assert_true(nonEmptyRequired.validity.valid,'Textareas without constraints will validate');
+ assert_true(nonEmptyNonRequired.validity.valid,'Textareas without constraints will validate');
+ [t1,t2,t3,t4].forEach(t => t.remove());
+},'Default validity based on emptiness');
+</script>
+
+<textarea id=t5 minlength=5 required></textarea>
+<script>
+promise_test(async () => {
+ const textarea = document.getElementById('t5');
+ document.querySelector('#t1');
+ assert_false(textarea.validity.valid,'By default, this textarea will validate (and fail) because it started empty');
+ textarea.defaultValue = 'abc';
+ assert_true(textarea.validity.valid,'Programmatically setting defaultValue is not a user edit - automatically valid');
+ textarea.replaceChildren('abcd');
+ assert_true(textarea.validity.valid,'Programmatically replacing children is not a user edit - automatically valid');
+ textarea.defaultValue = 'abcde';
+ assert_true(textarea.validity.valid,'Still valid');
+ textarea.remove();
+},'Setting textarea.defaultValue should not trigger validation');
+</script>
+
+<textarea id=t6 minlength=5 required></textarea>
+<script>
+promise_test(async () => {
+ const textarea = document.getElementById('t6');
+ assert_false(textarea.validity.valid,'By default, this textarea will validate (and fail) because it started empty');
+ await test_driver.send_keys(textarea, "abc");
+ assert_false(textarea.validity.valid,'Keystrokes should trigger validation, which will fail (length 3)');
+ await test_driver.send_keys(textarea, "de");
+ assert_equals(textarea.value,"abcde");
+ assert_true(textarea.validity.valid,'Now valid');
+ textarea.remove();
+},'User keystrokes should trigger validation');
+</script>
+
+<textarea id=t7 minlength=5 required></textarea>
+<script>
+promise_test(async () => {
+ const textarea = document.getElementById('t7');
+ textarea.addEventListener('input', (e) => {
+ e.target.defaultValue = e.target.value;
+ });
+ assert_false(textarea.validity.valid,'By default, this textarea will validate (and fail) because it started empty');
+ await test_driver.send_keys(textarea, "abc");
+ assert_equals(textarea.value,"abc");
+ assert_false(textarea.validity.valid,'Still invalid with 3 characters');
+ await test_driver.send_keys(textarea, "de");
+ assert_equals(textarea.value,"abcde");
+ assert_true(textarea.validity.valid,'With 5 characters, now valid');
+ textarea.remove();
+},'Setting textarea.defaultValue from the input event handler should trigger validation');
+</script>
+
+<textarea id=t8 minlength=5 required></textarea>
+<script>
+promise_test(async () => {
+ const textarea = document.getElementById('t8');
+ textarea.addEventListener('input', (e) => {
+ e.target.replaceChildren(e.target.value);
+ });
+ assert_false(textarea.validity.valid,'By default, this textarea will validate (and fail) because it started empty');
+ await test_driver.send_keys(textarea, "abc");
+ assert_equals(textarea.value,"abc");
+ assert_false(textarea.validity.valid,'Still invalid with 3 characters');
+ await test_driver.send_keys(textarea, "de");
+ assert_equals(textarea.value,"abcde");
+ assert_true(textarea.validity.valid,'With 5 characters, now valid');
+ textarea.remove();
+},'Calling textarea.replaceChildren() from the input event handler should trigger validation');
+</script>
+
+<style>
+ :invalid { background-color: rgb(248, 203, 203); }
+</style>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-button-element/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/forms/the-button-element/WEB_FEATURES.yml
new file mode 100644
index 0000000000..f5a2aeaf4f
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-button-element/WEB_FEATURES.yml
@@ -0,0 +1,10 @@
+features:
+- name: constraint-validation
+ files:
+ - button-checkvalidity.html
+ - button-setcustomvalidity.html
+ - button-validation.html
+ - button-validationmessage.html
+ - button-validity.html
+ - button-willvalidate-readonly-attribute.html
+ - button-willvalidate.html
diff --git a/testing/web-platform/tests/html/semantics/forms/the-fieldset-element/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/forms/the-fieldset-element/WEB_FEATURES.yml
new file mode 100644
index 0000000000..912cb47c6d
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-fieldset-element/WEB_FEATURES.yml
@@ -0,0 +1,8 @@
+features:
+- name: constraint-validation
+ files:
+ - fieldset-checkvalidity.html
+ - fieldset-setcustomvalidity.html
+ - fieldset-validationmessage.html
+ - fieldset-validity.html
+ - fieldset-willvalidate.html
diff --git a/testing/web-platform/tests/html/semantics/forms/the-form-element/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/forms/the-form-element/WEB_FEATURES.yml
new file mode 100644
index 0000000000..04c20cb5ec
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-form-element/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: constraint-validation
+ files:
+ - form-checkvalidity.html
diff --git a/testing/web-platform/tests/html/semantics/forms/the-input-element/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/forms/the-input-element/WEB_FEATURES.yml
new file mode 100644
index 0000000000..4957615f24
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-input-element/WEB_FEATURES.yml
@@ -0,0 +1,11 @@
+features:
+- name: constraint-validation
+ files:
+ - input-checkvalidity.html
+ - input-setcustomvalidity.html
+ - input-validationmessage.html
+ - input-validity.html
+ - input-willvalidate.html
+- name: show-picker-input
+ files:
+ - show-picker-*
diff --git a/testing/web-platform/tests/html/semantics/forms/the-input-element/input-stepdown-02.html b/testing/web-platform/tests/html/semantics/forms/the-input-element/input-stepdown-02.html
new file mode 100644
index 0000000000..db71d11009
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-input-element/input-stepdown-02.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<title>Input Step Down</title>
+
+<link rel="help" href="https://html.spec.whatwg.org/multipage/input.html#dom-input-stepup">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<input type='number' id='input'>
+
+<script>
+ const input = document.getElementById("input");
+
+ function testStepDown(initialValue, minValue, expectedValue) {
+ input.value = initialValue;
+ input.min = minValue;
+
+ input.stepDown();
+
+ assert_equals(input.value, expectedValue);
+ }
+
+ const tests = [
+ { initialValue: '', minValue: '', expectedValue: '-1', description: 'stepDown() on input with no initial or min values' },
+ { initialValue: '', minValue: '7', expectedValue: '7', description: 'stepDown() on input with no initial value and positive min value' },
+ { initialValue: '', minValue: '-7', expectedValue: '-1', description: 'stepDown() on input with no initial value and negative min value' },
+ { initialValue: '7', minValue: '7', expectedValue: '7', description: 'stepDown() on input with initial value equal to min value' },
+ { initialValue: '3', minValue: '7', expectedValue: '3', description: 'stepDown() on input with initial value less than min value' },
+ { initialValue: '10', minValue: '7', expectedValue: '9', description: 'stepDown() on input with initial value greater than min value' },
+ ];
+
+ for(const t of tests) {
+ test(()=>{
+ testStepDown(
+ t.initialValue,
+ t.minValue,
+ t.expectedValue
+ );
+ },
+ t.description);
+ }
+</script>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-output-element/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/forms/the-output-element/WEB_FEATURES.yml
new file mode 100644
index 0000000000..2f7f8e8cc2
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-output-element/WEB_FEATURES.yml
@@ -0,0 +1,5 @@
+features:
+- name: constraint-validation
+ files:
+ - output-setcustomvalidity.html
+ - output-validity.html
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/forms/the-select-element/WEB_FEATURES.yml
new file mode 100644
index 0000000000..9695c95294
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/WEB_FEATURES.yml
@@ -0,0 +1,9 @@
+features:
+- name: constraint-validation
+ files:
+ - select-setcustomvalidity.html
+ - select-validity.html
+ - select-willvalidate-readonly-attribute.html
+- name: show-picker-select
+ files:
+ - show-picker-*
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/native-popup-with-datalist-ref.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/native-popup-with-datalist-ref.html
new file mode 100644
index 0000000000..87918b6a92
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/native-popup-with-datalist-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<select>
+ <option>one</option>
+ <hr>
+ <option>two</option>
+</select>
+
+<script>
+(async () => {
+ await test_driver.click(document.querySelector('select'));
+ document.documentElement.classList.remove('reftest-wait');
+})();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/native-popup-with-datalist.tentative.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/native-popup-with-datalist.tentative.html
new file mode 100644
index 0000000000..a968c6a164
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/native-popup-with-datalist.tentative.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://github.com/whatwg/html/issues/9799">
+<link rel=match href="native-popup-with-datalist-ref.html">
+<link rel=assert title="The native popup of a select should show options descending from datalists">
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<select>
+ <datalist>
+ <div>
+ <option>one</option>
+ <hr>
+ <option>two</option>
+ </div>
+ </datalist>
+</select>
+
+<script>
+(async () => {
+ await test_driver.click(document.querySelector('select'));
+ document.documentElement.classList.remove('reftest-wait');
+})();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/nested-options.tenative.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/nested-options.tenative.html
new file mode 100644
index 0000000000..7e89a5ad42
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/nested-options.tenative.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://bugs.chromium.org/p/chromium/issues/detail?id=1422275">
+<link rel=help href="https://chromium-review.googlesource.com/c/chromium/src/+/5441435/1#message-cd8841d92a672a0276ab536dfaf3a20e93d5e6e3">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<select>
+ <datalist>
+ <option id=o1>
+ parent
+ <option id=o2>child</option>
+</option>
+ </datalist>
+</select>
+
+<script>
+const select = document.querySelector('select');
+
+test(() => {
+ assert_equals(select.innerHTML, `
+ <datalist>
+ <option id="o1">
+ parent
+ </option><option id="o2">child</option>
+
+ </datalist>
+`);
+}, 'The HTML parser should disallow nested options in select datalist.');
+
+// Manually nest the <options> anyway for the following tests.
+o1.appendChild(o2);
+
+test(() => {
+ assert_equals(select.options.length, 2, 'select.options.length');
+ assert_equals(select.options[0], o1, 'select.options[0]');
+ assert_equals(select.options[1], o2, 'select.options[1]');
+}, 'Nested <options> should be listed in <select> IDLs.');
+
+promise_test(async () => {
+ await test_driver.bless();
+ select.showPicker();
+}, 'Showing the popup with nested <option>s should not crash.');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/resources/select-reset-non-interoperable-styles.css b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/resources/select-reset-non-interoperable-styles.css
deleted file mode 100644
index d2b9d9df26..0000000000
--- a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/resources/select-reset-non-interoperable-styles.css
+++ /dev/null
@@ -1,5 +0,0 @@
-/* TODO(crbug.com/1511354): linux.css sets background-color on select, consider
- * removing it. */
-select {
- background-color: Field;
-}
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/resources/stylable-select-styles.css b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/resources/stylable-select-styles.css
index 042de838d1..5ee317d61a 100644
--- a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/resources/stylable-select-styles.css
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/resources/stylable-select-styles.css
@@ -1,9 +1,6 @@
/* These are UA styles for select and stylable select. */
.stylable-select-container {
- background-color: Field;
- border: 1px solid rgba(0, 0, 0, 0);
- border-radius: 0;
box-sizing: border-box;
display: inline-block;
}
@@ -14,7 +11,8 @@
overflow: auto;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 0.25em;
- padding: 0.25em 0;
+ padding-block: 0.25em;
+ padding-inline: 0;
background-color: Field;
margin: 0;
inset: auto;
@@ -35,3 +33,46 @@
padding: 0px 2px 1px;
white-space: nowrap;
}
+
+.stylable-select-button {
+ color: FieldText;
+ background-color: Field;
+ appearance: none;
+ padding: 0.25em;
+ border: 1px solid ButtonBorder;
+ cursor: default;
+ text-align: inherit;
+ display: inline-flex;
+ flex-grow: 1;
+ flex-shrink: 1;
+ align-items: center;
+ overflow-x: hidden;
+ overflow-y: hidden;
+}
+
+.stylable-select-button-icon {
+ background-image: url(data:image/svg+xml,%3Csvg%20width%3D%2220%22%20height%3D%2214%22%20viewBox%3D%220%200%2020%2016%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M4%206%20L10%2012%20L%2016%206%22%20stroke%3D%22WindowText%22%20stroke-width%3D%223%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E);
+ background-origin: content-box;
+ background-repeat: no-repeat;
+ background-size: contain;
+ opacity: 1;
+ outline: none;
+ margin-inline-start: 0.25em;
+ padding-block: 2px;
+ padding-inline: 3px;
+ block-size: 1.0em;
+ inline-size: 1.2em;
+ min-inline-size: 1.2em;
+ max-inline-size: 1.2em;
+ display: block;
+}
+
+.stylable-select-selectedoption {
+ color: inherit;
+ min-inline-size: 0px;
+ max-block-size: 100%;
+ flex-grow: 1;
+ flex-shrink: 1;
+ overflow: hidden;
+ display: inline;
+}
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-custom-datalist-ref.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-custom-button-no-datalist-ref.html
index 8e5eadaf57..10c966a107 100644
--- a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-custom-datalist-ref.html
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-custom-button-no-datalist-ref.html
@@ -2,7 +2,7 @@
<link rel=stylesheet href="resources/stylable-select-styles.css">
<div id=container class=stylable-select-container>
- <button popovertarget=popover id=button>one</button>
+ <button>one</button>
<div id=popover popover=auto anchor=container class=stylable-select-datalist>
<div class=stylable-select-option>one</div>
<div class=stylable-select-option>two</div>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-custom-button-no-datalist.tentative.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-custom-button-no-datalist.tentative.html
index 94d7fd53b3..aaceabcf05 100644
--- a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-custom-button-no-datalist.tentative.html
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-custom-button-no-datalist.tentative.html
@@ -2,8 +2,7 @@
<html class=reftest-wait>
<link rel=author href="mailto:jarhar@chromium.org">
<link rel=help href="https://github.com/whatwg/html/issues/9799">
-<link rel=match href="select-appearance-no-button-custom-datalist-ref.html">
-<link rel=stylesheet href="resources/select-reset-non-interoperable-styles.css">
+<link rel=match href="select-appearance-custom-button-no-datalist-ref.html">
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-custom-datalist.tentative.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-custom-datalist.tentative.html
index 87425cf7a3..cc8a4c60bd 100644
--- a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-custom-datalist.tentative.html
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-custom-datalist.tentative.html
@@ -2,8 +2,7 @@
<html class=reftest-wait>
<link rel=author href="mailto:jarhar@chromium.org">
<link rel=help href="https://github.com/whatwg/html/issues/9799">
-<link rel=match href="select-appearance-no-button-custom-datalist-ref.html">
-<link rel=stylesheet href="resources/select-reset-non-interoperable-styles.css">
+<link rel=match href="select-appearance-no-button-no-datalist-ref.html">
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-no-datalist-ref.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-no-datalist-ref.html
new file mode 100644
index 0000000000..3c6e9416b0
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-no-datalist-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<link rel=stylesheet href="resources/stylable-select-styles.css">
+
+<div id=container class=stylable-select-container>
+ <button class=stylable-select-button popovertarget=popover id=button>
+ <span class=stylable-select-selectedoption>one</span>
+ <div class=stylable-select-button-icon></div>
+ </button>
+ <div id=popover popover=auto anchor=container class=stylable-select-datalist>
+ <div class=stylable-select-option>one</div>
+ <div class=stylable-select-option>two</div>
+ </div>
+</div>
+
+<script>
+document.getElementById('popover').showPopover();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-no-datalist.tentative.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-no-datalist.tentative.html
index b2a6b5a6d3..29261b7f25 100644
--- a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-no-datalist.tentative.html
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-no-button-no-datalist.tentative.html
@@ -2,8 +2,7 @@
<html class=reftest-wait>
<link rel=author href="mailto:jarhar@chromium.org">
<link rel=help href="https://github.com/whatwg/html/issues/9799">
-<link rel=match href="select-appearance-no-button-custom-datalist-ref.html">
-<link rel=stylesheet href="resources/select-reset-non-interoperable-styles.css">
+<link rel=match href="select-appearance-no-button-no-datalist-ref.html">
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-lr-ref.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-lr-ref.html
new file mode 100644
index 0000000000..8b7e6402fb
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-lr-ref.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<link rel=stylesheet href="resources/stylable-select-styles.css">
+
+<style>
+html {
+ writing-mode: vertical-lr;
+}
+</style>
+
+<div id=container class=stylable-select-container>
+ <button class=stylable-select-button popovertarget=popover id=button>
+ <span class=stylable-select-selectedoption>one</span>
+ <div class=stylable-select-button-icon></div>
+ </button>
+ <div id=popover popover=auto anchor=container class=stylable-select-datalist>
+ <div class=stylable-select-option>one</div>
+ <div class=stylable-select-option>two</div>
+ </div>
+</div>
+
+<script>
+document.getElementById('popover').showPopover();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-lr.tentative.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-lr.tentative.html
new file mode 100644
index 0000000000..2f8a6aa886
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-lr.tentative.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://github.com/whatwg/html/issues/9799">
+<link rel=match href="select-appearance-writing-mode-vertical-lr-ref.html">
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<style>
+html {
+ writing-mode: vertical-lr;
+}
+select {
+ appearance: base-select;
+}
+</style>
+
+<select>
+ <option>one</option>
+ <option>two</option>
+</select>
+
+<script>
+(async () => {
+ await test_driver.bless();
+ document.querySelector('select').showPicker();
+ document.documentElement.classList.remove('reftest-wait');
+})();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-rl-ref.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-rl-ref.html
new file mode 100644
index 0000000000..158807ba58
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-rl-ref.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<link rel=stylesheet href="resources/stylable-select-styles.css">
+
+<style>
+html {
+ writing-mode: vertical-rl;
+}
+</style>
+
+<div id=container class=stylable-select-container>
+ <button class=stylable-select-button popovertarget=popover id=button>
+ <span class=stylable-select-selectedoption>one</span>
+ <div class=stylable-select-button-icon></div>
+ </button>
+ <div id=popover popover=auto anchor=container class=stylable-select-datalist>
+ <div class=stylable-select-option>one</div>
+ <div class=stylable-select-option>two</div>
+ </div>
+</div>
+
+<script>
+document.getElementById('popover').showPopover();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-rl.tentative.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-rl.tentative.html
new file mode 100644
index 0000000000..c2ea647be9
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-appearance-writing-mode-vertical-rl.tentative.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://github.com/whatwg/html/issues/9799">
+<link rel=match href="select-appearance-writing-mode-vertical-rl-ref.html">
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<style>
+html {
+ writing-mode: vertical-rl;
+}
+select {
+ appearance: base-select;
+}
+</style>
+
+<select>
+ <option>one</option>
+ <option>two</option>
+</select>
+
+<script>
+(async () => {
+ await test_driver.bless();
+ document.querySelector('select').showPicker();
+ document.documentElement.classList.remove('reftest-wait');
+})();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-child-button-and-datalist-invalidation.tentative.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-child-button-and-datalist-invalidation.tentative.html
index b6d85ac90a..822a63e104 100644
--- a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-child-button-and-datalist-invalidation.tentative.html
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-child-button-and-datalist-invalidation.tentative.html
@@ -3,7 +3,6 @@
<link rel=author href="mailto:jarhar@chromium.org">
<link rel=help href="https://github.com/whatwg/html/issues/9799">
<link rel=match href="select-child-button-and-datalist-ref.html">
-<link rel=stylesheet href="resources/select-reset-non-interoperable-styles.css">
<style>
.blue {
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-child-button-and-datalist.tentative.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-child-button-and-datalist.tentative.html
index 610861aad8..9b2f53df28 100644
--- a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-child-button-and-datalist.tentative.html
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-child-button-and-datalist.tentative.html
@@ -2,7 +2,6 @@
<link rel=author href="mailto:jarhar@chromium.org">
<link rel=help href="https://github.com/whatwg/html/issues/9799">
<link rel=match href="select-child-button-and-datalist-ref.html">
-<link rel=stylesheet href="resources/select-reset-non-interoperable-styles.css">
<style>
.blue {
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-datalist-popover-behavior.tentative.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-datalist-popover-behavior.tentative.html
new file mode 100644
index 0000000000..caea2a2f8d
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-datalist-popover-behavior.tentative.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://bugs.chromium.org/p/chromium/issues/detail?id=1422275">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+
+<select style="appearance:base-select">
+ <button type=popover>button</button>
+ <datalist>
+ <option class=one>one</option>
+ <option class=two>two</option>
+ </datalist>
+</select>
+
+<script>
+const select = document.querySelector('select');
+const datalist = document.querySelector('datalist');
+const firstOption = document.querySelector('option.one');
+const secondOption = document.querySelector('option.two');
+
+promise_test(async () => {
+ datalist.showPopover();
+ assert_true(datalist.matches(':popover-open'));
+ datalist.hidePopover();
+ assert_false(datalist.matches(':popover-open'));
+}, 'showPopover and hidePopover should work on the select datalist.');
+
+promise_test(async () => {
+ await test_driver.bless();
+ select.showPicker();
+ assert_true(datalist.matches(':popover-open'));
+ datalist.hidePopover();
+}, 'showPicker should show the select datalist.');
+
+promise_test(async () => {
+ datalist.addEventListener('beforetoggle', event => {
+ event.preventDefault();
+ }, {once: true});
+ await test_driver.bless();
+ select.showPicker();
+ assert_false(datalist.matches(':popover-open'));
+}, 'preventDefault on beforetoggle should prevent the datalist from showing.');
+
+promise_test(async () => {
+ select.remove();
+ assert_throws_dom('InvalidStateError', () => datalist.showPopover());
+ assert_false(datalist.matches(':popover-open'));
+ document.body.appendChild(select);
+}, 'showPopover on a disconnected datalist should throw an exception.');
+
+promise_test(async () => {
+ datalist.addEventListener('beforetoggle', event => {
+ select.remove();
+ }, {once: true});
+ await test_driver.bless();
+ select.showPicker();
+ assert_false(!!select.parentNode);
+ assert_false(datalist.matches(':popover-open'));
+ document.body.appendChild(select);
+}, 'Disconnecting while internally showing the datalist should not crash or show the popover.');
+
+promise_test(async () => {
+ datalist.showPopover();
+ datalist.addEventListener('beforetoggle', event => {
+ select.remove();
+ }, {once: true});
+ await test_driver.click(secondOption);
+ assert_false(!!select.parentNode);
+ assert_false(datalist.matches(':popover-open'));
+ document.body.appendChild(select);
+}, 'Disconnecting while internally hiding the datalist should not crash.');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-keyboard-behavior.tentative.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-keyboard-behavior.tentative.html
index 2fb11ba68b..8b06212169 100644
--- a/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-keyboard-behavior.tentative.html
+++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/stylable-select/select-keyboard-behavior.tentative.html
@@ -1,4 +1,5 @@
<!DOCTYPE html>
+<meta name=timeout content=long>
<link rel=author href="mailto:jarhar@chromium.org">
<link rel=help href="https://bugs.chromium.org/p/chromium/issues/detail?id=1422275">
<link rel=help href="https://github.com/openui/open-ui/issues/433#issuecomment-1452461404">
@@ -68,6 +69,7 @@ for (const id of ['defaultbutton-defaultdatalist',
async function closeListbox() {
await test_driver.click(select);
+ await new Promise(requestAnimationFrame);
}
function addCloseCleanup(t) {
@@ -96,6 +98,7 @@ for (const id of ['defaultbutton-defaultdatalist',
assert_false(select.matches(':open'),
'The select should initially be closed.');
await test_driver.send_keys(document.activeElement, Space);
+ await new Promise(requestAnimationFrame);
assert_true(select.matches(':open'),
'The select should be open after pressing space.');
}, `${id}: When the listbox is closed, spacebar should open the listbox.`);
@@ -108,6 +111,7 @@ for (const id of ['defaultbutton-defaultdatalist',
'The select should initially be closed.');
await test_driver.send_keys(document.activeElement, ArrowLeft);
+ await new Promise(requestAnimationFrame);
assert_true(select.matches(':open'),
'Arrow left should open the listbox.');
assert_equals(select.value, 'two',
@@ -115,6 +119,7 @@ for (const id of ['defaultbutton-defaultdatalist',
await closeListbox();
await test_driver.send_keys(document.activeElement, ArrowUp);
+ await new Promise(requestAnimationFrame);
assert_true(select.matches(':open'),
'Arrow up should open the listbox.');
assert_equals(select.value, 'two',
@@ -122,6 +127,7 @@ for (const id of ['defaultbutton-defaultdatalist',
await closeListbox();
await test_driver.send_keys(document.activeElement, ArrowRight);
+ await new Promise(requestAnimationFrame);
assert_true(select.matches(':open'),
'Arrow right should open the listbox.');
assert_equals(select.value, 'two',
@@ -129,6 +135,7 @@ for (const id of ['defaultbutton-defaultdatalist',
await closeListbox();
await test_driver.send_keys(document.activeElement, ArrowDown);
+ await new Promise(requestAnimationFrame);
assert_true(select.matches(':open'),
'Arrow down should open the listbox.');
assert_equals(select.value, 'two',
@@ -147,6 +154,7 @@ for (const id of ['defaultbutton-defaultdatalist',
} else {
await test_driver.send_keys(select, Enter);
}
+ await new Promise(requestAnimationFrame);
assert_false(select.matches(':open'),
'Enter should not open the listbox when outside a form.');
@@ -161,6 +169,7 @@ for (const id of ['defaultbutton-defaultdatalist',
} else {
await test_driver.send_keys(select, Enter);
}
+ await new Promise(requestAnimationFrame);
assert_true(formWasSubmitted,
'Enter should submit the form when the listbox is closed.');
assert_false(select.matches(':open'),
@@ -175,30 +184,35 @@ for (const id of ['defaultbutton-defaultdatalist',
select.value = 'two';
await test_driver.click(select);
+ await new Promise(requestAnimationFrame);
assert_true(select.matches(':open'),
'The select should open when clicked.');
assert_equals(document.activeElement, optionTwo,
'The selected option should receive initial focus.');
await test_driver.send_keys(document.activeElement, ArrowDown);
+ await new Promise(requestAnimationFrame);
assert_equals(document.activeElement, optionThree,
'The next option should receive focus when the down arrow key is pressed.');
assert_equals(select.value, 'two',
'The selects value should not change when focusing another option.');
await test_driver.send_keys(document.activeElement, ArrowUp);
+ await new Promise(requestAnimationFrame);
assert_equals(document.activeElement, optionTwo,
'The previous option should receive focus when the up arrow key is pressed.');
assert_equals(select.value, 'two',
'The selects value should not change when focusing another option.');
await test_driver.send_keys(document.activeElement, ArrowUp);
+ await new Promise(requestAnimationFrame);
assert_equals(document.activeElement, optionOne,
'The first option should be selected.');
assert_equals(select.value, 'two',
'The selects value should not change when focusing another option.');
await test_driver.send_keys(document.activeElement, Enter);
+ await new Promise(requestAnimationFrame);
assert_false(select.matches(':open'),
'The listbox should be closed after pressing enter.');
assert_equals(select.value, 'one',
diff --git a/testing/web-platform/tests/html/semantics/forms/the-selectlist-element/selectlist-option-arbitrary-content-displayed.tentative.html b/testing/web-platform/tests/html/semantics/forms/the-selectlist-element/selectlist-option-arbitrary-content-displayed.tentative.html
index 2d51002fb2..8c620bacca 100644
--- a/testing/web-platform/tests/html/semantics/forms/the-selectlist-element/selectlist-option-arbitrary-content-displayed.tentative.html
+++ b/testing/web-platform/tests/html/semantics/forms/the-selectlist-element/selectlist-option-arbitrary-content-displayed.tentative.html
@@ -3,6 +3,8 @@
<title>HTMLSelectListElement Test: option arbitrary content displayed</title>
<link rel="author" title="Ionel Popescu" href="mailto:iopopesc@microsoft.com">
<link rel=match href="selectlist-option-arbitrary-content-displayed-ref.tentative.html">
+<!-- Tolerate slight differences in the shadow gradient. -->
+<meta name=fuzzy content="maxDifference=0-1;totalPixels=0-200">
<link rel="stylesheet" href="/fonts/ahem.css">
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-actions.js"></script>
diff --git a/testing/web-platform/tests/html/semantics/forms/the-textarea-element/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/forms/the-textarea-element/WEB_FEATURES.yml
new file mode 100644
index 0000000000..14479d4bb3
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/forms/the-textarea-element/WEB_FEATURES.yml
@@ -0,0 +1,6 @@
+features:
+- name: constraint-validation
+ files:
+ - textarea-setcustomvalidity.html
+ - textarea-validity-clone.html
+ - textarea-validity-valueMissing-inside-datalist.html
diff --git a/testing/web-platform/tests/html/semantics/interactive-elements/the-details-element/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/interactive-elements/the-details-element/WEB_FEATURES.yml
new file mode 100644
index 0000000000..be3924a26a
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/interactive-elements/the-details-element/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: details-name
+ files:
+ - name-attribute.html
diff --git a/testing/web-platform/tests/html/semantics/invokers/interesttarget-anchor-event-dispatch.tentative.html b/testing/web-platform/tests/html/semantics/invokers/interesttarget-anchor-event-dispatch.tentative.html
new file mode 100644
index 0000000000..b5a481ae08
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/invokers/interesttarget-anchor-event-dispatch.tentative.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<meta charset="utf-8" />
+<meta name="author" title="Keith Cirkel" href="mailto:keithamus@github.com" />
+<meta name="author" title="Luke Warlow" href="mailto:lwarlow@igalia.com" />
+<link rel="help" href="https://open-ui.org/components/invokers.explainer/" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/invoker-utils.js"></script>
+
+<div id="interestee"></div>
+<a href="/" id="interestanchor" interesttarget="interestee">Anchor</a>
+<button id="otherbutton">Other Button</button>
+
+<script>
+ promise_test(async function (t) {
+ t.add_cleanup(() => otherbutton.focus());
+ let event = null;
+ interestee.addEventListener("interest", (e) => (event = e), { once: true });
+ interestanchor.focus();
+ assert_true(event instanceof InterestEvent, "event is InterestEvent");
+ assert_equals(event.type, "interest", "type");
+ assert_equals(event.bubbles, false, "bubbles");
+ assert_equals(event.composed, true, "composed");
+ assert_equals(event.isTrusted, true, "isTrusted");
+ assert_equals(event.action, "", "action");
+ assert_equals(event.target, interestee, "target");
+ assert_equals(event.invoker, interestanchor, "invoker");
+ }, "InterestEvent dispatches on anchor focus");
+
+ promise_test(async function (t) {
+ t.add_cleanup(() => otherbutton.focus());
+ let event = null;
+ interestee.addEventListener("interest", (e) => (event = e), { once: true });
+ await hoverOver(interestanchor);
+ assert_true(event instanceof InterestEvent, "event is InterestEvent");
+ assert_equals(event.type, "interest", "type");
+ assert_equals(event.bubbles, false, "bubbles");
+ assert_equals(event.composed, true, "composed");
+ assert_equals(event.isTrusted, true, "isTrusted");
+ assert_equals(event.action, "", "action");
+ assert_equals(event.target, interestee, "target");
+ assert_equals(event.invoker, interestanchor, "invoker");
+ }, "InterestEvent dispatches on anchor hover");
+</script>
diff --git a/testing/web-platform/tests/html/semantics/invokers/interesttarget-area-event-dispatch.tentative.html b/testing/web-platform/tests/html/semantics/invokers/interesttarget-area-event-dispatch.tentative.html
new file mode 100644
index 0000000000..358acbb73a
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/invokers/interesttarget-area-event-dispatch.tentative.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset="utf-8" />
+<meta name="author" title="Keith Cirkel" href="mailto:keithamus@github.com" />
+<meta name="author" title="Luke Warlow" href="mailto:lwarlow@igalia.com" />
+<link rel="help" href="https://open-ui.org/components/invokers.explainer/" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/invoker-utils.js"></script>
+
+<div id="interestee"></div>
+<map id="map">
+ <area href="/" shape="default" id="interestarea" interesttarget="interestee">
+</map>
+<img src="/images/blue.png" usemap="#map">
+<button id="otherbutton">Other Button</button>
+
+<script>
+ promise_test(async function (t) {
+ t.add_cleanup(() => otherbutton.focus());
+ let event = null;
+ interestee.addEventListener("interest", (e) => (event = e), { once: true });
+ interestarea.focus();
+ assert_true(event instanceof InterestEvent, "event is InterestEvent");
+ assert_equals(event.type, "interest", "type");
+ assert_equals(event.bubbles, false, "bubbles");
+ assert_equals(event.composed, true, "composed");
+ assert_equals(event.isTrusted, true, "isTrusted");
+ assert_equals(event.action, "", "action");
+ assert_equals(event.target, interestee, "target");
+ assert_equals(event.invoker, interestarea, "invoker");
+ }, "InterestEvent dispatches on area focus");
+
+ promise_test(async function (t) {
+ t.add_cleanup(() => otherbutton.focus());
+ let event = null;
+ interestee.addEventListener("interest", (e) => (event = e), { once: true });
+ await hoverOver(interestarea);
+ assert_true(event instanceof InterestEvent, "event is InterestEvent");
+ assert_equals(event.type, "interest", "type");
+ assert_equals(event.bubbles, false, "bubbles");
+ assert_equals(event.composed, true, "composed");
+ assert_equals(event.isTrusted, true, "isTrusted");
+ assert_equals(event.action, "", "action");
+ assert_equals(event.target, interestee, "target");
+ assert_equals(event.invoker, interestarea, "invoker");
+ }, "InterestEvent dispatches on area hover");
+</script>
diff --git a/testing/web-platform/tests/html/semantics/invokers/interesttarget-button-event-dispatch.tentative.html b/testing/web-platform/tests/html/semantics/invokers/interesttarget-button-event-dispatch.tentative.html
index 7fdfdfaa70..69126dbe14 100644
--- a/testing/web-platform/tests/html/semantics/invokers/interesttarget-button-event-dispatch.tentative.html
+++ b/testing/web-platform/tests/html/semantics/invokers/interesttarget-button-event-dispatch.tentative.html
@@ -12,7 +12,6 @@
<div id="interestee"></div>
<button id="interestbutton" interesttarget="interestee">Button</button>
-<a href="/" id="interestanchor" interesttarget="interestee">Anchor</a>
<button id="otherbutton">Other Button</button>
<script>
@@ -50,36 +49,6 @@
t.add_cleanup(() => otherbutton.focus());
let event = null;
interestee.addEventListener("interest", (e) => (event = e), { once: true });
- interestanchor.focus();
- assert_true(event instanceof InterestEvent, "event is InterestEvent");
- assert_equals(event.type, "interest", "type");
- assert_equals(event.bubbles, false, "bubbles");
- assert_equals(event.composed, true, "composed");
- assert_equals(event.isTrusted, true, "isTrusted");
- assert_equals(event.action, "", "action");
- assert_equals(event.target, interestee, "target");
- assert_equals(event.invoker, interestanchor, "invoker");
- }, "InterestEvent dispatches on anchor focus");
-
- promise_test(async function (t) {
- t.add_cleanup(() => otherbutton.focus());
- let event = null;
- interestee.addEventListener("interest", (e) => (event = e), { once: true });
- await hoverOver(interestanchor);
- assert_true(event instanceof InterestEvent, "event is InterestEvent");
- assert_equals(event.type, "interest", "type");
- assert_equals(event.bubbles, false, "bubbles");
- assert_equals(event.composed, true, "composed");
- assert_equals(event.isTrusted, true, "isTrusted");
- assert_equals(event.action, "", "action");
- assert_equals(event.target, interestee, "target");
- assert_equals(event.invoker, interestanchor, "invoker");
- }, "InterestEvent dispatches on anchor hover");
-
- promise_test(async function (t) {
- t.add_cleanup(() => otherbutton.focus());
- let event = null;
- interestee.addEventListener("interest", (e) => (event = e), { once: true });
interestbutton.interestAction = "fooBar";
interestbutton.focus();
assert_true(event instanceof InterestEvent, "event is InterestEvent");
diff --git a/testing/web-platform/tests/html/semantics/invokers/interesttarget-svg-a-event-dispatch.tentative.html b/testing/web-platform/tests/html/semantics/invokers/interesttarget-svg-a-event-dispatch.tentative.html
new file mode 100644
index 0000000000..7fb4b1c19d
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/invokers/interesttarget-svg-a-event-dispatch.tentative.html
@@ -0,0 +1,51 @@
+<!doctype html>
+<meta charset="utf-8" />
+<meta name="author" title="Keith Cirkel" href="mailto:keithamus@github.com" />
+<meta name="author" title="Luke Warlow" href="mailto:lwarlow@igalia.com" />
+<link rel="help" href="https://open-ui.org/components/invokers.explainer/" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/invoker-utils.js"></script>
+
+<div id="interestee"></div>
+<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <a href="/" id="interestsvga" interesttarget="interestee">
+ <text x="50" y="90">SVG A</text>
+ </a>
+</svg>
+<button id="otherbutton">Other Button</button>
+
+<script>
+ promise_test(async function (t) {
+ t.add_cleanup(() => otherbutton.focus());
+ let event = null;
+ interestee.addEventListener("interest", (e) => (event = e), { once: true });
+ interestsvga.focus();
+ assert_true(event instanceof InterestEvent, "event is InterestEvent");
+ assert_equals(event.type, "interest", "type");
+ assert_equals(event.bubbles, false, "bubbles");
+ assert_equals(event.composed, true, "composed");
+ assert_equals(event.isTrusted, true, "isTrusted");
+ assert_equals(event.action, "", "action");
+ assert_equals(event.target, interestee, "target");
+ assert_equals(event.invoker, interestsvga, "invoker");
+ }, "InterestEvent dispatches on svg a focus");
+
+ promise_test(async function (t) {
+ t.add_cleanup(() => otherbutton.focus());
+ let event = null;
+ interestee.addEventListener("interest", (e) => (event = e), { once: true });
+ await hoverOver(interestsvga);
+ assert_true(event instanceof InterestEvent, "event is InterestEvent");
+ assert_equals(event.type, "interest", "type");
+ assert_equals(event.bubbles, false, "bubbles");
+ assert_equals(event.composed, true, "composed");
+ assert_equals(event.isTrusted, true, "isTrusted");
+ assert_equals(event.action, "", "action");
+ assert_equals(event.target, interestee, "target");
+ assert_equals(event.invoker, interestsvga, "invoker");
+ }, "InterestEvent dispatches on svg a hover");
+</script>
diff --git a/testing/web-platform/tests/html/semantics/invokers/invoketarget-on-input-number.tentative.html b/testing/web-platform/tests/html/semantics/invokers/invoketarget-on-input-number.tentative.html
new file mode 100644
index 0000000000..b06053b9f1
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/invokers/invoketarget-on-input-number.tentative.html
@@ -0,0 +1,78 @@
+<!doctype html>
+<meta charset="utf-8" />
+<meta name="author" title="Luke Warlow" href="mailto:lwarlow@igalia.com" />
+<link rel="help" href="https://open-ui.org/components/invokers.explainer/" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/invoker-utils.js"></script>
+
+<input type="number" id="invokee" value="0">
+<button id="invokerbutton" invoketarget="invokee"></button>
+
+<script>
+ function reset() {
+ invokee.value = 0;
+ invokerbutton.removeAttribute('invokeaction');
+ }
+
+ // stepUp
+
+ promise_test(async function (t) {
+ t.add_cleanup(reset);
+ assert_equals(invokee.valueAsNumber, 0);
+ invokerbutton.setAttribute("invokeaction", "stepup");
+ await clickOn(invokerbutton);
+ assert_equals(invokee.valueAsNumber, 1);
+ }, "invoking number input with stepup action increments value");
+
+ promise_test(async function (t) {
+ t.add_cleanup(reset);
+ assert_equals(invokee.valueAsNumber, 0);
+ invokerbutton.setAttribute("invokeaction", "sTePuP");
+ await clickOn(invokerbutton);
+ assert_equals(invokee.valueAsNumber, 1);
+ }, "invoking number input with stepup action (case-insensitive) increments value");
+
+ promise_test(async function (t) {
+ t.add_cleanup(reset);
+ assert_equals(invokee.valueAsNumber, 0);
+ invokerbutton.setAttribute("invokeaction", "stepup");
+ invokee.addEventListener("invoke", (e) => e.preventDefault(), {
+ once: true,
+ });
+ await clickOn(invokerbutton);
+ assert_equals(invokee.valueAsNumber, 0);
+ }, "invoking number input with stepup action and preventDefault does not increment value");
+
+ // stepDown
+
+ promise_test(async function (t) {
+ t.add_cleanup(reset);
+ assert_equals(invokee.valueAsNumber, 0);
+ invokerbutton.setAttribute("invokeaction", "stepdown");
+ await clickOn(invokerbutton);
+ assert_equals(invokee.valueAsNumber, -1);
+ }, "invoking number input with stepdown action decrements value");
+
+ promise_test(async function (t) {
+ t.add_cleanup(reset);
+ assert_equals(invokee.valueAsNumber, 0);
+ invokerbutton.setAttribute("invokeaction", "sTePdOwN");
+ await clickOn(invokerbutton);
+ assert_equals(invokee.valueAsNumber, -1);
+ }, "invoking number input with stepdown action (case-insensitive) decrements value");
+
+ promise_test(async function (t) {
+ t.add_cleanup(reset);
+ assert_equals(invokee.valueAsNumber, 0);
+ invokerbutton.setAttribute("invokeaction", "stepdown");
+ invokee.addEventListener("invoke", (e) => e.preventDefault(), {
+ once: true,
+ });
+ await clickOn(invokerbutton);
+ assert_equals(invokee.valueAsNumber, 0);
+ }, "invoking number input with stepdown action and preventDefault does not decrement value");
+</script>
diff --git a/testing/web-platform/tests/html/semantics/permission-element/bounded-css-properties-reference-expected.html b/testing/web-platform/tests/html/semantics/permission-element/bounded-css-properties-reftest-ref.html
index c62ff5b24d..c62ff5b24d 100644
--- a/testing/web-platform/tests/html/semantics/permission-element/bounded-css-properties-reference-expected.html
+++ b/testing/web-platform/tests/html/semantics/permission-element/bounded-css-properties-reftest-ref.html
diff --git a/testing/web-platform/tests/html/semantics/permission-element/bounded-css-properties-reference.tentative.html b/testing/web-platform/tests/html/semantics/permission-element/bounded-css-properties-reftest.tentative.html
index b8337ab87d..ad6986f52b 100644
--- a/testing/web-platform/tests/html/semantics/permission-element/bounded-css-properties-reference.tentative.html
+++ b/testing/web-platform/tests/html/semantics/permission-element/bounded-css-properties-reftest.tentative.html
@@ -1,6 +1,6 @@
<!DOCTYPE html>
<meta charset=utf-8>
-<link rel="match" href="bounded-css-properties-reference-expected.html">
+<link rel="match" href="bounded-css-properties-reftest-ref.html">
<link rel="help" href="https://github.com/WICG/PEPC/blob/main/explainer.md#locking-the-pepc-style">
<body>
<div>
diff --git a/testing/web-platform/tests/html/semantics/permission-element/bounded-sizes-reftest-ref.html b/testing/web-platform/tests/html/semantics/permission-element/bounded-sizes-reftest-ref.html
new file mode 100644
index 0000000000..b186dd6445
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/permission-element/bounded-sizes-reftest-ref.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+<meta charset=utf-8>
+<body>
+ <div>
+ The permission element should have some limits for the min/max-width/height:
+ <ul>
+ <li>min-width should be sufficient to fit the element text (depends on user agent implementation)</li>
+ <li>max-width should be at most 3x min-width</li>
+ <li>min-height should be sufficient to fit the element text (1em)</li>
+ <li>max-height should be at most 3x min-height</li>
+ <li>padding-left/top only work with width/height: auto and are at most 5em/1em</li>
+ <li>padding-right/bottom are copied over from padding-left/top in this case</li>
+ </ul>
+ </div>
+
+ <style>
+ #id1 {
+ font-size: 10px;
+ height: 10px;
+ /* width set via JS */
+ }
+ #id2 {
+ font-size: 10px;
+ height: 30px;
+ /* width set via JS */
+ }
+ #id3 {
+ font-size: 10px;
+ height: 30px;
+ color:black;
+ background-color: black;
+
+ /* Used to compute width which will then have the padding
+ artificially added in JS */
+ width: fit-content;
+ }
+ </style>
+
+ <div><permission id="id1" type="geolocation"></div>
+ <div><permission id="id2" type="camera"></div>
+ <div><permission id="id3" type="microphone"></div>
+
+<script>
+ let el = document.getElementById("id1");
+ el.style.width = getComputedStyle(el).minWidth;
+
+ el = document.getElementById("id2");
+ el.style.width = getComputedStyle(el).maxWidth;
+
+ el = document.getElementById("id3");
+ let w = getComputedStyle(el).width;
+ el.style.width = `calc(${w} + 100px)`; // 100px is 2 * 5em;
+</script>
+</body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/semantics/permission-element/bounded-sizes-reftest.tentative.html b/testing/web-platform/tests/html/semantics/permission-element/bounded-sizes-reftest.tentative.html
new file mode 100644
index 0000000000..45ffb633c3
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/permission-element/bounded-sizes-reftest.tentative.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<link rel="match" href="bounded-sizes-reftest-ref.html">
+<link rel="help" href="https://github.com/WICG/PEPC/blob/main/explainer.md#locking-the-pepc-style">
+<body>
+ <div>
+ The permission element should have some limits for the min/max-width/height:
+ <ul>
+ <li>min-width should be sufficient to fit the element text (depends on user agent implementation)</li>
+ <li>max-width should be at most 3x min-width</li>
+ <li>min-height should be sufficient to fit the element text (1em)</li>
+ <li>max-height should be at most 3x min-height</li>
+ <li>padding-left/top only work with width/height: auto and are at most 5em/1em</li>
+ <li>padding-right/bottom are copied over from padding-left/top in this case</li>
+ </ul>
+ </div>
+
+<style>
+ #id1 {
+ font-size: 10px;
+ min-height: 1px;
+ max-height: 100px;
+
+ /* These values are extreme enough that they should be out of bounds for any implementation */
+ min-width: 10px;
+ max-width: 1000px;
+
+ /* This element will be as tiny as possible */
+ width: 1px;
+ height: 1px;
+ }
+ #id2 {
+ font-size: 10px;
+ min-height: 1px;
+ max-height: 100px;
+
+ /* These values are extreme enough that they should be out of bounds for any implementation */
+ min-width: 10px;
+ max-width: 1000px;
+
+ /* This element will be as large as possible */
+ width: 1000px;
+ height: 1000px;
+ }
+ #id3 {
+ font-size: 10px;
+ width: auto;
+ height: auto;
+
+ /* There is a slight misalignment of the text (by 1px) when using
+ padding vs using width/height. Since this test's purpose is to
+ check that the bounds are identical, the color and background-color
+ are set to the same value to cover the slight text misalignment */
+ color:black;
+ background-color: black;
+
+ /* Only padding-top and padding-left are taken into account */
+ padding-top: 1000px;
+ padding-left: 1000px;
+ padding-bottom: 1px;
+ padding-right: 1px;
+ }
+</style>
+
+<div><permission id="id1" type="geolocation"></div>
+<div><permission id="id2" type="camera"></div>
+<div><permission id="id3" type="microphone"></div>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/semantics/permission-element/bounded-sizes.tentative.html b/testing/web-platform/tests/html/semantics/permission-element/bounded-sizes.tentative.html
new file mode 100644
index 0000000000..2010cd0a54
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/permission-element/bounded-sizes.tentative.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<link rel="help" href="https://github.com/WICG/PEPC/blob/main/explainer.md#locking-the-pepc-style">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<!--The permission element should have some limits for the min/max-width/height:
+ * min-width should be sufficient to fit the element text (depends on user agent implementation)
+ * max-width should be at most 3x min-width
+ * min-height should be sufficient to fit the element text (1em)
+ * max-height should be at most 3x min-height
+-->
+<style>
+ #id1 {
+ font-size: 10px;
+ width: auto;
+ height: auto;
+
+ min-height: 1px;
+ max-height: 100px;
+
+ padding-top: 12px;
+ padding-left: 60px;
+ padding-bottom: 1000px;
+ padding-right: 1000px;
+
+ /* These values are extreme enough that they should be out of bounds for any implementation */
+ min-width: 10px;
+ max-width: 1000px;
+ }
+ #id2 {
+ font-size: 10px;
+ width: auto;
+ height: auto;
+
+ min-height: 11px;
+ max-height: 29px;
+
+ padding-top: 5px;
+ padding-left: 45px;
+ padding-bottom: 6px;
+ padding-right: 46px;
+ }
+</style>
+
+
+<permission id="id1" type="geolocation">
+<permission id="id2" type="camera">
+
+<script>
+ test(function(){
+ let el_outside_bounds = document.getElementById("id1");
+ let min_height = getComputedStyle(el_outside_bounds).minHeight;
+ let max_height = getComputedStyle(el_outside_bounds).maxHeight;
+ assert_true(min_height === "calc(10px)" || min_height === "10px", "min-height");
+ assert_true(max_height === "calc(30px)" || max_height === "30px", "max-height");
+ assert_not_equals(getComputedStyle(el_outside_bounds).minWidth, "10px", "min-width");
+ assert_not_equals(getComputedStyle(el_outside_bounds).maxWidth, "1000px", "max-width");
+ assert_equals(getComputedStyle(el_outside_bounds).paddingLeft, "50px", "padding-left");
+ assert_equals(getComputedStyle(el_outside_bounds).paddingRight, "50px", "padding-right");
+ assert_equals(getComputedStyle(el_outside_bounds).paddingTop, "10px", "padding-top");
+ assert_equals(getComputedStyle(el_outside_bounds).paddingBottom, "10px", "padding-bottom");
+ }, "Properties with out-of-bounds values should be corrected");
+
+ test(function(){
+ let el_inside_bounds = document.getElementById("id2");
+ assert_equals(getComputedStyle(el_inside_bounds).minHeight, "calc(11px)", "min-height");
+ assert_equals(getComputedStyle(el_inside_bounds).maxHeight, "calc(29px)", "max-height");
+ assert_equals(getComputedStyle(el_inside_bounds).paddingLeft, "45px", "padding-left");
+ assert_equals(getComputedStyle(el_inside_bounds).paddingRight, "45px", "padding-right");
+ assert_equals(getComputedStyle(el_inside_bounds).paddingTop, "5px", "padding-top");
+ assert_equals(getComputedStyle(el_inside_bounds).paddingBottom, "5px", "padding-bottom");
+ }, "Properties with values in bounds should not be modified");
+</script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/semantics/permission-element/display-css-property-reference-expected.html b/testing/web-platform/tests/html/semantics/permission-element/display-css-property-reftest-ref.html
index 6a04c94c03..6a04c94c03 100644
--- a/testing/web-platform/tests/html/semantics/permission-element/display-css-property-reference-expected.html
+++ b/testing/web-platform/tests/html/semantics/permission-element/display-css-property-reftest-ref.html
diff --git a/testing/web-platform/tests/html/semantics/permission-element/display-css-property-reference.tentative.html b/testing/web-platform/tests/html/semantics/permission-element/display-css-property-reftest.tentative.html
index 973a76d723..e83786373d 100644
--- a/testing/web-platform/tests/html/semantics/permission-element/display-css-property-reference.tentative.html
+++ b/testing/web-platform/tests/html/semantics/permission-element/display-css-property-reftest.tentative.html
@@ -1,6 +1,6 @@
<!DOCTYPE html>
<meta charset=utf-8>
-<link rel="match" href="display-css-property-reference-expected.html">
+<link rel="match" href="display-css-property-reftest-ref.html">
<link rel="help" href="https://github.com/WICG/PEPC/blob/main/explainer.md#locking-the-pepc-style">
<body>
<div>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display.tentative.html
index 435929a6c1..4312a156ce 100644
--- a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display.tentative.html
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display.tentative.html
@@ -42,6 +42,7 @@ window.addEventListener('load', runTest);
background: orange;
}
[popover] {
+ inset: auto;
background: lime;
padding:0;
border:0;
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-none.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-none.tentative.html
index 55a11fafdb..8db022b126 100644
--- a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-none.tentative.html
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-none.tentative.html
@@ -15,6 +15,7 @@
display: none;
}
[popover] {
+ inset: auto;
background: lime;
padding: 0;
border: 0;
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display.tentative.html
index bddc44006d..a713540094 100644
--- a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display.tentative.html
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display.tentative.html
@@ -61,6 +61,7 @@ showDefaultopenPopoversOnLoad();
background: orange;
}
[popover] {
+ inset: auto;
background: lime;
padding:0;
border:0;
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display.tentative.html
index 2c6b0bafb9..a301032a5b 100644
--- a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display.tentative.html
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display.tentative.html
@@ -40,6 +40,7 @@
background: orange;
}
[popover] {
+ inset: auto;
background: lime;
padding:0;
border:0;
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-transition.tentative.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-transition.tentative.tentative.html
index ae2a3a8e41..f6220f3c76 100644
--- a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-transition.tentative.tentative.html
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-transition.tentative.tentative.html
@@ -13,6 +13,7 @@ body {
}
#target {
+ inset: auto;
transition: display 2s;
}
</style>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-focus-2.html b/testing/web-platform/tests/html/semantics/popovers/popover-focus-2.html
index 892e5fd68f..8f24ace919 100644
--- a/testing/web-platform/tests/html/semantics/popovers/popover-focus-2.html
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-focus-2.html
@@ -80,7 +80,7 @@ promise_test(async t => {
assert_equals(document.activeElement,invoker2,'Focus should move within popover');
await sendShiftTab();
await sendShiftTab();
- assert_equals(document.activeElement, button1 ,'Focus should not move back to invoker as it is non-focusable');
+ assert_equals(document.activeElement,invoker0,'Focus should not move back to invoker as it is non-focusable');
// Reset invoker1 to focusable.
invoker1.disabled = false;
await verifyFocusOrder([button1, button2, invoker0, invoker1, inside_popover1, invoker2, inside_popover2, button3, button4],'set 1');
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-hover-crash-hang.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-hover-crash-hang.tentative.html
new file mode 100644
index 0000000000..60309398db
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-hover-crash-hang.tentative.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Crash/hang test for popover hover behavior</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover-hint.research.explainer/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/popover-utils.js"></script>
+
+<div id=menu popover=manual>
+ Popover
+ <button popovertarget="submenu"><span id=button>Button</span>
+ <div id=submenu popover=manual>Button-contained popover</div>
+ </button>
+</div>
+<button id=unrelated>Unrelated</button>
+
+<script>
+ promise_test(async (t) => {
+ menu.showPopover();
+ assert_true(menu.matches(':popover-open'));
+ await mouseHover(button,100);
+ button.click();
+ assert_true(menu.matches(':popover-open'));
+ assert_true(submenu.matches(':popover-open'));
+ await mouseHover(submenu,100);
+ await mouseHover(unrelated,100);
+ assert_true(submenu.matches(':popover-open'));
+ // This test passes if nothing crashes/hangs.
+ },'crash test');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-scroll-within.html b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-scroll-within.html
new file mode 100644
index 0000000000..2329aea201
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-scroll-within.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover light dismiss behavior when scrolled within</title>
+<meta name="timeout" content="long">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/popover-utils.js"></script>
+
+<style>
+ [popover] {
+ /* Position most popovers at the bottom-right, out of the way */
+ inset:auto;
+ bottom:0;
+ right:0;
+ }
+ [popover]::backdrop {
+ /* This should *not* affect anything: */
+ pointer-events: auto;
+ }
+</style>
+
+<div popover id=p>Inside popover
+ <div style="height:2000px;background:lightgreen"></div>
+ Bottom of popover6
+</div>
+<button popovertarget=p>Popover</button>
+<style>
+ #p6 {
+ width: 300px;
+ height: 300px;
+ overflow-y: scroll;
+ }
+</style>
+<script>
+ const popover = document.querySelector('#p');
+ promise_test(async () => {
+ popover.showPopover();
+ assert_equals(popover.scrollTop,0,'popover should start non-scrolled');
+ await new test_driver.Actions()
+ .scroll(0, 0, 0, 50, {origin: popover})
+ .send();
+ await waitForRender();
+ assert_true(popover.matches(':popover-open'),'popover should stay open');
+ assert_equals(popover.scrollTop,50,'popover should be scrolled');
+ popover.hidePopover();
+ },'Scrolling within a popover should not close the popover');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss.html b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss.html
index 45db242e91..78b8674513 100644
--- a/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss.html
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss.html
@@ -303,33 +303,6 @@
},'An invoking element that was not used to invoke the popover is not part of the ancestor chain');
</script>
-<div popover id=p6>Inside popover 6
- <div style="height:2000px;background:lightgreen"></div>
- Bottom of popover6
-</div>
-<button popovertarget=p6>Popover 6</button>
-<style>
- #p6 {
- width: 300px;
- height: 300px;
- overflow-y: scroll;
- }
-</style>
-<script>
- const popover6 = document.querySelector('#p6');
- promise_test(async () => {
- popover6.showPopover();
- assert_equals(popover6.scrollTop,0,'popover6 should start non-scrolled');
- await new test_driver.Actions()
- .scroll(0, 0, 0, 50, {origin: popover6})
- .send();
- await waitForRender();
- assert_true(popover6.matches(':popover-open'),'popover6 should stay open');
- assert_equals(popover6.scrollTop,50,'popover6 should be scrolled');
- popover6.hidePopover();
- },'Scrolling within a popover should not close the popover');
-</script>
-
<my-element id="myElement">
<template shadowrootmode="open">
<button id=b7 popovertarget=p7 popovertargetaction=show tabindex="0">Popover7</button>
@@ -602,18 +575,26 @@ promise_test(async () => {
p29.showPopover();
assert_true(p29.matches(':popover-open'),'showing');
let actions = new test_driver.Actions();
- await actions.pointerMove(0,0,{origin: b29})
+ // Using the iframe's contentDocument as the origin would throw an error, so
+ // we are using iframe29 as the origin instead.
+ const iframe_box = iframe29.getBoundingClientRect();
+
+ await actions
+ .pointerMove(1,1,{origin: b29})
.pointerDown({button: actions.ButtonType.LEFT})
+ .pointerMove(iframe_box.width / 2, iframe_box.height / 2, {origin: iframe29})
+ .pointerUp({button: actions.ButtonType.LEFT})
.send();
- await waitForRender();
- assert_true(p29.matches(':popover-open'),'showing after pointerdown');
+ assert_true(p29.matches(':popover-open'), 'popover should be open after pointerUp in iframe.');
actions = new test_driver.Actions();
- await actions.pointerMove(0,0,{origin: iframe29.contentDocument.body})
+ await actions
+ .pointerMove(iframe_box.width / 2, iframe_box.height / 2, {origin: iframe29})
+ .pointerDown({button: actions.ButtonType.LEFT})
+ .pointerMove(1,1,{origin: b29})
.pointerUp({button: actions.ButtonType.LEFT})
.send();
- await waitForRender();
- assert_true(p29.matches(':popover-open'),'showing after pointerup');
+ assert_true(p29.matches(':popover-open'), 'popover should be open after pointerUp on main frame button.');
},`Pointer down in one document and pointer up in another document shouldn't dismiss popover`);
</script>
@@ -626,13 +607,10 @@ promise_test(async () => {
p30.showPopover();
assert_true(p30.matches(':popover-open'),'showing');
let actions = new test_driver.Actions();
- await actions.pointerMove(0,0,{origin: b30})
+ await actions
+ .pointerMove(2,2,{origin: b30})
.pointerDown({button: actions.ButtonType.LEFT})
- .send();
- await waitForRender();
- assert_true(p30.matches(':popover-open'),'showing after pointerdown');
- actions = new test_driver.Actions();
- await actions.pointerMove(0,0,{origin: b30b})
+ .pointerMove(2,2,{origin: b30b})
.pointerUp({button: actions.ButtonType.LEFT})
.send();
await waitForRender();
diff --git a/testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/dynamic-import/microtasks/basic.any.js b/testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/dynamic-import/microtasks/basic.any.js
index 82cb3b215d..4876e82b0d 100644
--- a/testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/dynamic-import/microtasks/basic.any.js
+++ b/testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/dynamic-import/microtasks/basic.any.js
@@ -25,8 +25,8 @@ promise_test(async t => {
// Use Date.now() to ensure that the module is not in the module map
const specifier = "./empty-module.js?" + Date.now();
- const getCount = ticker(1e7);
+ const getCount = ticker(1e6);
await import(specifier);
- assert_equals(getCount(), 1e7);
+ assert_equals(getCount(), 1e6);
}, "import() should drain the microtask queue when fetching a new module");
diff --git a/testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/inline-async-inserted-execorder.html b/testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/inline-async-inserted-execorder.html
new file mode 100644
index 0000000000..0f51fd318b
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/inline-async-inserted-execorder.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Inline async="" module scripts execute or throw parse errors asynchronously</title>
+<link rel="help" href="https://github.com/whatwg/html/issues/9864">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+"use strict";
+setup({ allow_uncaught_exception: true });
+
+promise_test(async t => {
+ window.log = ["before any script execution"];
+
+ window.addEventListener("error", ev => {
+ window.log.push("error event on Window");
+ });
+
+ const noErrorScript = document.createElement("script");
+ noErrorScript.type = "module";
+ noErrorScript.async = true;
+ noErrorScript.textContent = "window.log.push('no error script executed');";
+
+ // This should queue a task to run the script, but not run it immediately.
+ document.head.append(noErrorScript);
+
+ log.push("after inserting noErrorScript");
+ assert_array_equals(window.log, [
+ "before any script execution",
+ "after inserting noErrorScript"
+ ]);
+
+ const parseErrorScript = document.createElement("script");
+ parseErrorScript.type = "module";
+ parseErrorScript.async = true;
+ parseErrorScript.textContent = "+++++";
+
+ // This should queue a task to fire the error event, but not fire it immediately.
+ document.head.append(parseErrorScript);
+
+ log.push("after inserting parseErrorScript");
+ assert_array_equals(window.log, [
+ "before any script execution",
+ "after inserting noErrorScript",
+ "after inserting parseErrorScript"
+ ]);
+
+ // After a microtask, the script run / error event must not happen.
+ queueMicrotask(t.step_func(() => {
+ assert_array_equals(window.log, [
+ "before any script execution",
+ "after inserting noErrorScript",
+ "after inserting parseErrorScript"
+ ]);
+ }));
+
+ // But it must eventually happen, after a full task.
+ await t.step_wait(() => window.log.length == 5, "5 items must eventually be logged");
+
+ // And when it does the order must be correct.
+ assert_array_equals(window.log, [
+ "before any script execution",
+ "after inserting noErrorScript",
+ "after inserting parseErrorScript",
+ "no error script executed",
+ "error event on Window"
+ ]);
+});
+</script>
diff --git a/testing/web-platform/tests/html/semantics/scripting-1/the-template-element/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/scripting-1/the-template-element/WEB_FEATURES.yml
new file mode 100644
index 0000000000..bd821885c7
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/scripting-1/the-template-element/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: template
+ files: "**"
diff --git a/testing/web-platform/tests/html/semantics/selectors/pseudo-classes/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/selectors/pseudo-classes/WEB_FEATURES.yml
new file mode 100644
index 0000000000..055a5fb4a3
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/selectors/pseudo-classes/WEB_FEATURES.yml
@@ -0,0 +1,11 @@
+features:
+- name: autofill
+ files:
+ - autofill.html
+- name: default
+ files:
+ - default.html
+- name: read-write-pseudos
+ files:
+ - readwrite-readonly-type-change.html
+ - readwrite-readonly.html
diff --git a/testing/web-platform/tests/html/syntax/parsing/template/WEB_FEATURES.yml b/testing/web-platform/tests/html/syntax/parsing/template/WEB_FEATURES.yml
new file mode 100644
index 0000000000..bd821885c7
--- /dev/null
+++ b/testing/web-platform/tests/html/syntax/parsing/template/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: template
+ files: "**"
diff --git a/testing/web-platform/tests/html/user-activation/WEB_FEATURES.yml b/testing/web-platform/tests/html/user-activation/WEB_FEATURES.yml
new file mode 100644
index 0000000000..4104016b75
--- /dev/null
+++ b/testing/web-platform/tests/html/user-activation/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: user-activation
+ files: "**"
diff --git a/testing/web-platform/tests/html/webappapis/structured-clone/WEB_FEATURES.yml b/testing/web-platform/tests/html/webappapis/structured-clone/WEB_FEATURES.yml
new file mode 100644
index 0000000000..423a469bf1
--- /dev/null
+++ b/testing/web-platform/tests/html/webappapis/structured-clone/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: structured-clone
+ files: "**"
diff --git a/testing/web-platform/tests/inert/WEB_FEATURES.yml b/testing/web-platform/tests/inert/WEB_FEATURES.yml
new file mode 100644
index 0000000000..074e33726c
--- /dev/null
+++ b/testing/web-platform/tests/inert/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: inert
+ files: "**"
diff --git a/testing/web-platform/tests/inert/inert-and-find-flat-tree.html b/testing/web-platform/tests/inert/inert-and-find-flat-tree.html
new file mode 100644
index 0000000000..951497e2c0
--- /dev/null
+++ b/testing/web-platform/tests/inert/inert-and-find-flat-tree.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://html.spec.whatwg.org/#inert-subtrees">
+<link rel=help href="https://issues.chromium.org/issues/336832613">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id=host>
+ <template shadowrootmode=open>
+ <dialog>
+ <div>inside shadowroot</div>
+ <slot></slot>
+ </dialog>
+ </template>
+ <div>slotted</div>
+</div>
+
+<script>
+document.getElementById('host').shadowRoot.querySelector('dialog').showModal();
+
+test(() => {
+ assert_true(window.find('inside shadowroot'));
+}, 'Text inside a dialog inside a shadowroot should be findable.');
+
+test(() => {
+ assert_true(window.find('slotted'));
+}, 'Text slotted into a dialog which is inside a shadowroot should be findable.');
+</script>
diff --git a/testing/web-platform/tests/infrastructure/metadata/infrastructure/testdriver/click_iframe_crossorigin.sub.html.ini b/testing/web-platform/tests/infrastructure/metadata/infrastructure/testdriver/click_iframe_crossorigin.sub.html.ini
index aa94652ad7..c0ca25ab65 100644
--- a/testing/web-platform/tests/infrastructure/metadata/infrastructure/testdriver/click_iframe_crossorigin.sub.html.ini
+++ b/testing/web-platform/tests/infrastructure/metadata/infrastructure/testdriver/click_iframe_crossorigin.sub.html.ini
@@ -2,3 +2,4 @@
[TestDriver click on a document in an iframe]
expected:
if product == "chrome": [PASS, FAIL] # https://github.com/web-platform-tests/wpt/issues/26295
+ if os == "mac" and product == "firefox": [PASS, FAIL] # https://github.com/web-platform-tests/wpt/issues/45987
diff --git a/testing/web-platform/tests/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html b/testing/web-platform/tests/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html
index 7c225cd051..3572d227f0 100644
--- a/testing/web-platform/tests/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html
+++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html
@@ -1020,8 +1020,8 @@ for (let childList of ["ul", "ol"]) {
{
startContainer: gEditor.querySelector(`${list} > ${childList} > li`).firstChild,
startOffset: 0,
- endContainer: gEditor.querySelector(`${list} > li`).firstChild,
- endOffset: gEditor.querySelector(`${list} > li`).firstChild.length,
+ endContainer: gEditor.querySelector(`${childList} + li`).firstChild,
+ endOffset: gEditor.querySelector(`${childList} + li`).firstChild.length,
},
];
},
diff --git a/testing/web-platform/tests/interfaces/DOM-Parsing.idl b/testing/web-platform/tests/interfaces/DOM-Parsing.idl
index d0d84ab697..af26260793 100644
--- a/testing/web-platform/tests/interfaces/DOM-Parsing.idl
+++ b/testing/web-platform/tests/interfaces/DOM-Parsing.idl
@@ -8,19 +8,3 @@ interface XMLSerializer {
constructor();
DOMString serializeToString(Node root);
};
-
-interface mixin InnerHTML {
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString innerHTML;
-};
-
-Element includes InnerHTML;
-ShadowRoot includes InnerHTML;
-
-partial interface Element {
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString outerHTML;
- [CEReactions] undefined insertAdjacentHTML(DOMString position, DOMString text);
-};
-
-partial interface Range {
- [CEReactions, NewObject] DocumentFragment createContextualFragment(DOMString fragment);
-};
diff --git a/testing/web-platform/tests/interfaces/compute-pressure.idl b/testing/web-platform/tests/interfaces/compute-pressure.idl
index c4dcb90af4..a90febffc3 100644
--- a/testing/web-platform/tests/interfaces/compute-pressure.idl
+++ b/testing/web-platform/tests/interfaces/compute-pressure.idl
@@ -14,9 +14,9 @@ callback PressureUpdateCallback = undefined (
[Exposed=(DedicatedWorker,SharedWorker,Window), SecureContext]
interface PressureObserver {
- constructor(PressureUpdateCallback callback, optional PressureObserverOptions options = {});
+ constructor(PressureUpdateCallback callback);
- Promise<undefined> observe(PressureSource source);
+ Promise<undefined> observe(PressureSource source, optional PressureObserverOptions options = {});
undefined unobserve(PressureSource source);
undefined disconnect();
sequence<PressureRecord> takeRecords();
diff --git a/testing/web-platform/tests/interfaces/css-anchor-position.idl b/testing/web-platform/tests/interfaces/css-anchor-position.idl
index b79e3fce89..5eeaa030b8 100644
--- a/testing/web-platform/tests/interfaces/css-anchor-position.idl
+++ b/testing/web-platform/tests/interfaces/css-anchor-position.idl
@@ -6,5 +6,79 @@
[Exposed=Window]
interface CSSPositionTryRule : CSSRule {
readonly attribute CSSOMString name;
- [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style;
+ [SameObject, PutForwards=cssText] readonly attribute CSSPositionTryDescriptors style;
+};
+
+[Exposed=Window]
+interface CSSPositionTryDescriptors : CSSStyleDeclaration {
+ attribute CSSOMString margin;
+ attribute CSSOMString marginTop;
+ attribute CSSOMString marginRight;
+ attribute CSSOMString marginBottom;
+ attribute CSSOMString marginLeft;
+ attribute CSSOMString marginBlock;
+ attribute CSSOMString marginBlockStart;
+ attribute CSSOMString marginBlockEnd;
+ attribute CSSOMString marginInline;
+ attribute CSSOMString marginInlineStart;
+ attribute CSSOMString marginInlineEnd;
+ attribute CSSOMString margin-top;
+ attribute CSSOMString margin-right;
+ attribute CSSOMString margin-bottom;
+ attribute CSSOMString margin-left;
+ attribute CSSOMString margin-block;
+ attribute CSSOMString margin-block-start;
+ attribute CSSOMString margin-block-end;
+ attribute CSSOMString margin-inline;
+ attribute CSSOMString margin-inline-start;
+ attribute CSSOMString margin-inline-end;
+ attribute CSSOMString inset;
+ attribute CSSOMString insetBlock;
+ attribute CSSOMString insetBlockStart;
+ attribute CSSOMString insetBlockEnd;
+ attribute CSSOMString insetInline;
+ attribute CSSOMString insetInlineStart;
+ attribute CSSOMString insetInlineEnd;
+ attribute CSSOMString top;
+ attribute CSSOMString left;
+ attribute CSSOMString right;
+ attribute CSSOMString bottom;
+ attribute CSSOMString inset-block;
+ attribute CSSOMString inset-block-start;
+ attribute CSSOMString inset-block-end;
+ attribute CSSOMString inset-inline;
+ attribute CSSOMString inset-inline-start;
+ attribute CSSOMString inset-inline-end;
+ attribute CSSOMString width;
+ attribute CSSOMString minWidth;
+ attribute CSSOMString maxWidth;
+ attribute CSSOMString height;
+ attribute CSSOMString minHeight;
+ attribute CSSOMString maxHeight;
+ attribute CSSOMString blockSize;
+ attribute CSSOMString minBlockSize;
+ attribute CSSOMString maxBlockSize;
+ attribute CSSOMString inlineSize;
+ attribute CSSOMString minInlineSize;
+ attribute CSSOMString maxInlineSize;
+ attribute CSSOMString min-width;
+ attribute CSSOMString max-width;
+ attribute CSSOMString min-height;
+ attribute CSSOMString max-height;
+ attribute CSSOMString block-size;
+ attribute CSSOMString min-block-size;
+ attribute CSSOMString max-block-size;
+ attribute CSSOMString inline-size;
+ attribute CSSOMString min-inline-size;
+ attribute CSSOMString max-inline-size;
+ attribute CSSOMString placeSelf;
+ attribute CSSOMString alignSelf;
+ attribute CSSOMString justifySelf;
+ attribute CSSOMString place-self;
+ attribute CSSOMString align-self;
+ attribute CSSOMString justify-self;
+ attribute CSSOMString positionAnchor;
+ attribute CSSOMString position-anchor;
+ attribute CSSOMString insetArea;
+ attribute CSSOMString inset-area;
};
diff --git a/testing/web-platform/tests/interfaces/css-nesting.idl b/testing/web-platform/tests/interfaces/css-nesting.idl
new file mode 100644
index 0000000000..58d3247f90
--- /dev/null
+++ b/testing/web-platform/tests/interfaces/css-nesting.idl
@@ -0,0 +1,9 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Nesting Module (https://drafts.csswg.org/css-nesting-1/)
+
+[Exposed=Window]
+interface CSSNestRule : CSSGroupingRule {
+ [SameObject, PutForwards=cssText] readonly attribute CSSStyleProperties style;
+};
diff --git a/testing/web-platform/tests/interfaces/css-properties-values-api.idl b/testing/web-platform/tests/interfaces/css-properties-values-api.idl
index eb7d7b027e..418e78375b 100644
--- a/testing/web-platform/tests/interfaces/css-properties-values-api.idl
+++ b/testing/web-platform/tests/interfaces/css-properties-values-api.idl
@@ -16,8 +16,8 @@ partial namespace CSS {
[Exposed=Window]
interface CSSPropertyRule : CSSRule {
- readonly attribute CSSOMString name;
- readonly attribute CSSOMString syntax;
- readonly attribute boolean inherits;
- readonly attribute CSSOMString? initialValue;
+ readonly attribute CSSOMString name;
+ readonly attribute CSSOMString syntax;
+ readonly attribute boolean inherits;
+ readonly attribute CSSOMString? initialValue;
};
diff --git a/testing/web-platform/tests/interfaces/css-view-transitions-2.idl b/testing/web-platform/tests/interfaces/css-view-transitions-2.idl
index 41337f4e1e..559870751a 100644
--- a/testing/web-platform/tests/interfaces/css-view-transitions-2.idl
+++ b/testing/web-platform/tests/interfaces/css-view-transitions-2.idl
@@ -10,14 +10,9 @@ partial interface CSSRule {
enum ViewTransitionNavigation { "auto", "none" };
[Exposed=Window]
-interface CSSViewTransitionTypeSet {
- readonly setlike<CSSOMString>;
-};
-
-[Exposed=Window]
interface CSSViewTransitionRule : CSSRule {
readonly attribute ViewTransitionNavigation navigation;
- readonly attribute CSSViewTransitionTypeSet types;
+ [SameObject] readonly attribute FrozenArray<CSSOMString> types;
};
[Exposed=Window]
diff --git a/testing/web-platform/tests/interfaces/device-attributes.idl b/testing/web-platform/tests/interfaces/device-attributes.idl
new file mode 100644
index 0000000000..cf62523ad8
--- /dev/null
+++ b/testing/web-platform/tests/interfaces/device-attributes.idl
@@ -0,0 +1,13 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Device Attributes API (https://wicg.github.io/WebApiDevice/device_attributes/)
+
+partial interface NavigatorManagedData {
+ // Device Attributes API.
+ Promise<DOMString> getAnnotatedAssetId();
+ Promise<DOMString> getAnnotatedLocation();
+ Promise<DOMString> getDirectoryId();
+ Promise<DOMString> getHostname();
+ Promise<DOMString> getSerialNumber();
+};
diff --git a/testing/web-platform/tests/interfaces/digital-identities.idl b/testing/web-platform/tests/interfaces/digital-identities.idl
index 9027ce61af..2d1b720850 100644
--- a/testing/web-platform/tests/interfaces/digital-identities.idl
+++ b/testing/web-platform/tests/interfaces/digital-identities.idl
@@ -17,11 +17,11 @@ dictionary DigitalCredentialRequestOptions {
dictionary IdentityRequestProvider {
required DOMString protocol;
- required DOMString request;
+ required object request;
};
[Exposed=Window, SecureContext]
interface DigitalCredential : Credential {
readonly attribute DOMString protocol;
- readonly attribute DOMString data;
+ [SameObject] readonly attribute Uint8Array data;
};
diff --git a/testing/web-platform/tests/interfaces/document-picture-in-picture.idl b/testing/web-platform/tests/interfaces/document-picture-in-picture.idl
index 888855b38f..ed34b3c216 100644
--- a/testing/web-platform/tests/interfaces/document-picture-in-picture.idl
+++ b/testing/web-platform/tests/interfaces/document-picture-in-picture.idl
@@ -20,7 +20,7 @@ interface DocumentPictureInPicture : EventTarget {
dictionary DocumentPictureInPictureOptions {
[EnforceRange] unsigned long long width = 0;
[EnforceRange] unsigned long long height = 0;
- boolean allowReturnToOpener = true;
+ boolean disallowReturnToOpener = false;
};
[Exposed=Window, SecureContext]
diff --git a/testing/web-platform/tests/interfaces/dom.idl b/testing/web-platform/tests/interfaces/dom.idl
index cf2d4e4adc..72d61f5cfd 100644
--- a/testing/web-platform/tests/interfaces/dom.idl
+++ b/testing/web-platform/tests/interfaces/dom.idl
@@ -120,9 +120,9 @@ interface mixin ParentNode {
readonly attribute Element? lastElementChild;
readonly attribute unsigned long childElementCount;
- [CEReactions, Unscopable] undefined prepend((Node or DOMString)... nodes);
- [CEReactions, Unscopable] undefined append((Node or DOMString)... nodes);
- [CEReactions, Unscopable] undefined replaceChildren((Node or DOMString)... nodes);
+ [CEReactions, Unscopable] undefined prepend((Node or TrustedScript or DOMString)... nodes);
+ [CEReactions, Unscopable] undefined append((Node or TrustedScript or DOMString)... nodes);
+ [CEReactions, Unscopable] undefined replaceChildren((Node or TrustedScript or DOMString)... nodes);
Element? querySelector(DOMString selectors);
[NewObject] NodeList querySelectorAll(DOMString selectors);
@@ -139,9 +139,9 @@ Element includes NonDocumentTypeChildNode;
CharacterData includes NonDocumentTypeChildNode;
interface mixin ChildNode {
- [CEReactions, Unscopable] undefined before((Node or DOMString)... nodes);
- [CEReactions, Unscopable] undefined after((Node or DOMString)... nodes);
- [CEReactions, Unscopable] undefined replaceWith((Node or DOMString)... nodes);
+ [CEReactions, Unscopable] undefined before((Node or TrustedScript or DOMString)... nodes);
+ [CEReactions, Unscopable] undefined after((Node or TrustedScript or DOMString)... nodes);
+ [CEReactions, Unscopable] undefined replaceWith((Node or TrustedScript or DOMString)... nodes);
[CEReactions, Unscopable] undefined remove();
};
DocumentType includes ChildNode;
@@ -340,6 +340,7 @@ interface ShadowRoot : DocumentFragment {
readonly attribute boolean delegatesFocus;
readonly attribute SlotAssignmentMode slotAssignment;
readonly attribute boolean clonable;
+ readonly attribute boolean serializable;
readonly attribute Element host;
attribute EventHandler onslotchange;
};
@@ -398,6 +399,7 @@ dictionary ShadowRootInit {
boolean delegatesFocus = false;
SlotAssignmentMode slotAssignment = "named";
boolean clonable = false;
+ boolean serializable = false;
};
[Exposed=Window,
diff --git a/testing/web-platform/tests/interfaces/gamepad.idl b/testing/web-platform/tests/interfaces/gamepad.idl
index 024e5ea58c..d922d7b80b 100644
--- a/testing/web-platform/tests/interfaces/gamepad.idl
+++ b/testing/web-platform/tests/interfaces/gamepad.idl
@@ -44,7 +44,8 @@ enum GamepadHapticsResult {
};
enum GamepadHapticEffectType {
- "dual-rumble"
+ "dual-rumble",
+ "trigger-rumble"
};
dictionary GamepadEffectParameters {
@@ -52,6 +53,8 @@ dictionary GamepadEffectParameters {
unsigned long long startDelay = 0;
double strongMagnitude = 0.0;
double weakMagnitude = 0.0;
+ double leftTrigger = 0.0;
+ double rightTrigger = 0.0;
};
[Exposed=Window]
diff --git a/testing/web-platform/tests/interfaces/geolocation.idl b/testing/web-platform/tests/interfaces/geolocation.idl
index 4b971f097b..8c0acfc6cc 100644
--- a/testing/web-platform/tests/interfaces/geolocation.idl
+++ b/testing/web-platform/tests/interfaces/geolocation.idl
@@ -42,6 +42,7 @@ dictionary PositionOptions {
interface GeolocationPosition {
readonly attribute GeolocationCoordinates coords;
readonly attribute EpochTimeStamp timestamp;
+ [Default] object toJSON();
};
[Exposed=Window, SecureContext]
@@ -53,6 +54,7 @@ interface GeolocationCoordinates {
readonly attribute double? altitudeAccuracy;
readonly attribute double? heading;
readonly attribute double? speed;
+ [Default] object toJSON();
};
[Exposed=Window]
diff --git a/testing/web-platform/tests/interfaces/html.idl b/testing/web-platform/tests/interfaces/html.idl
index 2f97e4dd60..aad8994b87 100644
--- a/testing/web-platform/tests/interfaces/html.idl
+++ b/testing/web-platform/tests/interfaces/html.idl
@@ -1245,6 +1245,7 @@ interface HTMLTemplateElement : HTMLElement {
[CEReactions] attribute DOMString shadowRootMode;
[CEReactions] attribute boolean shadowRootDelegatesFocus;
[CEReactions] attribute boolean shadowRootClonable;
+ [CEReactions] attribute boolean shadowRootSerializable;
};
[Exposed=Window]
@@ -1579,7 +1580,6 @@ interface OffscreenCanvas : EventTarget {
[Exposed=(Window,Worker)]
interface OffscreenCanvasRenderingContext2D {
- undefined commit();
readonly attribute OffscreenCanvas canvas;
};
@@ -2292,6 +2292,27 @@ interface mixin WindowOrWorkerGlobalScope {
Window includes WindowOrWorkerGlobalScope;
WorkerGlobalScope includes WindowOrWorkerGlobalScope;
+partial interface Element {
+ [CEReactions] undefined setHTMLUnsafe(HTMLString html);
+ DOMString getHTML(optional GetHTMLOptions options = {});
+
+ [CEReactions] attribute [LegacyNullToEmptyString] HTMLString innerHTML;
+ [CEReactions] attribute [LegacyNullToEmptyString] HTMLString outerHTML;
+ [CEReactions] undefined insertAdjacentHTML(DOMString position, HTMLString string);
+};
+
+partial interface ShadowRoot {
+ [CEReactions] undefined setHTMLUnsafe(HTMLString html);
+ DOMString getHTML(optional GetHTMLOptions options = {});
+
+ [CEReactions] attribute [LegacyNullToEmptyString] HTMLString innerHTML;
+};
+
+dictionary GetHTMLOptions {
+ boolean serializableShadowRoots = false;
+ sequence<ShadowRoot> shadowRoots = [];
+};
+
[Exposed=Window]
interface DOMParser {
constructor();
@@ -2307,12 +2328,8 @@ enum DOMParserSupportedType {
"image/svg+xml"
};
-partial interface Element {
- [CEReactions] undefined setHTMLUnsafe(HTMLString html);
-};
-
-partial interface ShadowRoot {
- [CEReactions] undefined setHTMLUnsafe(HTMLString html);
+partial interface Range {
+ [CEReactions, NewObject] DocumentFragment createContextualFragment(HTMLString string);
};
[Exposed=Window]
diff --git a/testing/web-platform/tests/interfaces/mediasession.idl b/testing/web-platform/tests/interfaces/mediasession.idl
index 8e9a21aff0..e6c8e46462 100644
--- a/testing/web-platform/tests/interfaces/mediasession.idl
+++ b/testing/web-platform/tests/interfaces/mediasession.idl
@@ -56,6 +56,7 @@ interface MediaMetadata {
attribute DOMString artist;
attribute DOMString album;
attribute FrozenArray<MediaImage> artwork;
+ [SameObject] readonly attribute FrozenArray<ChapterInformation> chapterInfo;
};
dictionary MediaMetadataInit {
@@ -63,6 +64,20 @@ dictionary MediaMetadataInit {
DOMString artist = "";
DOMString album = "";
sequence<MediaImage> artwork = [];
+ sequence<ChapterInformationInit> chapterInfo = [];
+};
+
+[Exposed=Window]
+interface ChapterInformation {
+ readonly attribute DOMString title;
+ readonly attribute double startTime;
+ [SameObject] readonly attribute FrozenArray<MediaImage> artwork;
+};
+
+dictionary ChapterInformationInit {
+ DOMString title = "";
+ double startTime = 0;
+ sequence<MediaImage> artwork = [];
};
dictionary MediaImage {
diff --git a/testing/web-platform/tests/interfaces/sanitizer-api.idl b/testing/web-platform/tests/interfaces/sanitizer-api.idl
index 599d8f82ea..8f5c667973 100644
--- a/testing/web-platform/tests/interfaces/sanitizer-api.idl
+++ b/testing/web-platform/tests/interfaces/sanitizer-api.idl
@@ -3,19 +3,15 @@
// (https://github.com/w3c/webref)
// Source: HTML Sanitizer API (https://wicg.github.io/sanitizer-api/)
-partial interface Element {
- [CEReactions] undefined setHTMLUnsafe__TO_BE_MERGED(DOMString html, optional SanitizerConfig config = {});
- [CEReactions] undefined setHTML(DOMString html, optional SanitizerConfig config = {});
+dictionary SetHTMLOptions {
+ (Sanitizer or SanitizerConfig) sanitizer = {};
};
-partial interface ShadowRoot {
- [CEReactions] undefined setHTMLUnsafe__TO_BE_MERGED(DOMString html, optional SanitizerConfig config = {});
- [CEReactions] undefined setHTML(DOMString html, optional SanitizerConfig config = {});
-};
-
-partial interface Document {
- static Document parseHTMLUnsafe__TO_BE_MERGED(DOMString html, optional SanitizerConfig config = {});
- static Document parseHTML(DOMString html, optional SanitizerConfig config = {});
+[Exposed=(Window,Worker)]
+interface Sanitizer {
+ constructor(optional SanitizerConfig config = {});
+ SanitizerConfig get();
+ SanitizerConfig getUnsafe();
};
dictionary SanitizerElementNamespace {
diff --git a/testing/web-platform/tests/interfaces/service-workers.idl b/testing/web-platform/tests/interfaces/service-workers.idl
index c740e1098a..1ddc6d71d8 100644
--- a/testing/web-platform/tests/interfaces/service-workers.idl
+++ b/testing/web-platform/tests/interfaces/service-workers.idl
@@ -183,6 +183,7 @@ dictionary RouterCondition {
RunningStatus runningStatus;
sequence<RouterCondition> _or;
+ RouterCondition not;
};
typedef (RouterSourceDict or RouterSourceEnum) RouterSource;
diff --git a/testing/web-platform/tests/interfaces/shape-detection-api.idl b/testing/web-platform/tests/interfaces/shape-detection-api.idl
index 4fc1f085ea..24d3b98085 100644
--- a/testing/web-platform/tests/interfaces/shape-detection-api.idl
+++ b/testing/web-platform/tests/interfaces/shape-detection-api.idl
@@ -17,11 +17,11 @@ dictionary FaceDetectorOptions {
dictionary DetectedFace {
required DOMRectReadOnly boundingBox;
- required FrozenArray<Landmark>? landmarks;
+ required sequence<Landmark>? landmarks;
};
dictionary Landmark {
- required FrozenArray<Point2D> locations;
+ required sequence<Point2D> locations;
LandmarkType type;
};
@@ -48,7 +48,7 @@ dictionary DetectedBarcode {
required DOMRectReadOnly boundingBox;
required DOMString rawValue;
required BarcodeFormat format;
- required FrozenArray<Point2D> cornerPoints;
+ required sequence<Point2D> cornerPoints;
};
enum BarcodeFormat {
diff --git a/testing/web-platform/tests/interfaces/shared-storage.idl b/testing/web-platform/tests/interfaces/shared-storage.idl
index edbe2c2bcc..c40344e74d 100644
--- a/testing/web-platform/tests/interfaces/shared-storage.idl
+++ b/testing/web-platform/tests/interfaces/shared-storage.idl
@@ -3,40 +3,30 @@
// (https://github.com/w3c/webref)
// Source: Shared Storage API (https://wicg.github.io/shared-storage/)
+typedef (USVString or FencedFrameConfig) SharedStorageResponse;
+
[Exposed=(Window)]
interface SharedStorageWorklet : Worklet {
+ Promise<SharedStorageResponse> selectURL(DOMString name,
+ FrozenArray<SharedStorageUrlWithMetadata> urls,
+ optional SharedStorageRunOperationMethodOptions options = {});
+ Promise<any> run(DOMString name,
+ optional SharedStorageRunOperationMethodOptions options = {});
};
+callback RunFunctionForSharedStorageSelectURLOperation = Promise<unsigned long>(sequence<USVString> urls, optional any data);
+
[Exposed=SharedStorageWorklet, Global=SharedStorageWorklet]
interface SharedStorageWorkletGlobalScope : WorkletGlobalScope {
undefined register(DOMString name,
- SharedStorageOperationConstructor operationCtor);
+ Function operationCtor);
readonly attribute WorkletSharedStorage sharedStorage;
};
-callback SharedStorageOperationConstructor =
- SharedStorageOperation(optional SharedStorageRunOperationMethodOptions options);
-
-[Exposed=SharedStorageWorklet]
-interface SharedStorageOperation {
-};
-
-dictionary SharedStorageRunOperationMethodOptions {
- object data;
- boolean resolveToConfig = false;
- boolean keepAlive = false;
-};
-
-[Exposed=SharedStorageWorklet]
-interface SharedStorageRunOperation : SharedStorageOperation {
- Promise<undefined> run(object data);
-};
-
-[Exposed=SharedStorageWorklet]
-interface SharedStorageSelectURLOperation : SharedStorageOperation {
- Promise<long> run(object data,
- FrozenArray<SharedStorageUrlWithMetadata> urls);
+dictionary SharedStorageUrlWithMetadata {
+ required USVString url;
+ object reportingMetadata;
};
[Exposed=(Window,SharedStorageWorklet)]
@@ -54,22 +44,23 @@ dictionary SharedStorageSetMethodOptions {
boolean ignoreIfPresent = false;
};
-typedef (USVString or FencedFrameConfig) SharedStorageResponse;
-
[Exposed=(Window)]
interface WindowSharedStorage : SharedStorage {
- Promise<any> run(DOMString name,
- optional SharedStorageRunOperationMethodOptions options = {});
Promise<SharedStorageResponse> selectURL(DOMString name,
FrozenArray<SharedStorageUrlWithMetadata> urls,
optional SharedStorageRunOperationMethodOptions options = {});
+ Promise<any> run(DOMString name,
+ optional SharedStorageRunOperationMethodOptions options = {});
+
+ Promise<SharedStorageWorklet> createWorklet(USVString moduleURL, optional WorkletOptions options = {});
readonly attribute SharedStorageWorklet worklet;
};
-dictionary SharedStorageUrlWithMetadata {
- required USVString url;
- object reportingMetadata;
+dictionary SharedStorageRunOperationMethodOptions {
+ object data;
+ boolean resolveToConfig = false;
+ boolean keepAlive = false;
};
partial interface Window {
diff --git a/testing/web-platform/tests/interfaces/text-detection-api.idl b/testing/web-platform/tests/interfaces/text-detection-api.idl
index 95b642749f..b6745b1875 100644
--- a/testing/web-platform/tests/interfaces/text-detection-api.idl
+++ b/testing/web-platform/tests/interfaces/text-detection-api.idl
@@ -14,5 +14,5 @@
dictionary DetectedText {
required DOMRectReadOnly boundingBox;
required DOMString rawValue;
- required FrozenArray<Point2D> cornerPoints;
+ required sequence<Point2D> cornerPoints;
};
diff --git a/testing/web-platform/tests/interfaces/trusted-types.idl b/testing/web-platform/tests/interfaces/trusted-types.idl
index db5bd635cf..a0f88e4e6c 100644
--- a/testing/web-platform/tests/interfaces/trusted-types.idl
+++ b/testing/web-platform/tests/interfaces/trusted-types.idl
@@ -32,12 +32,12 @@ interface TrustedScriptURL {
DOMString? getAttributeType(
DOMString tagName,
DOMString attribute,
- optional DOMString elementNs = "",
- optional DOMString attrNs = "");
+ optional DOMString? elementNs = "",
+ optional DOMString? attrNs = "");
DOMString? getPropertyType(
DOMString tagName,
DOMString property,
- optional DOMString elementNs = "");
+ optional DOMString? elementNs = "");
readonly attribute TrustedTypePolicy? defaultPolicy;
};
diff --git a/testing/web-platform/tests/interfaces/turtledove.idl b/testing/web-platform/tests/interfaces/turtledove.idl
index 2547e1fb54..39e90ddae1 100644
--- a/testing/web-platform/tests/interfaces/turtledove.idl
+++ b/testing/web-platform/tests/interfaces/turtledove.idl
@@ -82,6 +82,7 @@ dictionary AuctionAdConfig {
Promise<record<USVString, any>> perBuyerSignals;
Promise<record<USVString, unsigned long long>> perBuyerTimeouts;
Promise<record<USVString, unsigned long long>> perBuyerCumulativeTimeouts;
+ unsigned long long reportingTimeout;
USVString sellerCurrency;
Promise<record<USVString, USVString>> perBuyerCurrencies;
record<USVString, unsigned short> perBuyerGroupLimits;
diff --git a/testing/web-platform/tests/interfaces/wasm-js-api.idl b/testing/web-platform/tests/interfaces/wasm-js-api.idl
index 0d4384251d..b4f723d050 100644
--- a/testing/web-platform/tests/interfaces/wasm-js-api.idl
+++ b/testing/web-platform/tests/interfaces/wasm-js-api.idl
@@ -62,6 +62,8 @@ dictionary MemoryDescriptor {
interface Memory {
constructor(MemoryDescriptor descriptor);
unsigned long grow([EnforceRange] unsigned long delta);
+ ArrayBuffer toFixedLengthBuffer();
+ ArrayBuffer toResizableBuffer();
readonly attribute ArrayBuffer buffer;
};
diff --git a/testing/web-platform/tests/interfaces/webcodecs.idl b/testing/web-platform/tests/interfaces/webcodecs.idl
index 371546eb0d..c754b2b036 100644
--- a/testing/web-platform/tests/interfaces/webcodecs.idl
+++ b/testing/web-platform/tests/interfaces/webcodecs.idl
@@ -371,6 +371,8 @@ dictionary VideoFrameBufferInit {
VideoColorSpaceInit colorSpace;
sequence<ArrayBuffer> transfer = [];
+
+ VideoFrameMetadata metadata;
};
dictionary VideoFrameMetadata {
@@ -380,6 +382,8 @@ dictionary VideoFrameMetadata {
dictionary VideoFrameCopyToOptions {
DOMRectInit rect;
sequence<PlaneLayout> layout;
+ VideoPixelFormat format;
+ PredefinedColorSpace colorSpace;
};
dictionary PlaneLayout {
diff --git a/testing/web-platform/tests/interfaces/webgl1.idl b/testing/web-platform/tests/interfaces/webgl1.idl
index 1b711e1a4c..655c294fc1 100644
--- a/testing/web-platform/tests/interfaces/webgl1.idl
+++ b/testing/web-platform/tests/interfaces/webgl1.idl
@@ -37,6 +37,7 @@ dictionary WebGLContextAttributes {
[Exposed=(Window,Worker)]
interface WebGLObject {
+ attribute USVString label;
};
[Exposed=(Window,Worker)]
diff --git a/testing/web-platform/tests/interfaces/webidl.idl b/testing/web-platform/tests/interfaces/webidl.idl
index dff46c557c..f3db91096a 100644
--- a/testing/web-platform/tests/interfaces/webidl.idl
+++ b/testing/web-platform/tests/interfaces/webidl.idl
@@ -6,7 +6,7 @@
typedef (Int8Array or Int16Array or Int32Array or
Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or
BigInt64Array or BigUint64Array or
- Float32Array or Float64Array or DataView) ArrayBufferView;
+ Float16Array or Float32Array or Float64Array or DataView) ArrayBufferView;
typedef (ArrayBufferView or ArrayBuffer) BufferSource;
typedef (ArrayBuffer or SharedArrayBuffer or [AllowShared] ArrayBufferView) AllowSharedBufferSource;
diff --git a/testing/web-platform/tests/interfaces/webnn.idl b/testing/web-platform/tests/interfaces/webnn.idl
index 0b8ea7cb34..9af2879214 100644
--- a/testing/web-platform/tests/interfaces/webnn.idl
+++ b/testing/web-platform/tests/interfaces/webnn.idl
@@ -64,11 +64,7 @@ enum MLOperandDataType {
};
dictionary MLOperandDescriptor {
- // The operand type.
required MLOperandDataType dataType;
-
- // The dimensions field is empty for scalar operands,
- // and non-empty for tensor operands.
sequence<[EnforceRange] unsigned long> dimensions = [];
};
@@ -122,7 +118,7 @@ dictionary MLBatchNormalizationOptions {
partial interface MLGraphBuilder {
MLOperand batchNormalization(MLOperand input, MLOperand mean, MLOperand variance,
- optional MLBatchNormalizationOptions options = {});
+ optional MLBatchNormalizationOptions options = {});
};
partial interface MLGraphBuilder {
@@ -162,7 +158,9 @@ dictionary MLConv2dOptions {
};
partial interface MLGraphBuilder {
- MLOperand conv2d(MLOperand input, MLOperand filter, optional MLConv2dOptions options = {});
+ MLOperand conv2d(MLOperand input,
+ MLOperand filter,
+ optional MLConv2dOptions options = {});
};
enum MLConvTranspose2dFilterOperandLayout {
@@ -242,7 +240,14 @@ dictionary MLGatherOptions {
};
partial interface MLGraphBuilder {
- MLOperand gather(MLOperand input, MLOperand indices, optional MLGatherOptions options = {});
+ MLOperand gather(MLOperand input,
+ MLOperand indices,
+ optional MLGatherOptions options = {});
+};
+
+partial interface MLGraphBuilder {
+ MLOperand gelu(MLOperand input);
+ MLActivation gelu();
};
dictionary MLGemmOptions {
@@ -329,7 +334,7 @@ dictionary MLInstanceNormalizationOptions {
partial interface MLGraphBuilder {
MLOperand instanceNormalization(MLOperand input,
- optional MLInstanceNormalizationOptions options = {});
+ optional MLInstanceNormalizationOptions options = {});
};
dictionary MLLayerNormalizationOptions {
@@ -340,7 +345,8 @@ dictionary MLLayerNormalizationOptions {
};
partial interface MLGraphBuilder {
- MLOperand layerNormalization(MLOperand input, optional MLLayerNormalizationOptions options = {});
+ MLOperand layerNormalization(MLOperand input,
+ optional MLLayerNormalizationOptions options = {});
};
dictionary MLLeakyReluOptions {
@@ -509,17 +515,13 @@ partial interface MLGraphBuilder {
};
partial interface MLGraphBuilder {
- MLOperand softmax(MLOperand input);
- MLActivation softmax();
-};
-
-dictionary MLSoftplusOptions {
- float steepness = 1;
+ MLOperand softmax(MLOperand input, unsigned long axis);
+ MLActivation softmax(unsigned long axis);
};
partial interface MLGraphBuilder {
- MLOperand softplus(MLOperand input, optional MLSoftplusOptions options = {});
- MLActivation softplus(optional MLSoftplusOptions options = {});
+ MLOperand softplus(MLOperand input);
+ MLActivation softplus();
};
partial interface MLGraphBuilder {
@@ -532,9 +534,10 @@ dictionary MLSplitOptions {
};
partial interface MLGraphBuilder {
- sequence<MLOperand> split(MLOperand input,
- ([EnforceRange] unsigned long or sequence<[EnforceRange] unsigned long>) splits,
- optional MLSplitOptions options = {});
+ sequence<MLOperand> split(
+ MLOperand input,
+ ([EnforceRange] unsigned long or sequence<[EnforceRange] unsigned long>) splits,
+ optional MLSplitOptions options = {});
};
partial interface MLGraphBuilder {
diff --git a/testing/web-platform/tests/interfaces/webrtc-encoded-transform.idl b/testing/web-platform/tests/interfaces/webrtc-encoded-transform.idl
index 8a756702c7..0db2f2b9a8 100644
--- a/testing/web-platform/tests/interfaces/webrtc-encoded-transform.idl
+++ b/testing/web-platform/tests/interfaces/webrtc-encoded-transform.idl
@@ -78,10 +78,15 @@ dictionary RTCEncodedVideoFrameMetadata {
DOMString mimeType;
};
+dictionary RTCEncodedVideoFrameOptions {
+ RTCEncodedVideoFrameMetadata metadata;
+};
+
// New interfaces to define encoded video and audio frames. Will eventually
// re-use or extend the equivalent defined in WebCodecs.
[Exposed=(Window,DedicatedWorker), Serializable]
interface RTCEncodedVideoFrame {
+ constructor(RTCEncodedVideoFrame originalFrame, optional RTCEncodedVideoFrameOptions options = {});
readonly attribute RTCEncodedVideoFrameType type;
attribute ArrayBuffer data;
RTCEncodedVideoFrameMetadata getMetadata();
@@ -96,8 +101,13 @@ dictionary RTCEncodedAudioFrameMetadata {
DOMString mimeType;
};
+dictionary RTCEncodedAudioFrameOptions {
+ RTCEncodedAudioFrameMetadata metadata;
+};
+
[Exposed=(Window,DedicatedWorker), Serializable]
interface RTCEncodedAudioFrame {
+ constructor(RTCEncodedAudioFrame originalFrame, optional RTCEncodedAudioFrameOptions options = {});
attribute ArrayBuffer data;
RTCEncodedAudioFrameMetadata getMetadata();
};
diff --git a/testing/web-platform/tests/interfaces/webrtc.idl b/testing/web-platform/tests/interfaces/webrtc.idl
index e571abb527..65e7aa622c 100644
--- a/testing/web-platform/tests/interfaces/webrtc.idl
+++ b/testing/web-platform/tests/interfaces/webrtc.idl
@@ -368,6 +368,7 @@ interface RTCRtpReceiver {
sequence<RTCRtpContributingSource> getContributingSources();
sequence<RTCRtpSynchronizationSource> getSynchronizationSources();
Promise<RTCStatsReport> getStats();
+ attribute DOMHighResTimeStamp? jitterBufferTarget;
};
dictionary RTCRtpContributingSource {
@@ -387,7 +388,7 @@ interface RTCRtpTransceiver {
attribute RTCRtpTransceiverDirection direction;
readonly attribute RTCRtpTransceiverDirection? currentDirection;
undefined stop();
- undefined setCodecPreferences(sequence<RTCRtpCodecCapability> codecs);
+ undefined setCodecPreferences(sequence<RTCRtpCodec> codecs);
};
[Exposed=Window]
@@ -434,8 +435,8 @@ dictionary RTCIceParameters {
};
dictionary RTCIceCandidatePair {
- RTCIceCandidate local;
- RTCIceCandidate remote;
+ required RTCIceCandidate local;
+ required RTCIceCandidate remote;
};
enum RTCIceGathererState {
diff --git a/testing/web-platform/tests/interfaces/webxr.idl b/testing/web-platform/tests/interfaces/webxr.idl
index 3b7f8a55b7..8e02fbd38a 100644
--- a/testing/web-platform/tests/interfaces/webxr.idl
+++ b/testing/web-platform/tests/interfaces/webxr.idl
@@ -178,6 +178,7 @@ interface XRInputSource {
[SameObject] readonly attribute XRSpace targetRaySpace;
[SameObject] readonly attribute XRSpace? gripSpace;
[SameObject] readonly attribute FrozenArray<DOMString> profiles;
+ [SameObject] readonly attribute boolean skipRendering;
};
[SecureContext, Exposed=Window]
@@ -263,8 +264,8 @@ interface XRInputSourcesChangeEvent : Event {
dictionary XRInputSourcesChangeEventInit : EventInit {
required XRSession session;
- required FrozenArray<XRInputSource> added;
- required FrozenArray<XRInputSource> removed;
+ required sequence<XRInputSource> added;
+ required sequence<XRInputSource> removed;
};
diff --git a/testing/web-platform/tests/mathml/presentation-markup/operators/mo-axis-height-1.html b/testing/web-platform/tests/mathml/presentation-markup/operators/mo-axis-height-1.html
index 6de6284188..10ea4dcffc 100644
--- a/testing/web-platform/tests/mathml/presentation-markup/operators/mo-axis-height-1.html
+++ b/testing/web-platform/tests/mathml/presentation-markup/operators/mo-axis-height-1.html
@@ -29,24 +29,100 @@
window.addEventListener("load", () => { loadAllFonts().then(runTests); });
function runTests() {
+ const AxisHeight = 5000 * emToPx;
+
test(function() {
- var v1 = 5000 * emToPx;
var moMiddle = (getBox("mo1").bottom + getBox("mo1").top) / 2;
assert_approx_equals(getBox("mo1").height,
14000 * emToPx, epsilon, "mo: size");
assert_approx_equals(getBox("baseline1").bottom - moMiddle,
- v1, epsilon, "mo: axis height");
- }, "AxisHeight (size variant)");
+ AxisHeight, epsilon, "mo: axis height");
+ }, "symmetric stretching with respect to the math axis (size variant)");
test(function() {
- var v1 = 5000 * emToPx;
var moMiddle = (getBox("mo2").bottom + getBox("mo2").top) / 2;
assert_approx_equals(getBox("mo2").height,
- 2 * (getBox("target2").height - v1),
+ 2 * (getBox("target2").height - AxisHeight),
epsilon, "mo: size");
assert_approx_equals(getBox("baseline2").bottom - moMiddle,
- v1, epsilon, "mo: axis height");
- }, "AxisHeight (glyph assembly)");
+ AxisHeight, epsilon, "mo: axis height");
+ }, "symmetric stretching with respect to the math axis (glyph assembly)");
+
+ test(function() {
+ const minsize = 14000 * emToPx;
+ const Tascent = minsize / 2 + AxisHeight;
+ const Tdescent = minsize - Tascent;
+ assert_approx_equals(getBox("baseline3").bottom - getBox("mo3").top, Tascent, epsilon, "mo ascent");
+ assert_approx_equals(getBox("mo3").bottom - getBox("baseline3").bottom, Tdescent, epsilon, "mo descent");
+ }, "Tascent = Tdescent = 0, minsize = 14em");
+
+ test(function() {
+ const minsize = 14000 * emToPx;
+ var Tascent = getBox("baseline4").bottom - getBox("target4").top;
+ assert_greater_than(Tascent, AxisHeight);
+ var Tdescent = getBox("target4").bottom - getBox("baseline4").bottom;
+ const T = Tascent + Tdescent;
+ Tascent = Math.max(0, Tascent - AxisHeight) * minsize / T + AxisHeight;
+ Tdescent = minsize - Tascent;
+ assert_approx_equals(getBox("baseline4").bottom - getBox("mo4").top, Tascent, epsilon, "mo ascent");
+ assert_approx_equals(getBox("mo4").bottom - getBox("baseline4").bottom, Tdescent, epsilon, "mo descent");
+ }, "Tascent = 6em > AxisHeight, Tdescent = 1em, symmetric = false, minsize = 14em");
+
+ test(function() {
+ const minsize = 14000 * emToPx;
+ var Tascent = getBox("baseline5").bottom - getBox("target5").top;
+ assert_less_than(Tascent, AxisHeight);
+ var Tdescent = getBox("target5").bottom - getBox("baseline5").bottom;
+ const T = Tascent + Tdescent;
+ Tascent = Math.max(0, Tascent - AxisHeight) * minsize / T + AxisHeight;
+ Tdescent = minsize - Tascent;
+ assert_approx_equals(getBox("baseline5").bottom - getBox("mo5").top, Tascent, epsilon, "mo ascent");
+ assert_approx_equals(getBox("mo5").bottom - getBox("baseline5").bottom, Tdescent, epsilon, "mo descent");
+ }, "Tascent = 4em < AxisHeight, Tdescent = 3em, symmetric = false, minsize = 14em");
+
+ test(function() {
+ const maxsize = 14000 * emToPx;
+ var Tascent = getBox("baseline6").bottom - getBox("target6").top;
+ assert_greater_than(Tascent, AxisHeight);
+ var Tdescent = getBox("target6").bottom - getBox("baseline6").bottom;
+ const T = Tascent + Tdescent;
+ Tascent = Math.max(0, Tascent - AxisHeight) * maxsize / T + AxisHeight;
+ Tdescent = maxsize - Tascent;
+ assert_approx_equals(getBox("baseline6").bottom - getBox("mo6").top, Tascent, epsilon, "mo ascent");
+ assert_approx_equals(getBox("mo6").bottom - getBox("baseline6").bottom, Tdescent, epsilon, "mo descent");
+ }, "Tascent = 6em > AxisHeight, Tdescent = 22em, symmetric = false, maxsize = 14em");
+
+ test(function() {
+ const maxsize = 14000 * emToPx;
+ var Tascent = getBox("baseline7").bottom - getBox("target7").top;
+ assert_less_than(Tascent, AxisHeight);
+ var Tdescent = getBox("target7").bottom - getBox("baseline7").bottom;
+ var T = Tascent + Tdescent;
+ Tascent = Math.max(0, Tascent - AxisHeight) * maxsize / T + AxisHeight;
+ Tdescent = maxsize - Tascent;
+ assert_approx_equals(getBox("baseline7").bottom - getBox("mo7").top, Tascent, epsilon, "mo ascent");
+ assert_approx_equals(getBox("mo7").bottom - getBox("baseline7").bottom, Tdescent, epsilon, "mo descent");
+ }, "Tascent = 4em < AxisHeight, Tdescent = 24em, symmetric = false, maxsize = 14em");
+
+ test(function() {
+ const minsize = 14000 * emToPx;
+ const Uascent = getBox("baseline8").bottom - getBox("target8").top;
+ const Udescent = getBox("target8").bottom - getBox("baseline8").bottom;
+ assert_less_than(2 * Math.max(Uascent - AxisHeight, Udescent + AxisHeight), minsize, "Sascent + Sdescent < minsize");
+ assert_approx_equals(getBox("mo8").height, minsize, epsilon, "mo size");
+ const MathAxis = getBox("baseline8").bottom - AxisHeight;
+ assert_approx_equals(MathAxis - getBox("mo8").top, getBox("mo8").bottom - MathAxis, epsilon, "mo is symmetric");
+ }, "symmetric stretching with respect to the math axis (minsize = 14em)");
+
+ test(function() {
+ const maxsize = 14000 * emToPx;
+ const Uascent = getBox("baseline9").bottom - getBox("target9").top;
+ const Udescent = getBox("target9").bottom - getBox("baseline9").bottom;
+ assert_greater_than(2 * Math.max(Uascent - AxisHeight, Udescent + AxisHeight), maxsize, "Sascent + Sdescent > maxsize");
+ assert_approx_equals(getBox("mo9").height, maxsize, epsilon, "mo size");
+ const MathAxis = getBox("baseline9").bottom - AxisHeight;
+ assert_approx_equals(MathAxis - getBox("mo9").top, getBox("mo9").bottom - MathAxis, epsilon, "mo is symmetric");
+ }, "symmetric stretching with respect to the math axis (maxsize = 14em)");
done();
}
@@ -56,20 +132,87 @@
<div id="log"></div>
<p>
<math style="font-family: axisheight5000-verticalarrow14000;">
+ <mspace id="baseline1" style="background: blue" width="50px" height="1px"/>
+ <mpadded voffset="50px"><mspace style="background: cyan" width="50px" height="1px"/></mpadded>
<mrow>
- <mspace id="baseline1" style="background: blue" width="50px" height="1px"/>
- <mpadded voffset="50px"><mspace style="background: cyan" width="50px" height="1px"/></mpadded>
<mo id="mo1" symmetric="true" style="color: green">&#x21A8;</mo>
- <mspace style="background: gray" width="10px" height="50px"/>
+ <mpadded style="background: gray" width="10px" height="50px"><mn>1</mn></mpadded>
</mrow>
</math>
<math style="font-family: axisheight5000-verticalarrow14000;">
+ <mspace id="baseline2" style="background: blue" width="50px" height="1px"/>
+ <mpadded voffset="50px"><mspace style="background: cyan" width="50px" height="1px"/></mpadded>
<mrow>
- <mspace id="baseline2" style="background: blue" width="50px" height="1px"/>
- <mpadded voffset="50px"><mspace style="background: cyan" width="50px" height="1px"/></mpadded>
<mo id="mo2" symmetric="true" style="color: green">&#x21A8;</mo>
- <mspace id="target2" style="background: gray" width="10px" height="200px"/>
+ <mpadded id="target2" style="background: gray" width="10px" height="200px"><mn>2</mn></mpadded>
+ </mrow>
+ </math>
+ </p>
+ <p>
+ <math style="font-family: axisheight5000-verticalarrow14000;">
+ <mspace id="baseline3" style="background: blue" width="50px" height="1px"/>
+ <mpadded voffset="50px"><mspace style="background: cyan" width="50px" height="1px"/></mpadded>
+ <mrow>
+ <mo id="mo3" minsize="14em" style="color: green">&#x21A8;</mo>
+ <mpadded id="target3" style="background: gray" width="10px" height="0px" depth="0px"><mn>3</mn></mpadded>
+ </mrow>
+ </math>
+ </p>
+ <p>
+ <math style="font-family: axisheight5000-verticalarrow14000;">
+ <mspace id="baseline4" style="background: blue" width="50px" height="1px"/>
+ <mpadded voffset="50px"><mspace style="background: cyan" width="50px" height="1px"/></mpadded>
+ <mrow>
+ <mo id="mo4" minsize="14em" style="color: green">&#x21A8;</mo>
+ <mpadded id="target4" style="background: gray" width="10px" height="6em" depth="1em"><mn>4</mn></mpadded>
+ </mrow>
+ </math>
+ <math style="font-family: axisheight5000-verticalarrow14000;">
+ <mspace id="baseline5" style="background: blue" width="50px" height="1px"/>
+ <mpadded voffset="50px"><mspace style="background: cyan" width="50px" height="1px"/></mpadded>
+ <mrow>
+ <mo id="mo5" minsize="14em" style="color: green">&#x21A8;</mo>
+ <mpadded id="target5" style="background: gray" width="10px" height="4em" depth="3em"><mn>5</mn></mpadded>
+ </mrow>
+ </math>
+ </p>
+ <p>
+ <math style="font-family: axisheight5000-verticalarrow14000;">
+ <mspace id="baseline6" style="background: blue" width="50px" height="1px"/>
+ <mpadded voffset="50px"><mspace style="background: cyan" width="50px" height="1px"/></mpadded>
+ <mrow>
+ <mo id="mo6" maxsize="14em" style="color: green">&#x21A8;</mo>
+ <mpadded id="target6" style="background: gray" width="10px" height="6em" depth="22em"><mn>6</mn></mpadded>
+ </mrow>
+ </math>
+ <math style="font-family: axisheight5000-verticalarrow14000;">
+ <mspace id="baseline7" style="background: blue" width="50px" height="1px"/>
+ <mpadded voffset="50px"><mspace style="background: cyan" width="50px" height="1px"/></mpadded>
+ <mrow>
+ <mo id="mo7" maxsize="14em" style="color: green">&#x21A8;</mo>
+ <mpadded id="target7" style="background: gray" width="10px" height="4em" depth="24em"><mn>7</mn></mpadded>
+ </mrow>
+ </math>
+ </p>
+
+
+ <p>
+ <math style="font-family: axisheight5000-verticalarrow14000;">
+ <mspace id="baseline8" style="background: blue" width="50px" height="1px"/>
+ <mpadded voffset="50px"><mspace style="background: cyan" width="50px" height="1px"/></mpadded>
+ <mrow>
+ <mo id="mo8" symmetric="true" minsize="14em" style="color: green">&#x21A8;</mo>
+ <mpadded id="target8" style="background: gray" width="10px" height="6em" depth="1em"><mn>8</mn></mpadded>
+ </mrow>
+ </math>
+ <math style="font-family: axisheight5000-verticalarrow14000;">
+ <mspace id="baseline9" style="background: blue" width="50px" height="1px"/>
+ <mpadded voffset="50px"><mspace style="background: cyan" width="50px" height="1px"/></mpadded>
+ <mrow>
+ <mo id="mo9" symmetric="true" maxsize="14em" style="color: green">&#x21A8;</mo>
+ <mpadded id="target9" style="background: gray" width="10px" height="6em" depth="24em"><mn>9</mn></mpadded>
</mrow>
</math>
+ </p>
</body>
</html>
diff --git a/testing/web-platform/tests/mathml/presentation-markup/operators/mo-minsize-maxsize-001.html b/testing/web-platform/tests/mathml/presentation-markup/operators/mo-minsize-maxsize-001.html
index 3e7e5c9bcc..c07f64327a 100644
--- a/testing/web-platform/tests/mathml/presentation-markup/operators/mo-minsize-maxsize-001.html
+++ b/testing/web-platform/tests/mathml/presentation-markup/operators/mo-minsize-maxsize-001.html
@@ -20,6 +20,10 @@
mo {
font-family: operators;
}
+ @font-face {
+ font-family: stretchy;
+ src: url("/fonts/math/stretchy.woff");
+ }
</style>
<script>
setup({ explicit_done: true });
@@ -48,17 +52,27 @@
test(function() {
assert_approx_equals(document.getElementById("percent_minsize").getBoundingClientRect().height, 12 * emToPx, epsilon, "percent minsize");
assert_approx_equals(document.getElementById("percent_maxsize").getBoundingClientRect().height, 3 * emToPx, epsilon, "percent maxsize");
- }, `minsize/maxsize percentages are relative to the target size`);
+ }, `minsize/maxsize percentages are relative to the unstretched size`);
test(function() {
- // These tests are not really strong:
- // - The smallest glyph for this stretchy operator is a 1em square so
- // it can't go under a minsize of 1em anyway.
- // - The maxsize is theorically infinite, this only tests that a large
- // value of 300em is clamped.
- assert_approx_equals(document.getElementById("default_minsize").getBoundingClientRect().height, 1 * emToPx, epsilon, "default minsize is 1em");
+ // - The unstretched size is a lower bound for the stretched size, so
+ // specifying a lower minsize has no effect. This test only verifies
+ // that the default minsize is at most 100% the unstretched size.
+ const unstretched_size = 1 * emToPx;
+ assert_approx_equals(document.getElementById("default_minsize").getBoundingClientRect().height, unstretched_size, epsilon, "default minsize is 100%");
+
+ // Previous version of MathML Core were defining minsize as 1em rather
+ // than 100% the unstretched size. So try the same test with a .5em
+ // unstretched size.
+ const unstretched_size_2 = .5 * emToPx;
+ assert_approx_equals(document.getElementById("default_minsize_2").getBoundingClientRect().height, unstretched_size_2, epsilon, "default minsize is not 1em");
+
+ // - The target size is an upper bound for the stretched size, so
+ // specifying a larger maxsize has no effect. This test only
+ // verifies that the default maxsize is at least 300 times the
+ // unstretched size.
assert_approx_equals(document.getElementById("default_maxsize").getBoundingClientRect().height, 300 * emToPx, epsilon, "default maxsize is infinity");
- }, `default minsize/maxsize percentages`);
+ }, `default minsize/maxsize values`);
done();
}
@@ -71,7 +85,7 @@
<mrow>
<mspace width="1em" height="5em" style="background: blue"/>
<mo id="negative_minsize" minsize="-10em" stretchy="true" symmetric="false">⥯</mo>
- <mn><!-- not space like --></mn>
+ <mpadded height="0" depth="0"><mn><!-- not space like --></mn></mpadded>
</mrow>
</math>
</p>
@@ -80,7 +94,7 @@
<mrow>
<mspace width="1em" height="5em" style="background: blue"/>
<mo id="maxsize_less_than_minsize" minsize="7em" maxsize="2em" stretchy="true" symmetric="false">⥯</mo>
- <mn><!-- not space like --></mn>
+ <mpadded height="0" depth="0"><mn><!-- not space like --></mn></mpadded>
</mrow>
</math>
</p>
@@ -89,7 +103,7 @@
<mrow>
<mspace width="1em" height="5em" style="background: blue"/>
<mo id="minsize_less_than_negative_maxsize" minsize="-2em" maxsize="-1em" stretchy="true" symmetric="false">⥯</mo>
- <mn><!-- not space like --></mn>
+ <mpadded height="0" depth="0"><mn><!-- not space like --></mn></mpadded>
</mrow>
</math>
</p>
@@ -98,7 +112,7 @@
<mrow>
<mspace id="zero_target_size_with_minsize_math_axis" width="1em" height="0em" style="background: blue"/>
<mo id="zero_target_size_with_minsize" minsize="2em" stretchy="true" symmetric="true">⥯</mo>
- <mn><!-- not space like --></mn>
+ <mpadded height="0" depth="0"><mn><!-- not space like --></mn></mpadded>
</mrow>
</math>
</p>
@@ -106,8 +120,8 @@
<math>
<mrow>
<mspace width="1em" height="6em" style="background: blue"/>
- <mo id="percent_minsize" minsize="200%" stretchy="true" symmetric="false">⥯</mo>
- <mn><!-- not space like --></mn>
+ <mo id="percent_minsize" minsize="1200%" stretchy="true" symmetric="false">⥯</mo>
+ <mpadded height="0" depth="0"><mn><!-- not space like --></mn></mpadded>
</mrow>
</math>
</p>
@@ -115,8 +129,8 @@
<math>
<mrow>
<mspace width="1em" height="6em" style="background: blue"/>
- <mo id="percent_maxsize" maxsize="50%" stretchy="true" symmetric="false">⥯</mo>
- <mn><!-- not space like --></mn>
+ <mo id="percent_maxsize" maxsize="300%" stretchy="true" symmetric="false">⥯</mo>
+ <mpadded height="0" depth="0"><mn><!-- not space like --></mn></mpadded>
</mrow>
</math>
</p>
@@ -125,7 +139,16 @@
<mrow>
<mspace width="1em" height=".5em" style="background: blue"/>
<mo id="default_minsize" stretchy="true" symmetric="false">⥯</mo>
- <mn><!-- not space like --></mn>
+ <mpadded height="0" depth="0"><mn><!-- not space like --></mn></mpadded>
+ </mrow>
+ </math>
+ </p>
+ <p>
+ <math>
+ <mrow>
+ <mspace width="1em" height=".25em" style="background: blue"/>
+ <mo style="font-family: stretchy" id="default_minsize_2" stretchy="true" symmetric="false">↨</mo>
+ <mpadded height="0" depth="0"><mn><!-- not space like --></mn></mpadded>
</mrow>
</math>
</p>
@@ -134,10 +157,9 @@
<mrow>
<mspace width="1em" height="300em" style="background: blue"/>
<mo id="default_maxsize" stretchy="true" symmetric="false">⥯</mo>
- <mn><!-- not space like --></mn>
+ <mpadded height="0" depth="0"><mn><!-- not space like --></mn></mpadded>
</mrow>
</math>
</p>
-
</body>
</html>
diff --git a/testing/web-platform/tests/mathml/presentation-markup/operators/mo-stretch-properties-dynamic-001.html b/testing/web-platform/tests/mathml/presentation-markup/operators/mo-stretch-properties-dynamic-001.html
index 5d447aa1d2..1cb3c90bcb 100644
--- a/testing/web-platform/tests/mathml/presentation-markup/operators/mo-stretch-properties-dynamic-001.html
+++ b/testing/web-platform/tests/mathml/presentation-markup/operators/mo-stretch-properties-dynamic-001.html
@@ -42,7 +42,8 @@
element = document.getElementById("minsize_remove");
element.removeAttribute("minsize");
- assert_approx_equals(element.getBoundingClientRect().height, 1 * emToPx, epsilon, "remove");
+ const unstretched_size = 1 * emToPx;
+ assert_approx_equals(element.getBoundingClientRect().height, unstretched_size, epsilon, "remove");
}, `minsize`);
test(function() {
diff --git a/testing/web-platform/tests/mathml/presentation-markup/operators/size-and-position-of-stretchy-fences-with-default-font-001.html b/testing/web-platform/tests/mathml/presentation-markup/operators/size-and-position-of-stretchy-fences-with-default-font-001.html
new file mode 100644
index 0000000000..a5eb267c61
--- /dev/null
+++ b/testing/web-platform/tests/mathml/presentation-markup/operators/size-and-position-of-stretchy-fences-with-default-font-001.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Size of nested stretchy fences with inner mo</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=40066018">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=40068339">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=40856331">
+
+<div id="log"></div>
+
+<p>
+ <math>
+ <mrow>
+ <mo id="left1">(</mo>
+ <mrow>
+ <mo>(</mo>
+ <mrow>
+ <mi>x</mi>
+ <mo>+</mo>
+ <mi>y</mi>
+ </mrow>
+ <mo>)</mo>
+ </mrow>
+ <mo id="right1">)</mo>
+ </mrow>
+ </math>
+ <math>
+ <mrow>
+ <mo id="left2">(</mo>
+ <mrow>
+ <mo>(</mo>
+ <mrow>
+ <mi>x</mi>
+ </mrow>
+ <mo>)</mo>
+ </mrow>
+ <mo id="right2">)</mo>
+ </mrow>
+ </math>
+</p>
+
+<p>
+ <math>
+ <mn id="plus3">+</mn>
+ <mrow>
+ <mo id="left3" fence="true" form="prefix">(</mo>
+ <mi>x</mi>
+ <mo id="right3" fence="true" form="postfix">)</mo>
+ </mrow>
+ </math>
+</p>
+
+<p>
+ <math display="block">
+ <mrow>
+ <mo id="left4" fence="false" symmetric="true" minsize="2.4em" maxsize="2.4em">(</mo>
+ <mfrac>
+ <msup>
+ <mi>∂</mi>
+ <mn>2</mn>
+ </msup>
+ <mrow>
+ <mi>∂</mi>
+ <msup>
+ <mi>x</mi>
+ <mn>2</mn>
+ </msup>
+ </mrow>
+ </mfrac>
+ <mo id="plus4">+</mo>
+ <mfrac>
+ <msup>
+ <mi>∂</mi>
+ <mn>2</mn>
+ </msup>
+ <mrow>
+ <mi>∂</mi>
+ <msup>
+ <mi>y</mi>
+ <mn>2</mn>
+ </msup>
+ </mrow>
+ </mfrac>
+ <mo id="right4" fence="false" symmetric="true" minsize="2.4em" maxsize="2.4em">)</mo>
+ </mrow>
+ </math>
+</p>
+
+<script>
+ function getBox(id) {
+ return document.getElementById(id).getBoundingClientRect();
+ }
+ function middleOf(id) {
+ let box = getBox(id);
+ return (box.top + box.bottom) / 2;
+ }
+ const epsilon = 2;
+
+ test(t => {
+ assert_approx_equals(getBox("left1").top, getBox("left2").top, epsilon);
+ assert_approx_equals(getBox("left1").bottom, getBox("left2").bottom, epsilon);
+ assert_approx_equals(getBox("right1").top, getBox("right2").top, epsilon);
+ assert_approx_equals(getBox("right1").bottom, getBox("right2").bottom, epsilon);
+ }, "Inner binary operator should not affect position and size of outer fences.");
+
+ test(t => {
+ const math_axis_3 = middleOf("plus3")
+ assert_approx_equals(middleOf("left3"), math_axis_3, epsilon);
+ assert_approx_equals(middleOf("right3"), math_axis_3, epsilon);
+ const math_axis_4 = middleOf("plus4")
+ assert_approx_equals(middleOf("left4"), math_axis_4, epsilon);
+ assert_approx_equals(middleOf("right4"), math_axis_4, epsilon);
+ }, "Fences are stretched symmetrically with respect to the math axis");
+</script>
diff --git a/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-001-ref.html b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-001-ref.html
new file mode 100644
index 0000000000..5d4e6b7dca
--- /dev/null
+++ b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-001-ref.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>MathML and ::first-line/::first-letter pseudo-elements (reference)</title>
+<p>PASS if there is no red.</p>
+<ol>
+ <li>
+ <math>
+ <mtext class="firstline"><span>Hello,<br/>World!</span></mtext>
+ </math>
+ </li>
+ <li>
+ <math>
+ <mtext class="firstletter">Hello, World!</mtext>
+ </math>
+ </li>
+</ol>
diff --git a/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-001.html b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-001.html
new file mode 100644
index 0000000000..42d0f04ed8
--- /dev/null
+++ b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-001.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>MathML and ::first-line/::first-letter pseudo-elements</title>
+<link rel="help" href="https://w3c.github.io/mathml-core/#legacy-mathml-style-attributes">
+<link rel="match" href="first-line-first-letter-pseudo-elements-001-ref.html"/>
+<meta name="assert" content="::first-line and ::first-letter do not apply to MathML elements.">
+<style>
+ .firstline::first-line { background: red; }
+ .firstletter::first-letter { background: red; }
+</style>
+<p>PASS if there is no red.</p>
+<ol>
+ <li>
+ <math>
+ <mtext class="firstline"><span>Hello,<br/>World!</span></mtext>
+ </math>
+ </li>
+ <li>
+ <math>
+ <mtext class="firstletter">Hello, World!</mtext>
+ </math>
+ </li>
+</ol>
diff --git a/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-002-ref.html b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-002-ref.html
new file mode 100644
index 0000000000..d4c79c7218
--- /dev/null
+++ b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-002-ref.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>MathML and ::first-line/::first-letter pseudo-elements (reference)</title>
+<style>
+ .firstline > span { background: lime; }
+ .firstletter > span { background: lime; }
+</style>
+<p>PASS if the first line or letter is green.</p>
+<ol>
+ <li>
+ <div style="display: inline math" class="firstline">
+ <span>Hello,</span><br/>World!
+ </div>
+ </li>
+ <li>
+ <div style="display: inline math" class="firstletter">
+ <span>H</span>ello, World!
+ </div>
+ </li>
+ <li>
+ <div style="display: block math" class="firstline">
+ <span>Hello,</span><br/>World!
+ </div>
+ </li>
+ <li>
+ <div style="display: block math" class="firstletter">
+ <span>H</span>ello, World!
+ </div>
+ </li>
+</ol>
diff --git a/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-002.html b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-002.html
new file mode 100644
index 0000000000..7e206c951e
--- /dev/null
+++ b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-002.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>MathML and ::first-line/::first-letter pseudo-elements</title>
+<link rel="help" href="https://w3c.github.io/mathml-core/#legacy-mathml-style-attributes">
+<link rel="match" href="first-line-first-letter-pseudo-elements-002-ref.html"/>
+<meta name="assert" content="::first-line and ::first-letter do apply to non-MathML specified display math elements, because their computed values are block/inline flow.">
+<style>
+ .firstline::first-line { background: lime; }
+ .firstletter::first-letter { background: lime; }
+</style>
+<p>PASS if the first line or letter is green.</p>
+<ol>
+ <li>
+ <div style="display: inline math" class="firstline">
+ Hello,<br/>World!
+ </div>
+ </li>
+ <li>
+ <div style="display: inline math" class="firstletter">
+ Hello, World!
+ </div>
+ </li>
+ <li>
+ <div style="display: block math" class="firstline">
+ Hello,<br/>World!
+ </div>
+ </li>
+ <li>
+ <div style="display: block math" class="firstletter">
+ Hello, World!
+ </div>
+ </li>
+</ol>
diff --git a/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-003-ref.html b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-003-ref.html
new file mode 100644
index 0000000000..825b397057
--- /dev/null
+++ b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-003-ref.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>MathML and ::first-line/::first-letter pseudo-elements (reference)</title>
+<p>PASS if there is no red.</p>
+<ol>
+ <li>
+ <div class="firstline">
+ <math style="display: inline math"><mtext>Hello<br>World!</mtext></math>
+ </li>
+ <li>
+ <div class="firstletter">
+ <math>
+ <mtext style="display: inline math">Hello<br>World!</mtext>
+ </math>
+ </div>
+ </li>
+ <li>
+ <div class="firstline">
+ <math>
+ <mtext style="display: block math">Hello<br>World!</mtext>
+ </math>
+ </div>
+ </li>
+ <li>
+ <div class="firstletter">
+ <math>
+ <mtext style="display: block math">Hello<br>World!</mtext>
+ </math>
+ </div>
+ </li>
+</ol>
diff --git a/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-003.html b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-003.html
new file mode 100644
index 0000000000..042a9555e6
--- /dev/null
+++ b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-003.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>MathML and ::first-line/::first-letter pseudo-elements</title>
+<link rel="help" href="https://w3c.github.io/mathml-core/#legacy-mathml-style-attributes">
+<link rel="match" href="first-line-first-letter-pseudo-elements-003-ref.html"/>
+<meta name="assert" content="display math elements do not contribute a first formatted line/letter to ancestors.">
+<style>
+ .firstline::first-line { background: red; }
+ .firstletter::first-letter { background: red; }
+</style>
+<p>PASS if there is no red.</p>
+<ol>
+ <li>
+ <div class="firstline">
+ <math style="display: inline math"><mtext>Hello<br>World!</mtext></math>
+ </li>
+ <li>
+ <div class="firstletter">
+ <math>
+ <mtext style="display: inline math">Hello<br>World!</mtext>
+ </math>
+ </div>
+ </li>
+ <li>
+ <div class="firstline">
+ <math>
+ <mtext style="display: block math">Hello<br>World!</mtext>
+ </math>
+ </div>
+ </li>
+ <li>
+ <div class="firstletter">
+ <math>
+ <mtext style="display: block math">Hello<br>World!</mtext>
+ </math>
+ </div>
+ </li>
+</ol>
diff --git a/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-004-ref.html b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-004-ref.html
new file mode 100644
index 0000000000..60d3427489
--- /dev/null
+++ b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-004-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>MathML and ::first-line/::first-letter pseudo-elements (reference)</title>
+<style>
+ .firstline > span { background: lime; }
+ .firstletter > span { background: lime; }
+</style>
+<ol>
+ <li>PASS if first line is green:
+ <math>
+ <mtext><span class="firstline"><span>Hello,</span><br/>World!</span></mtext>
+ </math>
+ </li>
+ <li>PASS if first letter is green:
+ <math>
+ <mtext><span class="firstletter"><span>H</span>ello, World!</span></mtext>
+ </math>
+ </li>
+</ol>
diff --git a/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-004.html b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-004.html
new file mode 100644
index 0000000000..b463266bdb
--- /dev/null
+++ b/testing/web-platform/tests/mathml/relations/css-styling/first-line-first-letter-pseudo-elements-004.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>MathML and ::first-line/::first-letter pseudo-elements</title>
+<link rel="help" href="https://w3c.github.io/mathml-core/#legacy-mathml-style-attributes">
+<link rel="match" href="first-line-first-letter-pseudo-elements-004-ref.html"/>
+<meta name="assert" content="::first-letter/::first-line works for HTML elements inside MathML.">
+<style>
+ .firstline::first-line { background: lightgreen; }
+ .firstletter::first-letter { background: lightgreen; }
+</style>
+<ol>
+ <li>PASS if first line is green:
+ <math>
+ <mtext><span class="firstline">Hello,<br/>World!</span></mtext>
+ </math>
+ </li>
+ <li>PASS if first letter is green:
+ <math>
+ <mtext><span class="firstletter">Hello, World!</span></mtext>
+ </math>
+ </li>
+</ol>
diff --git a/testing/web-platform/tests/mathml/tools/stretchy.py b/testing/web-platform/tests/mathml/tools/stretchy.py
index 34530f5792..33d4decd4f 100755
--- a/testing/web-platform/tests/mathml/tools/stretchy.py
+++ b/testing/web-platform/tests/mathml/tools/stretchy.py
@@ -4,7 +4,9 @@ from utils import mathfont
import fontforge
# Create a WOFF font with glyphs for all the operator strings.
-font = mathfont.create("stretchy", "Copyright (c) 2021 Igalia S.L.")
+font = mathfont.create("stretchy", "Copyright (c) 2021-2024 Igalia S.L.")
+
+font.math.AxisHeight = 0
# Set parameters for stretchy tests.
font.math.MinConnectorOverlap = mathfont.em // 2
@@ -27,6 +29,7 @@ font.math.OverbarExtraAscender = 0
# These two characters will be stretchable in both directions.
horizontalArrow = 0x295A # LEFTWARDS HARPOON WITH BARB UP FROM BAR
verticalArrow = 0x295C # UPWARDS HARPOON WITH BARB RIGHT FROM BAR
+upDownArrowWithBase = 0x21A8 # UP DOWN ARROW WITH BASE
mathfont.createSizeVariants(font, aUsePUA=True, aCenterOnBaseline=False)
@@ -40,4 +43,10 @@ mathfont.createSquareGlyph(font, verticalArrow)
mathfont.createStretchy(font, verticalArrow, True)
mathfont.createStretchy(font, verticalArrow, False)
+# U+21A8 stretches vertically using two size variants: a base glyph (of height
+# half an em) and taller glyphs (of heights 1, 2, 3 and 4 em).
+g = font.createChar(upDownArrowWithBase)
+mathfont.drawRectangleGlyph(g, mathfont.em, mathfont.em/2, 0)
+font[upDownArrowWithBase].verticalVariants = "uni21A8 v0 v1 v2 v3"
+
mathfont.save(font)
diff --git a/testing/web-platform/tests/mediacapture-extensions/MediaStreamTrack-audio-stats.https.html b/testing/web-platform/tests/mediacapture-extensions/MediaStreamTrack-audio-stats.https.html
new file mode 100644
index 0000000000..83a2376911
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-extensions/MediaStreamTrack-audio-stats.https.html
@@ -0,0 +1,209 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<button id="button">User gesture</button>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+'use strict';
+
+async function getFrameStatsUntil(track, condition) {
+ while (true) {
+ const stats = track.stats.toJSON();
+ if (condition(stats)) {
+ return stats;
+ }
+ // Repeat in the next task execution cycle.
+ await Promise.resolve();
+ }
+};
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+
+ const firstStats =
+ await getFrameStatsUntil(track, stats => stats.totalFrames > 0);
+ await getFrameStatsUntil(track,
+ stats => stats.totalFrames > firstStats.totalFrames && stats.totalFramesDuration > firstStats.totalFramesDuration);
+}, `totalFrames and totalFramesDuration increase over time`);
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+
+ // Wait one second for stats
+ const stats =
+ await getFrameStatsUntil(track, stats => stats.totalFramesDuration > 1000);
+ assert_less_than_equal(stats.deliveredFrames, stats.totalFrames);
+ assert_less_than_equal(stats.deliveredFramesDuration, stats.totalFramesDuration);
+ assert_greater_than_equal(stats.deliveredFrames, 0);
+ assert_greater_than_equal(stats.deliveredFramesDuration, 0);
+}, `deliveredFrames and deliveredFramesDuration are at most as large as totalFrames and totalFramesDuration`);
+
+promise_test(async t => {
+ function assertLatencyStatsInBounds(stats) {
+ assert_greater_than_equal(stats.maximumLatency, stats.latency);
+ assert_greater_than_equal(stats.maximumLatency, stats.averageLatency);
+ assert_less_than_equal(stats.minimumLatency, stats.latency);
+ assert_less_than_equal(stats.minimumLatency, stats.averageLatency);
+ };
+
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+ const firstStats = track.stats.toJSON();
+ assertLatencyStatsInBounds(firstStats);
+
+ // Wait one second for a second stats object.
+ const secondStats =
+ await getFrameStatsUntil(track, stats => stats.totalFramesDuration - firstStats.totalFramesDuration >= 1000);
+ assertLatencyStatsInBounds(secondStats);
+
+ // Reset the latency stats and wait one second for a third stats object.
+ track.stats.resetLatency();
+ const thirdStats =
+ await getFrameStatsUntil(track, stats => stats.totalFramesDuration - secondStats.totalFramesDuration >= 1000);
+ assertLatencyStatsInBounds(thirdStats);
+}, `Latency and averageLatency is within the bounds of minimumLatency and maximumLatency`);
+
+promise_test(async t => {
+ // This behaviour is defined in
+ // https://w3c.github.io/mediacapture-extensions/#dom-mediastreamtrackaudiostats-resetlatency
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+
+ // Wait one second for stats
+ const stats =
+ await getFrameStatsUntil(track, stats => stats.totalFramesDuration > 1000);
+ track.stats.resetLatency();
+ assert_equals(track.stats.latency, stats.latency);
+ assert_equals(track.stats.averageLatency, stats.latency);
+ assert_equals(track.stats.minimumLatency, stats.latency);
+ assert_equals(track.stats.maximumLatency, stats.latency);
+}, `Immediately after resetLatency(), latency, averageLatency, minimumLatency and maximumLatency are equal to the most recent latency.`);
+
+promise_test(async t => {
+ // This behaviour is defined in
+ // https://w3c.github.io/mediacapture-extensions/#dfn-expose-audio-frame-counters-steps
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+
+ // Wait until we have meaningful data
+ await getFrameStatsUntil(track, stats => stats.totalFrames > 0);
+ const firstStats = track.stats.toJSON();
+
+ // Synchronously wait 500 ms.
+ const start = performance.now();
+ while(performance.now() - start < 500);
+
+ // The stats should still be the same, despite the time difference.
+ const secondStats = track.stats.toJSON();
+ assert_equals(JSON.stringify(firstStats), JSON.stringify(secondStats));
+}, `Stats do not change within the same task execution cycle.`);
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+
+ // Wait for media to flow before disabling the `track`.
+ const initialStats = await getFrameStatsUntil(track, stats => stats.totalFrames > 0);
+ track.enabled = false;
+ // Upon disabling, the counters are not reset.
+ const disabledSnapshot = track.stats.toJSON();
+ assert_greater_than_equal(disabledSnapshot.totalFramesDuration,
+ initialStats.totalFramesDuration);
+
+ await new Promise(r => t.step_timeout(r, 4000));
+
+ // Frame metrics should be frozen, but because `enabled = false` does not
+ // return a promise, we allow some lee-way in case som buffers were still in flight
+ // during the disabling.
+ assert_approx_equals(
+ track.stats.totalFramesDuration, disabledSnapshot.totalFramesDuration, 1000);
+}, `Stats are frozen while disabled`);
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+
+ const a = track.stats;
+ await getFrameStatsUntil(track, stats => stats.totalFrames > 0);
+ const b = track.stats;
+ // The counters may have changed, but `a` and `b` are still the same object.
+ assert_equals(a, b);
+}, `SameObject policy applies`);
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+
+ // Hold a reference directly to the [SameObject] stats, bypassing the
+ // `track.stats` getter in the subsequent getting of `totalFrames`.
+ const stats = track.stats;
+ const firstTotalFrames = stats.totalFrames;
+ while (stats.totalFrames == firstTotalFrames) {
+ await Promise.resolve();
+ }
+ assert_greater_than(stats.totalFrames, firstTotalFrames);
+}, `Counters increase even if we don't call the track.stats getter`);
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+
+ // Wait for 500 ms of audio to flow before disabling the `track`.
+ const initialStats = await getFrameStatsUntil(track, stats =>
+ stats.totalFramesDuration > 500);
+ track.enabled = false;
+
+ // Re-enable the track. The stats counters should be greater than or equal to
+ // what they were previously.
+ track.enabled = true;
+ assert_greater_than_equal(track.stats.totalFrames,
+ initialStats.totalFrames);
+ assert_greater_than_equal(track.stats.totalFramesDuration,
+ initialStats.totalFramesDuration);
+ // This should be true even in the next task execution cycle.
+ await Promise.resolve();
+ assert_greater_than_equal(track.stats.totalFrames,
+ initialStats.totalFrames);
+ assert_greater_than_equal(track.stats.totalFramesDuration,
+ initialStats.totalFramesDuration);
+}, `Disabling and re-enabling does not reset the counters`);
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ const [originalTrack] = stream.getTracks();
+ t.add_cleanup(() => originalTrack.stop());
+
+ // Wait for 500 ms of audio to flow.
+ await getFrameStatsUntil(originalTrack, stats =>
+ stats.totalFramesDuration > 500);
+
+ // Clone the track. While its counters should initially be zero, it would be
+ // racy to assert that they are exactly zero because media is flowing.
+ const clonedTrack = originalTrack.clone();
+ t.add_cleanup(() => clonedTrack.stop());
+
+ // Ensure that as media continues to flow, the cloned track will necessarily
+ // have less frames than the original track on all accounts since its counters
+ // will have started from zero.
+ const clonedTrackStats = await getFrameStatsUntil(clonedTrack, stats =>
+ stats.totalFramesDuration > 0);
+ assert_less_than(clonedTrackStats.totalFrames,
+ originalTrack.stats.totalFrames);
+ assert_less_than(clonedTrackStats.totalFramesDuration,
+ originalTrack.stats.totalFramesDuration);
+}, `New stats baselines when a track is cloned from an enabled track`);
+</script>
diff --git a/testing/web-platform/tests/mediacapture-extensions/MediaStreamTrack-video-stats.https.html b/testing/web-platform/tests/mediacapture-extensions/MediaStreamTrack-video-stats.https.html
index f1b6a2074a..374a22c7ca 100644
--- a/testing/web-platform/tests/mediacapture-extensions/MediaStreamTrack-video-stats.https.html
+++ b/testing/web-platform/tests/mediacapture-extensions/MediaStreamTrack-video-stats.https.html
@@ -322,14 +322,6 @@ promise_test(async t => {
}, `A low FPS clone does not affect the original track's discardedFrames`);
promise_test(async t => {
- const stream = await navigator.mediaDevices.getUserMedia({audio:true});
- const [track] = stream.getTracks();
- t.add_cleanup(() => track.stop());
-
- assert_equals(track.stats, null);
-}, `track.stats is null on audio tracks`);
-
-promise_test(async t => {
const canvas = document.createElement('canvas');
const stream = canvas.captureStream(10);
const [track] = stream.getTracks();
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-canvas-media-source.https.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-canvas-media-source.https.html
index e640714d5c..0680c21879 100644
--- a/testing/web-platform/tests/mediacapture-record/MediaRecorder-canvas-media-source.https.html
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-canvas-media-source.https.html
@@ -9,6 +9,8 @@
<meta name=variant content="?mimeType=video/webm;codecs=vp9,opus">
<meta name=variant content="?mimeType=video/webm;codecs=av1,opus">
<meta name=variant content="?mimeType=video/mp4;codecs=avc1,mp4a.40.2">
+ <meta name=variant content="?mimeType=video/mp4;codecs=vp9,opus">
+ <meta name=variant content="?mimeType=video/mp4">
<link rel="help"
href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType">
<script src="/resources/testharness.js"></script>
@@ -84,9 +86,9 @@ async_test(test => {
const params = new URLSearchParams(window.location.search);
const mimeType = params.get('mimeType');
- if (mimeType) {
- assert_implements_optional(MediaRecorder.isTypeSupported(mimeType),
- `"${mimeType}" for MediaRecorder is not supported`);
+ if (mimeType && !MediaRecorder.isTypeSupported(mimeType)) {
+ test.done();
+ return;
}
const canvas = document.querySelector("canvas");
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-events-and-exceptions.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-events-and-exceptions.html
index 409e46c91d..97ada21266 100644
--- a/testing/web-platform/tests/mediacapture-record/MediaRecorder-events-and-exceptions.html
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-events-and-exceptions.html
@@ -8,6 +8,8 @@
<meta name=variant content="?mimeType=video/webm;codecs=vp9,opus">
<meta name=variant content="?mimeType=video/webm;codecs=av1,opus">
<meta name=variant content="?mimeType=video/mp4;codecs=avc1,mp4a.40.2">
+ <meta name=variant content="?mimeType=video/mp4;codecs=vp9,opus">
+ <meta name=variant content="?mimeType=video/mp4">
<link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#mediarecorder">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
@@ -81,9 +83,9 @@
const params = new URLSearchParams(window.location.search);
const mimeType = params.get('mimeType');
- if (mimeType) {
- assert_implements_optional(MediaRecorder.isTypeSupported(mimeType),
- `"${mimeType}" for MediaRecorder is not supported`);
+ if (mimeType && !MediaRecorder.isTypeSupported(mimeType)) {
+ test.done();
+ return;
}
const recorder = new MediaRecorder(new MediaStream(), { mimeType });
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-mimetype.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-mimetype.html
index 74248d65f4..57baa1346f 100644
--- a/testing/web-platform/tests/mediacapture-record/MediaRecorder-mimetype.html
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-mimetype.html
@@ -21,6 +21,7 @@ const AUDIO_CODECS_MIME_TYPES = [
'audio/webm; codecs="vorbis"',
'audio/webm; codecs="opus"',
'audio/mp4: codecs="mp4a.40.2"',
+ 'audio/mp4: codecs="opus"',
];
const VIDEO_ONLY_MIME_TYPES = [
@@ -33,6 +34,7 @@ const VIDEO_CODECS_MIME_TYPES = [
'video/webm; codecs="vp9"',
'video/webm; codecs="av1"',
'video/mp4: codecs="avc1"',
+ 'video/mp4: codecs="vp9"',
];
const AUDIO_VIDEO_MIME_TYPES = [
@@ -42,6 +44,7 @@ const AUDIO_VIDEO_MIME_TYPES = [
'video/webm; codecs="vp9, opus"',
'video/webm; codecs="av1, opus"',
'video/mp4: codecs="avc1, mp4a.40.2"',
+ 'video/mp4; codecs="vp9, opus"',
];
const AUDIO_MIME_TYPES = [
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-pause-resume.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-pause-resume.html
index 8dc231279a..f584508a0d 100644
--- a/testing/web-platform/tests/mediacapture-record/MediaRecorder-pause-resume.html
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-pause-resume.html
@@ -8,6 +8,10 @@
<meta name=variant content="?mimeType=video/webm;codecs=vp9,opus">
<meta name=variant content="?mimeType=video/webm;codecs=av1,opus">
<meta name=variant content="?mimeType=video/mp4;codecs=avc1,mp4a.40.2">
+ <meta name=variant content="?mimeType=video/mp4;codecs=avc1,opus">
+ <meta name=variant content="?mimeType=video/mp4;codecs=vp9,opus">
+ <meta name=variant content="?mimeType=video/mp4;codecs=vp9,mp4a.40.2">
+ <meta name=variant content="?mimeType=video/mp4">
<link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#mediarecorder">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-peerconnection.https.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-peerconnection.https.html
index 3fbc1f0f2d..daae044fa8 100644
--- a/testing/web-platform/tests/mediacapture-record/MediaRecorder-peerconnection.https.html
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-peerconnection.https.html
@@ -4,6 +4,20 @@
<head>
<title>MediaRecorder peer connection</title>
+ <meta name=variant content="?kinds=video&mimeType=''">
+ <meta name=variant content="?kinds=audio&mimeType=''">
+ <meta name=variant content="?kinds=video,audio&mimeType=''">
+ <meta name=variant content="?kinds=audio&mimeType=audio/webm;codecs=opus">
+ <meta name=variant content="?kinds=video&mimeType=video/webm;codecs=vp8">
+ <meta name=variant content="?kinds=video,audio&mimeType=video/webm;codecs=vp8,opus">
+ <meta name=variant content="?kinds=video&mimeType=video/webm;codecs=vp9">
+ <meta name=variant content="?kinds=video,audio&mimeType=video/webm;codecs=vp9,opus">
+ <meta name=variant content="?kinds=video,audio&mimeType=video/mp4;codecs=avc1,mp4a.40.2">
+ <meta name=variant content="?kinds=video&mimeType=video/mp4;codecs=vp9">
+ <meta name=variant content="?kinds=audio&mimeType=audio/mp4;codecs=opus">
+ <meta name=variant content="?kinds=video,audio&mimeType=video/mp4;codecs=vp9,opus">
+ <meta name=variant content="?kinds=video,audio&mimeType=video/mp4">
+
<link rel="help"
href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType">
<script src="/resources/testharness.js"></script>
@@ -15,74 +29,83 @@
</head>
<body>
- <video id="remote" autoplay width="240"></video>
- <script>
+<video id="remote" autoplay width="240"></video>
+
+<script>
+ const params = new URLSearchParams(window.location.search);
+ const mimeType = params.get('mimeType');
+ const kinds = params.get('kinds');
+ const tag = `kinds "${kinds} "mimeType "${mimeType}"`;
+ let stream;
+ let pc;
-promise_setup(async () => {
- const t = {add_cleanup: add_completion_callback};
- const [, pc, stream] = await startConnection(t, true, true);
- const [audio] = stream.getAudioTracks();
- const [video] = stream.getVideoTracks();
+ promise_setup(async () => {
+ const t = {add_cleanup: add_completion_callback};
+ const [, connection_pc, connection_stream] = await startConnection(t, true, true);
+ pc = connection_pc;
+
+ let video = null;
+ if (kinds.indexOf('video') != -1) {
+ video = connection_stream.getVideoTracks()[0];
+ }
- // Needed for the tests to get exercised in Chrome (bug)
- document.getElementById('remote').srcObject = stream;
+ let audio = null;
+ if (kinds.indexOf('audio') != -1) {
+ audio = connection_stream.getAudioTracks()[0];
+ }
- for (const {kinds, mimeType} of [
- { kinds: { video }, mimeType: "" },
- { kinds: { audio }, mimeType: "" },
- { kinds: { video, audio }, mimeType: "" },
- { kinds: { audio }, mimeType: "audio/webm;codecs=opus" },
- { kinds: { video }, mimeType: "video/webm;codecs=vp8" },
- { kinds: { video, audio }, mimeType: "video/webm;codecs=vp8,opus" },
- { kinds: { video }, mimeType: "video/webm;codecs=vp9" },
- { kinds: { video, audio }, mimeType: "video/webm;codecs=vp9,opus" },
- { kinds: { audio }, mimeType: "audio/mp4;codecs=mp4a.40.2" },
- { kinds: { video, audio }, mimeType: "video/mp4;codecs=avc1,mp4a.40.2" }
- ]) {
- const tag = `${JSON.stringify(kinds)} mimeType "${mimeType}"`;
- const stream = new MediaStream([kinds.audio, kinds.video].filter(n => n));
+ // Needed for the tests to get exercised in Chrome (bug)
+ document.getElementById('remote').srcObject = connection_stream;
+ stream = new MediaStream([audio, video].filter(n => n));
+ });
+ promise_test(async t => {
// Spec doesn't mandate codecs, so if not supported, test failure instead.
if (mimeType && !MediaRecorder.isTypeSupported(mimeType)) {
promise_test(async t => {
assert_throws_dom('NotSupportedError',
() => new MediaRecorder(stream, { mimeType }));
- }, `MediaRecorder constructor throws on no support, ${tag}`);
- continue;
+ }, `MediaRecorder constructor throws on no support 1, ${tag}`);
+ return;
}
- promise_test(async t => {
- const recorder = new MediaRecorder(stream, { mimeType });
- recorder.start(200);
- await new Promise(r => recorder.onstart = r);
- let combinedSize = 0;
- // Wait for a small amount of data to appear. Kept small for mobile tests
- while (combinedSize < 2000) {
- const {data} = await new Promise(r => recorder.ondataavailable = r);
- combinedSize += data.size;
- }
- recorder.stop();
- }, `PeerConnection MediaRecorder receives data after onstart, ${tag}`);
+ const recorder = new MediaRecorder(stream, { mimeType });
+ recorder.start(200);
+ await new Promise(r => recorder.onstart = r);
+ let combinedSize = 0;
+ // Wait for a small amount of data to appear. Kept small for mobile tests
+ while (combinedSize < 2000) {
+ const {data} = await new Promise(r => recorder.ondataavailable = r);
+ combinedSize += data.size;
+ }
+ recorder.stop();
+ }, `PeerConnection MediaRecorder receives data after onstart, ${tag}`);
- promise_test(async t => {
- const clone = stream.clone();
- const recorder = new MediaRecorder(clone, { mimeType });
- recorder.start();
- await new Promise(r => recorder.onstart = r);
- await waitForReceivedFramesOrPackets(t, pc, kinds.audio, kinds.video, 10);
- for (const track of clone.getTracks()) {
- track.stop();
- }
- // As the tracks ended, expect data from the recorder.
- await Promise.all([
- new Promise(r => recorder.onstop = r),
- new Promise(r => recorder.ondataavailable = r)
- ]);
- }, `PeerConnection MediaRecorder gets ondata on stopping tracks, ${tag}`);
- }
-});
+ promise_test(async t => {
+ // Spec doesn't mandate codecs, so if not supported, test failure instead.
+ if (mimeType && !MediaRecorder.isTypeSupported(mimeType)) {
+ promise_test(async t => {
+ assert_throws_dom('NotSupportedError',
+ () => new MediaRecorder(stream, { mimeType }));
+ }, `MediaRecorder constructor throws on no support 2, ${tag}`);
+ return;
+ }
+
+ const clone = stream.clone();
+ const recorder = new MediaRecorder(clone, { mimeType });
+ recorder.start();
+ await new Promise(r => recorder.onstart = r);
+ await waitForReceivedFramesOrPackets(t, pc, kinds.audio, kinds.video, 10);
+ for (const track of clone.getTracks()) {
+ track.stop();
+ }
+ // As the tracks ended, expect data from the recorder.
+ await Promise.all([
+ new Promise(r => recorder.onstop = r),
+ new Promise(r => recorder.ondataavailable = r)
+ ]);
+ }, `PeerConnection MediaRecorder gets ondata on stopping tracks, ${tag}`);
</script>
</body>
-
</html>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-stop.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-stop.html
index d6ce370772..9ef5051638 100644
--- a/testing/web-platform/tests/mediacapture-record/MediaRecorder-stop.html
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-stop.html
@@ -7,6 +7,8 @@
<meta name=variant content="?mimeType=video/webm;codecs=vp9,opus">
<meta name=variant content="?mimeType=video/webm;codecs=av1,opus">
<meta name=variant content="?mimeType=video/mp4;codecs=avc1,mp4a.40.2">
+ <meta name=variant content="?mimeType=video/mp4;codecs=vp9,opus">
+ <meta name=variant content="?mimeType=video/mp4">
<link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#mediarecorder">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
@@ -38,18 +40,21 @@
return true;
}
- function doneWithUnsupportedType(mimeType) {
- if (mimeType) {
- assert_implements_optional(MediaRecorder.isTypeSupported(mimeType),
- `"${mimeType}" for MediaRecorder is not supported`);
+ function isMimetypeSupported(mimeType, t) {
+ if (mimeType && !MediaRecorder.isTypeSupported(mimeType)) {
+ t.done();
+ return false;
}
+ return true;
}
const params = new URLSearchParams(window.location.search);
const mimeType = params.get('mimeType');
const tag = `mimeType "${mimeType}"`;
promise_test(async t => {
- doneWithUnsupportedType(mimeType);
+ if (!isMimetypeSupported(mimeType, t)) {
+ return;
+ }
const {stream: video} = createVideoStream(t);
const recorder = new MediaRecorder(video, {mimeType});
@@ -73,7 +78,9 @@
}, "MediaRecorder will stop recording and fire a stop event when all tracks are ended");
promise_test(async t => {
- doneWithUnsupportedType(mimeType);
+ if (!isMimetypeSupported(mimeType, t)) {
+ return;
+ }
const {stream: video} = createVideoStream(t);
const recorder = new MediaRecorder(video, {mimeType});
@@ -96,7 +103,9 @@
}, "MediaRecorder will stop recording and fire a stop event when stop() is called");
promise_test(async t => {
- doneWithUnsupportedType(mimeType);
+ if (!isMimetypeSupported(mimeType, t)) {
+ return;
+ }
const recorder = new MediaRecorder(createVideoStream(t).stream, {mimeType});
recorder.stop();
@@ -107,7 +116,9 @@
}, "MediaRecorder will not fire an exception when stopped after creation");
promise_test(async t => {
- doneWithUnsupportedType(mimeType);
+ if (!isMimetypeSupported(mimeType, t)) {
+ return;
+ }
const recorder = new MediaRecorder(createVideoStream(t).stream, {mimeType});
recorder.start();
@@ -121,7 +132,9 @@
}, "MediaRecorder will not fire an exception when stopped after having just been stopped");
promise_test(async t => {
- doneWithUnsupportedType(mimeType);
+ if (!isMimetypeSupported(mimeType, t)) {
+ return;
+ }
const {stream} = createVideoStream(t);
const recorder = new MediaRecorder(stream, {mimeType});
@@ -136,7 +149,9 @@
}, "MediaRecorder will not fire an exception when stopped after having just been spontaneously stopped");
promise_test(async t => {
- doneWithUnsupportedType(mimeType);
+ if (!isMimetypeSupported(mimeType, t)) {
+ return;
+ }
const {stream} = createAudioVideoStream(t);
const recorder = new MediaRecorder(stream, {mimeType});
@@ -155,7 +170,9 @@
}, "MediaRecorder will fire start event even if stopped synchronously");
promise_test(async t => {
- doneWithUnsupportedType(mimeType);
+ if (!isMimetypeSupported(mimeType, t)) {
+ return;
+ }
const {stream} = createAudioVideoStream(t);
const recorder = new MediaRecorder(stream, {mimeType});
@@ -178,7 +195,9 @@
}, "MediaRecorder will fire start event even if a track is removed synchronously");
promise_test(async t => {
- doneWithUnsupportedType(mimeType);
+ if (!isMimetypeSupported(mimeType, t)) {
+ return;
+ }
const {stream} = createFlowingAudioVideoStream(t);
const recorder = new MediaRecorder(stream, {mimeType});
diff --git a/testing/web-platform/tests/mediacapture-streams/BrowserCaptureMediaStreamTrack-restrictTo.https.html b/testing/web-platform/tests/mediacapture-streams/BrowserCaptureMediaStreamTrack-restrictTo.https.html
new file mode 100644
index 0000000000..4b0da740bd
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/BrowserCaptureMediaStreamTrack-restrictTo.https.html
@@ -0,0 +1,245 @@
+<!doctype html>
+<html>
+
+<head>
+ <title>BrowserCaptureMediaStreamTrack restrictTo()</title>
+ <link rel="help" href="https://screen-share.github.io/element-capture/">
+</head>
+
+<body>
+ <p class="instructions">
+ When prompted, accept to give permission to use your audio, video devices.
+ </p>
+ <h1 class="instructions">Description</h1>
+ <p class="instructions">
+ This test checks that restricting BrowserCaptureMediaStreamTrack works as
+ expected.
+ </p>
+
+ <style>
+ div {
+ height: 100px;
+ }
+ .stacking {
+ opacity: 0.9;
+ }
+ #container {
+ columns:4;
+ column-fill:auto;
+ }
+ .fragmentize {
+ height: 50px;
+ }
+ #target {
+ background: linear-gradient(red, blue);
+ }
+ </style>
+
+
+ <div id='container'>
+ <div id='target'></div>
+ </div>
+ <video id="video"
+ style="border: 2px blue dotted; width: 250px; height: 250px;"
+ autoplay playsinline muted></video>
+
+ <script src=/resources/testharness.js></script>
+ <script src=/resources/testharnessreport.js></script>
+ <script src=/resources/testdriver.js></script>
+ <script src=/resources/testdriver-vendor.js></script>
+
+ <script>
+ "use strict";
+
+ // For more information, see:
+ // https://screen-share.github.io/element-capture/#elements-eligible-for-restriction
+ const EligibilityRequirement = {
+ StackingContext: "StackingContext",
+ OnlyOneBoxFragment: "OnlyOneBoxFragment",
+ FlattenedIn3D: "FlattenedIn3D",
+ };
+
+ // The target div.
+ const div = document.getElementById('target');
+
+ // Returns a promise that, if successful, will resolve to a media stream.
+ async function getDisplayMedia() {
+ return test_driver.bless('getDisplayMedia', () =>
+ navigator.mediaDevices.getDisplayMedia({
+ video: { displaySurface: "browser" },
+ selfBrowserSurface: "include",
+ }));
+ }
+
+ // Returns a promise that will resolve successfully if at least one frame is
+ // read before the timeout.
+ function assertFrameRead(t, state, message) {
+ const last_frame_count = state.frame_count;
+ return t.step_wait(() => state.frame_count > last_frame_count,
+ message, 5000, 10);
+ }
+
+ // Returns a promise that will resolve successfully if there are no frames
+ // produced for an entire second after being called.
+ function assertStopsProducingFrames(t, state, message) {
+ let last_frame_count = state.frame_count;
+
+ return t.step_timeout(() => {
+ assert_equals(state.frame_count, last_frame_count);
+ }, 1000);
+ }
+
+ function makeDivEligible() {
+ // Must always have a stacking context to be eligible.
+ div.classList.add("stacking");
+ div.parentElement.classList.remove("fragmented");
+ div.style.transform = "";
+ }
+
+ function makeDivIneligible(state) {
+ switch(state.eligibilityParam) {
+ case EligibilityRequirement.StackingContext:
+ div.classList.remove("stacking");
+ break;
+
+ case EligibilityRequirement.OnlyOneBoxFragment:
+ div.parentElement.classList.add("fragmented");
+ break;
+
+ case EligibilityRequirement.FlattenedIn3D:
+ div.style.transform = "rotateY(90deg)";
+ break;
+ }
+ }
+
+ // Restore element state after each test.
+ function cleanupDiv() {
+ div.classList.remove("stacking");
+ div.parentElement.classList.remove("fragmented");
+ div.style.transform = "";
+ }
+
+ function startAnimation(t, state) {
+ let count = 0;
+ function animate() {
+ if (!state.running) {
+ return;
+ }
+ count += 1;
+ div.innerText = count;
+ window.requestAnimationFrame(animate);
+ }
+ window.requestAnimationFrame(animate);
+
+ // Stop animation as part of cleanup.
+ t.add_cleanup(() => { state.running = false; });
+ }
+
+ // Updates the state.frame_count value whenever a new frame is received on
+ // the passed in media stream track.
+ async function readFromTrack(state, track) {
+ while (state.running) {
+ const reader = new MediaStreamTrackProcessor(track).readable.getReader();
+ while (true) {
+ const frameOrDone = await reader.read();
+ if (frameOrDone.done) {
+ break;
+ }
+ frameOrDone.value.close();
+ state.frame_count += 1;
+ }
+ }
+ }
+
+ // Parameterized test method. Note that this returns a Promise that will be
+ // resolved to represent success of the entire promise test.
+ async function runTest(t, eligibilityParam) {
+ let state = {
+ eligibilityParam: eligibilityParam,
+ frame_count: 0,
+ running: true,
+ reading: false,
+ };
+ startAnimation(t, state);
+
+ let videoTrack = undefined;
+ return getDisplayMedia().then(stream => {
+ t.add_cleanup(() => {
+ stream.getTracks().forEach(track => track.stop());
+ });
+ assert_true(!!stream, "should have resolved to a stream.");
+ assert_true(stream.active, "stream should be active.");
+ assert_equals(stream.getVideoTracks().length, 1);
+
+ [videoTrack] = stream.getVideoTracks();
+ assert_true(videoTrack instanceof MediaStreamTrack,
+ "track should be either MediaStreamTrack or a subclass thereof.");
+ assert_equals(videoTrack.readyState, "live", "track should be live.");
+
+ // Consume the stream in a video element.
+ const video = document.querySelector('video');
+ video.srcObject = stream;
+
+ // Remove the video source, so that the stream object can be released.
+ t.add_cleanup(() => {video.srcObject = null});
+
+ // Keep track of the number of frames used.
+ const readPromise = readFromTrack(state, videoTrack);
+ t.add_cleanup(() => readPromise);
+
+ return assertFrameRead(t, state, "Track should produce frames.");
+ }).then(() => {
+ assert_true(!!RestrictionTarget, "RestrictionTarget exposed.");
+ assert_true(!!RestrictionTarget.fromElement,
+ "RestrictionTarget.fromElement exposed.");
+
+ return RestrictionTarget.fromElement(div);
+ }).then(restrictionTarget => {
+ assert_true(!!videoTrack.restrictTo, "restrictTo exposed.");
+ assert_true(typeof videoTrack.restrictTo === 'function',
+ "restrictTo is a function.");
+
+ return videoTrack.restrictTo(restrictionTarget);
+ }).then(() => {
+ // By default, elements are not eligible for restriction due to not being
+ // placed in their own stacking context.
+ return assertStopsProducingFrames(t, state,
+ "No new frames after restriction.");
+ });
+
+ // TODO(crbug.com/333770107): once the issue with the
+ // --disable-threaded-compositing flag is resolved on Chrome's check in bots
+ // the rest of this test should be enabled.
+ // ).then(() => {
+ // // Should be unpaused now that the element is eligible.
+ // makeDivEligible();
+
+ // // Make sure the element state is restored to default between tests.
+ // t.add_cleanup(() => { cleanupDiv(); });
+
+ // // Restart the reader now that the stream is producing frames again.
+ // return assertFrameRead(t, state,
+ // "Received at least one frame after becoming eligible.");
+ // }).then(() => {
+
+ // // Should pause if it becomes ineligible again.
+ // makeDivIneligible(state);
+ // return assertStopsProducingFrames(t, state,
+ // "No new frames after becoming ineligible again.");
+ // });
+ }
+
+ // Test parameterizations.
+ [
+ EligibilityRequirement.StackingContext,
+ EligibilityRequirement.OnlyOneBoxFragment,
+ EligibilityRequirement.FlattenedIn3D,
+ ].forEach(param =>
+ promise_test(t => runTest(t, param),
+ `Tests that restricting MediaStreamTrack objects works as expected (${param}).`
+ ));
+
+ </script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/event-constructor.html b/testing/web-platform/tests/navigation-api/navigate-event/event-constructor.html
index 863681ced7..89ac934020 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/event-constructor.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/event-constructor.html
@@ -78,7 +78,8 @@ async_test(t => {
assert_equals(event.downloadRequest, downloadRequest);
assert_equals(event.info, info);
assert_equals(event.hasUAVisualTransition, hasUAVisualTransition);
- assert_equals(event.sourceElement, sourceElement);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(event.sourceElement, sourceElement);
});
history.pushState(2, null, "#2");
}, "all properties are reflected back");
@@ -98,7 +99,8 @@ async_test(t => {
assert_equals(event.formData, null);
assert_equals(event.downloadRequest, null);
assert_equals(event.info, undefined);
- assert_equals(event.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(event.sourceElement, null);
});
history.pushState(3, null, "#3");
}, "defaults are as expected");
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-cross-origin.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-cross-origin.html
index 62e5adb20a..a69b78196f 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-cross-origin.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-cross-origin.html
@@ -17,7 +17,8 @@ async_test(t => {
assert_equals(e.destination.key, "");
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
- assert_equals(e.sourceElement, document.getElementById("a"));
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, document.getElementById("a"));
e.preventDefault();
});
a.click();
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download-userInitiated.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download-userInitiated.html
index 6918142529..17109a2ae8 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download-userInitiated.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download-userInitiated.html
@@ -21,7 +21,8 @@ async_test(t => {
assert_equals(e.destination.key, "");
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
- assert_equals(e.sourceElement, document.getElementById("a"));
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, document.getElementById("a"));
e.preventDefault();
t.step_timeout(t.step_func_done(() => assert_equals(location.hash, "")), 0);
});
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download.html
index d04245ec25..fbeeb69b95 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download.html
@@ -25,7 +25,8 @@ for (const [tag, download_attr] of tests) {
assert_equals(e.destination.key, "");
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
- assert_equals(e.sourceElement, a);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, a);
e.preventDefault();
});
a.click();
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-fragment.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-fragment.html
index 6443ccecd1..69400c3d2f 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-fragment.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-fragment.html
@@ -17,7 +17,8 @@ async_test(t => {
assert_equals(e.destination.key, "");
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
- assert_equals(e.sourceElement, document.getElementById("a"));
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, document.getElementById("a"));
e.preventDefault();
t.step_timeout(t.step_func_done(() => assert_equals(location.hash, "")), 0);
});
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-same-origin-cross-document.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-same-origin-cross-document.html
index 5a6dce7ec8..084993540a 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-same-origin-cross-document.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-same-origin-cross-document.html
@@ -18,7 +18,8 @@ async_test(t => {
assert_equals(e.destination.key, "");
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
- assert_equals(e.sourceElement, document.getElementById("a"));
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, document.getElementById("a"));
e.preventDefault();
});
a.click();
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-userInitiated.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-userInitiated.html
index bb76e7a3fb..686d7ad6ee 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-userInitiated.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-userInitiated.html
@@ -20,7 +20,8 @@ async_test(t => {
assert_equals(e.destination.key, "");
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
- assert_equals(e.sourceElement, document.getElementById("a"));
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, document.getElementById("a"));
e.preventDefault();
t.step_timeout(t.step_func_done(() => assert_equals(location.hash, "")), 0);
});
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-with-target.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-with-target.html
index e4b897d82f..de74239b3b 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-with-target.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-with-target.html
@@ -20,10 +20,11 @@ async_test(t => {
assert_equals(new URL(e.destination.url).pathname,
"/navigation-api/navigate-event/foo.html");
assert_false(e.destination.sameDocument);
- assert_equals(e.destination.key, "");
- assert_equals(e.destination.id, "");
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
e.preventDefault();
});
a.click();
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-back-forward.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-back-forward.html
index 869fc16481..b206981821 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-back-forward.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-back-forward.html
@@ -19,7 +19,8 @@ async_test(t => {
assert_equals(e.destination.key, "");
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
});
navigation.back();
}), 0);
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-navigate.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-navigate.html
index d19a168514..1e622f9911 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-navigate.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-navigate.html
@@ -16,7 +16,8 @@ async_test(t => {
assert_equals(e.destination.key, "");
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
});
navigation.navigate("#foo", { state: navState });
}, 0);
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-reload.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-reload.html
index ac6528ce7e..be33b8120d 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-reload.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-reload.html
@@ -16,7 +16,8 @@ async_test(t => {
assert_equals(e.destination.key, "");
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
e.intercept();
});
navigation.updateCurrentEntry({ state: navState });
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-get.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-get.html
index 7a71a1c305..59661a1caf 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-get.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-get.html
@@ -21,7 +21,8 @@ async_test(t => {
// Because it's a GET, not a POST
assert_equals(e.formData, null);
- assert_equals(e.sourceElement, form);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, form);
});
window.onload = t.step_func(() => form.submit());
}, "<form> submission with GET method fires navigate event but with formData null");
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-reload.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-reload.html
index 2171690537..cdd3ade4ff 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-reload.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-reload.html
@@ -12,7 +12,8 @@ async_test(t => {
iframe.contentWindow.navigation.onnavigate = t.step_func(e => {
assert_equals(e.navigationType, "push");
assert_not_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
iframe.onload = t.step_func(() => {
iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-traverse.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-traverse.html
index b9665c8056..72b8dc36ac 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-traverse.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-traverse.html
@@ -12,7 +12,8 @@ async_test(t => {
iframe.contentWindow.navigation.onnavigate = t.step_func(e => {
assert_equals(e.navigationType, "push");
assert_not_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
iframe.onload = t.step_func(() => {
// Avoid the replace behavior that occurs if you navigate during the load handler
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-userInitiated.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-userInitiated.html
index 246e028a0d..079f546771 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-userInitiated.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-userInitiated.html
@@ -24,7 +24,8 @@ async_test(t => {
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
assert_not_equals(e.formData, null);
- assert_equals(e.sourceElement, submit);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, submit);
});
window.onload = t.step_func(() => test_driver.click(submit));
}, "<form> submission fires navigate event");
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-with-target.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-with-target.html
index 4b1a8ce9d3..07418366a7 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-with-target.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-with-target.html
@@ -22,7 +22,8 @@ async_test(t => {
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
assert_not_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
});
form.submit();
});
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form.html
index 653ef3b8eb..8cea889bec 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form.html
@@ -19,7 +19,8 @@ async_test(t => {
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
assert_not_equals(e.formData, null);
- assert_equals(e.sourceElement, form);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, form);
});
window.onload = t.step_func(() => form.submit());
}, "<form> submission fires navigate event");
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-fragment.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-fragment.html
index 9da4ddcd50..4529fe25ac 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-fragment.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-fragment.html
@@ -24,7 +24,8 @@ async_test(t => {
assert_equals(e.destination.id, target_id);
assert_equals(e.destination.index, start_index);
assert_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
});
history.back();
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-pushState.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-pushState.html
index fdee41a312..06c3b89bbc 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-pushState.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-pushState.html
@@ -24,7 +24,8 @@ async_test(t => {
assert_equals(e.destination.id, target_id);
assert_equals(e.destination.index, start_index);
assert_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
});
history.back();
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-cross-document.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-cross-document.html
index 29c626a522..a34b20a5e5 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-cross-document.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-cross-document.html
@@ -23,7 +23,8 @@ async_test(t => {
assert_equals(e.destination.index, 0);
assert_equals(e.formData, null);
assert_equals(e.info, undefined);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
});
assert_true(i.contentWindow.navigation.canGoBack);
i.contentWindow.history.back();
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-go-0.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-go-0.html
index 5508928e19..cd716bfafb 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-go-0.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-go-0.html
@@ -18,7 +18,8 @@ async_test(t => {
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
assert_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
e.preventDefault();
});
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-pushState.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-pushState.html
index 5079b21078..f89288b6ed 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-pushState.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-pushState.html
@@ -17,7 +17,8 @@ async_test(t => {
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
assert_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
e.preventDefault();
t.step_timeout(t.step_func_done(() => {
assert_equals(location.hash, "");
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-replaceState.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-replaceState.html
index 444116a632..d18feb2bc1 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-replaceState.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-replaceState.html
@@ -17,7 +17,8 @@ async_test(t => {
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
assert_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
e.preventDefault();
t.step_timeout(t.step_func_done(() => {
assert_equals(location.hash, "");
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-iframe-location.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-iframe-location.html
index 1bd7918823..c00a6e010d 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-iframe-location.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-iframe-location.html
@@ -21,7 +21,8 @@ async_test(t => {
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
assert_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
e.preventDefault();
t.step_timeout(t.step_func_done(() => assert_equals(location.hash, "")), 0);
});
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-location.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-location.html
index a0ed4dc0aa..b1ce204a62 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-location.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-location.html
@@ -17,7 +17,8 @@ async_test(t => {
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
assert_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
});
location.href = "#1";
}, "location API fires navigate event");
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-meta-refresh.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-meta-refresh.html
index d4d5b1c8e1..f64484c305 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-meta-refresh.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-meta-refresh.html
@@ -20,7 +20,8 @@ async_test(t => {
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
assert_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
e.preventDefault();
});
});
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-cross-document.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-cross-document.html
index 084051539b..de87fbbb80 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-cross-document.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-cross-document.html
@@ -25,7 +25,8 @@ async_test(t => {
assert_equals(e.destination.index, 0);
assert_equals(e.formData, null);
assert_equals(e.info, "hi");
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
});
i.contentWindow.onbeforeunload = () => beforeunload_called = true;
assert_true(i.contentWindow.navigation.canGoBack);
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document-in-iframe.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document-in-iframe.html
index 42c694e290..3d810d2f1d 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document-in-iframe.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document-in-iframe.html
@@ -27,7 +27,8 @@ promise_test(async t => {
assert_equals(e.destination.index, 0);
assert_equals(e.formData, null);
assert_equals(e.info, "hi");
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
}
await i.contentWindow.navigation.back({ info: "hi" }).finished;
}, "navigate event for navigation.back() - same-document in an iframe");
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document.html
index b08bbfcbf1..0f2faff7d7 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document.html
@@ -25,7 +25,8 @@ async_test(t => {
assert_equals(e.formData, null);
assert_equals(e.info, "hi");
assert_not_equals(e.hasUAVisualTransition, undefined);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
});
assert_true(navigation.canGoBack);
navigation.back({ info: "hi" });
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-navigate.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-navigate.html
index c56af1b40d..5dd1892cc0 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-navigate.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-navigate.html
@@ -16,7 +16,8 @@ async_test(t => {
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
assert_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
});
navigation.navigate("#foo");
}, "navigate event for navigation.navigate()");
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-svg-anchor-fragment.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-svg-anchor-fragment.html
index c6d210cccb..647496fd5c 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-svg-anchor-fragment.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-svg-anchor-fragment.html
@@ -17,7 +17,8 @@ async_test(t => {
assert_equals(e.destination.key, "");
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
- assert_equals(e.sourceElement, document.getElementById("a"));
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, document.getElementById("a"));
e.preventDefault();
t.step_timeout(t.step_func_done(() => assert_equals(location.hash, "")), 0);
});
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-to-srcdoc.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-to-srcdoc.html
index ea5c3f9216..bb598d8b90 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-to-srcdoc.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-to-srcdoc.html
@@ -21,7 +21,8 @@ async_test(t => {
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
assert_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
e.preventDefault();
// Make sure it doesn't navigate anyway.
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open-self.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open-self.html
index 4e569b6d54..1ef65ed82b 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open-self.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open-self.html
@@ -16,7 +16,8 @@ async_test(t => {
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
assert_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
e.preventDefault();
});
window.onload = t.step_func(() => window.open("#1", "_self"));
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open.html
index 42828308d7..13cdefb5d3 100644
--- a/testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open.html
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open.html
@@ -21,7 +21,8 @@ async_test(t => {
assert_equals(e.destination.id, "");
assert_equals(e.destination.index, -1);
assert_equals(e.formData, null);
- assert_equals(e.sourceElement, null);
+ // NavigateEvent sourceElement is still in development, so test whether it is available.
+ if ("sourceElement" in e) assert_equals(e.sourceElement, null);
e.preventDefault();
});
diff --git a/testing/web-platform/tests/notifications/tag-different-manual.https.html b/testing/web-platform/tests/notifications/tag-different-manual.https.html
deleted file mode 100644
index e463e97670..0000000000
--- a/testing/web-platform/tests/notifications/tag-different-manual.https.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>Notification.tag (two tags with different values)</title>
-<link rel="author" title="Intel" href="http://www.intel.com/">
-<link rel="author" title="Xin Liu" href="mailto:xinx.liu@intel.com">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="common.js"></script>
-<div id=passfail></div>
-<script>
-setup({ explicit_timeout: true })
-if (hasNotificationPermission()) {
- async_test(function (t) {
- t.step(function () {
- var notification1 = null,
- notification2 = null,
- notifications = [],
- text1 = "This is the body: Room 101",
- text2 = "This is the body: Room 202"
- createPassFail("If two notifications appear: First one with the"
- + " text \"" + text1 + "\", followed by one with the text \""
- + text2 + "\"",
- t, closeNotifications, notifications)
- notification1 = new Notification("New Email Received", {
- body: text1,
- tag: "Tom"
- })
- notification2 = new Notification("New Email Received", {
- body: text2,
- tag: "Rose"
- })
- notifications.push(notification1)
- notifications.push(notification2)
- })
- })
-}
-</script>
diff --git a/testing/web-platform/tests/notifications/tag-same-manual.https.html b/testing/web-platform/tests/notifications/tag-same-manual.https.html
deleted file mode 100644
index 4454944c53..0000000000
--- a/testing/web-platform/tests/notifications/tag-same-manual.https.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>Notification.tag (two tags with same value)</title>
-<link rel="author" title="Intel" href="http://www.intel.com/">
-<link rel="author" title="Xin Liu" href="mailto:xinx.liu@intel.com">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="common.js"></script>
-<div id=passfail></div>
-<script>
-setup({ explicit_timeout: true })
-if (hasNotificationPermission()) {
- async_test(function (t) {
- t.step(function () {
- var notification1 = null,
- notification2 = null,
- notifications = [],
- text1 = "This is the body: Room 101",
- text2 = "This is the body: Room 202"
- createPassFail("If a notification with the text \""
- + text2 + "\", replaces the notification with the text \""
- + text1 + "\" in the same position",
- t, closeNotifications, notifications)
- notification1 = new Notification("New Email Received", {
- body: text1,
- tag: "Tom"
- })
- notification2 = new Notification("New Email Received", {
- body: text2,
- tag: "Tom"
- })
- notifications.push(notification1)
- notifications.push(notification2)
- })
- })
-}
-</script>
diff --git a/testing/web-platform/tests/notifications/tag.https.html b/testing/web-platform/tests/notifications/tag.https.html
new file mode 100644
index 0000000000..99c61a7151
--- /dev/null
+++ b/testing/web-platform/tests/notifications/tag.https.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Notification.tag (two tags with same or different value)</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<link rel="author" title="Xin Liu" href="mailto:xinx.liu@intel.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<script>
+function promiseEvent(target, eventName, syncListener) {
+ return new Promise(resolve => {
+ target.addEventListener(eventName, ev => {
+ syncListener?.();
+ resolve(ev);
+ }, { once: true });
+ });
+}
+
+promise_setup(async () => {
+ await trySettingPermission("granted");
+});
+
+promise_test(async t => {
+ const tom1 = new Notification("New Email to tom", {
+ tag: "Tom"
+ });
+ t.add_cleanup(() => tom1.close());
+
+ let closed = false;
+ const promiseCloseEvent = promiseEvent(tom1, "close", () => closed = true);
+ await promiseEvent(tom1, "show");
+
+ const rose = new Notification("New Email to Rose", {
+ tag: "Rose"
+ });
+ t.add_cleanup(() => rose.close());
+ await promiseEvent(rose, "show");
+ assert_false(closed, "Different tag should not close the first notification");
+
+ const tom2 = new Notification("New Email to tom", {
+ tag: "Tom"
+ });
+ t.add_cleanup(() => tom2.close());
+
+ await promiseCloseEvent; // This should not timeout
+}, "Opening two notifications with the same tag should close the first one");
+</script>
diff --git a/testing/web-platform/tests/orientation-event/WEB_FEATURES.yml b/testing/web-platform/tests/orientation-event/WEB_FEATURES.yml
new file mode 100644
index 0000000000..414dbe7478
--- /dev/null
+++ b/testing/web-platform/tests/orientation-event/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: device-orientation-events
+ files: "**"
diff --git a/testing/web-platform/tests/permissions-policy/payment-allowed-by-permissions-policy-attribute.https.sub.html b/testing/web-platform/tests/permissions-policy/payment-allowed-by-permissions-policy-attribute.https.sub.html
index 779e9d666c..3a7e11b03c 100644
--- a/testing/web-platform/tests/permissions-policy/payment-allowed-by-permissions-policy-attribute.https.sub.html
+++ b/testing/web-platform/tests/permissions-policy/payment-allowed-by-permissions-policy-attribute.https.sub.html
@@ -1,26 +1,47 @@
<!DOCTYPE html>
<body>
- <script src=/resources/testharness.js></script>
- <script src=/resources/testharnessreport.js></script>
- <script src=/permissions-policy/resources/permissions-policy.js></script>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/permissions-policy/resources/permissions-policy.js"></script>
<script>
- 'use strict';
- var same_origin_src = '/permissions-policy/resources/permissions-policy-payment.html';
- var cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' +
- same_origin_src;
- var feature_name = 'permissions policy "payment"';
- var header = 'allow="payment" attribute';
+ "use strict";
+ const same_origin_src =
+ "/permissions-policy/resources/permissions-policy-payment.html";
+ const cross_origin_src =
+ "https://{{hosts[alt][]}}:{{ports[https][0]}}" + same_origin_src;
+ const feature_name = 'permissions policy "payment"';
+ const header = 'allow="payment" attribute';
- async_test(t => {
- test_feature_availability(
- 'PaymentRequest()', t, same_origin_src,
- expect_feature_available_default, 'payment');
- }, feature_name + ' can be enabled in same-origin iframe using ' + header);
+ promise_test((test) => {
+ return test_feature_availability({
+ feature_description: "PaymentRequest()",
+ test,
+ src: same_origin_src,
+ expect_feature_available: expect_feature_available_default,
+ is_promise_test: true
+ });
+ }, `${feature_name} is enabled by default`);
- async_test(t => {
- test_feature_availability(
- 'PaymentRequest()', t, cross_origin_src,
- expect_feature_available_default, 'payment');
- }, feature_name + ' can be enabled in cross-origin iframe using ' + header);
+ promise_test((test) => {
+ return test_feature_availability({
+ feature_description: "PaymentRequest()",
+ test,
+ src: same_origin_src,
+ expect_feature_available: expect_feature_available_default,
+ feature_name: "payment",
+ is_promise_test: true,
+ });
+ }, `${feature_name} can be enabled in same-origin iframe using ${header}`);
+
+ promise_test((test) => {
+ return test_feature_availability({
+ feature_description: "PaymentRequest()",
+ test,
+ src: cross_origin_src,
+ expect_feature_available: expect_feature_available_default,
+ feature_name: "payment",
+ is_promise_test: true,
+ });
+ }, `${feature_name} can be enabled in cross-origin iframe using ${header}`);
</script>
</body>
diff --git a/testing/web-platform/tests/permissions-policy/payment-allowed-by-permissions-policy.https.sub.html b/testing/web-platform/tests/permissions-policy/payment-allowed-by-permissions-policy.https.sub.html
index 456626c350..27583d3c9c 100644
--- a/testing/web-platform/tests/permissions-policy/payment-allowed-by-permissions-policy.https.sub.html
+++ b/testing/web-platform/tests/permissions-policy/payment-allowed-by-permissions-policy.https.sub.html
@@ -1,40 +1,64 @@
<!DOCTYPE html>
<body>
- <script src=/resources/testharness.js></script>
- <script src=/resources/testharnessreport.js></script>
- <script src=/permissions-policy/resources/permissions-policy.js></script>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/permissions-policy/resources/permissions-policy.js"></script>
<script>
- 'use strict';
- var same_origin_src = '/permissions-policy/resources/permissions-policy-payment.html';
- var cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' +
- same_origin_src;
- var header = 'permissions policy header "payment=*"';
+ "use strict";
+ const same_origin_src =
+ "/permissions-policy/resources/permissions-policy-payment.html";
+ const cross_origin_src =
+ "https://{{hosts[alt][]}}:{{ports[https][0]}}" + same_origin_src;
+ const header = 'permissions policy header "payment=*"';
- test(() => {
- var supportedMethods = [ { supportedMethods: 'https://{{domains[nonexistent]}}/payment-request' } ];
- var details = {
- total: { label: 'Test', amount: { currency: 'USD', value: '5.00' } }
- };
- try {
- new PaymentRequest(supportedMethods, details);
- } catch (e) {
- assert_unreached();
- }
- }, header + ' allows the top-level document.');
+ test(() => {
+ const supportedMethods = [
+ {
+ supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
+ },
+ ];
+ const details = {
+ total: {
+ label: "Test",
+ amount: { currency: "USD", value: "5.00" },
+ },
+ };
+ try {
+ new PaymentRequest(supportedMethods, details);
+ } catch (e) {
+ assert_unreached();
+ }
+ }, `${header} allows Payment Request API the top-level document.`);
- async_test(t => {
- test_feature_availability('PaymentRequest()', t, same_origin_src,
- expect_feature_available_default);
- }, header + ' allows same-origin iframes.');
+ promise_test((test) => {
+ return test_feature_availability({
+ feature_description: "PaymentRequest()",
+ test,
+ src: same_origin_src,
+ expect_feature_available: expect_feature_available_default,
+ is_promise_test: true,
+ });
+ }, `${header} allows Payment Request API same-origin iframes.`);
- async_test(t => {
- test_feature_availability('PaymentRequest()', t, cross_origin_src,
- expect_feature_unavailable_default);
- }, header + ' disallows cross-origin iframes.');
+ promise_test(async (test) => {
+ return test_feature_availability({
+ feature_description: "PaymentRequest()",
+ test,
+ src: cross_origin_src,
+ expect_feature_available: expect_feature_unavailable_default,
+ is_promise_test: true,
+ });
+ }, `${header} disallows Payment Request API cross-origin iframes.`);
- async_test(t => {
- test_feature_availability('PaymentRequest()', t, cross_origin_src,
- expect_feature_available_default, 'payment');
- }, header + ' allow="payment" allows cross-origin iframes.');
+ promise_test(async (test) => {
+ return test_feature_availability({
+ feature_description: "PaymentRequest()",
+ test,
+ src: cross_origin_src,
+ expect_feature_available: expect_feature_available_default,
+ feature_name: "payment",
+ is_promise_test: true,
+ });
+ }, `${header} allow="payment" allows Payment Request in cross-origin iframes.`);
</script>
</body>
diff --git a/testing/web-platform/tests/permissions-policy/payment-default-permissions-policy.https.sub.html b/testing/web-platform/tests/permissions-policy/payment-default-permissions-policy.https.sub.html
index da5fe80f66..92a0f7e14f 100644
--- a/testing/web-platform/tests/permissions-policy/payment-default-permissions-policy.https.sub.html
+++ b/testing/web-platform/tests/permissions-policy/payment-default-permissions-policy.https.sub.html
@@ -1,35 +1,48 @@
<!DOCTYPE html>
<body>
- <script src=/resources/testharness.js></script>
- <script src=/resources/testharnessreport.js></script>
- <script src=/permissions-policy/resources/permissions-policy.js></script>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/permissions-policy/resources/permissions-policy.js"></script>
<script>
- 'use strict';
- var same_origin_src = '/permissions-policy/resources/permissions-policy-payment.html';
- var cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' +
- same_origin_src;
- var header = 'Default "payment" permissions policy';
+ "use strict";
+ const same_origin_src =
+ "/permissions-policy/resources/permissions-policy-payment.html";
+ const cross_origin_src =
+ "https://{{hosts[alt][]}}:{{ports[https][0]}}" + same_origin_src;
- test(() => {
- var supportedInstruments = [ { supportedMethods: 'visa' } ];
- var details = {
- total: { label: 'Test', amount: { currency: 'USD', value: '5.00' } }
- };
- try {
- new PaymentRequest(supportedInstruments, details);
- } catch (e) {
- assert_unreached();
- }
- }, header + ' allows the top-level document.');
+ test(() => {
+ const supportedInstruments = [{ supportedMethods: "visa" }];
+ const details = {
+ total: {
+ label: "Test",
+ amount: { currency: "USD", value: "5.00" },
+ },
+ };
+ try {
+ new PaymentRequest(supportedInstruments, details);
+ } catch (e) {
+ assert_unreached();
+ }
+ }, `Payment Request API is enabled by default the top-level document.`);
- async_test(t => {
- test_feature_availability('PaymentRequest()', t, same_origin_src,
- expect_feature_available_default);
- }, header + ' allows same-origin iframes.');
+ promise_test((test) => {
+ return test_feature_availability({
+ feature_description: "PaymentRequest()",
+ test,
+ src: same_origin_src,
+ expect_feature_available: expect_feature_available_default,
+ is_promise_test: true,
+ });
+ }, `Payment Request API is enabled by default in same-origin iframes.`);
- async_test(t => {
- test_feature_availability('PaymentRequest()', t, cross_origin_src,
- expect_feature_unavailable_default);
- }, header + ' disallows cross-origin iframes.');
+ promise_test((test) => {
+ return test_feature_availability({
+ feature_description: "PaymentRequest()",
+ test,
+ src: cross_origin_src,
+ expect_feature_available: expect_feature_unavailable_default,
+ is_promise_test: true,
+ });
+ }, `Payment Request API is disabled by default in cross-origin iframes.`);
</script>
</body>
diff --git a/testing/web-platform/tests/permissions-policy/payment-disabled-by-permissions-policy.https.sub.html b/testing/web-platform/tests/permissions-policy/payment-disabled-by-permissions-policy.https.sub.html
index cc358a1c0f..b53eff996a 100644
--- a/testing/web-platform/tests/permissions-policy/payment-disabled-by-permissions-policy.https.sub.html
+++ b/testing/web-platform/tests/permissions-policy/payment-disabled-by-permissions-policy.https.sub.html
@@ -1,33 +1,47 @@
<!DOCTYPE html>
<body>
- <script src=/resources/testharness.js></script>
- <script src=/resources/testharnessreport.js></script>
- <script src=/permissions-policy/resources/permissions-policy.js></script>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/permissions-policy/resources/permissions-policy.js"></script>
<script>
- 'use strict';
- var same_origin_src = '/permissions-policy/resources/permissions-policy-payment.html';
- var cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' +
- same_origin_src;
- var header = 'permissions policy header "payment=()"';
+ "use strict";
+ const same_origin_src =
+ "/permissions-policy/resources/permissions-policy-payment.html";
+ const cross_origin_src =
+ "https://{{hosts[alt][]}}:{{ports[https][0]}}" + same_origin_src;
+ const header = 'permissions policy header "payment=()"';
- test(() => {
- var supportedInstruments = [ { supportedMethods: 'visa' } ];
- var details = {
- total: { label: 'Test', amount: { currency: 'USD', value: '5.00' } }
- };
- assert_throws_dom('SecurityError', () => {
- new PaymentRequest(supportedInstruments, details);
- });
- }, header + ' disallows the top-level document.');
+ test(() => {
+ const supportedInstruments = [{ supportedMethods: "visa" }];
+ const details = {
+ total: {
+ label: "Test",
+ amount: { currency: "USD", value: "5.00" },
+ },
+ };
+ assert_throws_dom("SecurityError", () => {
+ new PaymentRequest(supportedInstruments, details);
+ });
+ }, `${header} disallows Payment Request API in top-level document.`);
- async_test(t => {
- test_feature_availability('PaymentRequest()', t, same_origin_src,
- expect_feature_unavailable_default);
- }, header + ' disallows same-origin iframes.');
+ promise_test((test) => {
+ return test_feature_availability({
+ feature_description: "PaymentRequest()",
+ test,
+ src: same_origin_src,
+ expect_feature_available: expect_feature_unavailable_default,
+ is_promise_test: true,
+ });
+ }, `${header} disallows Payment Request API in same-origin iframes.`);
- async_test(t => {
- test_feature_availability('PaymentRequest()', t, cross_origin_src,
- expect_feature_unavailable_default,);
- }, header + ' disallows cross-origin iframes.');
+ promise_test((test) => {
+ return test_feature_availability({
+ feature_description: "PaymentRequest()",
+ test,
+ src: cross_origin_src,
+ expect_feature_available: expect_feature_unavailable_default,
+ is_promise_test: true,
+ });
+ }, `${header} disallows Payment Request API in cross-origin iframes.`);
</script>
</body>
diff --git a/testing/web-platform/tests/permissions-policy/resources/permissions-policy.js b/testing/web-platform/tests/permissions-policy/resources/permissions-policy.js
index 32fb4cfd4a..d30d1191d1 100644
--- a/testing/web-platform/tests/permissions-policy/resources/permissions-policy.js
+++ b/testing/web-platform/tests/permissions-policy/resources/permissions-policy.js
@@ -6,8 +6,9 @@ function assert_permissions_policy_supported() {
// Tests whether a feature that is enabled/disabled by permissions policy works
// as expected.
// Arguments:
-// feature_description: a short string describing what feature is being
-// tested. Examples: "usb.GetDevices()", "PaymentRequest()".
+// feature_descriptionOrObject: either and object, containing the following
+// properties, or a string describing what feature is being tested.
+// Examples: "usb.GetDevices()", "PaymentRequest()".
// test: test created by testharness. Examples: async_test, promise_test.
// src: URL where a feature's availability is checked. Examples:
// "/permissions-policy/resources/permissions-policy-payment.html",
@@ -24,13 +25,36 @@ function assert_permissions_policy_supported() {
// feature (https://w3c.github.io/webappsec-permissions-policy/#features).
// See examples at:
// https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md
-// allow_attribute: Optional argument, only used for testing fullscreen
+// allowfullscreen: Optional argument, only used for testing fullscreen
// by passing "allowfullscreen".
// is_promise_test: Optional argument, true if this call should return a
// promise. Used by test_feature_availability_with_post_message_result()
function test_feature_availability(
- feature_description, test, src, expect_feature_available, feature_name,
- allow_attribute, is_promise_test = false) {
+ feature_descriptionOrObject, test, src, expect_feature_available, feature_name,
+ allowfullscreen, is_promise_test = false) {
+
+ if (feature_descriptionOrObject && feature_descriptionOrObject instanceof Object) {
+ const {
+ feature_description,
+ test,
+ src,
+ expect_feature_available,
+ feature_name,
+ allowfullscreen,
+ is_promise_test,
+ } = feature_descriptionOrObject;
+ return test_feature_availability(
+ feature_description,
+ test,
+ src,
+ expect_feature_available,
+ feature_name,
+ allowfullscreen,
+ is_promise_test
+ );
+ }
+
+ const feature_description = feature_descriptionOrObject;
let frame = document.createElement('iframe');
frame.src = src;
@@ -38,8 +62,8 @@ function test_feature_availability(
frame.allow = frame.allow.concat(";" + feature_name);
}
- if (typeof allow_attribute !== 'undefined') {
- frame.setAttribute(allow_attribute, true);
+ if (typeof allowfullscreen !== 'undefined') {
+ frame.setAttribute(allowfullscreen, true);
}
function expectFeatureAvailable(evt) {
diff --git a/testing/web-platform/tests/pointerevents/deviceproperties/get-device-properties-uniqueid-from-pointer-event.tentative.html b/testing/web-platform/tests/pointerevents/deviceproperties/get-device-properties-uniqueid-from-pointer-event.tentative.html
new file mode 100644
index 0000000000..dc6b9379c1
--- /dev/null
+++ b/testing/web-platform/tests/pointerevents/deviceproperties/get-device-properties-uniqueid-from-pointer-event.tentative.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+ Tentative; contingent on merge of:
+ https://github.com/w3c/pointerevents/pull/495
+-->
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<style>
+ div {
+ user-select: none; // Prevents text selection on drag.
+ }
+</style>
+<div id="logger" draggable="false"></div>
+<div id="console"></div>
+<!-- This test verifies that pointerEvent.deviceProperties.uniqueId is 0
+ by default for a pointer with an invalid hardware id - in this case
+ a testdriver generated event, which does not support hardware id. -->
+<script>
+ function CheckDeviceId(event) {
+ eventFired++;
+ assert_equals(event.deviceProperties.uniqueId, 0, "deviceId is 0");
+ }
+
+ window.addEventListener("pointerdown", CheckDeviceId, false);
+ window.addEventListener("pointermove", CheckDeviceId, false);
+
+ promise_test(async () => {
+ if (!window.internals)
+ return;
+ eventFired = 0;
+ let actions = new test_driver.Actions()
+ .addPointer("TestPointer", "pen")
+ .pointerDown()
+ .pointerMove(100, 100)
+ .pointerUp();
+
+ await actions.send();
+
+ assert_true(eventFired == 2);
+ }, 'PointerEvent.deviceProperties.uniqueId');
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/pointerevents/deviceproperties/pointer-event-has-device-properties-uniqueid-from-pointer-event-init.tentative.html b/testing/web-platform/tests/pointerevents/deviceproperties/pointer-event-has-device-properties-uniqueid-from-pointer-event-init.tentative.html
new file mode 100644
index 0000000000..a37df4b421
--- /dev/null
+++ b/testing/web-platform/tests/pointerevents/deviceproperties/pointer-event-has-device-properties-uniqueid-from-pointer-event-init.tentative.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+ Tentative; contingent on merge of:
+ https://github.com/w3c/pointerevents/pull/495
+-->
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="console"></div>
+
+<!-- This test verifies that pointerEvent.deviceProperties.uniqueId
+ can be set via PointerEventInit. -->
+<script>
+ const UNIQUE_ID = 1001;
+ const INVALID_UNIQUE_ID = 0;
+
+ function CheckDeviceId(event, uniqueId) {
+ assert_equals(event.deviceProperties.uniqueId, uniqueId, "uniqueId is populated");
+ }
+
+ promise_test(async () => {
+ if (!window.internals)
+ return;
+ var deviceProps = new DeviceProperties({
+ uniqueId: 1001
+ });
+ var downEvent = new PointerEvent("pointerdown",
+ {pointerId: 1,
+ bubbles: true,
+ cancelable: true,
+ pointerType: "pen",
+ width: 100,
+ height: 100,
+ isPrimary: true,
+ deviceProperties: deviceProps
+ });
+ CheckDeviceId(downEvent, UNIQUE_ID);
+ var moveEvent = new PointerEvent("pointermove",
+ {pointerId: 1,
+ bubbles: true,
+ cancelable: true,
+ pointerType: "pen",
+ width: 100,
+ height: 100,
+ isPrimary: true,
+ deviceProperties: deviceProps
+ });
+ CheckDeviceId(moveEvent, UNIQUE_ID);
+ var upEvent = new PointerEvent("pointerup",
+ {pointerId: 1,
+ bubbles: true,
+ cancelable: true,
+ pointerType: "pen",
+ width: 100,
+ height: 100,
+ isPrimary: true,
+ deviceProperties: deviceProps
+ });
+ CheckDeviceId(upEvent, UNIQUE_ID);
+ }, 'PointerEvent.deviceProperties via DevicePropertiesInit');
+
+ promise_test(async () => {
+ if (!window.internals)
+ return;
+ var emptyDeviceProps = new DeviceProperties({});
+ var downEventEmptyProps = new PointerEvent("pointerdown",
+ {pointerId: 1,
+ bubbles: true,
+ cancelable: true,
+ pointerType: "pen",
+ width: 100,
+ height: 100,
+ isPrimary: true,
+ deviceProperties: emptyDeviceProps
+ });
+ CheckDeviceId(downEventEmptyProps, INVALID_UNIQUE_ID);
+ }, 'PointerEvent.deviceProperties via empty DevicePropertiesInit');
+
+ promise_test(async () => {
+ if (!window.internals)
+ return;
+ var downEventEmptyProps = new PointerEvent("pointerdown",
+ {pointerId: 1,
+ bubbles: true,
+ cancelable: true,
+ pointerType: "pen",
+ width: 100,
+ height: 100,
+ isPrimary: true,
+ });
+ CheckDeviceId(downEventEmptyProps, INVALID_UNIQUE_ID);
+ }, 'No deviceProperties in PointerEventInit');
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/pointerevents/deviceproperties/unique-id-is-unique-manual.tentative.html b/testing/web-platform/tests/pointerevents/deviceproperties/unique-id-is-unique-manual.tentative.html
new file mode 100644
index 0000000000..55db05353f
--- /dev/null
+++ b/testing/web-platform/tests/pointerevents/deviceproperties/unique-id-is-unique-manual.tentative.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<!--
+ Tentative; contingent on merge of:
+ https://github.com/w3c/pointerevents/pull/495
+
+ This manual test validates the behavior of PointerEvent.deviceProperties.uniqueId.
+ Specifically, this test ensures that pointing devices get their own unique id, and
+ that the unique id is persistent over the session.
+
+ In order to run this test, it is necessary to have multiple pointing devices; such as a
+ pen and a mouse. Please follow the instructions exactly as written in order to ensure
+ the correct results are obtained.
+-->
+<title>DeviceProperties.uniqueId is unique for pointer events from different devices</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #instructions {
+ display: inline-block;
+ border-right: 1px solid black;
+ padding-right: 10px;
+ width: 600px;
+ }
+ #testcontainer {
+ display: inline-block;
+ width: 300px;
+ touch-action: none;
+ }
+
+ #currentuniqueid {
+ display: inline-block;
+ }
+
+ .point1 {
+ height: 50px;
+ width: 50px;
+ background-color: #00eeee;
+ display: inline-block;
+ }
+ .point2 {
+ height: 50px;
+ width: 50px;
+ background-color: #aa33aa;
+ display: inline-block;
+ float: right;
+ }
+
+ .testarea {
+ border: 1px solid #000000;
+ margin-bottom: 50px;
+ width: 100%;
+ }
+
+ p {
+ padding-bottom: 10px;
+ }
+
+ html {
+ font-family: Arial, Helvetica, sans-serif;
+ }
+</style>
+<html>
+<div id="instructions">
+<h2>Instructions</h2>
+<p>1. With one pointing device (pointing device #1), drag the pointer from A to B</p>
+<p>2. With another pointing device (pointing device #2), drag the pointer from C to D</p>
+<p>3. Repeat step 1.</p>
+<p>4. Repeat step 2.</p>
+<p>5. Click finish and verify the test passes. There should be 4 passing test cases. </p>
+</div>
+<div id="testcontainer">
+ <div>
+ Current pointer's unique id: <p id="currentuniqueid"></p>
+ </div>
+ <div class="testarea" id="device1">
+ <div class="point1">A</div>
+ <div class="point2">B</div>
+ </div>
+ <div class="testarea" id="device2">
+ <div class="point1">C</div>
+ <div class="point2">D</div>
+ </div>
+
+ <p>Click on the button below after completing. If a "PASS" result appears the test
+ passes, otherwise it fails</p>
+ <button onclick="Finish()">Finish Test</button>
+</div>
+</html>
+
+<script>
+ let device1Ids = [];
+ let device2Ids = [];
+
+ setup({explicit_timeout: true, explicit_done: true});
+
+ function LogDeviceId(event, list) {
+ if (event.deviceProperties) {
+ const uniqueId = event.deviceProperties.uniqueId;
+ currentuniqueid.innerText = uniqueId ? uniqueId : "Unknown";
+ if (!uniqueId) {
+ return;
+ }
+ list.push(uniqueId);
+ }
+ }
+
+ function LogDeviceId1(event) {
+ LogDeviceId(event, device1Ids);
+ }
+
+ function LogDeviceId2(event) {
+ LogDeviceId(event, device2Ids);
+ }
+
+ function Finish() {
+ let device1UniqueIds = new Set(device1Ids);
+ let device2UniqueIds = new Set(device2Ids);
+
+ test(function () {
+ assert_greater_than(device1Ids.length, 1, "Events from Device 1 have uniqueIds.");
+ assert_equals(device1UniqueIds.size, 1, "Device 1 has a consistent uniqueId.");
+ }, "uniqueId is consistent for device 1");
+ test(function () {
+ assert_greater_than(device2Ids.length, 1, "Events from Device 2 have uniqueIds.");
+ assert_equals(device2UniqueIds.size, 1, "Device 2 has a consistent uniqueId.");
+ }, "uniqueId is consistent for device 2");
+ test(function () {
+ // Ensure the two sets are different.
+ assert_equals(device1UniqueIds.intersection(device2UniqueIds).size, 0, "Device 1 and 2 have different uniqueIds.");
+ }, "uniqueId is unique to device 1 and device 2");
+ done();
+ }
+
+ device1.addEventListener("pointerdown", LogDeviceId1, false);
+ device1.addEventListener("pointermove", LogDeviceId1, false);
+ device1.addEventListener("pointerup", LogDeviceId1, false);
+
+ device2.addEventListener("pointerdown", LogDeviceId2, false);
+ device2.addEventListener("pointermove", LogDeviceId2, false);
+ device2.addEventListener("pointerup", LogDeviceId2, false);
+
+</script>
diff --git a/testing/web-platform/tests/preload/WEB_FEATURES.yml b/testing/web-platform/tests/preload/WEB_FEATURES.yml
new file mode 100644
index 0000000000..384fd1a3e5
--- /dev/null
+++ b/testing/web-platform/tests/preload/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: modulepreload
+ files:
+ - "*modulepreload*"
diff --git a/testing/web-platform/tests/resource-timing/initiator-type/workers.html b/testing/web-platform/tests/resource-timing/initiator-type/workers.html
index 3a23ad71a3..a3da99356d 100644
--- a/testing/web-platform/tests/resource-timing/initiator-type/workers.html
+++ b/testing/web-platform/tests/resource-timing/initiator-type/workers.html
@@ -17,7 +17,7 @@
new Worker(moduleWorkerURL, {type: "module"});
new Worker(workerURL, {type: "classic"});
initiator_type_test(workerURL, "other", "classic worker");
- initiator_type_test(moduleWorkerURL, "other", "module worker");
+ initiator_type_test(moduleWorkerURL, "script", "module worker");
</script>
</body>
</html>
diff --git a/testing/web-platform/tests/resources/chromium/mock-pressure-service.js b/testing/web-platform/tests/resources/chromium/mock-pressure-service.js
index bd0e32c567..016c6d97e7 100644
--- a/testing/web-platform/tests/resources/chromium/mock-pressure-service.js
+++ b/testing/web-platform/tests/resources/chromium/mock-pressure-service.js
@@ -65,26 +65,20 @@ class MockPressureService {
if (this.pressureServiceReadingTimerId_ != null)
this.stopPlatformCollector();
- // The following code for calculating the timestamp was taken from
- // https://source.chromium.org/chromium/chromium/src/+/main:third_party/
- // blink/web_tests/http/tests/resources/
- // geolocation-mock.js;l=131;drc=37a9b6c03b9bda9fcd62fc0e5e8016c278abd31f
-
- // The new Date().getTime() returns the number of milliseconds since the
- // UNIX epoch (1970-01-01 00::00:00 UTC), while |internalValue| of the
- // device.mojom.PressureUpdate represents the value of microseconds since
- // the Windows FILETIME epoch (1601-01-01 00:00:00 UTC). So add the delta
- // when sets the |internalValue|. See more info in //base/time/time.h.
- const windowsEpoch = Date.UTC(1601, 0, 1, 0, 0, 0, 0);
- const unixEpoch = Date.UTC(1970, 0, 1, 0, 0, 0, 0);
- // |epochDeltaInMs| equals to base::Time::kTimeTToMicrosecondsOffset.
- const epochDeltaInMs = unixEpoch - windowsEpoch;
-
this.pressureServiceReadingTimerId_ = self.setInterval(() => {
if (this.pressureUpdate_ === null || this.observers_.length === 0)
return;
+
+ // Because we cannot retrieve directly the timeOrigin internal in
+ // TimeTicks from Chromium, we need to create a timestamp that
+ // would help to test basic functionality.
+ // by multiplying performance.timeOrigin by 10 we make sure that the
+ // origin is higher than the internal time origin in TimeTicks.
+ // performance.now() allows us to have an increase matching the TimeTicks
+ // that would be read from the platform collector.
this.pressureUpdate_.timestamp = {
- internalValue: BigInt((new Date().getTime() + epochDeltaInMs) * 1000)
+ internalValue:
+ Math.round((performance.timeOrigin * 10) + performance.now()) * 1000
};
for (let observer of this.observers_)
observer.onPressureUpdated(this.pressureUpdate_);
diff --git a/testing/web-platform/tests/resources/idlharness.js b/testing/web-platform/tests/resources/idlharness.js
index 8f741b09b2..4cf19234af 100644
--- a/testing/web-platform/tests/resources/idlharness.js
+++ b/testing/web-platform/tests/resources/idlharness.js
@@ -566,6 +566,7 @@ IdlArray.prototype.is_json_type = function(type)
case "Uint8ClampedArray":
case "BigInt64Array":
case "BigUint64Array":
+ case "Float16Array":
case "Float32Array":
case "Float64Array":
case "ArrayBuffer":
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html b/testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html
index 18e83a8e89..caea20067f 100644
--- a/testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html
@@ -39,6 +39,7 @@
assert_false(idl.is_json_type(typeFrom("Uint8ClampedArray")));
assert_false(idl.is_json_type(typeFrom("BigInt64Array")));
assert_false(idl.is_json_type(typeFrom("BigUint64Array")));
+ assert_false(idl.is_json_type(typeFrom("Float16Array")));
assert_false(idl.is_json_type(typeFrom("Float32Array")));
assert_false(idl.is_json_type(typeFrom("Float64Array")));
assert_false(idl.is_json_type(typeFrom("ArrayBuffer")));
diff --git a/testing/web-platform/tests/resources/test/tox.ini b/testing/web-platform/tests/resources/test/tox.ini
index 12013a1a70..49603ef64b 100644
--- a/testing/web-platform/tests/resources/test/tox.ini
+++ b/testing/web-platform/tests/resources/test/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py37,py38,py39,py310,py311
+envlist = py38,py39,py310,py311
skipsdist=True
[testenv]
diff --git a/testing/web-platform/tests/resources/testdriver.js b/testing/web-platform/tests/resources/testdriver.js
index 20140b2fc0..2d1a89690c 100644
--- a/testing/web-platform/tests/resources/testdriver.js
+++ b/testing/web-platform/tests/resources/testdriver.js
@@ -1023,6 +1023,49 @@
*/
get_virtual_sensor_information: function(sensor_type, context=null) {
return window.test_driver_internal.get_virtual_sensor_information(sensor_type, context);
+ },
+
+ /**
+ * Overrides device posture set by hardware.
+ *
+ * Matches the `Set device posture
+ * <https://w3c.github.io/device-posture/#set-device-posture>`_
+ * WebDriver command.
+ *
+ * @param {String} posture - A `DevicePostureType
+ * <https://w3c.github.io/device-posture/#dom-deviceposturetype>`_
+ * either "continuous" or "folded".
+ * @param {WindowProxy} [context=null] - Browsing context in which to
+ * run the call, or null for the
+ * current browsing context.
+ *
+ * @returns {Promise} Fulfilled when device posture is set.
+ * Rejected in case the WebDriver command errors out
+ * (including if a device posture of the given type
+ * does not exist).
+ */
+ set_device_posture: function(posture, context=null) {
+ return window.test_driver_internal.set_device_posture(posture, context);
+ },
+
+ /**
+ * Removes device posture override and returns device posture control
+ * back to hardware.
+ *
+ * Matches the `Clear device posture
+ * <https://w3c.github.io/device-posture/#clear-device-posture>`_
+ * WebDriver command.
+ *
+ * @param {WindowProxy} [context=null] - Browsing context in which to
+ * run the call, or null for the
+ * current browsing context.
+ *
+ * @returns {Promise} Fulfilled after the device posture override has
+ * been removed. Rejected in case the WebDriver
+ * command errors out.
+ */
+ clear_device_posture: function(context=null) {
+ return window.test_driver_internal.clear_device_posture(context);
}
};
@@ -1203,6 +1246,14 @@
async get_virtual_sensor_information(sensor_type, context=null) {
throw new Error("get_virtual_sensor_information() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_device_posture(posture, context=null) {
+ throw new Error("set_device_posture() is not implemented by testdriver-vendor.js");
+ },
+
+ async clear_device_posture(context=null) {
+ throw new Error("clear_device_posture() is not implemented by testdriver-vendor.js");
}
};
})();
diff --git a/testing/web-platform/tests/resources/testharness.js b/testing/web-platform/tests/resources/testharness.js
index 1a6a4bb341..c5c375e172 100644
--- a/testing/web-platform/tests/resources/testharness.js
+++ b/testing/web-platform/tests/resources/testharness.js
@@ -4194,11 +4194,7 @@
status
],
],
- ["button",
- {"onclick": "let evt = new Event('__test_restart'); " +
- "let canceled = !window.dispatchEvent(evt);" +
- "if (!canceled) { location.reload() }"},
- "Rerun"]
+ ["button", {"id":"rerun"}, "Rerun"]
]];
if (harness_status.status === harness_status.ERROR) {
@@ -4230,6 +4226,13 @@
log.appendChild(render(summary_template, {num_tests:tests.length}, output_document));
+ output_document.getElementById("rerun").addEventListener("click",
+ function() {
+ let evt = new Event('__test_restart');
+ let canceled = !window.dispatchEvent(evt);
+ if (!canceled) { location.reload(); }
+ });
+
forEach(output_document.querySelectorAll("section#summary label"),
function(element)
{
@@ -4254,18 +4257,6 @@
});
});
- // This use of innerHTML plus manual escaping is not recommended in
- // general, but is necessary here for performance. Using textContent
- // on each individual <td> adds tens of seconds of execution time for
- // large test suites (tens of thousands of tests).
- function escape_html(s)
- {
- return s.replace(/\&/g, "&amp;")
- .replace(/</g, "&lt;")
- .replace(/"/g, "&quot;")
- .replace(/'/g, "&#39;");
- }
-
function has_assertions()
{
for (var i = 0; i < tests.length; i++) {
@@ -4296,81 +4287,63 @@
});
function get_asserts_output(test) {
+ const asserts_output = render(
+ ["details", {},
+ ["summary", {}, "Asserts run"],
+ ["table", {}, ""] ]);
+
var asserts = asserts_run_by_test.get(test);
if (!asserts) {
- return "No asserts ran";
+ asserts_output.querySelector("summary").insertAdjacentText("afterend", "No asserts ran");
+ return asserts_output;
}
- rv = "<table>";
- rv += asserts.map(assert => {
- var output_fn = "<strong>" + escape_html(assert.assert_name) + "</strong>(";
- var prefix_len = output_fn.length;
- var output_args = assert.args;
- var output_len = output_args.reduce((prev, current) => prev+current, prefix_len);
- if (output_len[output_len.length - 1] > 50) {
- output_args = output_args.map((x, i) =>
- (i > 0 ? " ".repeat(prefix_len) : "" )+ x + (i < output_args.length - 1 ? ",\n" : ""));
- } else {
- output_args = output_args.map((x, i) => x + (i < output_args.length - 1 ? ", " : ""));
- }
- output_fn += escape_html(output_args.join(""));
- output_fn += ')';
- var output_location;
+
+ const table = asserts_output.querySelector("table");
+ for (const assert of asserts) {
+ const status_class_name = status_class(Test.prototype.status_formats[assert.status]);
+ var output_fn = "(" + assert.args.join(", ") + ")";
if (assert.stack) {
- output_location = assert.stack.split("\n", 1)[0].replace(/@?\w+:\/\/[^ "\/]+(?::\d+)?/g, " ");
+ output_fn += "\n";
+ output_fn += assert.stack.split("\n", 1)[0].replace(/@?\w+:\/\/[^ "\/]+(?::\d+)?/g, " ");
}
- return "<tr class='overall-" +
- status_class(Test.prototype.status_formats[assert.status]) + "'>" +
- "<td class='" +
- status_class(Test.prototype.status_formats[assert.status]) + "'>" +
- Test.prototype.status_formats[assert.status] + "</td>" +
- "<td><pre>" +
- output_fn +
- (output_location ? "\n" + escape_html(output_location) : "") +
- "</pre></td></tr>";
+ table.appendChild(render(
+ ["tr", {"class":"overall-" + status_class_name},
+ ["td", {"class":status_class_name}, Test.prototype.status_formats[assert.status]],
+ ["td", {}, ["pre", {}, ["strong", {}, assert.assert_name], output_fn]] ]));
}
- ).join("\n");
- rv += "</table>";
- return rv;
+ return asserts_output;
}
- log.appendChild(document.createElementNS(xhtml_ns, "section"));
var assertions = has_assertions();
- var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">" +
- "<thead><tr><th>Result</th><th>Test Name</th>" +
- (assertions ? "<th>Assertion</th>" : "") +
- "<th>Message</th></tr></thead>" +
- "<tbody>";
- for (var i = 0; i < tests.length; i++) {
- var test = tests[i];
- html += '<tr class="overall-' +
- status_class(test.format_status()) +
- '">' +
- '<td class="' +
- status_class(test.format_status()) +
- '">' +
- test.format_status() +
- "</td><td>" +
- escape_html(test.name) +
- "</td><td>" +
- (assertions ? escape_html(get_assertion(test)) + "</td><td>" : "") +
- escape_html(test.message ? tests[i].message : " ") +
- (tests[i].stack ? "<pre>" +
- escape_html(tests[i].stack) +
- "</pre>": "");
+ const section = render(
+ ["section", {},
+ ["h2", {}, "Details"],
+ ["table", {"id":"results", "class":(assertions ? "assertions" : "")},
+ ["thead", {},
+ ["tr", {},
+ ["th", {}, "Result"],
+ ["th", {}, "Test Name"],
+ (assertions ? ["th", {}, "Assertion"] : ""),
+ ["th", {}, "Message" ]]],
+ ["tbody", {}]]]);
+
+ const tbody = section.querySelector("tbody");
+ for (const test of tests) {
+ const status = test.format_status();
+ const status_class_name = status_class(status);
+ tbody.appendChild(render(
+ ["tr", {"class":"overall-" + status_class_name},
+ ["td", {"class":status_class_name}, status],
+ ["td", {}, test.name],
+ (assertions ? ["td", {}, get_assertion(test)] : ""),
+ ["td", {},
+ test.message ?? "",
+ ["pre", {}, test.stack ?? ""]]]));
if (!(test instanceof RemoteTest)) {
- html += "<details><summary>Asserts run</summary>" + get_asserts_output(test) + "</details>"
+ tbody.lastChild.lastChild.appendChild(get_asserts_output(test));
}
- html += "</td></tr>";
- }
- html += "</tbody></table>";
- try {
- log.lastChild.innerHTML = html;
- } catch (e) {
- log.appendChild(document.createElementNS(xhtml_ns, "p"))
- .textContent = "Setting innerHTML for the log threw an exception.";
- log.appendChild(document.createElementNS(xhtml_ns, "pre"))
- .textContent = html;
}
+ log.appendChild(section);
};
/*
diff --git a/testing/web-platform/tests/resources/testharnessreport.js b/testing/web-platform/tests/resources/testharnessreport.js
index e5cb40fe0e..405a2d8b06 100644
--- a/testing/web-platform/tests/resources/testharnessreport.js
+++ b/testing/web-platform/tests/resources/testharnessreport.js
@@ -14,31 +14,6 @@
* parameters they are called with see testharness.js
*/
-function dump_test_results(tests, status) {
- var results_element = document.createElement("script");
- results_element.type = "text/json";
- results_element.id = "__testharness__results__";
- var test_results = tests.map(function(x) {
- return {name:x.name, status:x.status, message:x.message, stack:x.stack}
- });
- var data = {test:window.location.href,
- tests:test_results,
- status: status.status,
- message: status.message,
- stack: status.stack};
- results_element.textContent = JSON.stringify(data);
-
- // To avoid a HierarchyRequestError with XML documents, ensure that 'results_element'
- // is inserted at a location that results in a valid document.
- var parent = document.body
- ? document.body // <body> is required in XHTML documents
- : document.documentElement; // fallback for optional <body> in HTML5, SVG, etc.
-
- parent.appendChild(results_element);
-}
-
-add_completion_callback(dump_test_results);
-
/* If the parent window has a testharness_properties object,
* we use this to provide the test settings. This is used by the
* default in-browser runner to configure the timeout and the
diff --git a/testing/web-platform/tests/scheduler/tentative/yield/yield-priority-posttask.any.js b/testing/web-platform/tests/scheduler/tentative/yield/yield-priority-posttask.any.js
index 0700094dcf..5e40eba29b 100644
--- a/testing/web-platform/tests/scheduler/tentative/yield/yield-priority-posttask.any.js
+++ b/testing/web-platform/tests/scheduler/tentative/yield/yield-priority-posttask.any.js
@@ -103,6 +103,15 @@ promise_test(async t => {
}, 'yield() with postTask tasks (inherit signal)');
promise_test(async t => {
+ for (const config of signalConfigs) {
+ const {tasks, ids} =
+ postTestTasks(config.options, {});
+ await Promise.all(tasks);
+ assert_equals(ids.join(), config.expected);
+ }
+}, 'yield() with postTask tasks (inherit signal by default)');
+
+promise_test(async t => {
const expected = 'y0,ub1,ub2,uv1,uv2,y1,y2,y3,bg1,bg2';
const {tasks, ids} = postTestTasks(
{priority: 'user-blocking'}, {priority: 'background'});
@@ -171,10 +180,10 @@ promise_test(async t => {
subtasks.push(scheduler.postTask(() => { ids.push('ub1'); }, {priority: 'user-blocking'}));
subtasks.push(scheduler.postTask(() => { ids.push('uv1'); }));
- // Ignore inherited signal (user-visible continuations).
- await scheduler.yield();
+ // Ignore inherited signal.
+ await scheduler.yield({priority: 'user-visible'});
ids.push('y1');
- await scheduler.yield();
+ await scheduler.yield({priority: 'user-visible'});
ids.push('y2');
subtasks.push(scheduler.postTask(() => { ids.push('ub2'); }, {priority: 'user-blocking'}));
diff --git a/testing/web-platform/tests/screen-capture/getallscreensmedia-exposure.tentative.https.window.js b/testing/web-platform/tests/screen-capture/getallscreensmedia-exposure.tentative.https.window.js
new file mode 100644
index 0000000000..9be2fa07dd
--- /dev/null
+++ b/testing/web-platform/tests/screen-capture/getallscreensmedia-exposure.tentative.https.window.js
@@ -0,0 +1,19 @@
+async_test(t => {
+ const i = document.createElement('iframe');
+ i.src = "/common/blank.html";
+ i.onload = t.step_func_done(_ => {
+ assert_equals('undefined', typeof i.contentWindow.navigator.mediaDevices.getAllScreensMedia);
+ });
+ document.body.appendChild(i);
+}, "No CSP, no exposure.");
+
+async_test(t => {
+ const i = document.createElement('iframe');
+ i.src = "/content-security-policy/support/echo-policy.py?policy=" +
+ encodeURIComponent("script-src 'none'; object-src 'none'; base-uri 'none'; require-trusted-types-for 'script'");
+ i.onload = t.step_func_done(_ => {
+ assert_equals("function", typeof i.contentWindow.navigator.mediaDevices.getAllScreensMedia);
+ });
+ document.body.appendChild(i);
+}, "Strict CSP + TT, exposure.");
+
diff --git a/testing/web-platform/tests/screen-details/META.yml b/testing/web-platform/tests/screen-details/META.yml
index abfee89ffb..2e3284c67c 100644
--- a/testing/web-platform/tests/screen-details/META.yml
+++ b/testing/web-platform/tests/screen-details/META.yml
@@ -1 +1 @@
-spec: https://webscreens.github.io/window-placement/
+spec: https://webscreens.github.io/window-management/
diff --git a/testing/web-platform/tests/screen-wake-lock/wakelock-active-document.https.window.js b/testing/web-platform/tests/screen-wake-lock/wakelock-active-document.https.window.js
index 724ce09196..6de27d49ef 100644
--- a/testing/web-platform/tests/screen-wake-lock/wakelock-active-document.https.window.js
+++ b/testing/web-platform/tests/screen-wake-lock/wakelock-active-document.https.window.js
@@ -38,7 +38,26 @@ promise_test(async t => {
);
// We are done, so clean up.
iframe.remove();
-}, "navigator.wakeLock.request() aborts if the document is not active.");
+}, "navigator.wakeLock.request() aborts if the document becomes not active.");
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ const wakeLock = await getWakeLockObject(
+ iframe,
+ "/screen-wake-lock/resources/page1.html"
+ );
+ // Save the DOMException of page1.html before navigating away.
+ const frameDOMException = iframe.contentWindow.DOMException;
+ iframe.remove();
+ await promise_rejects_dom(
+ t,
+ "NotAllowedError",
+ frameDOMException,
+ wakeLock.request('screen'),
+ "Inactive document, so must throw NotAllowedError"
+ );
+}, "navigator.wakeLock.request() aborts if the document is not fully active.");
promise_test(async t => {
// We nest two iframes and wait for them to load.
diff --git a/testing/web-platform/tests/scroll-animations/css/animation-shorthand.html b/testing/web-platform/tests/scroll-animations/css/animation-shorthand.html
index b7d5947a21..cb63137f5c 100644
--- a/testing/web-platform/tests/scroll-animations/css/animation-shorthand.html
+++ b/testing/web-platform/tests/scroll-animations/css/animation-shorthand.html
@@ -79,36 +79,6 @@ test((t) => {
});
target.style.animation = 'anim 1s';
- target.style.animationDelayEnd = '42s';
- assert_equals(target.style.animation, '');
- assert_equals(target.style.animationName, 'anim');
- assert_equals(target.style.animationDuration, '1s');
-
- target.style.animationDelayEnd = '0s, 0s';
- assert_equals(target.style.animation, '');
-}, 'Animation shorthand can not represent non-initial animation-delay-end (specified)');
-
-test((t) => {
- t.add_cleanup(() => {
- target.style = '';
- });
-
- target.style.animation = 'anim 1s';
- target.style.animationDelayEnd = '42s';
- assert_equals(getComputedStyle(target).animation, '');
- assert_equals(getComputedStyle(target).animationName, 'anim');
- assert_equals(getComputedStyle(target).animationDuration, '1s');
-
- target.style.animationDelayEnd = '0s, 0s';
- assert_equals(getComputedStyle(target).animation, '');
-}, 'Animation shorthand can not represent non-initial animation-delay-end (computed)');
-
-test((t) => {
- t.add_cleanup(() => {
- target.style = '';
- });
-
- target.style.animation = 'anim 1s';
target.style.animationRangeStart = 'entry';
assert_equals(target.style.animation, '');
assert_equals(target.style.animationName, 'anim');
diff --git a/testing/web-platform/tests/scroll-to-text-fragment/WEB_FEATURES.yml b/testing/web-platform/tests/scroll-to-text-fragment/WEB_FEATURES.yml
new file mode 100644
index 0000000000..0b260aaaec
--- /dev/null
+++ b/testing/web-platform/tests/scroll-to-text-fragment/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: text-fragments
+ files: "**"
diff --git a/testing/web-platform/tests/scroll-to-text-fragment/percent-encoding.html b/testing/web-platform/tests/scroll-to-text-fragment/percent-encoding.html
index 1f1794bdae..be688eabfc 100644
--- a/testing/web-platform/tests/scroll-to-text-fragment/percent-encoding.html
+++ b/testing/web-platform/tests/scroll-to-text-fragment/percent-encoding.html
@@ -35,17 +35,17 @@ let test_cases = [
},
{
fragment: '#:~:text=%',
- expect: 'noscroll',
+ expect: 'singlepercent',
description: 'Percent char without hex digits is invalid.'
},
{
fragment: '#:~:text=%%',
- expect: 'noscroll',
+ expect: 'doublepercent',
description: 'Percent char followed by percent char is invalid.'
},
{
fragment: '#:~:text=%F',
- expect: 'noscroll',
+ expect: 'percentf',
description: 'Single digit percent-encoding is invalid.'
},
{
@@ -65,21 +65,23 @@ let test_cases = [
},
];
-for (const test_case of test_cases) {
- promise_test(t => new Promise(resolve => {
- // Clear the fragment and reset the scroll offset to prepare for the next
- // test case.
- location = `${location.pathname}#`;
- scrollTo(0, 0);
+function runTests() {
+ for (const test_case of test_cases) {
+ promise_test(t => new Promise(resolve => {
+ // Clear the fragment and reset the scroll offset to prepare for the next
+ // test case.
+ location = `${location.pathname}#`;
+ scrollTo(0, 0);
- location = `${location.pathname}${test_case.fragment}`;
- requestAnimationFrame( () => requestAnimationFrame(resolve) );
- }).then(() => {
- assert_equals(determineResult(), test_case.expect);
- }), `Test navigation with fragment: ${test_case.description}.`);
+ location = `${location.pathname}${test_case.fragment}`;
+ requestAnimationFrame( () => requestAnimationFrame(resolve) );
+ }).then(() => {
+ assert_equals(determineResult(), test_case.expect);
+ }), `Test navigation with fragment: ${test_case.description}.`);
+ }
}
</script>
-
+<body onload="runTests()">
<p class="target" id="singlepercent">
%
</p>
@@ -100,3 +102,4 @@ for (const test_case of test_cases) {
Hello world
</p>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/service-workers/service-worker/WEB_FEATURES.yml b/testing/web-platform/tests/service-workers/service-worker/WEB_FEATURES.yml
new file mode 100644
index 0000000000..9ddc5b400d
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/WEB_FEATURES.yml
@@ -0,0 +1,5 @@
+features:
+- name: js-modules-service-workers
+ files:
+ - registration-script-module.https.html
+ - update-registration-with-type.https.html
diff --git a/testing/web-platform/tests/service-workers/service-worker/navigation-timing-sizes.https.html b/testing/web-platform/tests/service-workers/service-worker/navigation-timing-sizes.https.html
new file mode 100644
index 0000000000..a960cd57f3
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/navigation-timing-sizes.https.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker Navigation Timing</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 => {
+ var script = 'resources/pass-through-worker.js';
+ var 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 iframe = await with_iframe(scope);
+
+ // Sanity, to check that we actually loaded the document.
+ assert_equals(iframe.contentWindow.document.title, "Empty doc");
+ t.add_cleanup(() => iframe.remove());
+ const navigationEntry = iframe.contentWindow.performance.getEntriesByType("navigation")[0];
+
+ const main_page_resource_timing = performance.getEntriesByType("resource").filter(
+ e => e.name.includes('blank'))[0];
+
+ assert_greater_than(navigationEntry.encodedBodySize, 0,
+ 'Navigation timing should have encodedBodySize larger than 0.');
+
+ assert_equals(navigationEntry.decodedBodySize, navigationEntry.encodedBodySize,
+ 'Navigation timing\'s decodedBodySize and encodedBodySize should be equal.');
+
+ assert_greater_than(main_page_resource_timing.encodedBodySize, 0,
+ 'Corresponding resource timing emitted on parent page should have decodedBodySize larger than 0.');
+
+ assert_equals(main_page_resource_timing.encodedBodySize, main_page_resource_timing.decodedBodySize,
+ 'Corresponding resource timing emitted on parent page should have equal\
+ decodedBodySize and encodedBodySize.');
+
+ }, 'Body sizes in a regular pass-through');
+
+ promise_test(async t => {
+ var script = 'resources/pass-through-worker.js';
+ var 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 iframe = await with_iframe(scope + "?pipe=gzip");
+ // Sanity, to check that we actually loaded the document.
+ assert_equals(iframe.contentWindow.document.title, "Empty doc");
+ t.add_cleanup(() => iframe.remove());
+
+ const navigationEntry = iframe.contentWindow.performance.getEntriesByType("navigation")[0];
+
+ const main_page_resource_timing = performance.getEntriesByType("resource").filter(
+ e => e.name.includes('blank'))[0];
+
+ assert_greater_than(navigationEntry.decodedBodySize, 0,
+ 'Navigation timing should have decodedBodySize larger than 0.');
+
+ // The response body that comes from a service worker respondWith promise
+ // should have identical encoded and decoded body sizes, regardless of what
+ // the service worker itself saw, according to the spec.
+ assert_equals(navigationEntry.encodedBodySize, navigationEntry.decodedBodySize,
+ 'Navigation timing should have equal decodedBodySize and encodedBodySize.');
+
+ assert_greater_than(main_page_resource_timing.decodedBodySize, 0,
+ 'Corresponding resource timing emitted on parent page should have decodedBodySize larger than 0.');
+
+ assert_equals(main_page_resource_timing.encodedBodySize, navigationEntry.decodedBodySize,
+ 'Corresponding resource timing emitted on parent page should have equal decodedBodySize and \
+ encodedBodySize.');
+
+ }, 'Body sizes in a regular pass-through with gzip');
+ </script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/range-request-with-synth-head-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/range-request-with-synth-head-worker.js
index 6025d91b1a..6b6395aeca 100644
--- a/testing/web-platform/tests/service-workers/service-worker/resources/range-request-with-synth-head-worker.js
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/range-request-with-synth-head-worker.js
@@ -24,7 +24,7 @@ self.addEventListener('fetch', e => {
headers: {
"Accept-Ranges": "bytes",
"Content-Type": "video/webm",
- "Content-Range": "bytes 0-1/44447",
+ "Content-Range": "bytes 0-1/*",
"Content-Length": "2",
},
};
diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/router-rules.js b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/router-rules.js
index 014cd2ec95..fdc1c9e063 100644
--- a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/router-rules.js
+++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/resources/router-rules.js
@@ -1,10 +1,17 @@
const TEST_CACHE_NAME = 'v1';
+// The value is coming from:
+// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/public/common/service_worker/service_worker_router_rule.h;l=28;drc=6f3f85b321146cfc0f9eb81a74c7c2257821461e
+const CONDITION_MAX_RECURSION_DEPTH = 10;
const routerRules = {
'condition-urlpattern-constructed-source-network': [{
condition: {urlPattern: new URLPattern({pathname: '/**/direct.txt'})},
source: 'network'
}],
+ 'condition-urlpattern-not-source-network': [{
+ condition: {not: {urlPattern: new URLPattern({pathname: '/**/not.txt'})}},
+ source: 'network'
+ }],
'condition-urlpattern-constructed-match-all-source-cache': [
{condition: {urlPattern: new URLPattern({})}, source: 'cache'},
],
@@ -43,22 +50,37 @@ const routerRules = {
[{condition: {requestMethod: 'PUT'}, source: 'network'}],
'condition-request-method-delete-network':
[{condition: {requestMethod: 'DELETE'}, source: 'network'}],
+ 'condition-lack-of-condition': [{
+ source: 'network'
+ }],
+ 'condition-lack-of-source': [{
+ condition: {requestMode: 'no-cors'},
+ }],
'condition-invalid-request-method': [{
condition: {requestMethod: String.fromCodePoint(0x3042)},
source: 'network'
}],
'condition-invalid-or-condition-depth': (() => {
- const max = 10;
- const addOrCondition = (obj, depth) => {
- if (depth > max) {
- return obj;
+ const addOrCondition = (depth) => {
+ if (depth > CONDITION_MAX_RECURSION_DEPTH) {
+ return {urlPattern: '/foo'};
}
return {
- urlPattern: `/foo-${depth}`,
- or: [addOrCondition(obj, depth + 1)]
+ or: [addOrCondition(depth + 1)]
};
};
- return {condition: addOrCondition({}, 0), source: 'network'};
+ return {condition: addOrCondition(1), source: 'network'};
+ })(),
+ 'condition-invalid-not-condition-depth': (() => {
+ const generateNotCondition = (depth) => {
+ if (depth > CONDITION_MAX_RECURSION_DEPTH) {
+ return {
+ urlPattern: '/**/example.txt',
+ };
+ }
+ return {not: generateNotCondition(depth + 1)};
+ };
+ return {condition: generateNotCondition(1), source: 'network'};
})(),
'condition-invalid-router-size': [...Array(512)].map((val, i) => {
return {
diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-invalid-rules.https.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-invalid-rules.https.html
index 9ef7cfdc9f..15b8ef5742 100644
--- a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-invalid-rules.https.html
+++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-invalid-rules.https.html
@@ -17,8 +17,14 @@ const ROUTER_RULE_KEY_INVALID_REQUEST_METHOD =
'condition-invalid-request-method';
const ROUTER_RULE_KEY_INVALID_OR_CONDITION_DEPTH =
'condition-invalid-or-condition-depth';
+const ROUTER_RULE_KEY_INVALID_NOT_CONDITION_DEPTH =
+ 'condition-invalid-not-condition-depth';
const ROUTER_RULE_KEY_INVALID_ROUTER_SIZE =
'condition-invalid-router-size';
+const ROUTER_RULE_KEY_LACK_OF_CONDITION =
+ 'condition-lack-of-condition';
+const ROUTER_RULE_KEY_LACK_OF_SOURCE =
+ 'condition-lack-of-source';
promise_test(async t => {
const worker = await registerAndActivate(t, ROUTER_RULE_KEY_INVALID_REQUEST_METHOD);
@@ -35,11 +41,33 @@ promise_test(async t => {
}, 'addRoutes should raise if or condition exceeds the depth limit');
promise_test(async t => {
+ const worker = await registerAndActivate(t, ROUTER_RULE_KEY_INVALID_NOT_CONDITION_DEPTH);
+ t.add_cleanup(() => {reset_info_in_worker(worker)});
+ const {errors} = await get_info_from_worker(worker);
+ assert_equals(errors.length, 1);
+}, 'addRoutes should raise if not condition exceeds the depth limit');
+
+promise_test(async t => {
const worker = await registerAndActivate(t, ROUTER_RULE_KEY_INVALID_ROUTER_SIZE);
t.add_cleanup(() => {reset_info_in_worker(worker)});
const {errors} = await get_info_from_worker(worker);
assert_equals(errors.length, 1);
}, 'addRoutes should raise if the number of router rules exceeds the length limit');
+promise_test(async t => {
+ const worker = await registerAndActivate(t, ROUTER_RULE_KEY_LACK_OF_CONDITION);
+ t.add_cleanup(() => {reset_info_in_worker(worker)});
+ const {errors} = await get_info_from_worker(worker);
+ assert_equals(errors.length, 1);
+}, 'addRoutes should raise if the conditon does not exist in the rule');
+
+promise_test(async t => {
+ const worker = await registerAndActivate(t, ROUTER_RULE_KEY_LACK_OF_SOURCE);
+ t.add_cleanup(() => {reset_info_in_worker(worker)});
+ const {errors} = await get_info_from_worker(worker);
+ assert_equals(errors.length, 1);
+}, 'addRoutes should raise if the source does not exiswt in the rule');
+
+
</script>
</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html
index 7998af3f99..71bc0697f9 100644
--- a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html
+++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html
@@ -11,6 +11,7 @@
<body>
<script>
const ROUTER_RULE_KEY = 'condition-urlpattern-constructed-source-network';
+const ROUTER_RULE_NOT_KEY = 'condition-urlpattern-not-source-network';
const ROUTER_RULE_KEY_IGNORE_CASE =
'condition-urlpattern-constructed-ignore-case-source-network';
const ROUTER_RULE_KEY_RESPECT_CASE =
@@ -23,6 +24,7 @@ const ROUTER_RULE_KEY_URLPATTERN_CACHE_WITH_NAME =
const REGISTERED_ROUTE = 'resources/direct.txt';
const CACHED_ROUTE = 'resources/cache.txt';
const NON_REGISTERED_ROUTE = 'resources/simple.html';
+const NOT_ROUTE = 'resources/not.txt';
const host_info = get_host_info();
const path = new URL(".", window.location).pathname;
@@ -72,5 +74,20 @@ iframeTest(CACHED_ROUTE, ROUTER_RULE_KEY_URLPATTERN_CACHE_WITH_NAME, async (t, i
assert_equals(requests.length, 0);
assert_equals(iwin.document.body.innerText, "From cache");
}, 'Main resource load matched with the cache source, with specifying the cache name');
+
+iframeTest(NOT_ROUTE, ROUTER_RULE_NOT_KEY, async (t, iwin, worker) => {
+ const {requests} = await get_info_from_worker(worker);
+ assert_equals(requests.length, 1);
+ assert_equals(
+ requests[0].url,
+ `${host_info['HTTPS_ORIGIN']}${path}${NOT_ROUTE}`);
+ assert_equals(requests[0].mode, 'navigate');
+}, 'Main resource load should not match the condition with not');
+
+iframeTest(REGISTERED_ROUTE, ROUTER_RULE_NOT_KEY, async (t, iwin, worker) => {
+ const {requests} = await get_info_from_worker(worker);
+ assert_equals(requests.length, 0);
+ assert_equals(iwin.document.body.innerText, "Network\n");
+}, 'Main resource load should match the condition without not');
</script>
</body>
diff --git a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html
index 00b9070bf1..ab05a3d252 100644
--- a/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html
+++ b/testing/web-platform/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html
@@ -27,10 +27,12 @@ const ROUTER_RULE_KEY_URL_PATTERN_CONSTRUCTED_MATCH_ALL_CACHE =
const ROUTER_RULE_KEY_URLPATTERN_CACHE_WITH_NAME =
'condition-urlpattern-string-source-cache-with-name';
const ROUTER_RULE_KEY_OR = 'condition-or-source-network'
+const ROUTER_RULE_KEY_NOT = 'condition-urlpattern-not-source-network';
const SCOPE = 'resources/';
const HTML_FILE = 'resources/simple.html';
const TXT_FILE = 'resources/direct.txt';
const CSV_FILE = 'resources/simple.csv';
+const NOT_FILE = 'resources/not.txt';
// Warning: please prepare the corresponding `*.text.headers` files, otherwise
// iframeTest() fails to load the following files due to MIME mismatches.
const OR_TEST_FILES = [
@@ -184,5 +186,17 @@ iframeTest(HTML_FILE, ROUTER_RULE_KEY_URLPATTERN_CACHE_WITH_NAME, async (t, iwin
assert_equals(response_with_param.status, 404);
}, 'Subresource load matched with the cache source, with specifying the cache name');
+iframeTest(TXT_FILE, ROUTER_RULE_KEY_NOT, async (t, iwin) => {
+ const rnd = randomString();
+ const response = await iwin.fetch(`${NOT_FILE}?nonce=${rnd}`);
+ assert_equals(await response.text(), rnd);
+}, 'Subresource load should not match with the not condition');
+
+iframeTest(TXT_FILE, ROUTER_RULE_KEY_NOT, async (t, iwin) => {
+ const rnd = randomString();
+ const response = await iwin.fetch('?nonce=' + rnd);
+ assert_equals(await response.text(), "Network\n");
+}, 'Subresource load should match with a file other than not');
+
</script>
</body>
diff --git a/testing/web-platform/tests/shadow-dom/declarative/WEB_FEATURES.yml b/testing/web-platform/tests/shadow-dom/declarative/WEB_FEATURES.yml
new file mode 100644
index 0000000000..4e31ed4962
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/declarative/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: declarative-shadow-dom
+ files: "**"
diff --git a/testing/web-platform/tests/shadow-dom/declarative/gethtml.html b/testing/web-platform/tests/shadow-dom/declarative/gethtml.html
index d950ca7734..ff70653477 100644
--- a/testing/web-platform/tests/shadow-dom/declarative/gethtml.html
+++ b/testing/web-platform/tests/shadow-dom/declarative/gethtml.html
@@ -29,7 +29,6 @@ function testElementType(allowsShadowDom, elementType, runGetHTMLOnShadowRoot,
}
let shadowRoot;
- const isOpen = mode === 'open';
let initDict = {mode: mode, delegatesFocus: delegatesFocus, clonable};
let expectedSerializable = null;
switch (serializable) {
@@ -47,12 +46,8 @@ function testElementType(allowsShadowDom, elementType, runGetHTMLOnShadowRoot,
`shadowrootmode=${mode}${delegatesAttr}${serializableAttr}` +
`${clonableAttr}>`;
wrapper.setHTMLUnsafe(html);
- if (isOpen) {
- shadowRoot = wrapper.firstElementChild.shadowRoot;
- } else {
- // For closed shadow root, we rely on the behavior of attachShadow to return it to us
- shadowRoot = wrapper.firstElementChild.attachShadow(initDict);
- }
+ // Get hold of the declarative shadow root in a way that works when its mode is "closed"
+ shadowRoot = wrapper.firstElementChild.attachShadow(initDict);
} else {
// Imperative shadow dom
const element = document.createElement(elementType);
@@ -78,22 +73,14 @@ function testElementType(allowsShadowDom, elementType, runGetHTMLOnShadowRoot,
assert_equals(shadowRoot.clonable,clonable);
shadowRoot.appendChild(document.createElement('slot'));
const emptyElement = `<${elementType}>${lightDOMContent}</${elementType}>`;
- if (isOpen) {
- if (expectedSerializable) {
- assert_equals(wrapper.getHTML({serializableShadowRoots: true}),
- correctHtml);
- assert_equals(wrapper.firstElementChild.getHTML({
- serializableShadowRoots: true}),
- `${correctShadowHtml}${lightDOMContent}`);
- } else {
- assert_equals(wrapper.getHTML({serializableShadowRoots: true}), emptyElement);
- }
- } else {
- // Closed shadow roots should not be returned unless shadowRoots specifically contains the shadow root:
+ if (expectedSerializable) {
assert_equals(wrapper.getHTML({serializableShadowRoots: true}),
- emptyElement);
- assert_equals(wrapper.getHTML({serializableShadowRoots: true,
- shadowRoots: []}), emptyElement);
+ correctHtml);
+ assert_equals(wrapper.firstElementChild.getHTML({
+ serializableShadowRoots: true}),
+ `${correctShadowHtml}${lightDOMContent}`);
+ } else {
+ assert_equals(wrapper.getHTML({serializableShadowRoots: true}), emptyElement);
}
// If we provide the shadow root, serialize it, regardless of serializableShadowRoots.
assert_equals(wrapper.getHTML({serializableShadowRoots: true, shadowRoots:
diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-2levels.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-2levels.html
index 7d733ea7c2..1b867cc2a9 100644
--- a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-2levels.html
+++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-2levels.html
@@ -1,4 +1,5 @@
<!DOCTYPE html>
+<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots-in-slot.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots-in-slot.html
index 39b0806603..3c929de727 100644
--- a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots-in-slot.html
+++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots-in-slot.html
@@ -1,4 +1,5 @@
<!DOCTYPE html>
+<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-with-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-with-delegatesFocus.html
index 64942a109e..e37accbc8e 100644
--- a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-with-delegatesFocus.html
+++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-with-delegatesFocus.html
@@ -1,4 +1,5 @@
<!DOCTYPE html>
+<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation.html
index 9e593eb100..faf3c34cdb 100644
--- a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation.html
+++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation.html
@@ -1,4 +1,5 @@
<!DOCTYPE html>
+<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-on-shadow-host.html b/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-on-shadow-host.html
new file mode 100644
index 0000000000..a48eda1b57
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-on-shadow-host.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Display: reading-order-items with value grid-order on shadow host</title>
+<link rel="help" href="https://drafts.csswg.org/css-display-4/#reading-order-items">
+<link rel="author" title="Di Zhang" href="mailto:dizhangg@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src='../../resources/shadow-dom.js'></script>
+<script src="../../resources/focus-utils.js"></script>
+
+<style>
+.wrapper {
+ display: grid;
+ reading-order-items: grid-order;
+}
+</style>
+
+<div class="test-case" data-expect="root/B,root/A"
+ data-description="Grid items in shadow host with delegatesFocus">
+ <div id="root" class="wrapper" tabindex="0">
+ <template shadowrootmode="open" shadowrootdelegatesfocus>
+ <button id="A" style="order: 2">A</button>
+ <button id="B" style="order: 1">B</button>
+ </template>
+ </div>
+</div>
+
+<div class="test-case" data-expect="root2,root2/B,root2/A"
+ data-description="Grid items in shadow host without delegatesFocus">
+ <div id="root2" class="wrapper" tabindex="0">
+ <template shadowrootmode="open">
+ <button id="A" style="order: 2">A</button>
+ <button id="B" style="order: 1">B</button>
+ </template>
+ </div>
+</div>
+
+<script>
+runFocusTestCases();
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-iframe.html b/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-iframe.html
new file mode 100644
index 0000000000..87b9e0a83d
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-iframe.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Display: reading-order-items with value grid-order on iframe</title>
+<link rel="help" href="https://drafts.csswg.org/css-display-4/#reading-order-items">
+<link rel="author" title="Di Zhang" href="mailto:dizhangg@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src='../../resources/shadow-dom.js'></script>
+<script src="../../resources/focus-utils.js"></script>
+
+<style>
+.wrapper {
+ display: grid;
+ reading-order-items: grid-order;
+}
+</style>
+
+<div class="test-case" data-expect="start,frame2/B,frame1/A,end"
+ data-description="Grid items are iframes.">
+ <button id="start">Item Start</button>
+ <div class="wrapper">
+ <iframe id="frame1" style="order: 2" srcdoc="<button id=A>A</button>"></iframe>
+ <iframe id="frame2" style="order: 1" srcdoc="<button id=B>B</button>"></iframe>
+ </div>
+ <button id="end">Item End</button>
+</div>
+
+<script>
+window.onload = runFocusTestCases;
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-nested-grids.html b/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-nested-grids.html
new file mode 100644
index 0000000000..d3da6682a3
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-nested-grids.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Display: reading-order-items with value grid-order in nested grids</title>
+<link rel="help" href="https://drafts.csswg.org/css-display-4/#reading-order-items">
+<link rel="author" title="Di Zhang" href="mailto:dizhangg@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src='../../resources/shadow-dom.js'></script>
+<script src="../../resources/focus-utils.js"></script>
+
+<style>
+.box {
+ display: grid;
+ reading-order-items: grid-order;
+ border-radius: 5px;
+ padding: 20px;
+ border-style: dashed;
+}
+
+</style>
+
+<div class="test-case" data-expect="w,d3,b3a,b3b,d2,b2,d1,b1a,b1b"
+ data-description="Grid items are not grid containers.">
+ <div class="box" id="w" tabindex="0">
+ <div style="order: 2" id="d1" tabindex="0">Div 1
+ <button id="b1a" style="order: 3">Button 1A</button>
+ <button id="b1b">Button 1B</button>
+ </div>
+ <div id="d2" tabindex="0">Div 2
+ <button id="b2">Button 2</button>
+ </div>
+ <div id="d3" style="order: -1" tabindex="0">Div 3
+ <button id="b3a" style="order: 2">Button 3A</button>
+ <button id="b3b">Button 3B</button>
+ </div>
+ </div>
+</div>
+
+<div class="test-case"
+ data-expect="wrapper,div3,button3b,button3a,div2,button2,div1,button1b,button1a"
+ data-description="Grid items are grid containers.">
+ <div class="box" id="wrapper" tabindex="0">
+ <div class="box" style="order: 2" id="div1" tabindex="0">Div 1
+ <button id="button1a" style="order: 3">Button 1A</button>
+ <button id="button1b">Button 1B</button>
+ </div>
+ <div class="box" id="div2" tabindex="0">Div 2
+ <button id="button2">Button 2</button>
+ </div>
+ <div class="box" id="div3" style="order: -1" tabindex="0">Div 3
+ <button id="button3a" style="order: 2">Button 3A</button>
+ <button id="button3b">Button 3B</button>
+ </div>
+ </div>
+</div>
+
+<div class="test-case" data-expect="a,b,d,c"
+ data-description="Only has grid containers.">
+ <div class="box" id="a" tabindex="0">A
+ <div class="box" id="b" tabindex="0">B
+ <div class="box" id="c" tabindex="0" style="order: 2">C</div>
+ <div class="box" id="d" tabindex="0">D</div>
+ </div>
+ </div>
+</div>
+
+<script>
+runFocusTestCases();
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-popover.html b/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-popover.html
new file mode 100644
index 0000000000..2e98c04157
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-popover.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Display: reading-order-items with value grid-order</title>
+<link rel="help" href="https://drafts.csswg.org/css-display-4/#reading-order-items">
+<link rel="author" title="Di Zhang" href="mailto:dizhangg@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src='../../resources/shadow-dom.js'></script>
+<script src="../../resources/focus-utils.js"></script>
+
+<style>
+.wrapper {
+ display: grid;
+ reading-order-items: grid-order;
+}
+.popover {
+ inset:auto;
+ top:200px;
+ left:200px;
+}
+</style>
+
+<div class="wrapper">
+ <button id=a style="order: 4">A</button>
+ <button id=invoker style="order: 2" popovertarget=P>Invoker</button>
+ <button id=c style="order: 1">C</button>
+</div>
+<div popover id=P class="popover">
+ <button id=b1 style="order: 3">Popover button B1</button>
+ <button id=b2 style="order: 1">Popover button B2</button>
+ <button id=b3 style="order: 2">Popover button B3</button>
+</div>
+
+<script>
+document.querySelector('[popovertarget]').click();
+
+promise_test(async () => {
+ let elements = [
+ 'c',
+ 'invoker',
+ 'b1',
+ 'b2',
+ 'b3',
+ 'a',
+ ];
+ await assert_focus_navigation_forward(elements);
+}, `Popover in reading-order-items: grid-order container.`);
+
+promise_test(async () => {
+ P.classList.add("wrapper");
+ let elements = [
+ 'c',
+ 'invoker',
+ 'b2',
+ 'b3',
+ 'b1',
+ 'a',
+ ];
+ await assert_focus_navigation_forward(elements);
+}, `Popover in container and itself with reading-order-items: grid-order.`);
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-slots.html b/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-slots.html
new file mode 100644
index 0000000000..3045001e1d
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-slots.html
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Display: reading-order-items with value grid-order in Shadow DOM</title>
+<link rel="help" href="https://drafts.csswg.org/css-display-4/#reading-order-items">
+<link rel="author" title="Di Zhang" href="mailto:dizhangg@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src='../../resources/shadow-dom.js'></script>
+<script src="../../resources/focus-utils.js"></script>
+
+<style>
+.wrapper {
+ display: grid;
+ reading-order-items: grid-order;
+}
+</style>
+
+<span id="root1" class="test-case" data-expect="b1,a1,c1"
+ data-description="Slot assigned element is a grid with reading-order-items.">
+ <template shadowrootmode="open">
+ <style>
+ .wrapper {
+ display: grid;
+ reading-order-items: grid-order;
+ }
+ </style>
+ <slot></slot>
+ </template>
+ <div class="wrapper">
+ <button style="order: 2" id="a1">Item A</button>
+ <button style="order: 1" id="b1">Item B</button>
+ <button style="order: 3" id="c1">Item C</button>
+ </div>
+</span>
+<br>
+
+<span id="root2" class="test-case" data-expect="b2,a2,c2"
+ data-description="Slot is a grid with reading-order-items.">
+ <template shadowrootmode="open">
+ <style>
+ .wrapper {
+ display: grid;
+ reading-order-items: grid-order;
+ }
+ </style>
+ <slot class="wrapper"></slot>
+ </template>
+ <button style="order: 2" id="a2">Item A</button>
+ <button style="order: 1" id="b2">Item B</button>
+ <button style="order: 3" id="c2">Item C</button>
+</span>
+<br>
+
+<span id="root3" class="test-case" data-expect="root3/o2,root3/o4,o1,o3,o5"
+ data-description="Slot is inside a grid container with reading-order-items.">
+ <template shadowrootmode="open">
+ <style>
+ .wrapper {
+ display: grid;
+ reading-order-items: grid-order;
+ }
+ </style>
+ <div class="wrapper">
+ <button style="order: 4" id="o4">Order 4</button>
+ <slot style="order: 10"></slot>
+ <button style="order: 2" id="o2">Order 2</button>
+ </div>
+ </template>
+ <button style="order: 5" id="o5">Order 5</button>
+ <button style="order: 1" id="o1">Order 1</button>
+ <button style="order: 3" id="o3">Order 3</button>
+</span>
+<br>
+
+<span id="root4" class="test-case"
+ data-expect="root4/after,root4/before,b4,a4,d42,d41,d43,c4"
+ data-description="Slot is a grid with reading-order-items inside a grid container with reading-order-items.">
+ <template shadowrootmode="open">
+ <style>
+ .wrapper {
+ display: grid;
+ reading-order-items: grid-order;
+ }
+ </style>
+ <div class="wrapper">
+ <button style="order: 4" id="before">Before</button>
+ <slot style="order: 10"></slot>
+ <button style="order: 2" id="after">After</button>
+ </div>
+ </template>
+ <button style="order: 3" id="a4">Item A</button>
+ <button style="order: 1" id="b4">Item B</button>
+ <button style="order: 6" id="c4">Item C</button>
+ <div style="order: 5" class="wrapper">
+ <button style="order: 2" id="d41">Item D1</button>
+ <button style="order: 1" id="d42">Item D2</button>
+ <button style="order: 3" id="d43">Item D3</button>
+ </div>
+</span>
+<br>
+
+<span id="root5" class="test-case" data-expect="b51,a51,c51,b52,a52,c52"
+ data-description="Slot is not inside a shadow root.">
+ <div class="wrapper">
+ <slot name=slot1>
+ <button style="order: 2" id="a51">Item A</button>
+ <button style="order: 1" id="b51">Item B</button>
+ <button style="order: 3" id="c51">Item C</button>
+ </slot>
+ </div>
+ <slot class="wrapper" name=slot2>
+ <button style="order: 2" id="a52">Item A</button>
+ <button style="order: 1" id="b52">Item B</button>
+ <button style="order: 3" id="c52">Item C</button>
+ </slot>
+</span>
+<br>
+
+<span id="root6" class="test-case" data-expect="root6/after,root6/before,b6,a6"
+ data-description="Slot is a display contents inside a grid container.">
+ <template shadowrootmode="open">
+ <style>
+ .wrapper {
+ display: grid;
+ reading-order-items: grid-order;
+ }
+ </style>
+ <div class="wrapper">
+ <button style="order: 4" id="before">Before</button>
+ <slot style="display: contents" style="order: 4"></slot>
+ <button style="order: 3" id="after">After</button>
+ </div>
+ </template>
+ <button id="a6" style="order: 7">A</button>
+ <button id="b6" style="order: 2">B</button>
+</span>
+<br>
+
+<span id="root7" class="test-case" data-expect="a7,b7,root7/after,root7/before"
+ data-description="Slot is a display block inside a grid container.">
+ <template shadowrootmode="open">
+ <style>
+ .wrapper {
+ display: grid;
+ reading-order-items: grid-order;
+ }
+ </style>
+ <div class="wrapper">
+ <button style="order: 4" id="before">Before</button>
+ <slot style="display: block" style="order: 4"></slot>
+ <button style="order: 3" id="after">After</button>
+ </div>
+ </template>
+ <button id="a7" style="order: 7">A</button>
+ <button id="b7" style="order: 2">B</button>
+</span>
+<br>
+
+<script>
+runFocusTestCases();
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order.html b/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order.html
new file mode 100644
index 0000000000..1b86ab0b25
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Display: reading-order-items with value grid-order</title>
+<link rel="help" href="https://drafts.csswg.org/css-display-4/#reading-order-items">
+<link rel="author" title="Di Zhang" href="mailto:dizhangg@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src='../../resources/shadow-dom.js'></script>
+<script src="../../resources/focus-utils.js"></script>
+
+<style>
+.wrapper {
+ display: grid;
+ reading-order-items: grid-order;
+}
+</style>
+
+<div class="test-case" data-expect="a1,b1,c1,d1,e1"
+ data-description="Grid items without `order` property">
+ <div class="wrapper">
+ <button id="a1">Item A</button>
+ <button id="b1">Item B</button>
+ <button id="c1">Item C</button>
+ </div>
+ <div class="wrapper">
+ <button id="d1">Item D</button>
+ <button id="e1">Item E</button>
+ </div>
+</div>
+
+<div class="test-case" data-expect="b2,c2,a2,e2,d2"
+ data-description="Grid items with `order` property">
+ <div class="wrapper">
+ <button id="a2" style="order: 2">Item A</button>
+ <button id="b2">Item B</button>
+ <button id="c2">Item C</button>
+ </div>
+ <div class="wrapper">
+ <button id="d2">Item D</button>
+ <button id="e2" style="order: -1">Item E</button>
+ </div>
+</div>
+
+<div class="test-case" data-expect="c3,a3,b3,d3,f3,e3"
+ data-description="Grid items with `order` property and tabindex">
+ <div class="wrapper">
+ <button id="a3" style="order: -1">Item A</button>
+ <button id="b3" style="order: 0">Item B</button>
+ <button id="c3" tabindex="1" style="order: -1">Item C</button>
+ </div>
+ <div class="wrapper">
+ <button id="d3" tabindex="1" style="order: 1">Item D</button>
+ <button id="e3" tabindex="2" style="order: 0">Item E</button>
+ <button id="f3" tabindex="2" style="order: -1">Item F</button>
+ </div>
+</div>
+
+<div class="test-case" data-expect="order1,order2,order3,order4"
+ data-description="Items in display contents are sorted in same grid container.">
+ <div class="wrapper">
+ <div style="display: contents">
+ <button id="order3" style="order: 3">Order 3</button>
+ <button id="order1" style="order: 1">Order 1</button>
+ <div style="display: contents">
+ <button id="order4" style="order: 4">Order 4</button>
+ <button id="order2" style="order: 2">Order 2</button>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div class="test-case" data-expect="d4,b4,c4,a4"
+ data-description="Items in display block are not in parent grid container.">
+ <div class="wrapper">
+ <button id="a4" style="order: 3">A</button>
+ <div style="order: 1" style="display: block">
+ <button id="b4" style="order: 2">B</button>
+ <button id="c4" style="order: -1">C</button>
+ </div>
+ <button id="d4">D</button>
+ </div>
+</div>
+
+<script>
+runFocusTestCases();
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/resources/focus-utils.js b/testing/web-platform/tests/shadow-dom/focus-navigation/resources/focus-utils.js
index f593267cc3..f4056dc168 100644
--- a/testing/web-platform/tests/shadow-dom/focus-navigation/resources/focus-utils.js
+++ b/testing/web-platform/tests/shadow-dom/focus-navigation/resources/focus-utils.js
@@ -1,17 +1,27 @@
'use strict';
-function navigateFocusForward() {
- // TAB = '\ue004'
- return test_driver.send_keys(document.body, "\ue004");
+function waitForRender() {
+ return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
+}
+
+async function navigateFocusForward() {
+ await waitForRender();
+ const kTab = '\uE004';
+ await new test_driver.send_keys(document.documentElement,kTab);
+ await waitForRender();
}
async function navigateFocusBackward() {
- return new test_driver.Actions()
- .keyDown('\uE050')
- .keyDown('\uE004')
- .keyUp('\uE004')
- .keyUp('\uE050')
+ await waitForRender();
+ const kShift = '\uE008';
+ const kTab = '\uE004';
+ await new test_driver.Actions()
+ .keyDown(kShift)
+ .keyDown(kTab)
+ .keyUp(kTab)
+ .keyUp(kShift)
.send();
+ await waitForRender();
}
// If shadow root is open, can find element using element path
@@ -162,3 +172,18 @@ async function assert_focus_navigation_bidirectional_with_shadow_root(elements)
await assert_focus_navigation_backward_with_shadow_root(elements);
}
+// This Promise will run each test case that is:
+// 1. Wrapped in an element with class name "test-case".
+// 2. Has data-expect attribute be an ordered list of elements to focus.
+// 3. Has data-description attribute be a string explaining the test.
+// e.g <div class="test-case" data-expect="b,a,c"
+// data-description="Focus navigation">
+async function runFocusTestCases() {
+ const testCases = Array.from(document.querySelectorAll('.test-case'));
+ for (let testCase of testCases) {
+ promise_test(async () => {
+ const expected = testCase.dataset.expect.split(',');
+ await assert_focus_navigation_forward(expected);
+ }, testCase.dataset.description);
+ }
+}
diff --git a/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-include.tentative.https.sub.html b/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-include.tentative.https.sub.html
index 9c44d2a29f..4c0e91c156 100644
--- a/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-include.tentative.https.sub.html
+++ b/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-include.tentative.https.sub.html
@@ -19,6 +19,7 @@ promise_test(async () => {
`/shared-storage/resources/credentials-test-helper.py` +
`?access_control_allow_origin_header=${window.origin}` +
`&access_control_allow_credentials_header=true` +
+ `&shared_storage_cross_origin_worklet_allowed_header=?1` +
`&token=${ancestor_key}`;
await fetch(set_cookie_url, { mode: 'no-cors', credentials: 'include' });
diff --git a/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-omit.tentative.https.sub.html b/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-omit.tentative.https.sub.html
index ddda1809f2..86b56ce80d 100644
--- a/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-omit.tentative.https.sub.html
+++ b/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-omit.tentative.https.sub.html
@@ -18,6 +18,7 @@ promise_test(async () => {
const helper_url = crossOrigin +
`/shared-storage/resources/credentials-test-helper.py` +
`?access_control_allow_origin_header=${window.origin}` +
+ `&shared_storage_cross_origin_worklet_allowed_header=?1` +
`&token=${ancestor_key}`;
await fetch(set_cookie_url, { mode: 'no-cors', credentials: 'include' });
diff --git a/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-same-origin.tentative.https.sub.html b/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-same-origin.tentative.https.sub.html
index 99701d2b7d..0b8faad783 100644
--- a/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-same-origin.tentative.https.sub.html
+++ b/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-credentials-same-origin.tentative.https.sub.html
@@ -18,6 +18,7 @@ promise_test(async () => {
const helper_url = crossOrigin +
`/shared-storage/resources/credentials-test-helper.py` +
`?access_control_allow_origin_header=${window.origin}` +
+ `&shared_storage_cross_origin_worklet_allowed_header=?1` +
`&token=${ancestor_key}`;
await fetch(set_cookie_url, { mode: 'no-cors', credentials: 'include' });
diff --git a/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-false-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html b/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-false-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html
new file mode 100644
index 0000000000..f1f37b0aff
--- /dev/null
+++ b/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-false-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/shared-storage/resources/util.js"></script>
+<script src="/fenced-frame/resources/utils.js"></script>
+
+<body>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const ancestor_key = token();
+ const crossOrigin = 'https://{{domains[www]}}:{{ports[https][0]}}';
+ const helper_url = crossOrigin +
+ `/shared-storage/resources/credentials-test-helper.py` +
+ `?access_control_allow_origin_header=${window.origin}` +
+ `&access_control_allow_credentials_header=true` +
+ `&shared_storage_cross_origin_worklet_allowed_header=?0` +
+ `&token=${ancestor_key}`;
+
+ // The network error for `createWorklet()` won't be revealed to the
+ // cross-origin caller.
+ await sharedStorage.createWorklet(
+ helper_url + `&action=store-cookie`,
+ { credentials: "include" });
+}, 'createWorklet() with cross-origin module script and credentials ' +
+ '"include", and with the Shared-Storage-Cross-Origin-Worklet-Allowed ' +
+ 'response header value set to false (?0)');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-failure-missing-access-control-allow-credentials.tentative.https.sub.html b/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-access-control-allow-credentials.tentative.https.sub.html
index 598fd8f405..dd6347e463 100644
--- a/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-failure-missing-access-control-allow-credentials.tentative.https.sub.html
+++ b/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-access-control-allow-credentials.tentative.https.sub.html
@@ -15,12 +15,14 @@ promise_test(async t => {
const helper_url = crossOrigin +
`/shared-storage/resources/credentials-test-helper.py` +
`?access_control_allow_origin_header=${window.origin}` +
+ `&shared_storage_cross_origin_worklet_allowed_header=?1` +
`&token=${ancestor_key}`;
- return promise_rejects_dom(t, "OperationError",
- sharedStorage.createWorklet(
+ // The network error for `createWorklet()` won't be revealed to the
+ // cross-origin caller.
+ await sharedStorage.createWorklet(
helper_url + `&action=store-cookie`,
- { credentials: "include" }));
+ { credentials: "include" });
}, 'createWorklet() with cross-origin module script and credentials ' +
'"include", and without the Access-Control-Allow-Credentials response ' +
'header');
diff --git a/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-failure-missing-access-control-allow-origin.tentative.https.sub.html b/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-access-control-allow-origin.tentative.https.sub.html
index 4195d09fc0..1f3223a564 100644
--- a/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-failure-missing-access-control-allow-origin.tentative.https.sub.html
+++ b/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-access-control-allow-origin.tentative.https.sub.html
@@ -15,12 +15,14 @@ promise_test(async t => {
const helper_url = crossOrigin +
`/shared-storage/resources/credentials-test-helper.py` +
`&access_control_allow_credentials_header=true` +
+ `&shared_storage_cross_origin_worklet_allowed_header=?1` +
`&token=${ancestor_key}`;
- return promise_rejects_dom(t, "OperationError",
- sharedStorage.createWorklet(
+ // The network error for `createWorklet()` won't be revealed to the
+ // cross-origin caller.
+ await sharedStorage.createWorklet(
helper_url + `&action=store-cookie`,
- { credentials: "include" }));
+ { credentials: "include" });
}, 'createWorklet() with cross-origin module script and credentials ' +
'"include", and without the Access-Control-Allow-Origin response header');
diff --git a/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html b/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html
new file mode 100644
index 0000000000..f96e4d596e
--- /dev/null
+++ b/testing/web-platform/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/shared-storage/resources/util.js"></script>
+<script src="/fenced-frame/resources/utils.js"></script>
+
+<body>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const ancestor_key = token();
+ const crossOrigin = 'https://{{domains[www]}}:{{ports[https][0]}}';
+ const helper_url = crossOrigin +
+ `/shared-storage/resources/credentials-test-helper.py` +
+ `?access_control_allow_origin_header=${window.origin}` +
+ `&access_control_allow_credentials_header=true` +
+ `&token=${ancestor_key}`;
+
+ // The network error for `createWorklet()`` won't be revealed to the
+ // cross-origin caller.
+ await sharedStorage.createWorklet(
+ helper_url + `&action=store-cookie`,
+ { credentials: "include" });
+}, 'createWorklet() with cross-origin module script and credentials ' +
+ '"include", and without the Shared-Storage-Cross-Origin-Worklet-Allowed ' +
+ 'response header');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/shared-storage/cross-origin-worklet-select-url-and-verify-data-origin.tentative.https.sub.html b/testing/web-platform/tests/shared-storage/cross-origin-worklet-select-url-and-verify-data-origin.tentative.https.sub.html
new file mode 100644
index 0000000000..5b6b9d5f8f
--- /dev/null
+++ b/testing/web-platform/tests/shared-storage/cross-origin-worklet-select-url-and-verify-data-origin.tentative.https.sub.html
@@ -0,0 +1,46 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/shared-storage/resources/util.js"></script>
+<script src="/fenced-frame/resources/utils.js"></script>
+
+<body>
+<script>
+'use strict';
+
+promise_test(async () => {
+ const crossOrigin = 'https://{{domains[www]}}:{{ports[https][0]}}';
+ const script_url = crossOrigin +
+ `/shared-storage/resources/simple-module.js`;
+
+ const worklet = await sharedStorage.createWorklet(
+ script_url,
+ { credentials: "omit" });
+
+ const ancestor_key = token();
+ let url0 = generateURL("/shared-storage/resources/frame0.html",
+ [ancestor_key]);
+
+ let select_url_result = await worklet.selectURL(
+ "test-url-selection-operation",
+ [{ url: url0 }], {
+ data: {
+ 'mockResult': 0,
+ 'setKey': 'key0',
+ 'setValue': 'value0'
+ },
+ resolveToConfig: true,
+ keepAlive: true
+ });
+
+ assert_true(validateSelectURLResult(select_url_result, true));
+ attachFencedFrame(select_url_result, 'opaque-ads');
+ const result0 = await nextValueFromServer(ancestor_key);
+ assert_equals(result0, "frame0_loaded");
+
+ await verifyKeyValueForOrigin('key0', 'value0', crossOrigin);
+}, 'For a cross-origin worklet, test selectURL() and verify its data origin');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/shared-storage/resources/credentials-test-helper.py b/testing/web-platform/tests/shared-storage/resources/credentials-test-helper.py
index 46fc0ea6fb..575e504e64 100644
--- a/testing/web-platform/tests/shared-storage/resources/credentials-test-helper.py
+++ b/testing/web-platform/tests/shared-storage/resources/credentials-test-helper.py
@@ -19,6 +19,9 @@ def main(request, response):
if b"access_control_allow_origin_header" in request.GET:
response.headers.append(b"Access-Control-Allow-Origin", request.GET[b"access_control_allow_origin_header"])
+ if b"shared_storage_cross_origin_worklet_allowed_header" in request.GET:
+ response.headers.append(b"Shared-Storage-Cross-Origin-Worklet-Allowed", request.GET[b"shared_storage_cross_origin_worklet_allowed_header"])
+
if action == b"store-cookie":
cookie = request.headers.get(b"Cookie", b"NO_COOKIE_HEADER")
request.server.stash.put(token, cookie)
diff --git a/testing/web-platform/tests/shared-storage/resources/simple-module.js b/testing/web-platform/tests/shared-storage/resources/simple-module.js
index 620a3592f2..11b650811d 100644
--- a/testing/web-platform/tests/shared-storage/resources/simple-module.js
+++ b/testing/web-platform/tests/shared-storage/resources/simple-module.js
@@ -6,6 +6,10 @@ var globalVar = 0;
class TestURLSelectionOperation {
async run(urls, data) {
+ if (data && data.hasOwnProperty('setKey') && data.hasOwnProperty('setValue')) {
+ await sharedStorage.set(data['setKey'], data['setValue']);
+ }
+
if (data && data.hasOwnProperty('mockResult')) {
return data['mockResult'];
}
diff --git a/testing/web-platform/tests/shared-storage/resources/simple-module.js.headers b/testing/web-platform/tests/shared-storage/resources/simple-module.js.headers
new file mode 100644
index 0000000000..cf3e03e24c
--- /dev/null
+++ b/testing/web-platform/tests/shared-storage/resources/simple-module.js.headers
@@ -0,0 +1,2 @@
+Access-Control-Allow-Origin: *
+Shared-Storage-Cross-Origin-Worklet-Allowed: ?1
diff --git a/testing/web-platform/tests/speech-api/WEB_FEATURES.yml b/testing/web-platform/tests/speech-api/WEB_FEATURES.yml
new file mode 100644
index 0000000000..7a24460e1b
--- /dev/null
+++ b/testing/web-platform/tests/speech-api/WEB_FEATURES.yml
@@ -0,0 +1,7 @@
+features:
+- name: speech-recognition
+ files:
+ - SpeechRecognition*
+- name: speech-synthesis
+ files:
+ - SpeechSynthesis*
diff --git a/testing/web-platform/tests/storage-access-api/helpers.js b/testing/web-platform/tests/storage-access-api/helpers.js
index 0fd5d814db..416c4a401e 100644
--- a/testing/web-platform/tests/storage-access-api/helpers.js
+++ b/testing/web-platform/tests/storage-access-api/helpers.js
@@ -287,6 +287,13 @@ async function MaybeSetStorageAccess(origin, embedding_origin, value) {
}
}
+
+// Navigate the inner iframe using the given frame.
+function NavigateChild(frame, url) {
+ return PostMessageAndAwaitReply(
+ { command: "navigate_child", url }, frame.contentWindow);
+}
+
// Starts a dedicated worker in the given frame.
function StartDedicatedWorker(frame) {
return PostMessageAndAwaitReply(
diff --git a/testing/web-platform/tests/storage-access-api/requestStorageAccess-cross-origin-iframe-navigation-relax.tentative.sub.https.window.js b/testing/web-platform/tests/storage-access-api/requestStorageAccess-cross-origin-iframe-navigation-relax.tentative.sub.https.window.js
new file mode 100644
index 0000000000..7ccb9824e6
--- /dev/null
+++ b/testing/web-platform/tests/storage-access-api/requestStorageAccess-cross-origin-iframe-navigation-relax.tentative.sub.https.window.js
@@ -0,0 +1,65 @@
+// META: script=helpers.js
+// META: script=/cookies/resources/cookie-helper.sub.js
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+'use strict';
+
+(async function() {
+ // This is cross-domain from the current document.
+ const altWww = "https://{{hosts[alt][www]}}:{{ports[https][0]}}";
+ const altRoot = "https://{{hosts[alt][]}}:{{ports[https][0]}}";
+ const responderPath = "/storage-access-api/resources/script-with-cookie-header.py?script=embedded_responder.js";
+ const forwarderPath = "/storage-access-api/resources/script-with-cookie-header.py?script=embedded_forwarder.js";
+
+ const altWwwResponder = `${altWww}${responderPath}`;
+ const altRootResponder = `${altRoot}${responderPath}`;
+ const altWwwNestedCrossOriginResponder = `${altRoot}${forwarderPath}&inner_url=${encodeURI(altWwwResponder)}`;
+
+ async function SetUpResponderFrame(t, url) {
+ const frame = await CreateFrame(url);
+
+ await SetPermissionInFrame(frame, [{ name: 'storage-access' }, 'granted']);
+ t.add_cleanup(async () => {
+ await test_driver.delete_all_cookies();
+ await SetPermissionInFrame(frame, [{ name: 'storage-access' }, 'prompt']);
+ await MaybeSetStorageAccess("*", "*", "allowed");
+ });
+
+ assert_false(await FrameHasStorageAccess(frame), "frame initially does not have storage access.");
+ assert_false(await HasUnpartitionedCookie(frame), "frame initially does not have access to cookies.");
+
+ assert_true(await RequestStorageAccessInFrame(frame), "requestStorageAccess resolves without requiring a gesture.");
+
+ assert_true(await FrameHasStorageAccess(frame), "frame has storage access after request.");
+ assert_true(await HasUnpartitionedCookie(frame), "frame has access to cookies after request.");
+
+ return frame;
+ }
+
+ promise_test(async (t) => {
+ await MaybeSetStorageAccess("*", "*", "blocked");
+ await SetFirstPartyCookieAndUnsetStorageAccessPermission(altWww);
+
+ const frame = await SetUpResponderFrame(t, altWwwNestedCrossOriginResponder);
+
+ await NavigateChild(frame, altWwwResponder);
+
+ assert_true(await FrameHasStorageAccess(frame), "innermost frame has storage access after refresh.");
+ assert_true(await HasUnpartitionedCookie(frame), "innermost frame has access to cookies after refresh.");
+ }, "Same-site-initiated same-origin navigations preserve storage access");
+
+ promise_test(async (t) => {
+ await MaybeSetStorageAccess("*", "*", "blocked");
+ await SetFirstPartyCookieAndUnsetStorageAccessPermission(altWww);
+
+ const frame = await SetUpResponderFrame(t, altWwwNestedCrossOriginResponder);
+
+ await NavigateChild(frame, altRootResponder);
+
+ assert_false(await FrameHasStorageAccess(frame), "innermost frame has no storage access after refresh.");
+ assert_false(await HasUnpartitionedCookie(frame), "innermost frame has no access to cookies after refresh.");
+ let cookieOnLoad = await GetHTTPCookiesFromFrame(frame);
+ assert_false(cookieStringHasCookie("cookie", "unpartitioned", cookieOnLoad), "innermost frame has cookie in initial load");
+ }, "Same-site-initiated cross-origin navigations do not preserve storage access");
+
+})();
diff --git a/testing/web-platform/tests/storage-access-api/requestStorageAccess-dedicated-worker.tentative.sub.https.window.js b/testing/web-platform/tests/storage-access-api/requestStorageAccess-dedicated-worker.tentative.sub.https.window.js
index 6c3d616e26..5c3089bf34 100644
--- a/testing/web-platform/tests/storage-access-api/requestStorageAccess-dedicated-worker.tentative.sub.https.window.js
+++ b/testing/web-platform/tests/storage-access-api/requestStorageAccess-dedicated-worker.tentative.sub.https.window.js
@@ -40,10 +40,10 @@
assert_true(cookieStringHasCookie("cookie", "unpartitioned",
await MessageWorker(frame, {command: "load"})),
"Worker's load was credentialed.");
- assert_true(cookieStringHasCookie("cookie", "unpartitioned",
+ assert_false(cookieStringHasCookie("cookie", "unpartitioned",
await MessageWorker(frame, {command: "fetch", url: altRootEchoCookies})),
- "Worker's fetch is credentialed.");
- }, "Workers inherit storage access");
+ "Worker's fetch is uncredentialed.");
+ }, "Workers don't inherit storage access");
promise_test(async (t) => {
await MaybeSetStorageAccess("*", "*", "blocked");
diff --git a/testing/web-platform/tests/storage-access-api/resources/embedded_forwarder.js b/testing/web-platform/tests/storage-access-api/resources/embedded_forwarder.js
new file mode 100644
index 0000000000..c1e5e54443
--- /dev/null
+++ b/testing/web-platform/tests/storage-access-api/resources/embedded_forwarder.js
@@ -0,0 +1,50 @@
+"use strict";
+
+test_driver.set_test_context(window.top);
+
+function waitForMessage(timestamp) {
+ return new Promise(resolve => {
+ const listener = (event) => {
+ if (!timestamp || event.data.timestamp == timestamp) {
+ window.removeEventListener("message", listener);
+ resolve(event.data);
+ }
+ };
+ window.addEventListener("message", listener);
+ });
+}
+
+var iframe = document.createElement('iframe');
+const queryString = window.location.search;
+const urlParams = new URLSearchParams(queryString);
+iframe.src = urlParams.get("inner_url");
+document.body.appendChild(iframe);
+
+window.addEventListener("message", async (event) => {
+ function replyToParent(data) {
+ parent.postMessage(
+ {timestamp: event.data.timestamp, data}, "*");
+ }
+
+ if (!event.data["command"]) {
+ return;
+ }
+
+ switch (event.data["command"]) {
+ case "navigate_child":
+ iframe.onload = () => replyToParent(event.data.url);
+ iframe.src = event.data.url;
+ break;
+ case "reload":
+ case "navigate":
+ iframe.contentWindow.postMessage({timestamp, ...event.data}, "*");
+ break;
+ default:{
+ const timestamp = event.data.timestamp;
+ const p = waitForMessage(timestamp);
+ iframe.contentWindow.postMessage({timestamp, ...event.data}, "*");
+ replyToParent(await p.then(resp => resp.data));
+ break;
+ }
+ }
+});
diff --git a/testing/web-platform/tests/storage-access-api/resources/embedded_responder.js b/testing/web-platform/tests/storage-access-api/resources/embedded_responder.js
index 228a262f16..3cca86154a 100644
--- a/testing/web-platform/tests/storage-access-api/resources/embedded_responder.js
+++ b/testing/web-platform/tests/storage-access-api/resources/embedded_responder.js
@@ -31,7 +31,7 @@ function connectAndGetRequestCookiesFrom(origin) {
window.addEventListener("message", async (event) => {
function reply(data) {
event.source.postMessage(
- {timestamp: event.data.timestamp, data}, event.origin);
+ {timestamp: event.data.timestamp, data}, "*");
}
switch (event.data["command"]) {
diff --git a/testing/web-platform/tests/storage-access-api/resources/script-with-cookie-header.py b/testing/web-platform/tests/storage-access-api/resources/script-with-cookie-header.py
index 83129a5559..ae95b7593f 100644
--- a/testing/web-platform/tests/storage-access-api/resources/script-with-cookie-header.py
+++ b/testing/web-platform/tests/storage-access-api/resources/script-with-cookie-header.py
@@ -13,7 +13,10 @@ def main(request, response):
var httpCookies = "%s";
</script>
+ <body>
<script src="%s"></script>
+ </body>
+
""" % (cookie_header, script)
return (200, [], body)
diff --git a/testing/web-platform/tests/storage/buckets/detached-iframe.https.html b/testing/web-platform/tests/storage/buckets/detached-iframe.https.html
index a67c89efa3..6b3f3a2bd5 100644
--- a/testing/web-platform/tests/storage/buckets/detached-iframe.https.html
+++ b/testing/web-platform/tests/storage/buckets/detached-iframe.https.html
@@ -23,10 +23,12 @@ promise_test(async testCase => {
// too late to delete buckets.
await bucketManager.delete('iframe-bucket');
+ const IFrameTypeError = iframe.contentWindow.TypeError;
iframe.remove();
// Calling open() from a detached iframe should fail but not crash.
- assert_equals(bucketManager.open('iframe-bucket'), undefined);
+ await promise_rejects_js(testCase, IFrameTypeError,
+ bucketManager.open('iframe-bucket'));
}, 'Verify open() on detached iframe returns an error');
promise_test(async testCase => {
@@ -39,10 +41,12 @@ promise_test(async testCase => {
assert_equals(bucketKeys.length, 1);
await bucketManager.delete('iframe-bucket');
+ const IFrameTypeError = iframe.contentWindow.TypeError;
iframe.remove();
// Calling keys() from a detached iframe should fail but not crash.
- assert_equals(bucketManager.keys(), undefined);
+ await promise_rejects_js(testCase, IFrameTypeError,
+ bucketManager.keys());
}, 'Verify keys() on detached iframe returns an error');
promise_test(async testCase => {
@@ -52,10 +56,12 @@ promise_test(async testCase => {
await bucketManager.open('iframe-bucket');
await bucketManager.delete('iframe-bucket');
+ const IFrameTypeError = iframe.contentWindow.TypeError;
iframe.remove();
// Calling delete() from a detached iframe should fail but not crash.
- assert_equals(bucketManager.delete('foo-bucket'), undefined);
+ await promise_rejects_js(testCase, IFrameTypeError,
+ bucketManager.delete('foo-bucket'));
}, 'Verify delete() on detached iframe returns an error');
</script>
diff --git a/testing/web-platform/tests/streams/readable-streams/async-iterator.any.js b/testing/web-platform/tests/streams/readable-streams/async-iterator.any.js
index 4b674bea84..e192201b53 100644
--- a/testing/web-platform/tests/streams/readable-streams/async-iterator.any.js
+++ b/testing/web-platform/tests/streams/readable-streams/async-iterator.any.js
@@ -475,16 +475,86 @@ promise_test(async () => {
const rs = new ReadableStream();
const it = rs.values();
- const iterResults = await Promise.allSettled([it.return('return value'), it.next()]);
+ const resolveOrder = [];
+ const iterResults = await Promise.allSettled([
+ it.return('return value').then(result => {
+ resolveOrder.push('return');
+ return result;
+ }),
+ it.next().then(result => {
+ resolveOrder.push('next');
+ return result;
+ })
+ ]);
assert_equals(iterResults[0].status, 'fulfilled', 'return() promise status');
assert_iter_result(iterResults[0].value, 'return value', true, 'return()');
assert_equals(iterResults[1].status, 'fulfilled', 'next() promise status');
assert_iter_result(iterResults[1].value, undefined, true, 'next()');
+
+ assert_array_equals(resolveOrder, ['return', 'next'], 'next() resolves after return()');
}, 'return(); next() [no awaiting]');
promise_test(async () => {
+ let resolveCancelPromise;
+ const rs = recordingReadableStream({
+ cancel(reason) {
+ return new Promise(r => resolveCancelPromise = r);
+ }
+ });
+ const it = rs.values();
+
+ let returnResolved = false;
+ const returnPromise = it.return('return value').then(result => {
+ returnResolved = true;
+ return result;
+ });
+ await flushAsyncEvents();
+ assert_false(returnResolved, 'return() should not resolve while cancel() promise is pending');
+
+ resolveCancelPromise();
+ const iterResult1 = await returnPromise;
+ assert_iter_result(iterResult1, 'return value', true, 'return()');
+
+ const iterResult2 = await it.next();
+ assert_iter_result(iterResult2, undefined, true, 'next()');
+}, 'return(); next() with delayed cancel()');
+
+promise_test(async () => {
+ let resolveCancelPromise;
+ const rs = recordingReadableStream({
+ cancel(reason) {
+ return new Promise(r => resolveCancelPromise = r);
+ }
+ });
+ const it = rs.values();
+
+ const resolveOrder = [];
+ const returnPromise = it.return('return value').then(result => {
+ resolveOrder.push('return');
+ return result;
+ });
+ const nextPromise = it.next().then(result => {
+ resolveOrder.push('next');
+ return result;
+ });
+
+ assert_array_equals(rs.events, ['cancel', 'return value'], 'return() should call cancel()');
+ assert_array_equals(resolveOrder, [], 'return() should not resolve before cancel() resolves');
+
+ resolveCancelPromise();
+ const iterResult1 = await returnPromise;
+ assert_iter_result(iterResult1, 'return value', true, 'return() should resolve with original reason');
+ const iterResult2 = await nextPromise;
+ assert_iter_result(iterResult2, undefined, true, 'next() should resolve with done result');
+
+ assert_array_equals(rs.events, ['cancel', 'return value'], 'no pull() after cancel()');
+ assert_array_equals(resolveOrder, ['return', 'next'], 'next() should resolve after return() resolves');
+
+}, 'return(); next() with delayed cancel() [no awaiting]');
+
+promise_test(async () => {
const rs = new ReadableStream();
const it = rs.values();
@@ -499,13 +569,25 @@ promise_test(async () => {
const rs = new ReadableStream();
const it = rs.values();
- const iterResults = await Promise.allSettled([it.return('return value 1'), it.return('return value 2')]);
+ const resolveOrder = [];
+ const iterResults = await Promise.allSettled([
+ it.return('return value 1').then(result => {
+ resolveOrder.push('return 1');
+ return result;
+ }),
+ it.return('return value 2').then(result => {
+ resolveOrder.push('return 2');
+ return result;
+ })
+ ]);
assert_equals(iterResults[0].status, 'fulfilled', '1st return() promise status');
assert_iter_result(iterResults[0].value, 'return value 1', true, '1st return()');
assert_equals(iterResults[1].status, 'fulfilled', '2nd return() promise status');
assert_iter_result(iterResults[1].value, 'return value 2', true, '1st return()');
+
+ assert_array_equals(resolveOrder, ['return 1', 'return 2'], '2nd return() resolves after 1st return()');
}, 'return(); return() [no awaiting]');
test(() => {
diff --git a/testing/web-platform/tests/streams/readable-streams/tee-detached-context-crash.html b/testing/web-platform/tests/streams/readable-streams/tee-detached-context-crash.html
new file mode 100644
index 0000000000..9488da7273
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/tee-detached-context-crash.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<body>
+<script>
+const i = document.createElement("iframe");
+document.body.appendChild(i);
+
+const rs = new i.contentWindow.ReadableStream();
+i.remove();
+
+// tee() on a ReadableStream from a detached iframe should not crash.
+rs.tee();
+</script>
+</body>
diff --git a/testing/web-platform/tests/svg/crashtests/chrome-bug-333487749.html b/testing/web-platform/tests/svg/crashtests/chrome-bug-333487749.html
new file mode 100644
index 0000000000..1613f4998a
--- /dev/null
+++ b/testing/web-platform/tests/svg/crashtests/chrome-bug-333487749.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<body style="mask: url(#marker)">
+ <svg>
+ <marker id="marker">
+ <rect width="10" height="10" fill="yellow"/>
+ </marker>
+ <path d="M50,50h100" marker-start="url(#marker1)"/>
+ </svg>
+</body>
+<script>
+ document.documentElement.offsetTop;
+ document.documentElement.style.display = 'none';
+ document.documentElement.offsetTop;
+</script>
diff --git a/testing/web-platform/tests/svg/layout/svg-embed-intrinsic-size-min-size.html b/testing/web-platform/tests/svg/layout/svg-embed-intrinsic-size-min-size.html
new file mode 100644
index 0000000000..79dfb07b42
--- /dev/null
+++ b/testing/web-platform/tests/svg/layout/svg-embed-intrinsic-size-min-size.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="author" href="https://mozilla.org" title="Mozilla">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1894363">
+<link rel="match" href="svg-intrinsic-size-min-size-ref.html">
+<style>
+ embed {
+ width: auto;
+ height: auto;
+ vertical-align: top;
+ }
+</style>
+<div style="width: min-content; background-color: green">
+ <embed src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='56' height='56' viewBox='0 0 56 56'></svg>">
+</div>
diff --git a/testing/web-platform/tests/svg/layout/svg-intrinsic-size-min-size-ref.html b/testing/web-platform/tests/svg/layout/svg-intrinsic-size-min-size-ref.html
new file mode 100644
index 0000000000..f9ae2910d8
--- /dev/null
+++ b/testing/web-platform/tests/svg/layout/svg-intrinsic-size-min-size-ref.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<div style="width: 56px; height: 56px; background-color: green"></div>
diff --git a/testing/web-platform/tests/svg/layout/svg-intrinsic-size-min-size.html b/testing/web-platform/tests/svg/layout/svg-intrinsic-size-min-size.html
new file mode 100644
index 0000000000..41c68e56e1
--- /dev/null
+++ b/testing/web-platform/tests/svg/layout/svg-intrinsic-size-min-size.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="author" href="https://mozilla.org" title="Mozilla">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1894363">
+<link rel="match" href="svg-intrinsic-size-min-size-ref.html">
+<style>
+ svg {
+ width: auto;
+ height: auto;
+ vertical-align: top;
+ }
+</style>
+<div style="width: min-content; background-color: green">
+ <svg width="56" height="56" viewBox="0 0 56 56"></svg>
+</div>
diff --git a/testing/web-platform/tests/svg/painting/reftests/green-100x100.svg b/testing/web-platform/tests/svg/painting/reftests/green-100x100.svg
new file mode 100644
index 0000000000..120941444a
--- /dev/null
+++ b/testing/web-platform/tests/svg/painting/reftests/green-100x100.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+ <rect width="100" height="100" fill="green"/>
+</svg>
diff --git a/testing/web-platform/tests/svg/painting/reftests/non-scaling-stroke-001.html b/testing/web-platform/tests/svg/painting/reftests/non-scaling-stroke-001.html
new file mode 100644
index 0000000000..a9870ac8bb
--- /dev/null
+++ b/testing/web-platform/tests/svg/painting/reftests/non-scaling-stroke-001.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<title>non-scaling-stroke with scaling</title>
+<link rel="help" href="https://svgwg.org/svg2-draft/painting.html#PaintingVectorEffects" />
+<link rel="match" href="green-100x100.svg" />
+<body>
+ <style>
+ body {
+ border: none;
+ margin: 0;
+ width: 200px;
+ height: 200px;
+ transform: scale(2);
+ }
+ svg {
+ width: 100px;
+ height: 100px;
+ }
+ rect {
+ fill: red;
+ stroke: green;
+ stroke-width: 50px;
+ vector-effect: non-scaling-stroke;
+ }
+ </style>
+ <svg>
+ <rect width="75" height="75"/>
+ </svg>
+</body>
+</html>
diff --git a/testing/web-platform/tests/svg/painting/reftests/paint-context-007-ref.svg b/testing/web-platform/tests/svg/painting/reftests/paint-context-007-ref.svg
new file mode 100644
index 0000000000..0328272c6a
--- /dev/null
+++ b/testing/web-platform/tests/svg/painting/reftests/paint-context-007-ref.svg
@@ -0,0 +1,29 @@
+<svg id="svg-root"
+ width="100%" height="100%" viewBox="0 0 400 300"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+ <g id="testmeta">
+ <title>Paint: 'context-fill' and 'context-stroke' in 'use' with transform</title>
+ <html:link rel="author"
+ title="Stefan Zager"
+ href="mailto:szager@chromium.org"/>
+ </g>
+
+ <defs>
+ <linearGradient id="lg1" x2="125%">
+ <stop offset="0" stop-color="red"/>
+ <stop offset="1" stop-color="blue"/>
+ </linearGradient>
+
+ <linearGradient id="lg2" x1="-25%">
+ <stop offset="0" stop-color="red"/>
+ <stop offset="1" stop-color="blue"/>
+ </linearGradient>
+ </defs>
+
+ <g id="test-reference">
+ <rect x="50" y="90" width="240" height="120" fill="url(#lg1)"/>
+ <path d="M 110 30 l 240 120 l -240 120 Z" fill="url(#lg2)" />
+ </g>
+</svg>
diff --git a/testing/web-platform/tests/svg/painting/reftests/paint-context-007.svg b/testing/web-platform/tests/svg/painting/reftests/paint-context-007.svg
new file mode 100644
index 0000000000..50360a302e
--- /dev/null
+++ b/testing/web-platform/tests/svg/painting/reftests/paint-context-007.svg
@@ -0,0 +1,32 @@
+<svg id="svg-root"
+ width="100%" height="100%" viewBox="0 0 400 300"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+ <g id="testmeta">
+ <title>Paint: 'context-fill' and 'context-stroke' in 'use' with transform</title>
+ <html:link rel="author"
+ title="Stefan Zager"
+ href="mailto:szager@chromium.org"/>
+ <html:link rel="help"
+ href="https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint"/>
+ <html:link rel="match" href="paint-context-007-ref.svg" />
+ </g>
+
+ <defs>
+ <linearGradient id="lg">
+ <stop offset="0" stop-color="red"/>
+ <stop offset="1" stop-color="blue"/>
+ </linearGradient>
+
+ <g id="shapes">
+ <rect x="50" y="90" width="240" height="120" fill="context-fill"/>
+ <path d="M 170 -30 l -120 240 l 240 0 Z" fill="context-fill"
+ transform-origin="170 150" transform="rotate(90)"/>
+ </g>
+ </defs>
+
+ <g id="test-body-content">
+ <use x="0" y="0" fill="url(#lg)" xlink:href="#shapes"/>
+ </g>
+</svg>
diff --git a/testing/web-platform/tests/svg/painting/reftests/paint-context-008-ref.svg b/testing/web-platform/tests/svg/painting/reftests/paint-context-008-ref.svg
new file mode 100644
index 0000000000..49d4f8947b
--- /dev/null
+++ b/testing/web-platform/tests/svg/painting/reftests/paint-context-008-ref.svg
@@ -0,0 +1,22 @@
+<svg id="svg-root"
+ width="100%" height="100%" viewBox="0 0 400 300"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+ <g id="testmeta">
+ <title>Paint: paint server transform for 'context-fill' based on nearest 'use' ancestor</title>
+ <html:link rel="author"
+ title="Stefan Zager"
+ href="mailto:szager@chromium.org"/>
+ </g>
+
+ <defs>
+ <pattern id="grid" x="0" y="0" width="0.125" height="0.25" stroke="blue" stroke-width="0.03125" patternContentUnits="objectBoundingBox">
+ <path fill="none" d="M 0.0625 0 l 0.0625 0.125 l -0.0625 0.125 l -0.0625 -0.125 Z"/>
+ </pattern>
+ </defs>
+
+ <g id="test-reference">
+ <rect x="69" y="113" width="256" height="128" fill="url(#grid)"/>
+ </g>
+</svg>
diff --git a/testing/web-platform/tests/svg/painting/reftests/paint-context-008.svg b/testing/web-platform/tests/svg/painting/reftests/paint-context-008.svg
new file mode 100644
index 0000000000..83308f09b1
--- /dev/null
+++ b/testing/web-platform/tests/svg/painting/reftests/paint-context-008.svg
@@ -0,0 +1,33 @@
+<svg id="svg-root"
+ width="100%" height="100%" viewBox="0 0 400 300"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+ <g id="testmeta">
+ <title>Paint: paint server transform for 'context-fill' based on nearest 'use' ancestor</title>
+ <html:link rel="author"
+ title="Stefan Zager"
+ href="mailto:szager@chromium.org"/>
+ <html:link rel="help"
+ href="https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint"/>
+ <html:link rel="match" href="paint-context-008-ref.svg" />
+ </g>
+
+ <defs>
+ <pattern id="grid" x="0" y="0" width="0.125" height="0.25" stroke="blue" stroke-width="0.03125" patternContentUnits="objectBoundingBox">
+ <path fill="none" d="M 0.0625 0 l 0.0625 0.125 l -0.0625 0.125 l -0.0625 -0.125 Z"/>
+ </pattern>
+
+ <g id="shapes">
+ <rect x="50" y="90" width="256" height="128" fill="context-fill"/>
+ </g>
+
+ <g id="intermediate">
+ <use x="19" y="23" fill="url(#grid)" xlink:href="#shapes"/>
+ </g>
+ </defs>
+
+ <g id="test-body-content">
+ <use xlink:href="#intermediate"/>
+ </g>
+</svg>
diff --git a/testing/web-platform/tests/svg/painting/scripted/marker-element-added.html b/testing/web-platform/tests/svg/painting/scripted/marker-element-added.html
new file mode 100644
index 0000000000..39d3cb190c
--- /dev/null
+++ b/testing/web-platform/tests/svg/painting/scripted/marker-element-added.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<html class="reftest-wait">
+<title>&lt;marker> element added after first paint</title>
+<script src="/common/reftest-wait.js"></script>
+<script src="/common/rendering-utils.js"></script>
+<link rel="match" href="../../struct/reftests/reference/green-100x100.html">
+<svg>
+ <path d="M0,0h100" marker-start="url(#m)"/>
+</svg>
+<script>
+ function createSvgElement(name, attrData) {
+ const svgNs = 'http://www.w3.org/2000/svg';
+ const element = document.createElementNS(svgNs, name);
+ attrData.forEach(([name, value]) => element.setAttribute(name, value));
+ return element;
+ }
+ waitForAtLeastOneFrame().then(() => {
+ const svgNs = 'http://www.w3.org/2000/svg';
+ const markerElement = createSvgElement('marker', [
+ ['id', 'm'], ['orient', '0'], ['overflow', 'visible']
+ ]);
+ markerElement.appendChild(createSvgElement('rect', [
+ ['width', '100'], ['height', '100'], ['fill', 'green']
+ ]));
+ const svg = document.querySelector('svg');
+ svg.insertBefore(markerElement, svg.firstElementChild);
+ takeScreenshot();
+ });
+</script>
diff --git a/testing/web-platform/tests/svg/path/property/serialization.svg b/testing/web-platform/tests/svg/path/property/serialization.svg
index 3199beb92b..2ad336d5f7 100644
--- a/testing/web-platform/tests/svg/path/property/serialization.svg
+++ b/testing/web-platform/tests/svg/path/property/serialization.svg
@@ -17,7 +17,7 @@
let test2 = 'path("M 0 0 L 100 100 m 0 100 l 100 0 Z l 160 20 Z")';
test_valid_value('d', test2);
- test_computed_value('d', test2);
+ test_computed_value('d', test2, 'path("M 0 0 L 100 100 M 100 200 L 200 200 Z L 260 220 Z")');
let test3 = 'path("m 10 20 l 20 30 Z l 50 60 Z m 70 80 l 90 60 Z t 70 120")';
test_valid_value('d', test3, 'path("m 10 20 l 20 30 Z l 50 60 Z m 70 80 l 90 60 Z t 70 120")');
diff --git a/testing/web-platform/tests/svg/types/scripted/SVGLength-rlh.html b/testing/web-platform/tests/svg/types/scripted/SVGLength-rlh.html
new file mode 100644
index 0000000000..411013b65b
--- /dev/null
+++ b/testing/web-platform/tests/svg/types/scripted/SVGLength-rlh.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<title>SVGLength with 'rlh' unit</title>
+<link rel="help" href="https://www.w3.org/TR/SVG/types.html#InterfaceSVGLength">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+:root {
+ font-family: initial;
+ font-size: 20px;
+}
+</style>
+<div style="font-size: 10px">
+ <div id="rlh_ref" style="width:10rlh"></div>
+ <svg>
+ <text id="rlh_test" x="10rlh"></text>
+ </svg>
+</div>
+<script>
+ let ref_width = rlh_ref.offsetWidth;
+ let rlh_length = rlh_test.x.baseVal[0];
+
+ test(() => {
+ assert_equals(rlh_length.unitType, SVGLength.SVG_LENGTHTYPE_UNKNOWN);
+ assert_equals(rlh_length.value, ref_width);
+ }, "rlh unit in SVGLength");
+
+ test(() => {
+ rlh_length.value = ref_width * 2;
+ assert_equals(rlh_length.valueInSpecifiedUnits, 20);
+ }, "Convert back to rlh from new user unit value");
+</script>
diff --git a/testing/web-platform/tests/tools/ci/azure/install_chrome.yml b/testing/web-platform/tests/tools/ci/azure/install_chrome.yml
index 7599321be2..2dde99286c 100644
--- a/testing/web-platform/tests/tools/ci/azure/install_chrome.yml
+++ b/testing/web-platform/tests/tools/ci/azure/install_chrome.yml
@@ -1,11 +1,11 @@
steps:
# The conflicting google-chrome and chromedriver casks are first uninstalled.
-# The raw google-chrome-dev cask URL is used to bypass caching.
+# The raw google-chrome@dev.rb cask URL is used to bypass caching.
- script: |
set -eux -o pipefail
HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --cask google-chrome || true
HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --cask chromedriver || true
- curl https://raw.githubusercontent.com/Homebrew/homebrew-cask-versions/master/Casks/google-chrome-dev.rb > google-chrome-dev.rb
- HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask google-chrome-dev.rb
+ curl https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/g/google-chrome@dev.rb > google-chrome@dev.rb
+ HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask google-chrome@dev.rb
displayName: 'Install Chrome Dev'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin'))
diff --git a/testing/web-platform/tests/tools/ci/azure/install_firefox.yml b/testing/web-platform/tests/tools/ci/azure/install_firefox.yml
index 73af597665..d43e28b274 100644
--- a/testing/web-platform/tests/tools/ci/azure/install_firefox.yml
+++ b/testing/web-platform/tests/tools/ci/azure/install_firefox.yml
@@ -1,9 +1,8 @@
steps:
-# This is equivalent to `Homebrew/homebrew-cask-versions/firefox-nightly`,
-# but the raw URL is used to bypass caching.
+# The raw firefox@nightly.rb cask URL is used to bypass caching.
- script: |
set -eux -o pipefail
- curl https://raw.githubusercontent.com/Homebrew/homebrew-cask-versions/master/Casks/firefox-nightly.rb > firefox-nightly.rb
- HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask firefox-nightly.rb
+ curl https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/f/firefox@nightly.rb > firefox@nightly.rb
+ HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask firefox@nightly.rb
displayName: 'Install Firefox Nightly'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin'))
diff --git a/testing/web-platform/tests/tools/ci/jobs.py b/testing/web-platform/tests/tools/ci/jobs.py
index 44de9fe1ad..fe8eaae069 100644
--- a/testing/web-platform/tests/tools/ci/jobs.py
+++ b/testing/web-platform/tests/tools/ci/jobs.py
@@ -23,7 +23,7 @@ EXCLUDES = [
]
# Rules are just regex on the path, with a leading ! indicating a regex that must not
-# match for the job. Paths should be kept in sync with update-built-tests.sh.
+# match for the job. Paths should be kept in sync with scripts in update_built.py.
job_path_map = {
"affected_tests": [".*/.*", "!resources/(?!idlharness.js)"] + EXCLUDES,
"stability": [".*/.*", "!resources/.*"] + EXCLUDES,
@@ -32,10 +32,11 @@ job_path_map = {
"resources_unittest": ["resources/", "tools/"],
"tools_unittest": ["tools/"],
"wptrunner_unittest": ["tools/"],
- "update_built": ["update-built-tests\\.sh",
- "conformance-checkers/",
+ "update_built": ["conformance-checkers/",
+ "css/css-images/",
"css/css-ui/",
"css/css-writing-modes/",
+ "fetch/metadata/",
"html/",
"infrastructure/",
"mimesniff/"],
diff --git a/testing/web-platform/tests/tools/ci/requirements_build.txt b/testing/web-platform/tests/tools/ci/requirements_build.txt
index 54f21efbd9..7b4f8619b2 100644
--- a/testing/web-platform/tests/tools/ci/requirements_build.txt
+++ b/testing/web-platform/tests/tools/ci/requirements_build.txt
@@ -1,5 +1,5 @@
-cairocffi==1.6.1
-fonttools==4.47.2
+cairocffi==1.7.0
+fonttools==4.51.0
genshi==0.7.7
jinja2==3.1.3
pyyaml==6.0.1
diff --git a/testing/web-platform/tests/tools/ci/requirements_macos_color_profile.txt b/testing/web-platform/tests/tools/ci/requirements_macos_color_profile.txt
index 7505a98d9f..8e178d1d2c 100644
--- a/testing/web-platform/tests/tools/ci/requirements_macos_color_profile.txt
+++ b/testing/web-platform/tests/tools/ci/requirements_macos_color_profile.txt
@@ -1,4 +1,4 @@
-pyobjc-core==9.2
+pyobjc-core==10.2
pyobjc-framework-Cocoa==9.2
pyobjc-framework-ColorSync==9.2
pyobjc-framework-Quartz==9.2
diff --git a/testing/web-platform/tests/tools/ci/requirements_tc.txt b/testing/web-platform/tests/tools/ci/requirements_tc.txt
index e1ae4dbf70..aa57643b9b 100644
--- a/testing/web-platform/tests/tools/ci/requirements_tc.txt
+++ b/testing/web-platform/tests/tools/ci/requirements_tc.txt
@@ -1,4 +1,4 @@
-pygithub==2.2.0
+pygithub==2.3.0
pyyaml==6.0.1
requests==2.31.0
-taskcluster==60.4.1
+taskcluster==64.2.7
diff --git a/testing/web-platform/tests/tools/ci/tc/tasks/test.yml b/testing/web-platform/tests/tools/ci/tc/tasks/test.yml
index c172e6b731..a9ca07c6ce 100644
--- a/testing/web-platform/tests/tools/ci/tc/tasks/test.yml
+++ b/testing/web-platform/tests/tools/ci/tc/tasks/test.yml
@@ -115,25 +115,24 @@ components:
chunks-override:
testharness: 24
- tox-python3_7:
+ tox-python3_8:
env:
- TOXENV: py37
+ TOXENV: py38
PY_COLORS: "0"
install:
- - python3.7
- - python3.7-distutils
- - python3.7-dev
- - python3.7-venv
+ - python3.8
+ - python3.8-distutils
+ - python3.8-dev
+ - python3.8-venv
- tox-python3_11:
+ tox-python3_12:
env:
- TOXENV: py311
+ TOXENV: py312
PY_COLORS: "0"
install:
- - python3.11
- - python3.11-distutils
- - python3.11-dev
- - python3.11-venv
+ - python3.12
+ - python3.12-dev
+ - python3.12-venv
tests-affected:
options:
browser:
@@ -438,13 +437,13 @@ tasks:
- update_built
command: "./tools/ci/ci_built_diff.sh"
- - tools/ unittests (Python 3.7):
+ - tools/ unittests (Python 3.8):
description: >-
- Unit tests for tools running under Python 3.7, excluding wptrunner
+ Unit tests for tools running under Python 3.8, excluding wptrunner
use:
- wpt-base
- trigger-pr
- - tox-python3_7
+ - tox-python3_8
command: ./tools/ci/ci_tools_unittest.sh
env:
HYPOTHESIS_PROFILE: ci
@@ -452,13 +451,13 @@ tasks:
run-job:
- tools_unittest
- - tools/ unittests (Python 3.11):
+ - tools/ unittests (Python 3.12):
description: >-
- Unit tests for tools running under Python 3.11, excluding wptrunner
+ Unit tests for tools running under Python 3.12, excluding wptrunner
use:
- wpt-base
- trigger-pr
- - tox-python3_11
+ - tox-python3_12
command: ./tools/ci/ci_tools_unittest.sh
env:
HYPOTHESIS_PROFILE: ci
@@ -466,13 +465,13 @@ tasks:
run-job:
- tools_unittest
- - tools/ integration tests (Python 3.7):
+ - tools/ integration tests (Python 3.8):
description: >-
- Integration tests for tools running under Python 3.7
+ Integration tests for tools running under Python 3.8
use:
- wpt-base
- trigger-pr
- - tox-python3_7
+ - tox-python3_8
command: ./tools/ci/ci_tools_integration_test.sh
install:
- libnss3-tools
@@ -488,13 +487,13 @@ tasks:
run-job:
- wpt_integration
- - tools/ integration tests (Python 3.11):
+ - tools/ integration tests (Python 3.12):
description: >-
- Integration tests for tools running under Python 3.11
+ Integration tests for tools running under Python 3.12
use:
- wpt-base
- trigger-pr
- - tox-python3_11
+ - tox-python3_12
command: ./tools/ci/ci_tools_integration_test.sh
install:
- libnss3-tools
@@ -510,13 +509,13 @@ tasks:
run-job:
- wpt_integration
- - resources/ tests (Python 3.7):
+ - resources/ tests (Python 3.8):
description: >-
- Tests for testharness.js and other files in resources/ under Python 3.7
+ Tests for testharness.js and other files in resources/ under Python 3.8
use:
- wpt-base
- trigger-pr
- - tox-python3_7
+ - tox-python3_8
command: ./tools/ci/ci_resources_unittest.sh
install:
- libnss3-tools
@@ -529,13 +528,13 @@ tasks:
run-job:
- resources_unittest
- - resources/ tests (Python 3.11):
+ - resources/ tests (Python 3.12):
description: >-
- Tests for testharness.js and other files in resources/ under Python 3.11
+ Tests for testharness.js and other files in resources/ under Python 3.12
use:
- wpt-base
- trigger-pr
- - tox-python3_11
+ - tox-python3_12
command: ./tools/ci/ci_resources_unittest.sh
install:
- libnss3-tools
diff --git a/testing/web-platform/tests/tools/ci/tc/tests/test_valid.py b/testing/web-platform/tests/tools/ci/tc/tests/test_valid.py
index 62bb09a1c3..dd8d732654 100644
--- a/testing/web-platform/tests/tools/ci/tc/tests/test_valid.py
+++ b/testing/web-platform/tests/tools/ci/tc/tests/test_valid.py
@@ -202,12 +202,12 @@ def test_verify_payload():
'lint']),
("pr_event.json", True, {".taskcluster.yml", ".travis.yml", "tools/ci/start.sh"},
['lint',
- 'tools/ unittests (Python 3.7)',
- 'tools/ unittests (Python 3.11)',
- 'tools/ integration tests (Python 3.7)',
- 'tools/ integration tests (Python 3.11)',
- 'resources/ tests (Python 3.7)',
- 'resources/ tests (Python 3.11)',
+ 'tools/ unittests (Python 3.8)',
+ 'tools/ unittests (Python 3.12)',
+ 'tools/ integration tests (Python 3.8)',
+ 'tools/ integration tests (Python 3.12)',
+ 'resources/ tests (Python 3.8)',
+ 'resources/ tests (Python 3.12)',
'download-firefox-nightly',
'infrastructure/ tests',
'sink-task']),
@@ -224,8 +224,8 @@ def test_verify_payload():
'sink-task']),
("pr_event_tests_affected.json", True, {"resources/testharness.js"},
['lint',
- 'resources/ tests (Python 3.7)',
- 'resources/ tests (Python 3.11)',
+ 'resources/ tests (Python 3.8)',
+ 'resources/ tests (Python 3.12)',
'download-firefox-nightly',
'infrastructure/ tests',
'sink-task']),
diff --git a/testing/web-platform/tests/tools/ci/update_built.py b/testing/web-platform/tests/tools/ci/update_built.py
index 8e0f18589d..929b09f9fe 100644
--- a/testing/web-platform/tests/tools/ci/update_built.py
+++ b/testing/web-platform/tests/tools/ci/update_built.py
@@ -9,6 +9,7 @@ logger = logging.getLogger()
wpt_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
+# These paths should be kept in sync with job_path_map in jobs.py.
scripts = {
"canvas": ["html/canvas/tools/gentest.py"],
"conformance-checkers": ["conformance-checkers/tools/dl.py",
diff --git a/testing/web-platform/tests/tools/manifest/item.py b/testing/web-platform/tests/tools/manifest/item.py
index 86f7bd6020..e25f7ca2c2 100644
--- a/testing/web-platform/tests/tools/manifest/item.py
+++ b/testing/web-platform/tests/tools/manifest/item.py
@@ -279,7 +279,7 @@ class PrintRefTest(RefTest):
@property
def page_ranges(self) -> PageRanges:
- return self._extras.get("page_ranges", {})
+ return cast(PageRanges, self._extras.get("page_ranges", {}))
def to_json(self): # type: ignore
rv = super().to_json()
diff --git a/testing/web-platform/tests/tools/manifest/requirements.txt b/testing/web-platform/tests/tools/manifest/requirements.txt
index d7c173723e..70ad0df0e9 100644
--- a/testing/web-platform/tests/tools/manifest/requirements.txt
+++ b/testing/web-platform/tests/tools/manifest/requirements.txt
@@ -1 +1 @@
-zstandard==0.21.0
+zstandard==0.22.0
diff --git a/testing/web-platform/tests/tools/manifest/sourcefile.py b/testing/web-platform/tests/tools/manifest/sourcefile.py
index 71eab54bea..3563fb9e5e 100644
--- a/testing/web-platform/tests/tools/manifest/sourcefile.py
+++ b/testing/web-platform/tests/tools/manifest/sourcefile.py
@@ -4,8 +4,8 @@ import os
from collections import deque
from fnmatch import fnmatch
from io import BytesIO
-from typing import (Any, BinaryIO, Callable, Deque, Dict, Iterable, List, Optional, Pattern,
- Set, Text, Tuple, Union, cast)
+from typing import (Any, BinaryIO, Callable, Deque, Dict, Iterable, List,
+ Optional, Pattern, Set, Text, Tuple, TypedDict, Union, cast)
from urllib.parse import urljoin
try:
@@ -68,7 +68,13 @@ def read_script_metadata(f: BinaryIO, regexp: Pattern[bytes]) -> Iterable[Tuple[
yield (m.groups()[0].decode("utf8"), m.groups()[1].decode("utf8"))
-_any_variants: Dict[Text, Dict[Text, Any]] = {
+class VariantData(TypedDict, total=False):
+ suffix: str
+ force_https: bool
+ longhand: Set[str]
+
+
+_any_variants: Dict[Text, VariantData] = {
"window": {"suffix": ".any.html"},
"window-module": {},
"serviceworker": {"force_https": True},
@@ -205,9 +211,11 @@ class SourceFile:
type_flag = None
if "-" in name:
- type_flag = name.rsplit("-", 1)[1].split(".")[0]
-
- meta_flags = name.split(".")[1:]
+ type_meta = name.rsplit("-", 1)[1].split(".")
+ type_flag = type_meta[0]
+ meta_flags = type_meta[1:]
+ else:
+ meta_flags = name.split(".")[1:]
self.tests_root: Text = tests_root
self.rel_path: Text = rel_path
diff --git a/testing/web-platform/tests/tools/mypy.ini b/testing/web-platform/tests/tools/mypy.ini
index 1e58e9625e..cc22a770b0 100644
--- a/testing/web-platform/tests/tools/mypy.ini
+++ b/testing/web-platform/tests/tools/mypy.ini
@@ -57,9 +57,6 @@ ignore_missing_imports = True
[mypy-marionette_driver.*]
ignore_missing_imports = True
-[mypy-mod_pywebsocket.*]
-ignore_missing_imports = True
-
[mypy-mozcrash.*]
ignore_missing_imports = True
@@ -108,6 +105,9 @@ ignore_missing_imports = True
[mypy-pytest.*]
ignore_missing_imports = True
+[mypy-pywebsocket3.*]
+ignore_missing_imports = True
+
[mypy-selenium.*]
ignore_missing_imports = True
diff --git a/testing/web-platform/tests/tools/pytest.ini b/testing/web-platform/tests/tools/pytest.ini
index 81666e01db..650d07caf3 100644
--- a/testing/web-platform/tests/tools/pytest.ini
+++ b/testing/web-platform/tests/tools/pytest.ini
@@ -15,7 +15,7 @@ filterwarnings =
# ignore mozinfo deprecation warnings
ignore:distutils Version classes are deprecated\. Use packaging\.version instead\.:DeprecationWarning:mozinfo
# ingore mozinfo's dependency on distro
- ignore:distro\.linux_distribution\(\) is deprecated\. It should only be used as a compatibility shim with Python's platform\.linux_distribution\(\)\. Please use distro\.id\(\), distro\.version\(\) and distro\.name\(\) instead\.:DeprecationWarning
+ ignore:distro\.linux_distribution\(\) is deprecated\. It should only be used as a compatibility shim with Python\'s platform\.linux_distribution\(\)\. Please use distro\.id\(\), distro\.version\(\) and distro\.name\(\) instead\.:DeprecationWarning
# ignore mozversion deprecation warnings
ignore:This method will be removed in .*\.\s+Use 'parser\.read_file\(\)' instead\.:DeprecationWarning:mozversion
# ignore mozversion not cleanly closing .ini files
@@ -32,3 +32,8 @@ filterwarnings =
always:the imp module is deprecated in favour of importlib:DeprecationWarning
# https://github.com/web-platform-tests/wpt/issues/39827
always:pkg_resources is deprecated as an API:DeprecationWarning
+ # taskcluster and jsone use datetime.utcnow()
+ ignore:datetime\.datetime\.utcnow\(\) is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC:DeprecationWarning:jsone
+ ignore:datetime\.datetime\.utcnow\(\) is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC:DeprecationWarning:taskcluster
+ # mozfile not yet updated to pass a filter argument to tarfile.extract
+ ignore:Python 3\.14 will, by default, filter extracted tar archives and reject files or modify their metadata\. Use the filter argument to control this behavior\.:DeprecationWarning
diff --git a/testing/web-platform/tests/tools/requirements_flake8.txt b/testing/web-platform/tests/tools/requirements_flake8.txt
index fc1f92a69f..3f7f3121ca 100644
--- a/testing/web-platform/tests/tools/requirements_flake8.txt
+++ b/testing/web-platform/tests/tools/requirements_flake8.txt
@@ -1,7 +1,5 @@
flake8==5.0.4; python_version < '3.9'
-pycodestyle==2.9.1; python_version < '3.8'
-pyflakes==2.5.0; python_version < '3.8'
flake8==6.1.0; python_version >= '3.9'
-pycodestyle==2.11.0; python_version >= '3.9'
+pycodestyle==2.11.1; python_version >= '3.9'
pyflakes==3.1.0; python_version >= '3.9'
pep8-naming==0.13.3
diff --git a/testing/web-platform/tests/tools/requirements_mypy.txt b/testing/web-platform/tests/tools/requirements_mypy.txt
index c3db2292af..3b1d3b03d6 100644
--- a/testing/web-platform/tests/tools/requirements_mypy.txt
+++ b/testing/web-platform/tests/tools/requirements_mypy.txt
@@ -1,14 +1,14 @@
-mypy==1.4.1
+mypy==1.10.0
mypy-extensions==1.0.0
toml==0.10.2
tomli==2.0.1
typed-ast==1.5.5
types-atomicwrites==1.4.5.1
-types-python-dateutil==2.8.19.14
+types-python-dateutil==2.9.0.20240316
types-PyYAML==6.0.12.12
types-requests==2.31.0.20231231
-types-setuptools==68.0.0.3
-types-six==1.16.21.9
+types-setuptools==69.5.0.20240423
+types-six==1.16.21.20240425
types-ujson==5.9.0.0
types-urllib3==1.26.25.14
typing_extensions==4.7.1
diff --git a/testing/web-platform/tests/tools/requirements_pytest.txt b/testing/web-platform/tests/tools/requirements_pytest.txt
index 9034cda719..64d38583a2 100644
--- a/testing/web-platform/tests/tools/requirements_pytest.txt
+++ b/testing/web-platform/tests/tools/requirements_pytest.txt
@@ -1,3 +1,3 @@
pytest==7.4.4
pytest-cov==4.1.0
-hypothesis==6.78.2
+hypothesis==6.100.2
diff --git a/testing/web-platform/tests/tools/requirements_tests.txt b/testing/web-platform/tests/tools/requirements_tests.txt
index 6455286736..24785f3531 100644
--- a/testing/web-platform/tests/tools/requirements_tests.txt
+++ b/testing/web-platform/tests/tools/requirements_tests.txt
@@ -1,6 +1,6 @@
-httpx[http2]==0.24.1
-json-e==4.5.3
+httpx[http2]==0.27.0
+json-e==4.7.0
jsonschema==4.17.3
pyyaml==6.0.1
-taskcluster==60.4.1
+taskcluster==64.2.7
mozterm==1.0.0
diff --git a/testing/web-platform/tests/tools/serve/serve.py b/testing/web-platform/tests/tools/serve/serve.py
index 300f8270a6..42d8091802 100644
--- a/testing/web-platform/tests/tools/serve/serve.py
+++ b/testing/web-platform/tests/tools/serve/serve.py
@@ -30,7 +30,7 @@ from wptserve import config
from wptserve.handlers import filesystem_path, wrap_pipeline
from wptserve.response import ResponseHeaders
from wptserve.utils import get_port, HTTPException, http2_compatible
-from mod_pywebsocket import standalone as pywebsocket
+from pywebsocket3 import standalone as pywebsocket
EDIT_HOSTS_HELP = ("Please ensure all the necessary WPT subdomains "
@@ -829,7 +829,8 @@ def start_http_server(logger, host, port, paths, routes, bind_address, config, *
key_file=None,
certificate=None,
latency=kwargs.get("latency"))
- except Exception:
+ except Exception as error:
+ logger.critical(f"start_http_server: Caught exception from wptserve.WebTestHttpd: {error}")
startup_failed(logger)
@@ -847,7 +848,8 @@ def start_https_server(logger, host, port, paths, routes, bind_address, config,
certificate=config.ssl_config["cert_path"],
encrypt_after_connect=config.ssl_config["encrypt_after_connect"],
latency=kwargs.get("latency"))
- except Exception:
+ except Exception as error:
+ logger.critical(f"start_https_server: Caught exception from wptserve.WebTestHttpd: {error}")
startup_failed(logger)
@@ -868,7 +870,8 @@ def start_http2_server(logger, host, port, paths, routes, bind_address, config,
encrypt_after_connect=config.ssl_config["encrypt_after_connect"],
latency=kwargs.get("latency"),
http2=True)
- except Exception:
+ except Exception as error:
+ logger.critical(f"start_http2_server: Caught exception from wptserve.WebTestHttpd: {error}")
startup_failed(logger)
@@ -935,7 +938,8 @@ def start_ws_server(logger, host, port, paths, routes, bind_address, config, **k
config.paths["ws_doc_root"],
bind_address,
ssl_config=None)
- except Exception:
+ except Exception as error:
+ logger.critical(f"start_ws_server: Caught exception from WebSocketDomain: {error}")
startup_failed(logger)
@@ -947,7 +951,8 @@ def start_wss_server(logger, host, port, paths, routes, bind_address, config, **
config.paths["ws_doc_root"],
bind_address,
config.ssl_config)
- except Exception:
+ except Exception as error:
+ logger.critical(f"start_wss_server: Caught exception from WebSocketDomain: {error}")
startup_failed(logger)
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/.gitignore b/testing/web-platform/tests/tools/third_party/pywebsocket3/.gitignore
deleted file mode 100644
index 70f2867054..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-*.pyc
-build/
-*.egg-info/
-dist/
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/.travis.yml b/testing/web-platform/tests/tools/third_party/pywebsocket3/.travis.yml
deleted file mode 100644
index 2065a644dd..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/.travis.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-language: python
-python:
- - 2.7
- - 3.5
- - 3.6
- - 3.7
- - 3.8
- - nightly
-
-matrix:
- allow_failures:
- - python: 3.5, nightly
-install:
- - pip install six yapf
-script:
- - python test/run_all.py
- - yapf --diff --recursive .
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/CONTRIBUTING b/testing/web-platform/tests/tools/third_party/pywebsocket3/CONTRIBUTING
deleted file mode 100644
index f975be126f..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/CONTRIBUTING
+++ /dev/null
@@ -1,30 +0,0 @@
-# How to Contribute
-
-We'd love to accept your patches and contributions to this project. There are
-just a few small guidelines you need to follow.
-
-## Contributor License Agreement
-
-Contributions to this project must be accompanied by a Contributor License
-Agreement. You (or your employer) retain the copyright to your contribution;
-this simply gives us permission to use and redistribute your contributions as
-part of the project. Head over to <https://cla.developers.google.com/> to see
-your current agreements on file or to sign a new one.
-
-You generally only need to submit a CLA once, so if you've already submitted one
-(even if it was for a different project), you probably don't need to do it
-again.
-
-## Code reviews
-
-All submissions, including submissions by project members, require review. We
-use GitHub pull requests for this purpose. Consult
-[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
-information on using pull requests.
-For instructions for contributing code, please read:
-https://github.com/google/pywebsocket/wiki/CodeReviewInstruction
-
-## Community Guidelines
-
-This project follows
-[Google's Open Source Community Guidelines](https://opensource.google/conduct/).
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/MANIFEST.in b/testing/web-platform/tests/tools/third_party/pywebsocket3/MANIFEST.in
index 19256882c5..116235d18f 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/MANIFEST.in
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/MANIFEST.in
@@ -2,5 +2,5 @@ include COPYING
include MANIFEST.in
include README
recursive-include example *.py
-recursive-include mod_pywebsocket *.py
+recursive-include pywebsocket3 *.py
recursive-include test *.py
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/PKG-INFO b/testing/web-platform/tests/tools/third_party/pywebsocket3/PKG-INFO
new file mode 100644
index 0000000000..289dfa8649
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/PKG-INFO
@@ -0,0 +1,13 @@
+Metadata-Version: 2.1
+Name: pywebsocket3
+Version: 4.0.2
+Summary: Standalone WebSocket Server for testing purposes.
+Home-page: https://github.com/GoogleChromeLabs/pywebsocket3
+Author: Yuzo Fujishima
+Author-email: yuzo@chromium.org
+License: See LICENSE
+Requires-Python: >=2.7
+License-File: LICENSE
+Requires-Dist: six
+
+pywebsocket3 is a standalone server for the WebSocket Protocol (RFC 6455). See pywebsocket3/__init__.py for more detail.
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/README.md b/testing/web-platform/tests/tools/third_party/pywebsocket3/README.md
index 8684f2cc7e..b46b416735 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/README.md
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/README.md
@@ -1,13 +1,14 @@
# pywebsocket3 #
-The pywebsocket project aims to provide a [WebSocket](https://tools.ietf.org/html/rfc6455) standalone server.
+The pywebsocket3 project aims to provide a [WebSocket](https://tools.ietf.org/html/rfc6455) standalone server.
pywebsocket is intended for **testing** or **experimental** purposes.
Run this to read the general document:
-```
-$ pydoc mod_pywebsocket
+
+```bash
+pydoc pywebsocket3
```
Please see [Wiki](https://github.com/GoogleChromeLabs/pywebsocket3/wiki) for more details.
@@ -15,22 +16,27 @@ Please see [Wiki](https://github.com/GoogleChromeLabs/pywebsocket3/wiki) for mor
# INSTALL #
To install this package to the system, run this:
-```
-$ python setup.py build
-$ sudo python setup.py install
+
+```bash
+python setup.py build
+sudo python setup.py install
```
To install this package as a normal user, run this instead:
+```bash
+python setup.py build
+python setup.py install --user
```
-$ python setup.py build
-$ python setup.py install --user
-```
+
# LAUNCH #
To use pywebsocket as standalone server, run this to read the document:
+
+```bash
+pydoc pywebsocket3.standalone
```
-$ pydoc mod_pywebsocket.standalone
-```
+
# Disclaimer #
+
This is not an officially supported Google product
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py
index 1b719ca897..1bad8c02f2 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py
@@ -28,7 +28,8 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
-from mod_pywebsocket import handshake
+
+from pywebsocket3 import handshake
def web_socket_do_extra_handshake(request):
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_wsh.py
index d4c240bf2c..c0495c7107 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_wsh.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_wsh.py
@@ -28,7 +28,8 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
-from mod_pywebsocket import handshake
+
+from pywebsocket3 import handshake
def web_socket_do_extra_handshake(request):
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/arraybuffer_benchmark.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/arraybuffer_benchmark.html
deleted file mode 100644
index 869cd7e1ee..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/arraybuffer_benchmark.html
+++ /dev/null
@@ -1,134 +0,0 @@
-<!--
-Copyright 2013, Google Inc.
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
- * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
- * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--->
-
-<html>
-<head>
-<title>ArrayBuffer benchmark</title>
-<script src="util.js"></script>
-<script>
-var PRINT_SIZE = true;
-
-// Initial size of arrays.
-var START_SIZE = 10 * 1024;
-// Stops benchmark when the size of an array exceeds this threshold.
-var STOP_THRESHOLD = 100000 * 1024;
-// If the size of each array is small, write/read the array multiple times
-// until the sum of sizes reaches this threshold.
-var MIN_TOTAL = 100000 * 1024;
-var MULTIPLIERS = [5, 2];
-
-// Repeat benchmark for several times to measure performance of optimized
-// (such as JIT) run.
-var REPEAT_FOR_WARMUP = 3;
-
-function writeBenchmark(size, minTotal) {
- var totalSize = 0;
- while (totalSize < minTotal) {
- var arrayBuffer = new ArrayBuffer(size);
-
- // Write 'a's.
- fillArrayBuffer(arrayBuffer, 0x61);
-
- totalSize += size;
- }
- return totalSize;
-}
-
-function readBenchmark(size, minTotal) {
- var totalSize = 0;
- while (totalSize < minTotal) {
- var arrayBuffer = new ArrayBuffer(size);
-
- if (!verifyArrayBuffer(arrayBuffer, 0x00)) {
- queueLog('Verification failed');
- return -1;
- }
-
- totalSize += size;
- }
- return totalSize;
-}
-
-function runBenchmark(benchmarkFunction,
- size,
- stopThreshold,
- minTotal,
- multipliers,
- multiplierIndex) {
- while (size <= stopThreshold) {
- var maxSpeed = 0;
-
- for (var i = 0; i < REPEAT_FOR_WARMUP; ++i) {
- var startTimeInMs = getTimeStamp();
-
- var totalSize = benchmarkFunction(size, minTotal);
-
- maxSpeed = Math.max(maxSpeed,
- calculateSpeedInKB(totalSize, startTimeInMs));
- }
- queueLog(formatResultInKiB(size, maxSpeed, PRINT_SIZE));
-
- size *= multipliers[multiplierIndex];
- multiplierIndex = (multiplierIndex + 1) % multipliers.length;
- }
-}
-
-function runBenchmarks() {
- queueLog('Message size in KiB, Speed in kB/s');
-
- queueLog('Write benchmark');
- runBenchmark(
- writeBenchmark, START_SIZE, STOP_THRESHOLD, MIN_TOTAL, MULTIPLIERS, 0);
- queueLog('Finished');
-
- queueLog('Read benchmark');
- runBenchmark(
- readBenchmark, START_SIZE, STOP_THRESHOLD, MIN_TOTAL, MULTIPLIERS, 0);
- addToLog('Finished');
-}
-
-function init() {
- logBox = document.getElementById('log');
-
- queueLog(window.navigator.userAgent.toLowerCase());
-
- addToLog('Started...');
-
- setTimeout(runBenchmarks, 0);
-}
-
-</script>
-</head>
-<body onload="init()">
-<textarea
- id="log" rows="50" style="width: 100%" readonly></textarea>
-</body>
-</html>
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py
index 2df50e77db..9ea2f93159 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py
@@ -35,7 +35,9 @@ value. <count> must be an integer value.
"""
from __future__ import absolute_import
+
import time
+
from six.moves import range
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.html
deleted file mode 100644
index f1e5c97b3a..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.html
+++ /dev/null
@@ -1,175 +0,0 @@
-<!--
-Copyright 2013, Google Inc.
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
- * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
- * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--->
-
-<html>
-<head>
-<title>WebSocket benchmark</title>
-<script src="util_main.js"></script>
-<script src="util.js"></script>
-<script src="benchmark.js"></script>
-<script>
-var addressBox = null;
-
-function getConfig() {
- return {
- prefixUrl: addressBox.value,
- printSize: getBoolFromCheckBox('printsize'),
- numSockets: getIntFromInput('numsockets'),
- // Initial size of messages.
- numIterations: getIntFromInput('numiterations'),
- numWarmUpIterations: getIntFromInput('numwarmupiterations'),
- startSize: getIntFromInput('startsize'),
- // Stops benchmark when the size of message exceeds this threshold.
- stopThreshold: getIntFromInput('stopthreshold'),
- // If the size of each message is small, send/receive multiple messages
- // until the sum of sizes reaches this threshold.
- minTotal: getIntFromInput('mintotal'),
- multipliers: getFloatArrayFromInput('multipliers'),
- verifyData: getBoolFromCheckBox('verifydata'),
- addToLog: addToLog,
- addToSummary: addToSummary,
- measureValue: measureValue,
- notifyAbort: notifyAbort
- };
-}
-
-function onSendBenchmark() {
- var config = getConfig();
- doAction(config, getBoolFromCheckBox('worker'), 'sendBenchmark');
-}
-
-function onReceiveBenchmark() {
- var config = getConfig();
- doAction(config, getBoolFromCheckBox('worker'), 'receiveBenchmark');
-}
-
-function onBatchBenchmark() {
- var config = getConfig();
- doAction(config, getBoolFromCheckBox('worker'), 'batchBenchmark');
-}
-
-function onStop() {
- var config = getConfig();
- doAction(config, getBoolFromCheckBox('worker'), 'stop');
-}
-
-function init() {
- addressBox = document.getElementById('address');
- logBox = document.getElementById('log');
-
- summaryBox = document.getElementById('summary');
-
- var scheme = window.location.protocol == 'https:' ? 'wss://' : 'ws://';
- var defaultAddress = scheme + window.location.host + '/benchmark_helper';
-
- addressBox.value = defaultAddress;
-
- addToLog(window.navigator.userAgent.toLowerCase());
- addToSummary(window.navigator.userAgent.toLowerCase());
-
- if (!('WebSocket' in window)) {
- addToLog('WebSocket is not available');
- }
-
- initWorker('');
-}
-</script>
-</head>
-<body onload="init()">
-
-<div id="benchmark_div">
- url <input type="text" id="address" size="40">
- <input type="button" value="send" onclick="onSendBenchmark()">
- <input type="button" value="receive" onclick="onReceiveBenchmark()">
- <input type="button" value="batch" onclick="onBatchBenchmark()">
- <input type="button" value="stop" onclick="onStop()">
-
- <br/>
-
- <input type="checkbox" id="printsize" checked>
- <label for="printsize">Print size and time per message</label>
- <input type="checkbox" id="verifydata" checked>
- <label for="verifydata">Verify data</label>
- <input type="checkbox" id="worker">
- <label for="worker">Run on worker</label>
-
- <br/>
-
- Parameters:
-
- <br/>
-
- <table>
- <tr>
- <td>Num sockets</td>
- <td><input type="text" id="numsockets" value="1"></td>
- </tr>
- <tr>
- <td>Number of iterations</td>
- <td><input type="text" id="numiterations" value="1"></td>
- </tr>
- <tr>
- <td>Number of warm-up iterations</td>
- <td><input type="text" id="numwarmupiterations" value="0"></td>
- </tr>
- <tr>
- <td>Start size</td>
- <td><input type="text" id="startsize" value="10240"></td>
- </tr>
- <tr>
- <td>Stop threshold</td>
- <td><input type="text" id="stopthreshold" value="102400000"></td>
- </tr>
- <tr>
- <td>Minimum total</td>
- <td><input type="text" id="mintotal" value="102400000"></td>
- </tr>
- <tr>
- <td>Multipliers</td>
- <td><input type="text" id="multipliers" value="5, 2"></td>
- </tr>
- </table>
-</div>
-
-<div id="log_div">
- <textarea
- id="log" rows="20" style="width: 100%" readonly></textarea>
-</div>
-<div id="summary_div">
- Summary
- <textarea
- id="summary" rows="20" style="width: 100%" readonly></textarea>
-</div>
-
-Note: Effect of RTT is not eliminated.
-
-</body>
-</html>
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.js
deleted file mode 100644
index 2701472a4f..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.js
+++ /dev/null
@@ -1,238 +0,0 @@
-// Copyright 2014, Google Inc.
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are
-// met:
-//
-// * Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-// * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following disclaimer
-// in the documentation and/or other materials provided with the
-// distribution.
-// * Neither the name of Google Inc. nor the names of its
-// contributors may be used to endorse or promote products derived from
-// this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
-if (typeof importScripts !== "undefined") {
- // Running on a worker
- importScripts('util.js', 'util_worker.js');
-}
-
-// Namespace for holding globals.
-var benchmark = {startTimeInMs: 0};
-
-var sockets = [];
-var numEstablishedSockets = 0;
-
-var timerID = null;
-
-function destroySocket(socket) {
- socket.onopen = null;
- socket.onmessage = null;
- socket.onerror = null;
- socket.onclose = null;
- socket.close();
-}
-
-function destroyAllSockets() {
- for (var i = 0; i < sockets.length; ++i) {
- destroySocket(sockets[i]);
- }
- sockets = [];
-}
-
-function sendBenchmarkStep(size, config, isWarmUp) {
- timerID = null;
-
- var totalSize = 0;
- var totalReplied = 0;
-
- var onMessageHandler = function(event) {
- if (!verifyAcknowledgement(config, event.data, size)) {
- destroyAllSockets();
- config.notifyAbort();
- return;
- }
-
- totalReplied += size;
-
- if (totalReplied < totalSize) {
- return;
- }
-
- calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize,
- isWarmUp);
-
- runNextTask(config);
- };
-
- for (var i = 0; i < sockets.length; ++i) {
- var socket = sockets[i];
- socket.onmessage = onMessageHandler;
- }
-
- var dataArray = [];
-
- while (totalSize < config.minTotal) {
- var buffer = new ArrayBuffer(size);
-
- fillArrayBuffer(buffer, 0x61);
-
- dataArray.push(buffer);
- totalSize += size;
- }
-
- benchmark.startTimeInMs = getTimeStamp();
-
- totalSize = 0;
-
- var socketIndex = 0;
- var dataIndex = 0;
- while (totalSize < config.minTotal) {
- var command = ['send'];
- command.push(config.verifyData ? '1' : '0');
- sockets[socketIndex].send(command.join(' '));
- sockets[socketIndex].send(dataArray[dataIndex]);
- socketIndex = (socketIndex + 1) % sockets.length;
-
- totalSize += size;
- ++dataIndex;
- }
-}
-
-function receiveBenchmarkStep(size, config, isWarmUp) {
- timerID = null;
-
- var totalSize = 0;
- var totalReplied = 0;
-
- var onMessageHandler = function(event) {
- var bytesReceived = event.data.byteLength;
- if (bytesReceived != size) {
- config.addToLog('Expected ' + size + 'B but received ' +
- bytesReceived + 'B');
- destroyAllSockets();
- config.notifyAbort();
- return;
- }
-
- if (config.verifyData && !verifyArrayBuffer(event.data, 0x61)) {
- config.addToLog('Response verification failed');
- destroyAllSockets();
- config.notifyAbort();
- return;
- }
-
- totalReplied += bytesReceived;
-
- if (totalReplied < totalSize) {
- return;
- }
-
- calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize,
- isWarmUp);
-
- runNextTask(config);
- };
-
- for (var i = 0; i < sockets.length; ++i) {
- var socket = sockets[i];
- socket.binaryType = 'arraybuffer';
- socket.onmessage = onMessageHandler;
- }
-
- benchmark.startTimeInMs = getTimeStamp();
-
- var socketIndex = 0;
- while (totalSize < config.minTotal) {
- sockets[socketIndex].send('receive ' + size);
- socketIndex = (socketIndex + 1) % sockets.length;
-
- totalSize += size;
- }
-}
-
-function createSocket(config) {
- // TODO(tyoshino): Add TCP warm up.
- var url = config.prefixUrl;
-
- config.addToLog('Connect ' + url);
-
- var socket = new WebSocket(url);
- socket.onmessage = function(event) {
- config.addToLog('Unexpected message received. Aborting.');
- };
- socket.onerror = function() {
- config.addToLog('Error');
- };
- socket.onclose = function(event) {
- config.addToLog('Closed');
- config.notifyAbort();
- };
- return socket;
-}
-
-function startBenchmark(config) {
- clearTimeout(timerID);
- destroyAllSockets();
-
- numEstablishedSockets = 0;
-
- for (var i = 0; i < config.numSockets; ++i) {
- var socket = createSocket(config);
- socket.onopen = function() {
- config.addToLog('Opened');
-
- ++numEstablishedSockets;
-
- if (numEstablishedSockets == sockets.length) {
- runNextTask(config);
- }
- };
- sockets.push(socket);
- }
-}
-
-function getConfigString(config) {
- return '(WebSocket' +
- ', ' + (typeof importScripts !== "undefined" ? 'Worker' : 'Main') +
- ', numSockets=' + config.numSockets +
- ', numIterations=' + config.numIterations +
- ', verifyData=' + config.verifyData +
- ', minTotal=' + config.minTotal +
- ', numWarmUpIterations=' + config.numWarmUpIterations +
- ')';
-}
-
-function batchBenchmark(config) {
- config.addToLog('Batch benchmark');
- config.addToLog(buildLegendString(config));
-
- tasks = [];
- clearAverageData();
- addTasks(config, sendBenchmarkStep);
- addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config));
- addTasks(config, receiveBenchmarkStep);
- addResultReportingTask(config, 'Receive Benchmark ' +
- getConfigString(config));
- startBenchmark(config);
-}
-
-function cleanup() {
- destroyAllSockets();
-}
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py
index fc17533335..9ea9f56422 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py
@@ -27,7 +27,9 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Handler for benchmark.html."""
+
from __future__ import absolute_import
+
import six
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py
index 8f0005ffea..2463bc7b31 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py
@@ -28,10 +28,8 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
-import struct
-from mod_pywebsocket import common
-from mod_pywebsocket import stream
+from pywebsocket3 import common
def web_socket_do_extra_handshake(request):
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/console.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/console.html
deleted file mode 100644
index ccd6d8f806..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/console.html
+++ /dev/null
@@ -1,317 +0,0 @@
-<!--
-Copyright 2011, Google Inc.
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
- * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
- * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--->
-
-<!--
-A simple console for testing WebSocket server.
-
-Type an address into the top text input and click connect to establish
-WebSocket. Then, type some message into the bottom text input and click send
-to send the message. Received/sent messages and connection state will be shown
-on the middle textarea.
--->
-
-<html>
-<head>
-<title>WebSocket console</title>
-<script>
-var socket = null;
-
-var showTimeStamp = false;
-
-var addressBox = null;
-var protocolsBox = null;
-var logBox = null;
-var messageBox = null;
-var fileBox = null;
-var codeBox = null;
-var reasonBox = null;
-
-function getTimeStamp() {
- return new Date().getTime();
-}
-
-function addToLog(log) {
- if (showTimeStamp) {
- logBox.value += '[' + getTimeStamp() + '] ';
- }
- logBox.value += log + '\n'
- // Large enough to keep showing the latest message.
- logBox.scrollTop = 1000000;
-}
-
-function setbinarytype(binaryType) {
- if (!socket) {
- addToLog('Not connected');
- return;
- }
-
- socket.binaryType = binaryType;
- addToLog('Set binaryType to ' + binaryType);
-}
-
-function send() {
- if (!socket) {
- addToLog('Not connected');
- return;
- }
-
- socket.send(messageBox.value);
- addToLog('> ' + messageBox.value);
- messageBox.value = '';
-}
-
-function sendfile() {
- if (!socket) {
- addToLog('Not connected');
- return;
- }
-
- var files = fileBox.files;
-
- if (files.length == 0) {
- addToLog('File not selected');
- return;
- }
-
- socket.send(files[0]);
- addToLog('> Send ' + files[0].name);
-}
-
-function parseProtocols(protocolsText) {
- var protocols = protocolsText.split(',');
- for (var i = 0; i < protocols.length; ++i) {
- protocols[i] = protocols[i].trim();
- }
-
- if (protocols.length == 0) {
- // Don't pass.
- protocols = null;
- } else if (protocols.length == 1) {
- if (protocols[0].length == 0) {
- // Don't pass.
- protocols = null;
- } else {
- // Pass as a string.
- protocols = protocols[0];
- }
- }
-
- return protocols;
-}
-
-function connect() {
- var url = addressBox.value;
- var protocols = parseProtocols(protocolsBox.value);
-
- if ('WebSocket' in window) {
- if (protocols) {
- socket = new WebSocket(url, protocols);
- } else {
- socket = new WebSocket(url);
- }
- } else {
- return;
- }
-
- socket.onopen = function () {
- var extraInfo = [];
- if (('protocol' in socket) && socket.protocol) {
- extraInfo.push('protocol = ' + socket.protocol);
- }
- if (('extensions' in socket) && socket.extensions) {
- extraInfo.push('extensions = ' + socket.extensions);
- }
-
- var logMessage = 'Opened';
- if (extraInfo.length > 0) {
- logMessage += ' (' + extraInfo.join(', ') + ')';
- }
- addToLog(logMessage);
- };
- socket.onmessage = function (event) {
- if (('ArrayBuffer' in window) && (event.data instanceof ArrayBuffer)) {
- addToLog('< Received an ArrayBuffer of ' + event.data.byteLength +
- ' bytes')
- } else if (('Blob' in window) && (event.data instanceof Blob)) {
- addToLog('< Received a Blob of ' + event.data.size + ' bytes')
- } else {
- addToLog('< ' + event.data);
- }
- };
- socket.onerror = function () {
- addToLog('Error');
- };
- socket.onclose = function (event) {
- var logMessage = 'Closed (';
- if ((arguments.length == 1) && ('CloseEvent' in window) &&
- (event instanceof CloseEvent)) {
- logMessage += 'wasClean = ' + event.wasClean;
- // code and reason are present only for
- // draft-ietf-hybi-thewebsocketprotocol-06 and later
- if ('code' in event) {
- logMessage += ', code = ' + event.code;
- }
- if ('reason' in event) {
- logMessage += ', reason = ' + event.reason;
- }
- } else {
- logMessage += 'CloseEvent is not available';
- }
- addToLog(logMessage + ')');
- };
-
- if (protocols) {
- addToLog('Connect ' + url + ' (protocols = ' + protocols + ')');
- } else {
- addToLog('Connect ' + url);
- }
-}
-
-function closeSocket() {
- if (!socket) {
- addToLog('Not connected');
- return;
- }
-
- if (codeBox.value || reasonBox.value) {
- socket.close(codeBox.value, reasonBox.value);
- } else {
- socket.close();
- }
-}
-
-function printState() {
- if (!socket) {
- addToLog('Not connected');
- return;
- }
-
- addToLog(
- 'url = ' + socket.url +
- ', readyState = ' + socket.readyState +
- ', bufferedAmount = ' + socket.bufferedAmount);
-}
-
-function init() {
- var scheme = window.location.protocol == 'https:' ? 'wss://' : 'ws://';
- var defaultAddress = scheme + window.location.host + '/echo';
-
- addressBox = document.getElementById('address');
- protocolsBox = document.getElementById('protocols');
- logBox = document.getElementById('log');
- messageBox = document.getElementById('message');
- fileBox = document.getElementById('file');
- codeBox = document.getElementById('code');
- reasonBox = document.getElementById('reason');
-
- addressBox.value = defaultAddress;
-
- if (!('WebSocket' in window)) {
- addToLog('WebSocket is not available');
- }
-}
-</script>
-<style type="text/css">
-form {
- margin: 0px;
-}
-
-#connect_div, #log_div, #send_div, #sendfile_div, #close_div, #printstate_div {
- padding: 5px;
- margin: 5px;
- border-width: 0px 0px 0px 10px;
- border-style: solid;
- border-color: silver;
-}
-</style>
-</head>
-<body onload="init()">
-
-<div>
-
-<div id="connect_div">
- <form action="#" onsubmit="connect(); return false;">
- url <input type="text" id="address" size="40">
- <input type="submit" value="connect">
- <br/>
- protocols <input type="text" id="protocols" size="20">
- </form>
-</div>
-
-<div id="log_div">
- <textarea id="log" rows="10" cols="40" readonly></textarea>
- <br/>
- <input type="checkbox"
- name="showtimestamp"
- value="showtimestamp"
- onclick="showTimeStamp = this.checked">Show time stamp
-</div>
-
-<div id="send_div">
- <form action="#" onsubmit="send(); return false;">
- data <input type="text" id="message" size="40">
- <input type="submit" value="send">
- </form>
-</div>
-
-<div id="sendfile_div">
- <form action="#" onsubmit="sendfile(); return false;">
- <input type="file" id="file" size="40">
- <input type="submit" value="send file">
- </form>
-
- Set binaryType
- <input type="radio"
- name="binarytype"
- value="blob"
- onclick="setbinarytype('blob')" checked>blob
- <input type="radio"
- name="binarytype"
- value="arraybuffer"
- onclick="setbinarytype('arraybuffer')">arraybuffer
-</div>
-
-<div id="close_div">
- <form action="#" onsubmit="closeSocket(); return false;">
- code <input type="text" id="code" size="10">
- reason <input type="text" id="reason" size="20">
- <input type="submit" value="close">
- </form>
-</div>
-
-<div id="printstate_div">
- <input type="button" value="print state" onclick="printState();">
-</div>
-
-</div>
-
-</body>
-</html>
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py
index 815209694e..1ca2c84386 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py
@@ -27,6 +27,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
+
from six.moves import urllib
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py
index 2ed60b3b59..5063f00a51 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py
@@ -30,13 +30,13 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Simple WebSocket client named echo_client just because of historical reason.
-mod_pywebsocket directory must be in PYTHONPATH.
+pywebsocket3 directory must be in PYTHONPATH.
Example Usage:
# server setup
- % cd $pywebsocket
- % PYTHONPATH=$cwd/src python ./mod_pywebsocket/standalone.py -p 8880 \
+ % cd $pywebsocket3
+ % PYTHONPATH=$cwd/src python ./pywebsocket3/standalone.py -p 8880 \
-d $cwd/src/example
# run client
@@ -47,27 +47,27 @@ Example Usage:
from __future__ import absolute_import
from __future__ import print_function
+
+import argparse
import base64
import codecs
-from hashlib import sha1
import logging
-import argparse
import os
-import random
import re
-import six
import socket
import ssl
-import struct
import sys
+from hashlib import sha1
+
+import six
-from mod_pywebsocket import common
-from mod_pywebsocket.extensions import PerMessageDeflateExtensionProcessor
-from mod_pywebsocket.extensions import _PerMessageDeflateFramer
-from mod_pywebsocket.extensions import _parse_window_bits
-from mod_pywebsocket.stream import Stream
-from mod_pywebsocket.stream import StreamOptions
-from mod_pywebsocket import util
+from pywebsocket3 import common, util
+from pywebsocket3.extensions import (
+ PerMessageDeflateExtensionProcessor,
+ _PerMessageDeflateFramer,
+ _parse_window_bits,
+)
+from pywebsocket3.stream import Stream, StreamOptions
_TIMEOUT_SEC = 10
_UNDEFINED_PORT = -1
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/handler_map.txt b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/handler_map.txt
deleted file mode 100644
index 21c4c09aa0..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/handler_map.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-# websocket handler map file, used by standalone.py -m option.
-# A line starting with '#' is a comment line.
-# Each line consists of 'alias_resource_path' and 'existing_resource_path'
-# separated by spaces.
-# Aliasing is processed from the top to the bottom of the line, and
-# 'existing_resource_path' must exist before it is aliased.
-# For example,
-# / /echo
-# means that a request to '/' will be handled by handlers for '/echo'.
-/ /echo
-
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/internal_error_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/internal_error_wsh.py
index 04aa684283..cbc0fd294e 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/internal_error_wsh.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/internal_error_wsh.py
@@ -28,7 +28,8 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
-from mod_pywebsocket import msgutil
+
+from pywebsocket3 import msgutil
def web_socket_do_extra_handshake(request):
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.html
deleted file mode 100644
index c18b2c08f6..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!--
-Copyright 2020, Google Inc.
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
- * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
- * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--->
-
-<!DOCTYPE html>
-<head>
-<script src="util.js"></script>
-<script src="performance_test_iframe.js"></script>
-<script src="benchmark.js"></script>
-</head>
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.js
deleted file mode 100644
index 270409aa6e..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.js
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright 2020, Google Inc.
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are
-// met:
-//
-// * Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-// * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following disclaimer
-// in the documentation and/or other materials provided with the
-// distribution.
-// * Neither the name of Google Inc. nor the names of its
-// contributors may be used to endorse or promote products derived from
-// this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-function perfTestAddToLog(text) {
- parent.postMessage({'command': 'log', 'value': text}, '*');
-}
-
-function perfTestAddToSummary(text) {
-}
-
-function perfTestMeasureValue(value) {
- parent.postMessage({'command': 'measureValue', 'value': value}, '*');
-}
-
-function perfTestNotifyAbort() {
- parent.postMessage({'command': 'notifyAbort'}, '*');
-}
-
-function getConfigForPerformanceTest(dataType, async,
- verifyData, numIterations,
- numWarmUpIterations) {
-
- return {
- prefixUrl: 'ws://' + location.host + '/benchmark_helper',
- printSize: true,
- numSockets: 1,
- // + 1 is for a warmup iteration by the Telemetry framework.
- numIterations: numIterations + numWarmUpIterations + 1,
- numWarmUpIterations: numWarmUpIterations,
- minTotal: 10240000,
- startSize: 10240000,
- stopThreshold: 10240000,
- multipliers: [2],
- verifyData: verifyData,
- dataType: dataType,
- async: async,
- addToLog: perfTestAddToLog,
- addToSummary: perfTestAddToSummary,
- measureValue: perfTestMeasureValue,
- notifyAbort: perfTestNotifyAbort
- };
-}
-
-var data;
-onmessage = function(message) {
- var action;
- if (message.data.command === 'start') {
- data = message.data;
- initWorker('http://' + location.host);
- action = data.benchmarkName;
- } else {
- action = 'stop';
- }
-
- var config = getConfigForPerformanceTest(data.dataType, data.async,
- data.verifyData,
- data.numIterations,
- data.numWarmUpIterations);
- doAction(config, data.isWorker, action);
-};
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/special_headers.cgi b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/special_headers.cgi
deleted file mode 100755
index 703cb7401b..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/special_headers.cgi
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/usr/bin/python
-
-# Copyright 2014 Google Inc. All rights reserved.
-#
-# Use of this source code is governed by a BSD-style
-# license that can be found in the COPYING file or at
-# https://developers.google.com/open-source/licenses/bsd
-"""CGI script sample for testing effect of HTTP headers on the origin page.
-
-Note that CGI scripts don't work on the standalone pywebsocket running in TLS
-mode.
-"""
-
-print """Content-type: text/html
-Content-Security-Policy: connect-src self
-
-<html>
-<head>
-<title></title>
-</head>
-<body>
-<script>
-var socket = new WebSocket("ws://example.com");
-</script>
-</body>
-</html>"""
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util.js
deleted file mode 100644
index 990160cb40..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util.js
+++ /dev/null
@@ -1,323 +0,0 @@
-// Copyright 2013, Google Inc.
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are
-// met:
-//
-// * Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-// * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following disclaimer
-// in the documentation and/or other materials provided with the
-// distribution.
-// * Neither the name of Google Inc. nor the names of its
-// contributors may be used to endorse or promote products derived from
-// this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
-// Utilities for example applications (for both main and worker thread).
-
-var results = {};
-
-function getTimeStamp() {
- return Date.now();
-}
-
-function formatResultInKiB(size, timePerMessageInMs, stddevTimePerMessageInMs,
- speed, printSize) {
- if (printSize) {
- return (size / 1024) +
- '\t' + timePerMessageInMs.toFixed(3) +
- (stddevTimePerMessageInMs == -1 ?
- '' :
- '\t' + stddevTimePerMessageInMs.toFixed(3)) +
- '\t' + speed.toFixed(3);
- } else {
- return speed.toString();
- }
-}
-
-function clearAverageData() {
- results = {};
-}
-
-function reportAverageData(config) {
- config.addToSummary(
- 'Size[KiB]\tAverage time[ms]\tStddev time[ms]\tSpeed[KB/s]');
- for (var size in results) {
- var averageTimePerMessageInMs = results[size].sum_t / results[size].n;
- var speed = calculateSpeedInKB(size, averageTimePerMessageInMs);
- // Calculate sample standard deviation
- var stddevTimePerMessageInMs = Math.sqrt(
- (results[size].sum_t2 / results[size].n -
- averageTimePerMessageInMs * averageTimePerMessageInMs) *
- results[size].n /
- (results[size].n - 1));
- config.addToSummary(formatResultInKiB(
- size, averageTimePerMessageInMs, stddevTimePerMessageInMs, speed,
- true));
- }
-}
-
-function calculateSpeedInKB(size, timeSpentInMs) {
- return Math.round(size / timeSpentInMs * 1000) / 1000;
-}
-
-function calculateAndLogResult(config, size, startTimeInMs, totalSize,
- isWarmUp) {
- var timeSpentInMs = getTimeStamp() - startTimeInMs;
- var speed = calculateSpeedInKB(totalSize, timeSpentInMs);
- var timePerMessageInMs = timeSpentInMs / (totalSize / size);
- if (!isWarmUp) {
- config.measureValue(timePerMessageInMs);
- if (!results[size]) {
- results[size] = {n: 0, sum_t: 0, sum_t2: 0};
- }
- results[size].n ++;
- results[size].sum_t += timePerMessageInMs;
- results[size].sum_t2 += timePerMessageInMs * timePerMessageInMs;
- }
- config.addToLog(formatResultInKiB(size, timePerMessageInMs, -1, speed,
- config.printSize));
-}
-
-function repeatString(str, count) {
- var data = '';
- var expChunk = str;
- var remain = count;
- while (true) {
- if (remain % 2) {
- data += expChunk;
- remain = (remain - 1) / 2;
- } else {
- remain /= 2;
- }
-
- if (remain == 0)
- break;
-
- expChunk = expChunk + expChunk;
- }
- return data;
-}
-
-function fillArrayBuffer(buffer, c) {
- var i;
-
- var u32Content = c * 0x01010101;
-
- var u32Blocks = Math.floor(buffer.byteLength / 4);
- var u32View = new Uint32Array(buffer, 0, u32Blocks);
- // length attribute is slow on Chrome. Don't use it for loop condition.
- for (i = 0; i < u32Blocks; ++i) {
- u32View[i] = u32Content;
- }
-
- // Fraction
- var u8Blocks = buffer.byteLength - u32Blocks * 4;
- var u8View = new Uint8Array(buffer, u32Blocks * 4, u8Blocks);
- for (i = 0; i < u8Blocks; ++i) {
- u8View[i] = c;
- }
-}
-
-function verifyArrayBuffer(buffer, expectedChar) {
- var i;
-
- var expectedU32Value = expectedChar * 0x01010101;
-
- var u32Blocks = Math.floor(buffer.byteLength / 4);
- var u32View = new Uint32Array(buffer, 0, u32Blocks);
- for (i = 0; i < u32Blocks; ++i) {
- if (u32View[i] != expectedU32Value) {
- return false;
- }
- }
-
- var u8Blocks = buffer.byteLength - u32Blocks * 4;
- var u8View = new Uint8Array(buffer, u32Blocks * 4, u8Blocks);
- for (i = 0; i < u8Blocks; ++i) {
- if (u8View[i] != expectedChar) {
- return false;
- }
- }
-
- return true;
-}
-
-function verifyBlob(config, blob, expectedChar, doneCallback) {
- var reader = new FileReader(blob);
- reader.onerror = function() {
- config.addToLog('FileReader Error: ' + reader.error.message);
- doneCallback(blob.size, false);
- }
- reader.onloadend = function() {
- var result = verifyArrayBuffer(reader.result, expectedChar);
- doneCallback(blob.size, result);
- }
- reader.readAsArrayBuffer(blob);
-}
-
-function verifyAcknowledgement(config, message, size) {
- if (typeof message != 'string') {
- config.addToLog('Invalid ack type: ' + typeof message);
- return false;
- }
- var parsedAck = parseInt(message);
- if (isNaN(parsedAck)) {
- config.addToLog('Invalid ack value: ' + message);
- return false;
- }
- if (parsedAck != size) {
- config.addToLog(
- 'Expected ack for ' + size + 'B but received one for ' + parsedAck +
- 'B');
- return false;
- }
-
- return true;
-}
-
-function cloneConfig(obj) {
- var newObj = {};
- for (key in obj) {
- newObj[key] = obj[key];
- }
- return newObj;
-}
-
-var tasks = [];
-
-function runNextTask(config) {
- var task = tasks.shift();
- if (task == undefined) {
- config.addToLog('Finished');
- cleanup();
- return;
- }
- timerID = setTimeout(task, 0);
-}
-
-function buildLegendString(config) {
- var legend = ''
- if (config.printSize)
- legend = 'Message size in KiB, Time/message in ms, ';
- legend += 'Speed in kB/s';
- return legend;
-}
-
-function addTasks(config, stepFunc) {
- for (var i = 0;
- i < config.numWarmUpIterations + config.numIterations; ++i) {
- var multiplierIndex = 0;
- for (var size = config.startSize;
- size <= config.stopThreshold;
- ++multiplierIndex) {
- var task = stepFunc.bind(
- null,
- size,
- config,
- i < config.numWarmUpIterations);
- tasks.push(task);
- var multiplier = config.multipliers[
- multiplierIndex % config.multipliers.length];
- if (multiplier <= 1) {
- config.addToLog('Invalid multiplier ' + multiplier);
- config.notifyAbort();
- throw new Error('Invalid multipler');
- }
- size = Math.ceil(size * multiplier);
- }
- }
-}
-
-function addResultReportingTask(config, title) {
- tasks.push(function(){
- timerID = null;
- config.addToSummary(title);
- reportAverageData(config);
- clearAverageData();
- runNextTask(config);
- });
-}
-
-function sendBenchmark(config) {
- config.addToLog('Send benchmark');
- config.addToLog(buildLegendString(config));
-
- tasks = [];
- clearAverageData();
- addTasks(config, sendBenchmarkStep);
- addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config));
- startBenchmark(config);
-}
-
-function receiveBenchmark(config) {
- config.addToLog('Receive benchmark');
- config.addToLog(buildLegendString(config));
-
- tasks = [];
- clearAverageData();
- addTasks(config, receiveBenchmarkStep);
- addResultReportingTask(config,
- 'Receive Benchmark ' + getConfigString(config));
- startBenchmark(config);
-}
-
-function stop(config) {
- clearTimeout(timerID);
- timerID = null;
- tasks = [];
- config.addToLog('Stopped');
- cleanup();
-}
-
-var worker;
-
-function initWorker(origin) {
- worker = new Worker(origin + '/benchmark.js');
-}
-
-function doAction(config, isWindowToWorker, action) {
- if (isWindowToWorker) {
- worker.onmessage = function(addToLog, addToSummary,
- measureValue, notifyAbort, message) {
- if (message.data.type === 'addToLog')
- addToLog(message.data.data);
- else if (message.data.type === 'addToSummary')
- addToSummary(message.data.data);
- else if (message.data.type === 'measureValue')
- measureValue(message.data.data);
- else if (message.data.type === 'notifyAbort')
- notifyAbort();
- }.bind(undefined, config.addToLog, config.addToSummary,
- config.measureValue, config.notifyAbort);
- config.addToLog = undefined;
- config.addToSummary = undefined;
- config.measureValue = undefined;
- config.notifyAbort = undefined;
- worker.postMessage({type: action, config: config});
- } else {
- if (action === 'sendBenchmark')
- sendBenchmark(config);
- else if (action === 'receiveBenchmark')
- receiveBenchmark(config);
- else if (action === 'batchBenchmark')
- batchBenchmark(config);
- else if (action === 'stop')
- stop(config);
- }
-}
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_main.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_main.js
deleted file mode 100644
index 78add48731..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_main.js
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright 2014, Google Inc.
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are
-// met:
-//
-// * Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-// * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following disclaimer
-// in the documentation and/or other materials provided with the
-// distribution.
-// * Neither the name of Google Inc. nor the names of its
-// contributors may be used to endorse or promote products derived from
-// this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
-// Utilities for example applications (for the main thread only).
-
-var logBox = null;
-var queuedLog = '';
-
-var summaryBox = null;
-
-function queueLog(log) {
- queuedLog += log + '\n';
-}
-
-function addToLog(log) {
- logBox.value += queuedLog;
- queuedLog = '';
- logBox.value += log + '\n';
- logBox.scrollTop = 1000000;
-}
-
-function addToSummary(log) {
- summaryBox.value += log + '\n';
- summaryBox.scrollTop = 1000000;
-}
-
-// value: execution time in milliseconds.
-// config.measureValue is intended to be used in Performance Tests.
-// Do nothing here in non-PerformanceTest.
-function measureValue(value) {
-}
-
-// config.notifyAbort is called when the benchmark failed and aborted, and
-// intended to be used in Performance Tests.
-// Do nothing here in non-PerformanceTest.
-function notifyAbort() {
-}
-
-function getIntFromInput(id) {
- return parseInt(document.getElementById(id).value);
-}
-
-function getStringFromRadioBox(name) {
- var list = document.getElementById('benchmark_form')[name];
- for (var i = 0; i < list.length; ++i)
- if (list.item(i).checked)
- return list.item(i).value;
- return undefined;
-}
-function getBoolFromCheckBox(id) {
- return document.getElementById(id).checked;
-}
-
-function getIntArrayFromInput(id) {
- var strArray = document.getElementById(id).value.split(',');
- return strArray.map(function(str) { return parseInt(str, 10); });
-}
-
-function getFloatArrayFromInput(id) {
- var strArray = document.getElementById(id).value.split(',');
- return strArray.map(parseFloat);
-}
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_worker.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_worker.js
deleted file mode 100644
index dd90449a90..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_worker.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2014, Google Inc.
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are
-// met:
-//
-// * Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-// * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following disclaimer
-// in the documentation and/or other materials provided with the
-// distribution.
-// * Neither the name of Google Inc. nor the names of its
-// contributors may be used to endorse or promote products derived from
-// this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
-// Utilities for example applications (for the worker threads only).
-
-onmessage = function (message) {
- var config = message.data.config;
- config.addToLog = function(text) {
- postMessage({type: 'addToLog', data: text}); };
- config.addToSummary = function(text) {
- postMessage({type: 'addToSummary', data: text}); };
- config.measureValue = function(value) {
- postMessage({type: 'measureValue', data: value}); };
- config.notifyAbort = function() { postMessage({type: 'notifyAbort'}); };
-
- doAction(config, false, message.data.type);
-};
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/fast_masking.i b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/fast_masking.i
deleted file mode 100644
index ddaad27f53..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/fast_masking.i
+++ /dev/null
@@ -1,98 +0,0 @@
-// Copyright 2013, Google Inc.
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are
-// met:
-//
-// * Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-// * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following disclaimer
-// in the documentation and/or other materials provided with the
-// distribution.
-// * Neither the name of Google Inc. nor the names of its
-// contributors may be used to endorse or promote products derived from
-// this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
-%module fast_masking
-
-%include "cstring.i"
-
-%{
-#include <cstring>
-
-#ifdef __SSE2__
-#include <emmintrin.h>
-#endif
-%}
-
-%apply (char *STRING, int LENGTH) {
- (const char* payload, int payload_length),
- (const char* masking_key, int masking_key_length) };
-%cstring_output_allocate_size(
- char** result, int* result_length, delete [] *$1);
-
-%inline %{
-
-void mask(
- const char* payload, int payload_length,
- const char* masking_key, int masking_key_length,
- int masking_key_index,
- char** result, int* result_length) {
- *result = new char[payload_length];
- *result_length = payload_length;
- memcpy(*result, payload, payload_length);
-
- char* cursor = *result;
- char* cursor_end = *result + *result_length;
-
-#ifdef __SSE2__
- while ((cursor < cursor_end) &&
- (reinterpret_cast<size_t>(cursor) & 0xf)) {
- *cursor ^= masking_key[masking_key_index];
- ++cursor;
- masking_key_index = (masking_key_index + 1) % masking_key_length;
- }
- if (cursor == cursor_end) {
- return;
- }
-
- const int kBlockSize = 16;
- __m128i masking_key_block;
- for (int i = 0; i < kBlockSize; ++i) {
- *(reinterpret_cast<char*>(&masking_key_block) + i) =
- masking_key[masking_key_index];
- masking_key_index = (masking_key_index + 1) % masking_key_length;
- }
-
- while (cursor + kBlockSize <= cursor_end) {
- __m128i payload_block =
- _mm_load_si128(reinterpret_cast<__m128i*>(cursor));
- _mm_stream_si128(reinterpret_cast<__m128i*>(cursor),
- _mm_xor_si128(payload_block, masking_key_block));
- cursor += kBlockSize;
- }
-#endif
-
- while (cursor < cursor_end) {
- *cursor ^= masking_key[masking_key_index];
- ++cursor;
- masking_key_index = (masking_key_index + 1) % masking_key_length;
- }
-}
-
-%}
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/PKG-INFO b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/PKG-INFO
new file mode 100644
index 0000000000..289dfa8649
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/PKG-INFO
@@ -0,0 +1,13 @@
+Metadata-Version: 2.1
+Name: pywebsocket3
+Version: 4.0.2
+Summary: Standalone WebSocket Server for testing purposes.
+Home-page: https://github.com/GoogleChromeLabs/pywebsocket3
+Author: Yuzo Fujishima
+Author-email: yuzo@chromium.org
+License: See LICENSE
+Requires-Python: >=2.7
+License-File: LICENSE
+Requires-Dist: six
+
+pywebsocket3 is a standalone server for the WebSocket Protocol (RFC 6455). See pywebsocket3/__init__.py for more detail.
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/SOURCES.txt b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/SOURCES.txt
new file mode 100644
index 0000000000..9a74c73196
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/SOURCES.txt
@@ -0,0 +1,64 @@
+LICENSE
+MANIFEST.in
+README.md
+setup.py
+example/abort_handshake_wsh.py
+example/abort_wsh.py
+example/bench_wsh.py
+example/benchmark_helper_wsh.py
+example/close_wsh.py
+example/cookie_wsh.py
+example/echo_client.py
+example/echo_noext_wsh.py
+example/echo_wsh.py
+example/hsts_wsh.py
+example/internal_error_wsh.py
+example/origin_check_wsh.py
+example/cgi-bin/hi.py
+pywebsocket3/__init__.py
+pywebsocket3/_stream_exceptions.py
+pywebsocket3/common.py
+pywebsocket3/dispatch.py
+pywebsocket3/extensions.py
+pywebsocket3/http_header_util.py
+pywebsocket3/memorizingfile.py
+pywebsocket3/msgutil.py
+pywebsocket3/request_handler.py
+pywebsocket3/server_util.py
+pywebsocket3/standalone.py
+pywebsocket3/stream.py
+pywebsocket3/util.py
+pywebsocket3/websocket_server.py
+pywebsocket3.egg-info/PKG-INFO
+pywebsocket3.egg-info/SOURCES.txt
+pywebsocket3.egg-info/dependency_links.txt
+pywebsocket3.egg-info/requires.txt
+pywebsocket3.egg-info/top_level.txt
+pywebsocket3/handshake/__init__.py
+pywebsocket3/handshake/base.py
+pywebsocket3/handshake/hybi.py
+test/__init__.py
+test/client_for_testing.py
+test/mock.py
+test/run_all.py
+test/set_sys_path.py
+test/test_dispatch.py
+test/test_endtoend.py
+test/test_extensions.py
+test/test_handshake.py
+test/test_handshake_hybi.py
+test/test_http_header_util.py
+test/test_memorizingfile.py
+test/test_mock.py
+test/test_msgutil.py
+test/test_stream.py
+test/test_util.py
+test/testdata/handlers/abort_by_user_wsh.py
+test/testdata/handlers/blank_wsh.py
+test/testdata/handlers/origin_check_wsh.py
+test/testdata/handlers/sub/exception_in_transfer_wsh.py
+test/testdata/handlers/sub/no_wsh_at_the_end.py
+test/testdata/handlers/sub/non_callable_wsh.py
+test/testdata/handlers/sub/plain_wsh.py
+test/testdata/handlers/sub/wrong_handshake_sig_wsh.py
+test/testdata/handlers/sub/wrong_transfer_sig_wsh.py \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/dependency_links.txt b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/dependency_links.txt
index 8b13789179..8b13789179 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/dependency_links.txt
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/dependency_links.txt
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/requires.txt b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/requires.txt
new file mode 100644
index 0000000000..ffe2fce498
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/requires.txt
@@ -0,0 +1 @@
+six
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/top_level.txt b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/top_level.txt
new file mode 100644
index 0000000000..db62422f0b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/top_level.txt
@@ -0,0 +1 @@
+pywebsocket3
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/__init__.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/__init__.py
index 28d5f5950f..8f4ade0582 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/__init__.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/__init__.py
@@ -28,7 +28,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" A Standalone WebSocket Server for testing purposes
-mod_pywebsocket is an API that provides WebSocket functionalities with
+pywebsocket3 is an API that provides WebSocket functionalities with
a standalone WebSocket server. It is intended for testing or
experimental purposes.
@@ -37,7 +37,7 @@ Installation
1. Follow standalone server documentation to start running the
standalone server. It can be read by running the following command:
- $ pydoc mod_pywebsocket.standalone
+ $ pydoc pywebsocket3.standalone
2. Once the standalone server is launched verify it by accessing
http://localhost[:port]/console.html. Include the port number when
@@ -96,7 +96,7 @@ Data Transfer
web_socket_transfer_data is called after the handshake completed
successfully. A handler can receive/send messages from/to the client
-using request. mod_pywebsocket.msgutil module provides utilities
+using request. pywebsocket3.msgutil module provides utilities
for data transfer.
You can receive a message by the following statement.
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/_stream_exceptions.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/_stream_exceptions.py
index b47878bc4a..b47878bc4a 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/_stream_exceptions.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/_stream_exceptions.py
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/common.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/common.py
index 9cb11f15cb..a3321e4c68 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/common.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/common.py
@@ -30,7 +30,9 @@
"""
from __future__ import absolute_import
-from mod_pywebsocket import http_header_util
+
+from pywebsocket3 import http_header_util
+
# Additional log level definitions.
LOGLEVEL_FINE = 9
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/dispatch.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/dispatch.py
index 56cbb3c8a5..fd35ceab29 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/dispatch.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/dispatch.py
@@ -30,17 +30,19 @@
"""
from __future__ import absolute_import
+
import io
-import logging
import os
import re
import traceback
-from mod_pywebsocket import common
-from mod_pywebsocket import handshake
-from mod_pywebsocket import msgutil
-from mod_pywebsocket import stream
-from mod_pywebsocket import util
+from pywebsocket3 import (
+ common,
+ handshake,
+ msgutil,
+ stream,
+ util
+)
_SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$')
_SOURCE_SUFFIX = '_wsh.py'
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/extensions.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/extensions.py
index 314a949d45..4b5b9e8fb2 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/extensions.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/extensions.py
@@ -28,9 +28,9 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
-from mod_pywebsocket import common
-from mod_pywebsocket import util
-from mod_pywebsocket.http_header_util import quote_if_necessary
+
+from pywebsocket3 import common, util
+from pywebsocket3.http_header_util import quote_if_necessary
# The list of available server side extension processor classes.
_available_processors = {}
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/__init__.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/__init__.py
index 4bc1c67c57..275e447552 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/__init__.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/__init__.py
@@ -32,15 +32,19 @@ successfully established.
"""
from __future__ import absolute_import
+
import logging
-from mod_pywebsocket import common
-from mod_pywebsocket.handshake import hybi
+from pywebsocket3 import common
+from pywebsocket3.handshake import hybi
# Export AbortedByUserException, HandshakeException, and VersionException
# symbol from this module.
-from mod_pywebsocket.handshake.base import AbortedByUserException
-from mod_pywebsocket.handshake.base import HandshakeException
-from mod_pywebsocket.handshake.base import VersionException
+from pywebsocket3.handshake.base import (
+ AbortedByUserException,
+ HandshakeException,
+ VersionException
+)
+
_LOGGER = logging.getLogger(__name__)
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/base.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/base.py
index ffad0614d6..561f7b650a 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/base.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/base.py
@@ -32,15 +32,12 @@ processors.
from __future__ import absolute_import
-from mod_pywebsocket import common
-from mod_pywebsocket import http_header_util
-from mod_pywebsocket.extensions import get_extension_processor
-from mod_pywebsocket.stream import StreamOptions
-from mod_pywebsocket.stream import Stream
-from mod_pywebsocket import util
-
-from six.moves import map
-from six.moves import range
+from pywebsocket3 import common, http_header_util, util
+from pywebsocket3.extensions import get_extension_processor
+from pywebsocket3.stream import Stream, StreamOptions
+
+from six.moves import map, range
+
# Defining aliases for values used frequently.
_VERSION_LATEST = common.VERSION_HYBI_LATEST
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/hybi.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/hybi.py
index cf931db5a5..2e26532c3f 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/hybi.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/hybi.py
@@ -34,17 +34,20 @@ http://tools.ietf.org/html/rfc6455
"""
from __future__ import absolute_import
+
import base64
import re
from hashlib import sha1
-from mod_pywebsocket import common
-from mod_pywebsocket.handshake.base import get_mandatory_header
-from mod_pywebsocket.handshake.base import HandshakeException
-from mod_pywebsocket.handshake.base import parse_token_list
-from mod_pywebsocket.handshake.base import validate_mandatory_header
-from mod_pywebsocket.handshake.base import HandshakerBase
-from mod_pywebsocket import util
+from pywebsocket3 import common, util
+from pywebsocket3.handshake.base import (
+ get_mandatory_header,
+ HandshakeException,
+ parse_token_list,
+ validate_mandatory_header,
+ HandshakerBase
+)
+
# Used to validate the value in the Sec-WebSocket-Key header strictly. RFC 4648
# disallows non-zero padding, so the character right before == must be any of
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/http_header_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/http_header_util.py
index 21fde59af1..63e698bc16 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/http_header_util.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/http_header_util.py
@@ -31,8 +31,10 @@ in HTTP RFC http://www.ietf.org/rfc/rfc2616.txt.
"""
from __future__ import absolute_import
+
import six.moves.urllib.parse
+
_SEPARATORS = '()<>@,;:\\"/[]?={} \t'
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/memorizingfile.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/memorizingfile.py
index d353967618..4ee132fae6 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/memorizingfile.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/memorizingfile.py
@@ -34,6 +34,7 @@ A memorizing file wraps a file and memorizes lines read by readline.
"""
from __future__ import absolute_import
+
import sys
@@ -61,7 +62,7 @@ class MemorizingFile(object):
def __getattribute__(self, name):
"""Return a file attribute.
-
+
Returns the value overridden by this class for some attributes,
and forwards the call to _file for the other attributes.
"""
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/msgutil.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/msgutil.py
index f58ca78e14..dd6a6fc410 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/msgutil.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/msgutil.py
@@ -36,14 +36,18 @@ bytes writing/reading.
"""
from __future__ import absolute_import
-import six.moves.queue
+
import threading
+import six.moves.queue
+
# Export Exception symbols from msgutil for backward compatibility
-from mod_pywebsocket._stream_exceptions import ConnectionTerminatedException
-from mod_pywebsocket._stream_exceptions import InvalidFrameException
-from mod_pywebsocket._stream_exceptions import BadOperationException
-from mod_pywebsocket._stream_exceptions import UnsupportedFrameException
+from pywebsocket3._stream_exceptions import (
+ ConnectionTerminatedException,
+ InvalidFrameException,
+ BadOperationException,
+ UnsupportedFrameException
+)
# An API for handler to send/receive WebSocket messages.
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/request_handler.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/request_handler.py
index 5e9c875dc7..9d89b47c69 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/request_handler.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/request_handler.py
@@ -34,12 +34,14 @@ import os
from six.moves import CGIHTTPServer
from six.moves import http_client
-from mod_pywebsocket import common
-from mod_pywebsocket import dispatch
-from mod_pywebsocket import handshake
-from mod_pywebsocket import http_header_util
-from mod_pywebsocket import memorizingfile
-from mod_pywebsocket import util
+from pywebsocket3 import (
+ common,
+ dispatch,
+ handshake,
+ http_header_util,
+ memorizingfile,
+ util
+)
# 1024 is practically large enough to contain WebSocket handshake lines.
_MAX_MEMORIZED_LINES = 1024
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/server_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/server_util.py
index 8f9e273e97..3bf07f885b 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/server_util.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/server_util.py
@@ -33,8 +33,7 @@ import logging.handlers
import threading
import time
-from mod_pywebsocket import common
-from mod_pywebsocket import util
+from pywebsocket3 import common, util
def _get_logger_from_class(c):
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/standalone.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/standalone.py
index bd32158790..0c324c4221 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/standalone.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/standalone.py
@@ -38,7 +38,7 @@ BASIC USAGE
Go to the src directory and run
- $ python mod_pywebsocket/standalone.py [-p <ws_port>]
+ $ python pywebsocket3/standalone.py [-p <ws_port>]
[-w <websock_handlers>]
[-d <document_root>]
@@ -48,11 +48,11 @@ Go to the src directory and run
<websock_handlers> is the path to the root directory of WebSocket handlers.
If not specified, <document_root> will be used. See __init__.py (or
-run $ pydoc mod_pywebsocket) for how to write WebSocket handlers.
+run $ pydoc pywebsocket3) for how to write WebSocket handlers.
For more detail and other options, run
- $ python mod_pywebsocket/standalone.py --help
+ $ python pywebsocket3/standalone.py --help
or see _build_option_parser method below.
@@ -66,7 +66,7 @@ Go to the src directory and run standalone.py with -d option to set the
document root to the directory containing example HTMLs and handlers like this:
$ cd src
- $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example
+ $ PYTHONPATH=. python pywebsocket3/standalone.py -d example
to launch pywebsocket with the sample handler and html on port 80. Open
http://localhost/console.html, click the connect button, type something into
@@ -85,7 +85,7 @@ TLS connection silently fails while pyOpenSSL fails on startup.
Example:
- $ PYTHONPATH=. python mod_pywebsocket/standalone.py \
+ $ PYTHONPATH=. python pywebsocket3/standalone.py \
-d example \
-p 10443 \
-t \
@@ -105,7 +105,7 @@ TLS support.
Example:
- $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example -p 10443 -t \
+ $ PYTHONPATH=. python pywebsocket3/standalone.py -d example -p 10443 -t \
-c ../test/cert/cert.pem -k ../test/cert/key.pem \
--tls-client-auth \
--tls-client-ca=../test/cert/cacert.pem
@@ -154,19 +154,20 @@ used outside a firewall.
"""
from __future__ import absolute_import
-from six.moves import configparser
+
+import argparse
import base64
import logging
-import argparse
import os
-import six
import sys
import traceback
-from mod_pywebsocket import common
-from mod_pywebsocket import util
-from mod_pywebsocket import server_util
-from mod_pywebsocket.websocket_server import WebSocketServer
+import six
+from six.moves import configparser
+
+from pywebsocket3 import common, server_util, util
+from pywebsocket3.websocket_server import WebSocketServer
+
_DEFAULT_LOG_MAX_BYTES = 1024 * 256
_DEFAULT_LOG_BACKUP_COUNT = 5
@@ -480,8 +481,8 @@ def _main(args=None):
server = WebSocketServer(options)
server.serve_forever()
except Exception as e:
- logging.critical('mod_pywebsocket: %s' % e)
- logging.critical('mod_pywebsocket: %s' % traceback.format_exc())
+ logging.critical('pywebsocket3: %s' % e)
+ logging.critical('pywebsocket3: %s' % traceback.format_exc())
sys.exit(1)
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/stream.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/stream.py
index 82d1ea619c..dd41850dc4 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/stream.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/stream.py
@@ -33,21 +33,22 @@ Specification:
http://tools.ietf.org/html/rfc6455
"""
-from collections import deque
import logging
import os
import struct
import time
-import socket
+from collections import deque
+
import six
-from mod_pywebsocket import common
-from mod_pywebsocket import util
-from mod_pywebsocket._stream_exceptions import BadOperationException
-from mod_pywebsocket._stream_exceptions import ConnectionTerminatedException
-from mod_pywebsocket._stream_exceptions import InvalidFrameException
-from mod_pywebsocket._stream_exceptions import InvalidUTF8Exception
-from mod_pywebsocket._stream_exceptions import UnsupportedFrameException
+from pywebsocket3 import common, util
+from pywebsocket3._stream_exceptions import (
+ BadOperationException,
+ ConnectionTerminatedException,
+ InvalidFrameException,
+ InvalidUTF8Exception,
+ UnsupportedFrameException
+)
_NOOP_MASKER = util.NoopMasker()
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/util.py
index 04006ecacd..9c25ab8315 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/util.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/util.py
@@ -29,20 +29,18 @@
"""WebSocket utilities."""
from __future__ import absolute_import
-import array
-import errno
+
import logging
import os
import re
-import six
-from six.moves import map
-from six.moves import range
-import socket
import struct
import zlib
+import six
+from six.moves import map, range
+
try:
- from mod_pywebsocket import fast_masking
+ from pywebsocket3 import fast_masking
except ImportError:
pass
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/websocket_server.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/websocket_server.py
index 9f67c9f02d..dab2f079ff 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/websocket_server.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/websocket_server.py
@@ -34,8 +34,7 @@ to use standalone.py, since it is intended to act as a skeleton of this module.
"""
from __future__ import absolute_import
-from six.moves import BaseHTTPServer
-from six.moves import socketserver
+
import logging
import re
import select
@@ -44,9 +43,10 @@ import ssl
import threading
import traceback
-from mod_pywebsocket import dispatch
-from mod_pywebsocket import util
-from mod_pywebsocket.request_handler import WebSocketRequestHandler
+from six.moves import BaseHTTPServer, socketserver
+
+from pywebsocket3 import dispatch, util
+from pywebsocket3.request_handler import WebSocketRequestHandler
def _alias_handlers(dispatcher, websock_handlers_map_file):
@@ -157,12 +157,13 @@ class WebSocketServer(socketserver.ThreadingMixIn, BaseHTTPServer.HTTPServer):
client_cert_ = ssl.CERT_REQUIRED
else:
client_cert_ = ssl.CERT_NONE
- socket_ = ssl.wrap_socket(
- socket_,
- keyfile=server_options.private_key,
- certfile=server_options.certificate,
- ca_certs=server_options.tls_client_ca,
- cert_reqs=client_cert_)
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+ ssl_context.verify_mode = client_cert_
+ ssl_context.load_cert_chain(keyfile=server_options.private_key,
+ certfile=server_options.certificate)
+ if client_cert_ != ssl.CERT_NONE:
+ ssl_context.load_verify_locations(cafile=server_options.tls_client_ca)
+ socket_ = ssl_context.wrap_socket(socket_, server_side=True)
self._sockets.append((socket_, addrinfo))
def server_bind(self):
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.cfg b/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.cfg
new file mode 100644
index 0000000000..8bfd5a12f8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.cfg
@@ -0,0 +1,4 @@
+[egg_info]
+tag_build =
+tag_date = 0
+
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.py
index 12c60d8617..ab9a24a3e7 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.py
@@ -28,7 +28,8 @@
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-"""Set up script for mod_pywebsocket.
+
+"""Set up script for pywebsocket3.
"""
from __future__ import absolute_import
@@ -36,7 +37,7 @@ from __future__ import print_function
from setuptools import setup, Extension
import sys
-_PACKAGE_NAME = 'mod_pywebsocket'
+_PACKAGE_NAME = 'pywebsocket3'
# Build and use a C++ extension for faster masking. SWIG is required.
_USE_FAST_MASKING = False
@@ -49,8 +50,8 @@ if sys.hexversion < 0x020700f0:
if _USE_FAST_MASKING:
setup(ext_modules=[
- Extension('mod_pywebsocket/_fast_masking',
- ['mod_pywebsocket/fast_masking.i'],
+ Extension('pywebsocket3/_fast_masking',
+ ['pywebsocket3/fast_masking.i'],
swig_opts=['-c++'])
])
@@ -58,16 +59,16 @@ setup(
author='Yuzo Fujishima',
author_email='yuzo@chromium.org',
description='Standalone WebSocket Server for testing purposes.',
- long_description=('mod_pywebsocket is a standalone server for '
+ long_description=('pywebsocket3 is a standalone server for '
'the WebSocket Protocol (RFC 6455). '
- 'See mod_pywebsocket/__init__.py for more detail.'),
+ 'See pywebsocket3/__init__.py for more detail.'),
license='See LICENSE',
name=_PACKAGE_NAME,
packages=[_PACKAGE_NAME, _PACKAGE_NAME + '.handshake'],
python_requires='>=2.7',
install_requires=['six'],
url='https://github.com/GoogleChromeLabs/pywebsocket3',
- version='3.0.2',
+ version='4.0.2',
)
# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cacert.pem b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cacert.pem
deleted file mode 100644
index 4dadae121b..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cacert.pem
+++ /dev/null
@@ -1,17 +0,0 @@
------BEGIN CERTIFICATE-----
-MIICvDCCAiWgAwIBAgIJAKqVghkGF1rSMA0GCSqGSIb3DQEBBQUAMEkxCzAJBgNV
-BAYTAkpQMQ4wDAYDVQQIEwVUb2t5bzEUMBIGA1UEChMLcHl3ZWJzb2NrZXQxFDAS
-BgNVBAMTC3B5d2Vic29ja2V0MB4XDTEyMDYwNjA3MjQzM1oXDTM5MTAyMzA3MjQz
-M1owSTELMAkGA1UEBhMCSlAxDjAMBgNVBAgTBVRva3lvMRQwEgYDVQQKEwtweXdl
-YnNvY2tldDEUMBIGA1UEAxMLcHl3ZWJzb2NrZXQwgZ8wDQYJKoZIhvcNAQEBBQAD
-gY0AMIGJAoGBAKoSEW2biQxVrMMKdn/8PJzDYiSXDPR9WQbLRRQ1Gm5jkCYiahXW
-u2CbTThfPPfi2NHA3I+HlT7gO9yR7RVUvN6ISUzGwXDEq4f4UNqtQOhQaqqK+CZ9
-LO/BhO/YYfNrbSPlYzHUKaT9ese7xO9VzVKLW+qUf2Mjh4/+SzxBDNP7AgMBAAGj
-gaswgagwHQYDVR0OBBYEFOsWdxCSuyhwaZeab6BoTho3++bzMHkGA1UdIwRyMHCA
-FOsWdxCSuyhwaZeab6BoTho3++bzoU2kSzBJMQswCQYDVQQGEwJKUDEOMAwGA1UE
-CBMFVG9reW8xFDASBgNVBAoTC3B5d2Vic29ja2V0MRQwEgYDVQQDEwtweXdlYnNv
-Y2tldIIJAKqVghkGF1rSMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEA
-gsMI1WEYqNw/jhUIdrTBcCxJ0X6hJvA9ziKANVm1Rs+4P3YDArkQ8bCr6xY+Kw7s
-Zp0yE7dM8GMdi+DU6hL3t3E5eMkTS1yZr9WCK4f2RLo+et98selZydpHemF3DJJ3
-gAj8Sx4LBaG8Cb/WnEMPv3MxG3fBE5favF6V4jU07hQ=
------END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cert.pem b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cert.pem
deleted file mode 100644
index 25379a72b0..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cert.pem
+++ /dev/null
@@ -1,61 +0,0 @@
-Certificate:
- Data:
- Version: 3 (0x2)
- Serial Number: 1 (0x1)
- Signature Algorithm: sha1WithRSAEncryption
- Issuer: C=JP, ST=Tokyo, O=pywebsocket, CN=pywebsocket
- Validity
- Not Before: Jun 6 07:25:08 2012 GMT
- Not After : Oct 23 07:25:08 2039 GMT
- Subject: C=JP, ST=Tokyo, O=pywebsocket, CN=pywebsocket
- Subject Public Key Info:
- Public Key Algorithm: rsaEncryption
- RSA Public Key: (1024 bit)
- Modulus (1024 bit):
- 00:de:10:ce:3a:5a:04:a4:1c:29:93:5c:23:82:1a:
- f2:06:01:e6:2b:a4:0f:dd:77:49:76:89:03:a2:21:
- de:04:75:c6:e2:dd:fb:35:27:3a:a2:92:8e:12:62:
- 2b:3e:1f:f4:78:df:b6:94:cb:27:d6:cb:d6:37:d7:
- 5c:08:f0:09:3e:c9:ce:24:2d:00:c9:df:4a:e0:99:
- e5:fb:23:a9:e2:d6:c9:3d:96:fa:01:88:de:5a:89:
- b0:cf:03:67:6f:04:86:1d:ef:62:1c:55:a9:07:9a:
- 2e:66:2a:73:5b:4c:62:03:f9:82:83:db:68:bf:b8:
- 4b:0b:8b:93:11:b8:54:73:7b
- Exponent: 65537 (0x10001)
- X509v3 extensions:
- X509v3 Basic Constraints:
- CA:FALSE
- Netscape Cert Type:
- SSL Server
- Netscape Comment:
- OpenSSL Generated Certificate
- X509v3 Subject Key Identifier:
- 82:A1:73:8B:16:0C:7C:E4:D3:46:95:13:95:1A:32:C1:84:E9:06:00
- X509v3 Authority Key Identifier:
- keyid:EB:16:77:10:92:BB:28:70:69:97:9A:6F:A0:68:4E:1A:37:FB:E6:F3
-
- Signature Algorithm: sha1WithRSAEncryption
- 6b:b3:46:29:02:df:b0:c8:8e:c4:d7:7f:a0:1e:0d:1a:eb:2f:
- df:d1:48:57:36:5f:95:8c:1b:f0:51:d6:52:e7:8d:84:3b:9f:
- d8:ed:22:9c:aa:bd:ee:9b:90:1d:84:a3:4c:0b:cb:eb:64:73:
- ba:f7:15:ce:da:5f:db:8b:15:07:a6:28:7f:b9:8c:11:9b:64:
- d3:f1:be:52:4f:c3:d8:58:fe:de:56:63:63:3b:51:ed:a7:81:
- f9:05:51:70:63:32:09:0e:94:7e:05:fe:a1:56:18:34:98:d5:
- 99:1e:4e:27:38:89:90:6a:e5:ce:60:35:01:f5:de:34:60:b1:
- cb:ae
------BEGIN CERTIFICATE-----
-MIICmDCCAgGgAwIBAgIBATANBgkqhkiG9w0BAQUFADBJMQswCQYDVQQGEwJKUDEO
-MAwGA1UECBMFVG9reW8xFDASBgNVBAoTC3B5d2Vic29ja2V0MRQwEgYDVQQDEwtw
-eXdlYnNvY2tldDAeFw0xMjA2MDYwNzI1MDhaFw0zOTEwMjMwNzI1MDhaMEkxCzAJ
-BgNVBAYTAkpQMQ4wDAYDVQQIEwVUb2t5bzEUMBIGA1UEChMLcHl3ZWJzb2NrZXQx
-FDASBgNVBAMTC3B5d2Vic29ja2V0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
-gQDeEM46WgSkHCmTXCOCGvIGAeYrpA/dd0l2iQOiId4Edcbi3fs1Jzqiko4SYis+
-H/R437aUyyfWy9Y311wI8Ak+yc4kLQDJ30rgmeX7I6ni1sk9lvoBiN5aibDPA2dv
-BIYd72IcVakHmi5mKnNbTGID+YKD22i/uEsLi5MRuFRzewIDAQABo4GPMIGMMAkG
-A1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQDAgZAMCwGCWCGSAGG+EIBDQQfFh1PcGVu
-U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUgqFzixYMfOTTRpUT
-lRoywYTpBgAwHwYDVR0jBBgwFoAU6xZ3EJK7KHBpl5pvoGhOGjf75vMwDQYJKoZI
-hvcNAQEFBQADgYEAa7NGKQLfsMiOxNd/oB4NGusv39FIVzZflYwb8FHWUueNhDuf
-2O0inKq97puQHYSjTAvL62RzuvcVztpf24sVB6Yof7mMEZtk0/G+Uk/D2Fj+3lZj
-YztR7aeB+QVRcGMyCQ6UfgX+oVYYNJjVmR5OJziJkGrlzmA1AfXeNGCxy64=
------END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/key.pem b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/key.pem
deleted file mode 100644
index fae858318f..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/key.pem
+++ /dev/null
@@ -1,15 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIICXgIBAAKBgQDeEM46WgSkHCmTXCOCGvIGAeYrpA/dd0l2iQOiId4Edcbi3fs1
-Jzqiko4SYis+H/R437aUyyfWy9Y311wI8Ak+yc4kLQDJ30rgmeX7I6ni1sk9lvoB
-iN5aibDPA2dvBIYd72IcVakHmi5mKnNbTGID+YKD22i/uEsLi5MRuFRzewIDAQAB
-AoGBAIuCuV1Vcnb7rm8CwtgZP5XgmY8vSjxTldafa6XvawEYUTP0S77v/1llg1Yv
-UIV+I+PQgG9oVoYOl22LoimHS/Z3e1fsot5tDYszGe8/Gkst4oaReSoxvBUa6WXp
-QSo7YFCajuHtE+W/gzF+UHbdzzXIDjQZ314LNF5t+4UnsEPBAkEA+girImqWoM2t
-3UR8f8oekERwsmEMf9DH5YpH4cvUnvI+kwesC/r2U8Sho++fyEMUNm7aIXGqNLga
-ogAM+4NX4QJBAONdSxSay22egTGNoIhLndljWkuOt/9FWj2klf/4QxD4blMJQ5Oq
-QdOGAh7nVQjpPLQ5D7CBVAKpGM2CD+QJBtsCQEP2kz35pxPylG3urcC2mfQxBkkW
-ZCViBNP58GwJ0bOauTOSBEwFXWuLqTw8aDwxL49UNmqc0N0fpe2fAehj3UECQQCm
-FH/DjU8Lw7ybddjNtm6XXPuYNagxz3cbkB4B3FchDleIUDwMoVF0MW9bI5/54mV1
-QDk1tUKortxvQZJaAD4BAkEAhGOHQqPd6bBBoFBvpaLzPJMxwLKrB+Wtkq/QlC72
-ClRiMn2g8SALiIL3BDgGXKcKE/Wy7jo/af/JCzQ/cPqt/A==
------END RSA PRIVATE KEY-----
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py
index a45e8f5cf2..6275676371 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py
@@ -33,8 +33,8 @@
This module contains helper methods for performing handshake, frame
sending/receiving as a WebSocket client.
-This is code for testing mod_pywebsocket. Keep this code independent from
-mod_pywebsocket. Don't import e.g. Stream class for generating frame for
+This is code for testing pywebsocket3. Keep this code independent from
+pywebsocket3. Don't import e.g. Stream class for generating frame for
testing. Using util.hexify, etc. that are not related to protocol processing
is allowed.
@@ -43,22 +43,20 @@ This code is far from robust, e.g., we cut corners in handshake.
"""
from __future__ import absolute_import
+
import base64
import errno
-import logging
import os
-import random
import re
import socket
import struct
import time
from hashlib import sha1
-from six import iterbytes
-from six import indexbytes
-from mod_pywebsocket import common
-from mod_pywebsocket import util
-from mod_pywebsocket.handshake import HandshakeException
+from six import indexbytes, iterbytes
+
+from pywebsocket3 import common, util
+from pywebsocket3.handshake import HandshakeException
DEFAULT_PORT = 80
DEFAULT_SECURE_PORT = 443
@@ -702,15 +700,15 @@ class Client(object):
try:
read_data = receive_bytes(self._socket, 1)
except Exception as e:
- if str(e).find(
- 'Connection closed before receiving requested length '
- ) == 0:
+ if str(e).find('Connection closed before receiving requested length ') == 0:
return
+
try:
- error_number, message = e
for error_name in ['ECONNRESET', 'WSAECONNRESET']:
- if (error_name in dir(errno)
- and error_number == getattr(errno, error_name)):
+ if (
+ error_name in dir(errno) and
+ e.errno == getattr(errno, error_name)
+ ):
return
except:
raise e
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py
index eeaef52ecf..c460d9b7f0 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py
@@ -30,17 +30,14 @@
"""
from __future__ import absolute_import
-import six.moves.queue
-import threading
-import struct
-import six
-from mod_pywebsocket import common
-from mod_pywebsocket import util
-from mod_pywebsocket.stream import Stream
-from mod_pywebsocket.stream import StreamOptions
+import six
+import six.moves.queue
from six.moves import range
+from pywebsocket3 import common, util
+from pywebsocket3.stream import Stream, StreamOptions
+
class _MockConnBase(object):
"""Base class of mocks for mod_python.apache.mp_conn.
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py
index ea52223cea..569bdb4c07 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py
@@ -31,7 +31,7 @@
"""Run all tests in the same directory.
This suite is expected to be run under pywebsocket's src directory, i.e. the
-directory containing mod_pywebsocket, test, etc.
+directory containing pywebsocket3, test, etc.
To change loggin level, please specify --log-level option.
python test/run_test.py --log-level debug
@@ -42,14 +42,16 @@ example, run this for making the test runner verbose.
"""
from __future__ import absolute_import
-import logging
+
import argparse
+import logging
import os
import re
-import six
import sys
import unittest
+import six
+
_TEST_MODULE_PATTERN = re.compile(r'^(test_.+)\.py$')
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py
index 48d0e116a5..c35cb6f972 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py
@@ -28,14 +28,15 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Configuration for testing.
-Test files should import this module before mod_pywebsocket.
+Test files should import this module before pywebsocket3.
"""
from __future__ import absolute_import
+
import os
import sys
-# Add the parent directory to sys.path to enable importing mod_pywebsocket.
+# Add the parent directory to sys.path to enable importing pywebsocket3.
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py
index 132dd92d76..18a39d1af9 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py
@@ -31,15 +31,16 @@
"""Tests for dispatch module."""
from __future__ import absolute_import
+
import os
import unittest
-import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+from six.moves import zip
-from mod_pywebsocket import dispatch
-from mod_pywebsocket import handshake
+import set_sys_path # Update sys.path to locate pywebsocket3 module.
+from pywebsocket3 import dispatch, handshake
from test import mock
-from six.moves import zip
+
_TEST_HANDLERS_DIR = os.path.join(os.path.dirname(__file__), 'testdata',
'handlers')
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py
index 2789e4a57e..9718c6a2b2 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py
@@ -28,23 +28,23 @@
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-"""End-to-end tests for pywebsocket. Tests standalone.py.
+"""End-to-end tests for pywebsocket3. Tests standalone.py.
"""
from __future__ import absolute_import
-from six.moves import urllib
+
import locale
import logging
import os
-import signal
import socket
import subprocess
import sys
import time
import unittest
-import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+from six.moves import urllib
+import set_sys_path # Update sys.path to locate pywebsocket3 module.
from test import client_for_testing
# Special message that tells the echo server to start closing handshake
@@ -137,7 +137,7 @@ class EndToEndTestBase(unittest.TestCase):
self.server_stderr = None
self.top_dir = os.path.join(os.path.dirname(__file__), '..')
os.putenv('PYTHONPATH', os.path.pathsep.join(sys.path))
- self.standalone_command = os.path.join(self.top_dir, 'mod_pywebsocket',
+ self.standalone_command = os.path.join(self.top_dir, 'pywebsocket3',
'standalone.py')
self.document_root = os.path.join(self.top_dir, 'example')
s = socket.socket()
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py
index 39a111888b..008df24827 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py
@@ -31,13 +31,12 @@
"""Tests for extensions module."""
from __future__ import absolute_import
+
import unittest
import zlib
-import set_sys_path # Update sys.path to locate mod_pywebsocket module.
-
-from mod_pywebsocket import common
-from mod_pywebsocket import extensions
+import set_sys_path # Update sys.path to locate pywebsocket3 module.
+from pywebsocket3 import common, extensions
class ExtensionsTest(unittest.TestCase):
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py
index 7f4acf56ff..c8e25ab099 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py
@@ -31,16 +31,17 @@
"""Tests for handshake.base module."""
from __future__ import absolute_import
-import unittest
-import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+import unittest
-from mod_pywebsocket.common import ExtensionParameter
-from mod_pywebsocket.common import ExtensionParsingException
-from mod_pywebsocket.common import format_extensions
-from mod_pywebsocket.common import parse_extensions
-from mod_pywebsocket.handshake.base import HandshakeException
-from mod_pywebsocket.handshake.base import validate_subprotocol
+import set_sys_path # Update sys.path to locate pywebsocket3 module.
+from pywebsocket3.common import (
+ ExtensionParameter,
+ ExtensionParsingException,
+ format_extensions,
+ parse_extensions,
+)
+from pywebsocket3.handshake.base import HandshakeException, validate_subprotocol
class ValidateSubprotocolTest(unittest.TestCase):
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py
index 8c65822170..ee63ed45b4 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py
@@ -31,15 +31,17 @@
"""Tests for handshake module."""
from __future__ import absolute_import
-import unittest
-import set_sys_path # Update sys.path to locate mod_pywebsocket module.
-from mod_pywebsocket import common
-from mod_pywebsocket.handshake.base import AbortedByUserException
-from mod_pywebsocket.handshake.base import HandshakeException
-from mod_pywebsocket.handshake.base import VersionException
-from mod_pywebsocket.handshake.hybi import Handshaker
+import unittest
+import set_sys_path # Update sys.path to locate pywebsocket3 module.
+from pywebsocket3 import common
+from pywebsocket3.handshake.base import (
+ AbortedByUserException,
+ HandshakeException,
+ VersionException,
+)
+from pywebsocket3.handshake.hybi import Handshaker
from test import mock
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py
index f8c8e7a981..bd9b9bfc2e 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py
@@ -31,10 +31,11 @@
"""Tests for http_header_util module."""
from __future__ import absolute_import
+
import unittest
import sys
-from mod_pywebsocket import http_header_util
+from pywebsocket3 import http_header_util
class UnitTest(unittest.TestCase):
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py
index f7288c510b..4749085962 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py
@@ -31,12 +31,13 @@
"""Tests for memorizingfile module."""
from __future__ import absolute_import
+
import unittest
-import six
-import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+import six
-from mod_pywebsocket import memorizingfile
+import set_sys_path # Update sys.path to locate pywebsocket3 module.
+from pywebsocket3 import memorizingfile
class UtilTest(unittest.TestCase):
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py
index 073873dde9..df5353bc59 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py
@@ -31,12 +31,13 @@
"""Tests for mock module."""
from __future__ import absolute_import
-import six.moves.queue
+
import threading
import unittest
-import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+import six.moves.queue
+import set_sys_path # Update sys.path to locate pywebsocket3 module.
from test import mock
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py
index 1122c281b7..99aa200ba4 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py
@@ -33,26 +33,26 @@
from __future__ import absolute_import
from __future__ import print_function
from __future__ import division
-import array
-import six.moves.queue
+
import random
import struct
import unittest
import zlib
-import set_sys_path # Update sys.path to locate mod_pywebsocket module.
-
-from mod_pywebsocket import common
-from mod_pywebsocket.extensions import PerMessageDeflateExtensionProcessor
-from mod_pywebsocket import msgutil
-from mod_pywebsocket.stream import InvalidUTF8Exception
-from mod_pywebsocket.stream import Stream
-from mod_pywebsocket.stream import StreamOptions
-from mod_pywebsocket import util
-from test import mock
+from six import iterbytes
from six.moves import map
from six.moves import range
-from six import iterbytes
+import six.moves.queue
+
+import set_sys_path # Update sys.path to locate pywebsocket3 module.
+from pywebsocket3 import common, msgutil, util
+from pywebsocket3.extensions import PerMessageDeflateExtensionProcessor
+from pywebsocket3.stream import (
+ InvalidUTF8Exception,
+ Stream,
+ StreamOptions,
+)
+from test import mock
# We use one fixed nonce for testing instead of cryptographically secure PRNG.
_MASKING_NONCE = b'ABCD'
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py
index 153899d205..c165e84688 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py
@@ -31,12 +31,11 @@
"""Tests for stream module."""
from __future__ import absolute_import
-import unittest
-import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+import unittest
-from mod_pywebsocket import common
-from mod_pywebsocket import stream
+import set_sys_path # Update sys.path to locate pywebsocket3 module.
+from pywebsocket3 import common, stream
class StreamTest(unittest.TestCase):
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py
index bf4bd32bba..24c4b5bfbd 100755
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py
@@ -32,18 +32,16 @@
from __future__ import absolute_import
from __future__ import print_function
+
import os
import random
-import sys
import unittest
-import struct
-
-import set_sys_path # Update sys.path to locate mod_pywebsocket module.
-from mod_pywebsocket import util
+from six import int2byte, PY3
from six.moves import range
-from six import PY3
-from six import int2byte
+
+import set_sys_path # Update sys.path to locate pywebsocket3 module.
+from pywebsocket3 import util
_TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'testdata')
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/README b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/README
deleted file mode 100644
index c001aa5595..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/README
+++ /dev/null
@@ -1 +0,0 @@
-Test data directory
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py
index 63cb541bb7..a6e0831847 100644
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py
@@ -27,7 +27,7 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-from mod_pywebsocket import handshake
+from pywebsocket3 import handshake
def web_socket_do_extra_handshake(request):
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/hello.pl b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/hello.pl
deleted file mode 100644
index 882ef5a100..0000000000
--- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/hello.pl
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/perl -wT
-#
-# Copyright 2012, Google Inc.
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-# * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-# * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-print "Hello\n";
diff --git a/testing/web-platform/tests/tools/third_party/websockets/.github/FUNDING.yml b/testing/web-platform/tests/tools/third_party/websockets/.github/FUNDING.yml
new file mode 100644
index 0000000000..c6c5426a5a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+github: python-websockets
+open_collective: websockets
+tidelift: pypi/websockets
diff --git a/testing/web-platform/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/config.yml b/testing/web-platform/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..3ba13e0cec
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: false
diff --git a/testing/web-platform/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/issue.md b/testing/web-platform/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/issue.md
new file mode 100644
index 0000000000..3cf4e3b770
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/issue.md
@@ -0,0 +1,29 @@
+---
+name: Report an issue
+about: Let us know about a problem with websockets
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+<!--
+
+Thanks for taking the time to report an issue!
+
+Did you check the FAQ? Perhaps you'll find the answer you need:
+https://websockets.readthedocs.io/en/stable/faq/index.html
+
+Is your question really about asyncio? Perhaps the dev guide will help:
+https://docs.python.org/3/library/asyncio-dev.html
+
+Did you look for similar issues? Please keep the discussion in one place :-)
+https://github.com/python-websockets/websockets/issues?q=is%3Aissue
+
+Is your issue related to cryptocurrency in any way? Please don't file it.
+https://websockets.readthedocs.io/en/stable/project/contributing.html#cryptocurrency-users
+
+For bugs, providing a reproduction helps a lot. Take an existing example and tweak it!
+https://github.com/python-websockets/websockets/tree/main/example
+
+-->
diff --git a/testing/web-platform/tests/tools/third_party/websockets/.github/dependabot.yml b/testing/web-platform/tests/tools/third_party/websockets/.github/dependabot.yml
new file mode 100644
index 0000000000..ad1e824b4a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/.github/dependabot.yml
@@ -0,0 +1,9 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "saturday"
+ time: "07:00"
+ timezone: "Europe/Paris"
diff --git a/testing/web-platform/tests/tools/third_party/websockets/.github/workflows/tests.yml b/testing/web-platform/tests/tools/third_party/websockets/.github/workflows/tests.yml
new file mode 100644
index 0000000000..470f5bc960
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/.github/workflows/tests.yml
@@ -0,0 +1,83 @@
+name: Run tests
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+env:
+ WEBSOCKETS_TESTS_TIMEOUT_FACTOR: 10
+
+jobs:
+ coverage:
+ name: Run test coverage checks
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Python 3.x
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.x"
+ - name: Install tox
+ run: pip install tox
+ - name: Run tests with coverage
+ run: tox -e coverage
+ - name: Run tests with per-module coverage
+ run: tox -e maxi_cov
+
+ quality:
+ name: Run code quality checks
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Python 3.x
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.x"
+ - name: Install tox
+ run: pip install tox
+ - name: Check code formatting
+ run: tox -e black
+ - name: Check code style
+ run: tox -e ruff
+ - name: Check types statically
+ run: tox -e mypy
+
+ matrix:
+ name: Run tests on Python ${{ matrix.python }}
+ needs:
+ - coverage
+ - quality
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python:
+ - "3.8"
+ - "3.9"
+ - "3.10"
+ - "3.11"
+ - "pypy-3.8"
+ - "pypy-3.9"
+ is_main:
+ - ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
+ exclude:
+ - python: "pypy-3.8"
+ is_main: false
+ - python: "pypy-3.9"
+ is_main: false
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Python ${{ matrix.python }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python }}
+ - name: Install tox
+ run: pip install tox
+ - name: Run tests
+ run: tox -e py
diff --git a/testing/web-platform/tests/tools/third_party/websockets/.github/workflows/wheels.yml b/testing/web-platform/tests/tools/third_party/websockets/.github/workflows/wheels.yml
new file mode 100644
index 0000000000..707ef2c60d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/.github/workflows/wheels.yml
@@ -0,0 +1,88 @@
+name: Build wheels
+
+on:
+ push:
+ tags:
+ - '*'
+ workflow_dispatch:
+
+jobs:
+ sdist:
+ name: Build source distribution and architecture-independent wheel
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Python 3.x
+ uses: actions/setup-python@v4
+ with:
+ python-version: 3.x
+ - name: Build sdist
+ run: python setup.py sdist
+ - name: Save sdist
+ uses: actions/upload-artifact@v3
+ with:
+ path: dist/*.tar.gz
+ - name: Install wheel
+ run: pip install wheel
+ - name: Build wheel
+ env:
+ BUILD_EXTENSION: no
+ run: python setup.py bdist_wheel
+ - name: Save wheel
+ uses: actions/upload-artifact@v3
+ with:
+ path: dist/*.whl
+
+ wheels:
+ name: Build architecture-specific wheels on ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os:
+ - ubuntu-latest
+ - windows-latest
+ - macOS-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Python 3.x
+ uses: actions/setup-python@v4
+ with:
+ python-version: 3.x
+ - name: Set up QEMU
+ if: runner.os == 'Linux'
+ uses: docker/setup-qemu-action@v3
+ with:
+ platforms: all
+ - name: Build wheels
+ uses: pypa/cibuildwheel@v2.16.2
+ env:
+ BUILD_EXTENSION: yes
+ - name: Save wheels
+ uses: actions/upload-artifact@v3
+ with:
+ path: wheelhouse/*.whl
+
+ release:
+ name: Release
+ needs:
+ - sdist
+ - wheels
+ runs-on: ubuntu-latest
+ # Don't release when running the workflow manually from GitHub's UI.
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
+ steps:
+ - name: Download artifacts
+ uses: actions/download-artifact@v3
+ with:
+ name: artifact
+ path: dist
+ - name: Upload to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ password: ${{ secrets.PYPI_API_TOKEN }}
+ - name: Create GitHub release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: gh release create ${{ github.ref_name }} --notes "See https://websockets.readthedocs.io/en/stable/project/changelog.html for details."
diff --git a/testing/web-platform/tests/tools/third_party/websockets/.gitignore b/testing/web-platform/tests/tools/third_party/websockets/.gitignore
new file mode 100644
index 0000000000..324e77069a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/.gitignore
@@ -0,0 +1,16 @@
+*.pyc
+*.so
+.coverage
+.direnv
+.envrc
+.idea/
+.mypy_cache
+.tox
+build/
+compliance/reports/
+experiments/compression/corpus.pkl
+dist/
+docs/_build/
+htmlcov/
+MANIFEST
+websockets.egg-info/
diff --git a/testing/web-platform/tests/tools/third_party/websockets/.readthedocs.yml b/testing/web-platform/tests/tools/third_party/websockets/.readthedocs.yml
new file mode 100644
index 0000000000..0369e06565
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/.readthedocs.yml
@@ -0,0 +1,13 @@
+version: 2
+
+build:
+ os: ubuntu-20.04
+ tools:
+ python: "3.10"
+
+sphinx:
+ configuration: docs/conf.py
+
+python:
+ install:
+ - requirements: docs/requirements.txt
diff --git a/testing/web-platform/tests/tools/third_party/websockets/CODE_OF_CONDUCT.md b/testing/web-platform/tests/tools/third_party/websockets/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..80f80d51b1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/CODE_OF_CONDUCT.md
@@ -0,0 +1,46 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at aymeric DOT augustin AT fractalideas DOT com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/testing/web-platform/tests/tools/third_party/websockets/LICENSE b/testing/web-platform/tests/tools/third_party/websockets/LICENSE
index 119b29ef35..5d61ece22a 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/LICENSE
+++ b/testing/web-platform/tests/tools/third_party/websockets/LICENSE
@@ -1,5 +1,4 @@
-Copyright (c) 2013-2021 Aymeric Augustin and contributors.
-All rights reserved.
+Copyright (c) Aymeric Augustin and contributors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
@@ -9,9 +8,9 @@ modification, are permitted provided that the following conditions are met:
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- * Neither the name of websockets nor the names of its contributors may
- be used to endorse or promote products derived from this software without
- specific prior written permission.
+ * Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
diff --git a/testing/web-platform/tests/tools/third_party/websockets/Makefile b/testing/web-platform/tests/tools/third_party/websockets/Makefile
new file mode 100644
index 0000000000..cf3b533939
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/Makefile
@@ -0,0 +1,35 @@
+.PHONY: default style types tests coverage maxi_cov build clean
+
+export PYTHONASYNCIODEBUG=1
+export PYTHONPATH=src
+export PYTHONWARNINGS=default
+
+default: style types tests
+
+style:
+ black src tests
+ ruff --fix src tests
+
+types:
+ mypy --strict src
+
+tests:
+ python -m unittest
+
+coverage:
+ coverage run --source src/websockets,tests -m unittest
+ coverage html
+ coverage report --show-missing --fail-under=100
+
+maxi_cov:
+ python tests/maxi_cov.py
+ coverage html
+ coverage report --show-missing --fail-under=100
+
+build:
+ python setup.py build_ext --inplace
+
+clean:
+ find . -name '*.pyc' -o -name '*.so' -delete
+ find . -name __pycache__ -delete
+ rm -rf .coverage .mypy_cache build compliance/reports dist docs/_build htmlcov MANIFEST src/websockets.egg-info
diff --git a/testing/web-platform/tests/tools/third_party/websockets/PKG-INFO b/testing/web-platform/tests/tools/third_party/websockets/PKG-INFO
deleted file mode 100644
index 3b042a3f9f..0000000000
--- a/testing/web-platform/tests/tools/third_party/websockets/PKG-INFO
+++ /dev/null
@@ -1,174 +0,0 @@
-Metadata-Version: 2.1
-Name: websockets
-Version: 10.3
-Summary: An implementation of the WebSocket Protocol (RFC 6455 & 7692)
-Home-page: https://github.com/aaugustin/websockets
-Author: Aymeric Augustin
-Author-email: aymeric.augustin@m4x.org
-License: BSD
-Project-URL: Changelog, https://websockets.readthedocs.io/en/stable/project/changelog.html
-Project-URL: Documentation, https://websockets.readthedocs.io/
-Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme
-Project-URL: Tracker, https://github.com/aaugustin/websockets/issues
-Platform: UNKNOWN
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Environment :: Web Environment
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: BSD License
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Requires-Python: >=3.7
-License-File: LICENSE
-
-.. image:: logo/horizontal.svg
- :width: 480px
- :alt: websockets
-
-|licence| |version| |pyversions| |wheel| |tests| |docs|
-
-.. |licence| image:: https://img.shields.io/pypi/l/websockets.svg
- :target: https://pypi.python.org/pypi/websockets
-
-.. |version| image:: https://img.shields.io/pypi/v/websockets.svg
- :target: https://pypi.python.org/pypi/websockets
-
-.. |pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg
- :target: https://pypi.python.org/pypi/websockets
-
-.. |wheel| image:: https://img.shields.io/pypi/wheel/websockets.svg
- :target: https://pypi.python.org/pypi/websockets
-
-.. |tests| image:: https://img.shields.io/github/checks-status/aaugustin/websockets/main
- :target: https://github.com/aaugustin/websockets/actions/workflows/tests.yml
-
-.. |docs| image:: https://img.shields.io/readthedocs/websockets.svg
- :target: https://websockets.readthedocs.io/
-
-What is ``websockets``?
------------------------
-
-websockets is a library for building WebSocket_ servers and clients in Python
-with a focus on correctness, simplicity, robustness, and performance.
-
-.. _WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
-
-Built on top of ``asyncio``, Python's standard asynchronous I/O framework, it
-provides an elegant coroutine-based API.
-
-`Documentation is available on Read the Docs. <https://websockets.readthedocs.io/>`_
-
-Here's how a client sends and receives messages:
-
-.. copy-pasted because GitHub doesn't support the include directive
-
-.. code:: python
-
- #!/usr/bin/env python
-
- import asyncio
- from websockets import connect
-
- async def hello(uri):
- async with connect(uri) as websocket:
- await websocket.send("Hello world!")
- await websocket.recv()
-
- asyncio.run(hello("ws://localhost:8765"))
-
-And here's an echo server:
-
-.. code:: python
-
- #!/usr/bin/env python
-
- import asyncio
- from websockets import serve
-
- async def echo(websocket):
- async for message in websocket:
- await websocket.send(message)
-
- async def main():
- async with serve(echo, "localhost", 8765):
- await asyncio.Future() # run forever
-
- asyncio.run(main())
-
-Does that look good?
-
-`Get started with the tutorial! <https://websockets.readthedocs.io/en/stable/intro/index.html>`_
-
-Why should I use ``websockets``?
---------------------------------
-
-The development of ``websockets`` is shaped by four principles:
-
-1. **Correctness**: ``websockets`` is heavily tested for compliance
- with :rfc:`6455`. Continuous integration fails under 100% branch
- coverage.
-
-2. **Simplicity**: all you need to understand is ``msg = await ws.recv()`` and
- ``await ws.send(msg)``. ``websockets`` takes care of managing connections
- so you can focus on your application.
-
-3. **Robustness**: ``websockets`` is built for production. For example, it was
- the only library to `handle backpressure correctly`_ before the issue
- became widely known in the Python community.
-
-4. **Performance**: memory usage is optimized and configurable. A C extension
- accelerates expensive operations. It's pre-compiled for Linux, macOS and
- Windows and packaged in the wheel format for each system and Python version.
-
-Documentation is a first class concern in the project. Head over to `Read the
-Docs`_ and see for yourself.
-
-.. _Read the Docs: https://websockets.readthedocs.io/
-.. _handle backpressure correctly: https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/#websocket-servers
-
-Why shouldn't I use ``websockets``?
------------------------------------
-
-* If you prefer callbacks over coroutines: ``websockets`` was created to
- provide the best coroutine-based API to manage WebSocket connections in
- Python. Pick another library for a callback-based API.
-
-* If you're looking for a mixed HTTP / WebSocket library: ``websockets`` aims
- at being an excellent implementation of :rfc:`6455`: The WebSocket Protocol
- and :rfc:`7692`: Compression Extensions for WebSocket. Its support for HTTP
- is minimal — just enough for a HTTP health check.
-
- If you want to do both in the same server, look at HTTP frameworks that
- build on top of ``websockets`` to support WebSocket connections, like
- Sanic_.
-
-.. _Sanic: https://sanicframework.org/en/
-
-What else?
-----------
-
-Bug reports, patches and suggestions are welcome!
-
-To report a security vulnerability, please use the `Tidelift security
-contact`_. Tidelift will coordinate the fix and disclosure.
-
-.. _Tidelift security contact: https://tidelift.com/security
-
-For anything else, please open an issue_ or send a `pull request`_.
-
-.. _issue: https://github.com/aaugustin/websockets/issues/new
-.. _pull request: https://github.com/aaugustin/websockets/compare/
-
-Participants must uphold the `Contributor Covenant code of conduct`_.
-
-.. _Contributor Covenant code of conduct: https://github.com/aaugustin/websockets/blob/main/CODE_OF_CONDUCT.md
-
-``websockets`` is released under the `BSD license`_.
-
-.. _BSD license: https://github.com/aaugustin/websockets/blob/main/LICENSE
-
-
diff --git a/testing/web-platform/tests/tools/third_party/websockets/README.rst b/testing/web-platform/tests/tools/third_party/websockets/README.rst
index 2b9a445ea5..870b208baa 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/README.rst
+++ b/testing/web-platform/tests/tools/third_party/websockets/README.rst
@@ -2,7 +2,7 @@
:width: 480px
:alt: websockets
-|licence| |version| |pyversions| |wheel| |tests| |docs|
+|licence| |version| |pyversions| |tests| |docs| |openssf|
.. |licence| image:: https://img.shields.io/pypi/l/websockets.svg
:target: https://pypi.python.org/pypi/websockets
@@ -13,15 +13,15 @@
.. |pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg
:target: https://pypi.python.org/pypi/websockets
-.. |wheel| image:: https://img.shields.io/pypi/wheel/websockets.svg
- :target: https://pypi.python.org/pypi/websockets
-
-.. |tests| image:: https://img.shields.io/github/checks-status/aaugustin/websockets/main
- :target: https://github.com/aaugustin/websockets/actions/workflows/tests.yml
+.. |tests| image:: https://img.shields.io/github/checks-status/python-websockets/websockets/main?label=tests
+ :target: https://github.com/python-websockets/websockets/actions/workflows/tests.yml
.. |docs| image:: https://img.shields.io/readthedocs/websockets.svg
:target: https://websockets.readthedocs.io/
+.. |openssf| image:: https://bestpractices.coreinfrastructure.org/projects/6475/badge
+ :target: https://bestpractices.coreinfrastructure.org/projects/6475
+
What is ``websockets``?
-----------------------
@@ -30,37 +30,24 @@ with a focus on correctness, simplicity, robustness, and performance.
.. _WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
-Built on top of ``asyncio``, Python's standard asynchronous I/O framework, it
-provides an elegant coroutine-based API.
+Built on top of ``asyncio``, Python's standard asynchronous I/O framework, the
+default implementation provides an elegant coroutine-based API.
-`Documentation is available on Read the Docs. <https://websockets.readthedocs.io/>`_
+An implementation on top of ``threading`` and a Sans-I/O implementation are also
+available.
-Here's how a client sends and receives messages:
+`Documentation is available on Read the Docs. <https://websockets.readthedocs.io/>`_
.. copy-pasted because GitHub doesn't support the include directive
-.. code:: python
-
- #!/usr/bin/env python
-
- import asyncio
- from websockets import connect
-
- async def hello(uri):
- async with connect(uri) as websocket:
- await websocket.send("Hello world!")
- await websocket.recv()
-
- asyncio.run(hello("ws://localhost:8765"))
-
-And here's an echo server:
+Here's an echo server with the ``asyncio`` API:
.. code:: python
#!/usr/bin/env python
import asyncio
- from websockets import serve
+ from websockets.server import serve
async def echo(websocket):
async for message in websocket:
@@ -72,6 +59,23 @@ And here's an echo server:
asyncio.run(main())
+Here's how a client sends and receives messages with the ``threading`` API:
+
+.. code:: python
+
+ #!/usr/bin/env python
+
+ from websockets.sync.client import connect
+
+ def hello():
+ with connect("ws://localhost:8765") as websocket:
+ websocket.send("Hello world!")
+ message = websocket.recv()
+ print(f"Received: {message}")
+
+ hello()
+
+
Does that look good?
`Get started with the tutorial! <https://websockets.readthedocs.io/en/stable/intro/index.html>`_
@@ -79,7 +83,7 @@ Does that look good?
.. raw:: html
<hr>
- <img align="left" height="150" width="150" src="https://raw.githubusercontent.com/aaugustin/websockets/main/logo/tidelift.png">
+ <img align="left" height="150" width="150" src="https://raw.githubusercontent.com/python-websockets/websockets/main/logo/tidelift.png">
<h3 align="center"><i>websockets for enterprise</i></h3>
<p align="center"><i>Available as part of the Tidelift Subscription</i></p>
<p align="center"><i>The maintainers of websockets and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. <a href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme">Learn more.</a></i></p>
@@ -91,9 +95,8 @@ Why should I use ``websockets``?
The development of ``websockets`` is shaped by four principles:
-1. **Correctness**: ``websockets`` is heavily tested for compliance
- with :rfc:`6455`. Continuous integration fails under 100% branch
- coverage.
+1. **Correctness**: ``websockets`` is heavily tested for compliance with
+ :rfc:`6455`. Continuous integration fails under 100% branch coverage.
2. **Simplicity**: all you need to understand is ``msg = await ws.recv()`` and
``await ws.send(msg)``. ``websockets`` takes care of managing connections
@@ -123,7 +126,7 @@ Why shouldn't I use ``websockets``?
* If you're looking for a mixed HTTP / WebSocket library: ``websockets`` aims
at being an excellent implementation of :rfc:`6455`: The WebSocket Protocol
and :rfc:`7692`: Compression Extensions for WebSocket. Its support for HTTP
- is minimal — just enough for a HTTP health check.
+ is minimal — just enough for an HTTP health check.
If you want to do both in the same server, look at HTTP frameworks that
build on top of ``websockets`` to support WebSocket connections, like
@@ -143,13 +146,13 @@ contact`_. Tidelift will coordinate the fix and disclosure.
For anything else, please open an issue_ or send a `pull request`_.
-.. _issue: https://github.com/aaugustin/websockets/issues/new
-.. _pull request: https://github.com/aaugustin/websockets/compare/
+.. _issue: https://github.com/python-websockets/websockets/issues/new
+.. _pull request: https://github.com/python-websockets/websockets/compare/
Participants must uphold the `Contributor Covenant code of conduct`_.
-.. _Contributor Covenant code of conduct: https://github.com/aaugustin/websockets/blob/main/CODE_OF_CONDUCT.md
+.. _Contributor Covenant code of conduct: https://github.com/python-websockets/websockets/blob/main/CODE_OF_CONDUCT.md
``websockets`` is released under the `BSD license`_.
-.. _BSD license: https://github.com/aaugustin/websockets/blob/main/LICENSE
+.. _BSD license: https://github.com/python-websockets/websockets/blob/main/LICENSE
diff --git a/testing/web-platform/tests/tools/third_party/websockets/SECURITY.md b/testing/web-platform/tests/tools/third_party/websockets/SECURITY.md
new file mode 100644
index 0000000000..175b20c589
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/SECURITY.md
@@ -0,0 +1,12 @@
+# Security
+
+## Policy
+
+Only the latest version receives security updates.
+
+## Contact information
+
+Please report security vulnerabilities to the
+[Tidelift security team](https://tidelift.com/security).
+
+Tidelift will coordinate the fix and disclosure.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/README.rst b/testing/web-platform/tests/tools/third_party/websockets/compliance/README.rst
new file mode 100644
index 0000000000..8570f9176d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/README.rst
@@ -0,0 +1,50 @@
+Autobahn Testsuite
+==================
+
+General information and installation instructions are available at
+https://github.com/crossbario/autobahn-testsuite.
+
+To improve performance, you should compile the C extension first::
+
+ $ python setup.py build_ext --inplace
+
+Running the test suite
+----------------------
+
+All commands below must be run from the directory containing this file.
+
+To test the server::
+
+ $ PYTHONPATH=.. python test_server.py
+ $ wstest -m fuzzingclient
+
+To test the client::
+
+ $ wstest -m fuzzingserver
+ $ PYTHONPATH=.. python test_client.py
+
+Run the first command in a shell. Run the second command in another shell.
+It should take about ten minutes to complete — wstest is the bottleneck.
+Then kill the first one with Ctrl-C.
+
+The test client or server shouldn't display any exceptions. The results are
+stored in reports/clients/index.html.
+
+Note that the Autobahn software only supports Python 2, while ``websockets``
+only supports Python 3; you need two different environments.
+
+Conformance notes
+-----------------
+
+Some test cases are more strict than the RFC. Given the implementation of the
+library and the test echo client or server, ``websockets`` gets a "Non-Strict"
+in these cases.
+
+In 3.2, 3.3, 4.1.3, 4.1.4, 4.2.3, 4.2.4, and 5.15 ``websockets`` notices the
+protocol error and closes the connection before it has had a chance to echo
+the previous frame.
+
+In 6.4.3 and 6.4.4, even though it uses an incremental decoder, ``websockets``
+doesn't notice the invalid utf-8 fast enough to get a "Strict" pass. These
+tests are more strict than the RFC.
+
diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingclient.json b/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingclient.json
new file mode 100644
index 0000000000..202ff49a03
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingclient.json
@@ -0,0 +1,11 @@
+
+{
+ "options": {"failByDrop": false},
+ "outdir": "./reports/servers",
+
+ "servers": [{"agent": "websockets", "url": "ws://localhost:8642", "options": {"version": 18}}],
+
+ "cases": ["*"],
+ "exclude-cases": [],
+ "exclude-agent-cases": {}
+}
diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingserver.json b/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingserver.json
new file mode 100644
index 0000000000..1bdb42723e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingserver.json
@@ -0,0 +1,12 @@
+
+{
+ "url": "ws://localhost:8642",
+
+ "options": {"failByDrop": false},
+ "outdir": "./reports/clients",
+ "webport": 8080,
+
+ "cases": ["*"],
+ "exclude-cases": [],
+ "exclude-agent-cases": {}
+}
diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/test_client.py b/testing/web-platform/tests/tools/third_party/websockets/compliance/test_client.py
new file mode 100644
index 0000000000..1ed4d711e9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/test_client.py
@@ -0,0 +1,48 @@
+import json
+import logging
+import urllib.parse
+
+import asyncio
+import websockets
+
+
+logging.basicConfig(level=logging.WARNING)
+
+# Uncomment this line to make only websockets more verbose.
+# logging.getLogger('websockets').setLevel(logging.DEBUG)
+
+
+SERVER = "ws://127.0.0.1:8642"
+AGENT = "websockets"
+
+
+async def get_case_count(server):
+ uri = f"{server}/getCaseCount"
+ async with websockets.connect(uri) as ws:
+ msg = ws.recv()
+ return json.loads(msg)
+
+
+async def run_case(server, case, agent):
+ uri = f"{server}/runCase?case={case}&agent={agent}"
+ async with websockets.connect(uri, max_size=2 ** 25, max_queue=1) as ws:
+ async for msg in ws:
+ await ws.send(msg)
+
+
+async def update_reports(server, agent):
+ uri = f"{server}/updateReports?agent={agent}"
+ async with websockets.connect(uri):
+ pass
+
+
+async def run_tests(server, agent):
+ cases = await get_case_count(server)
+ for case in range(1, cases + 1):
+ print(f"Running test case {case} out of {cases}", end="\r")
+ await run_case(server, case, agent)
+ print(f"Ran {cases} test cases ")
+ await update_reports(server, agent)
+
+
+asyncio.run(run_tests(SERVER, urllib.parse.quote(AGENT)))
diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/test_server.py b/testing/web-platform/tests/tools/third_party/websockets/compliance/test_server.py
new file mode 100644
index 0000000000..92f895d926
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/test_server.py
@@ -0,0 +1,29 @@
+import logging
+
+import asyncio
+import websockets
+
+
+logging.basicConfig(level=logging.WARNING)
+
+# Uncomment this line to make only websockets more verbose.
+# logging.getLogger('websockets').setLevel(logging.DEBUG)
+
+
+HOST, PORT = "127.0.0.1", 8642
+
+
+async def echo(ws):
+ async for msg in ws:
+ await ws.send(msg)
+
+
+async def main():
+ with websockets.serve(echo, HOST, PORT, max_size=2 ** 25, max_queue=1):
+ try:
+ await asyncio.Future()
+ except KeyboardInterrupt:
+ pass
+
+
+asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile b/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile
new file mode 100644
index 0000000000..0458706458
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile
@@ -0,0 +1,23 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+livehtml:
+ sphinx-autobuild --watch "$(SOURCEDIR)/../src" "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png b/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png
deleted file mode 100644
index 317dc4d985..0000000000
--- a/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png
+++ /dev/null
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py b/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py
new file mode 100644
index 0000000000..9d61dc7173
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py
@@ -0,0 +1,171 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+import datetime
+import importlib
+import inspect
+import os
+import subprocess
+import sys
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.join(os.path.abspath(".."), "src"))
+
+
+# -- Project information -----------------------------------------------------
+
+project = "websockets"
+copyright = f"2013-{datetime.date.today().year}, Aymeric Augustin and contributors"
+author = "Aymeric Augustin"
+
+from websockets.version import tag as version, version as release
+
+
+# -- General configuration ---------------------------------------------------
+
+nitpicky = True
+
+nitpick_ignore = [
+ # topics/design.rst discusses undocumented APIs
+ ("py:meth", "client.WebSocketClientProtocol.handshake"),
+ ("py:meth", "server.WebSocketServerProtocol.handshake"),
+ ("py:attr", "legacy.protocol.WebSocketCommonProtocol.is_client"),
+ ("py:attr", "legacy.protocol.WebSocketCommonProtocol.messages"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.close_connection"),
+ ("py:attr", "legacy.protocol.WebSocketCommonProtocol.close_connection_task"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.keepalive_ping"),
+ ("py:attr", "legacy.protocol.WebSocketCommonProtocol.keepalive_ping_task"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.transfer_data"),
+ ("py:attr", "legacy.protocol.WebSocketCommonProtocol.transfer_data_task"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.connection_open"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.ensure_open"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.fail_connection"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.connection_lost"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.read_message"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.write_frame"),
+]
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.linkcode",
+ "sphinx.ext.napoleon",
+ "sphinx_copybutton",
+ "sphinx_inline_tabs",
+ "sphinxcontrib.spelling",
+ "sphinxcontrib_trio",
+ "sphinxext.opengraph",
+]
+# It is currently inconvenient to install PyEnchant on Apple Silicon.
+try:
+ import sphinxcontrib.spelling
+except ImportError:
+ extensions.remove("sphinxcontrib.spelling")
+
+autodoc_typehints = "description"
+
+autodoc_typehints_description_target = "documented"
+
+# Workaround for https://github.com/sphinx-doc/sphinx/issues/9560
+from sphinx.domains.python import PythonDomain
+
+assert PythonDomain.object_types["data"].roles == ("data", "obj")
+PythonDomain.object_types["data"].roles = ("data", "class", "obj")
+
+intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
+
+spelling_show_suggestions = True
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+# Configure viewcode extension.
+from websockets.version import commit
+
+code_url = f"https://github.com/python-websockets/websockets/blob/{commit}"
+
+def linkcode_resolve(domain, info):
+ # Non-linkable objects from the starter kit in the tutorial.
+ if domain == "js" or info["module"] == "connect4":
+ return
+
+ assert domain == "py", "expected only Python objects"
+
+ mod = importlib.import_module(info["module"])
+ if "." in info["fullname"]:
+ objname, attrname = info["fullname"].split(".")
+ obj = getattr(mod, objname)
+ try:
+ # object is a method of a class
+ obj = getattr(obj, attrname)
+ except AttributeError:
+ # object is an attribute of a class
+ return None
+ else:
+ obj = getattr(mod, info["fullname"])
+
+ try:
+ file = inspect.getsourcefile(obj)
+ lines = inspect.getsourcelines(obj)
+ except TypeError:
+ # e.g. object is a typing.Union
+ return None
+ file = os.path.relpath(file, os.path.abspath(".."))
+ if not file.startswith("src/websockets"):
+ # e.g. object is a typing.NewType
+ return None
+ start, end = lines[1], lines[1] + len(lines[0]) - 1
+
+ return f"{code_url}/{file}#L{start}-L{end}"
+
+# Configure opengraph extension
+
+# Social cards don't support the SVG logo. Also, the text preview looks bad.
+ogp_social_cards = {"enable": False}
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = "furo"
+
+html_theme_options = {
+ "light_css_variables": {
+ "color-brand-primary": "#306998", # blue from logo
+ "color-brand-content": "#0b487a", # blue more saturated and less dark
+ },
+ "dark_css_variables": {
+ "color-brand-primary": "#ffd43bcc", # yellow from logo, more muted than content
+ "color-brand-content": "#ffd43bd9", # yellow from logo, transparent like text
+ },
+ "sidebar_hide_name": True,
+}
+
+html_logo = "_static/websockets.svg"
+
+html_favicon = "_static/favicon.ico"
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static"]
+
+html_copy_source = False
+
+html_show_sphinx = False
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/asyncio.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/asyncio.rst
new file mode 100644
index 0000000000..e77f50addd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/asyncio.rst
@@ -0,0 +1,69 @@
+Using asyncio
+=============
+
+.. currentmodule:: websockets
+
+How do I run two coroutines in parallel?
+----------------------------------------
+
+You must start two tasks, which the event loop will run concurrently. You can
+achieve this with :func:`asyncio.gather` or :func:`asyncio.create_task`.
+
+Keep track of the tasks and make sure they terminate or you cancel them when
+the connection terminates.
+
+Why does my program never receive any messages?
+-----------------------------------------------
+
+Your program runs a coroutine that never yields control to the event loop. The
+coroutine that receives messages never gets a chance to run.
+
+Putting an ``await`` statement in a ``for`` or a ``while`` loop isn't enough
+to yield control. Awaiting a coroutine may yield control, but there's no
+guarantee that it will.
+
+For example, :meth:`~legacy.protocol.WebSocketCommonProtocol.send` only yields
+control when send buffers are full, which never happens in most practical
+cases.
+
+If you run a loop that contains only synchronous operations and
+a :meth:`~legacy.protocol.WebSocketCommonProtocol.send` call, you must yield
+control explicitly with :func:`asyncio.sleep`::
+
+ async def producer(websocket):
+ message = generate_next_message()
+ await websocket.send(message)
+ await asyncio.sleep(0) # yield control to the event loop
+
+:func:`asyncio.sleep` always suspends the current task, allowing other tasks
+to run. This behavior is documented precisely because it isn't expected from
+every coroutine.
+
+See `issue 867`_.
+
+.. _issue 867: https://github.com/python-websockets/websockets/issues/867
+
+Why am I having problems with threads?
+--------------------------------------
+
+If you choose websockets' default implementation based on :mod:`asyncio`, then
+you shouldn't use threads. Indeed, choosing :mod:`asyncio` to handle concurrency
+is mutually exclusive with :mod:`threading`.
+
+If you believe that you need to run websockets in a thread and some logic in
+another thread, you should run that logic in a :class:`~asyncio.Task` instead.
+If it blocks the event loop, :meth:`~asyncio.loop.run_in_executor` will help.
+
+This question is really about :mod:`asyncio`. Please review the advice about
+:ref:`asyncio-multithreading` in the Python documentation.
+
+Why does my simple program misbehave mysteriously?
+--------------------------------------------------
+
+You are using :func:`time.sleep` instead of :func:`asyncio.sleep`, which
+blocks the event loop and prevents asyncio from operating normally.
+
+This may lead to messages getting send but not received, to connection
+timeouts, and to unexpected results of shotgun debugging e.g. adding an
+unnecessary call to :meth:`~legacy.protocol.WebSocketCommonProtocol.send`
+makes the program functional.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/client.rst
new file mode 100644
index 0000000000..c590ac107d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/client.rst
@@ -0,0 +1,101 @@
+Client
+======
+
+.. currentmodule:: websockets
+
+Why does the client close the connection prematurely?
+-----------------------------------------------------
+
+You're exiting the context manager prematurely. Wait for the work to be
+finished before exiting.
+
+For example, if your code has a structure similar to::
+
+ async with connect(...) as websocket:
+ asyncio.create_task(do_some_work())
+
+change it to::
+
+ async with connect(...) as websocket:
+ await do_some_work()
+
+How do I access HTTP headers?
+-----------------------------
+
+Once the connection is established, HTTP headers are available in
+:attr:`~client.WebSocketClientProtocol.request_headers` and
+:attr:`~client.WebSocketClientProtocol.response_headers`.
+
+How do I set HTTP headers?
+--------------------------
+
+To set the ``Origin``, ``Sec-WebSocket-Extensions``, or
+``Sec-WebSocket-Protocol`` headers in the WebSocket handshake request, use the
+``origin``, ``extensions``, or ``subprotocols`` arguments of
+:func:`~client.connect`.
+
+To override the ``User-Agent`` header, use the ``user_agent_header`` argument.
+Set it to :obj:`None` to remove the header.
+
+To set other HTTP headers, for example the ``Authorization`` header, use the
+``extra_headers`` argument::
+
+ async with connect(..., extra_headers={"Authorization": ...}) as websocket:
+ ...
+
+In the :mod:`threading` API, this argument is named ``additional_headers``::
+
+ with connect(..., additional_headers={"Authorization": ...}) as websocket:
+ ...
+
+How do I force the IP address that the client connects to?
+----------------------------------------------------------
+
+Use the ``host`` argument of :meth:`~asyncio.loop.create_connection`::
+
+ await websockets.connect("ws://example.com", host="192.168.0.1")
+
+:func:`~client.connect` accepts the same arguments as
+:meth:`~asyncio.loop.create_connection`.
+
+How do I close a connection?
+----------------------------
+
+The easiest is to use :func:`~client.connect` as a context manager::
+
+ async with connect(...) as websocket:
+ ...
+
+The connection is closed when exiting the context manager.
+
+How do I reconnect when the connection drops?
+---------------------------------------------
+
+Use :func:`~client.connect` as an asynchronous iterator::
+
+ async for websocket in websockets.connect(...):
+ try:
+ ...
+ except websockets.ConnectionClosed:
+ continue
+
+Make sure you handle exceptions in the ``async for`` loop. Uncaught exceptions
+will break out of the loop.
+
+How do I stop a client that is processing messages in a loop?
+-------------------------------------------------------------
+
+You can close the connection.
+
+Here's an example that terminates cleanly when it receives SIGTERM on Unix:
+
+.. literalinclude:: ../../example/faq/shutdown_client.py
+ :emphasize-lines: 10-13
+
+How do I disable TLS/SSL certificate verification?
+--------------------------------------------------
+
+Look at the ``ssl`` argument of :meth:`~asyncio.loop.create_connection`.
+
+:func:`~client.connect` accepts the same arguments as
+:meth:`~asyncio.loop.create_connection`.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/common.rst
new file mode 100644
index 0000000000..2c63c4f36f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/common.rst
@@ -0,0 +1,161 @@
+Both sides
+==========
+
+.. currentmodule:: websockets
+
+What does ``ConnectionClosedError: no close frame received or sent`` mean?
+--------------------------------------------------------------------------
+
+If you're seeing this traceback in the logs of a server:
+
+.. code-block:: pytb
+
+ connection handler failed
+ Traceback (most recent call last):
+ ...
+ asyncio.exceptions.IncompleteReadError: 0 bytes read on a total of 2 expected bytes
+
+ The above exception was the direct cause of the following exception:
+
+ Traceback (most recent call last):
+ ...
+ websockets.exceptions.ConnectionClosedError: no close frame received or sent
+
+or if a client crashes with this traceback:
+
+.. code-block:: pytb
+
+ Traceback (most recent call last):
+ ...
+ ConnectionResetError: [Errno 54] Connection reset by peer
+
+ The above exception was the direct cause of the following exception:
+
+ Traceback (most recent call last):
+ ...
+ websockets.exceptions.ConnectionClosedError: no close frame received or sent
+
+it means that the TCP connection was lost. As a consequence, the WebSocket
+connection was closed without receiving and sending a close frame, which is
+abnormal.
+
+You can catch and handle :exc:`~exceptions.ConnectionClosed` to prevent it
+from being logged.
+
+There are several reasons why long-lived connections may be lost:
+
+* End-user devices tend to lose network connectivity often and unpredictably
+ because they can move out of wireless network coverage, get unplugged from
+ a wired network, enter airplane mode, be put to sleep, etc.
+* HTTP load balancers or proxies that aren't configured for long-lived
+ connections may terminate connections after a short amount of time, usually
+ 30 seconds, despite websockets' keepalive mechanism.
+
+If you're facing a reproducible issue, :ref:`enable debug logs <debugging>` to
+see when and how connections are closed.
+
+What does ``ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received`` mean?
+---------------------------------------------------------------------------------------------------------------------
+
+If you're seeing this traceback in the logs of a server:
+
+.. code-block:: pytb
+
+ connection handler failed
+ Traceback (most recent call last):
+ ...
+ asyncio.exceptions.CancelledError
+
+ The above exception was the direct cause of the following exception:
+
+ Traceback (most recent call last):
+ ...
+ websockets.exceptions.ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received
+
+or if a client crashes with this traceback:
+
+.. code-block:: pytb
+
+ Traceback (most recent call last):
+ ...
+ asyncio.exceptions.CancelledError
+
+ The above exception was the direct cause of the following exception:
+
+ Traceback (most recent call last):
+ ...
+ websockets.exceptions.ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received
+
+it means that the WebSocket connection suffered from excessive latency and was
+closed after reaching the timeout of websockets' keepalive mechanism.
+
+You can catch and handle :exc:`~exceptions.ConnectionClosed` to prevent it
+from being logged.
+
+There are two main reasons why latency may increase:
+
+* Poor network connectivity.
+* More traffic than the recipient can handle.
+
+See the discussion of :doc:`timeouts <../topics/timeouts>` for details.
+
+If websockets' default timeout of 20 seconds is too short for your use case,
+you can adjust it with the ``ping_timeout`` argument.
+
+How do I set a timeout on :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`?
+--------------------------------------------------------------------------------
+
+On Python ≥ 3.11, use :func:`asyncio.timeout`::
+
+ async with asyncio.timeout(timeout=10):
+ message = await websocket.recv()
+
+On older versions of Python, use :func:`asyncio.wait_for`::
+
+ message = await asyncio.wait_for(websocket.recv(), timeout=10)
+
+This technique works for most APIs. When it doesn't, for example with
+asynchronous context managers, websockets provides an ``open_timeout`` argument.
+
+How can I pass arguments to a custom protocol subclass?
+-------------------------------------------------------
+
+You can bind additional arguments to the protocol factory with
+:func:`functools.partial`::
+
+ import asyncio
+ import functools
+ import websockets
+
+ class MyServerProtocol(websockets.WebSocketServerProtocol):
+ def __init__(self, *args, extra_argument=None, **kwargs):
+ super().__init__(*args, **kwargs)
+ # do something with extra_argument
+
+ create_protocol = functools.partial(MyServerProtocol, extra_argument=42)
+ start_server = websockets.serve(..., create_protocol=create_protocol)
+
+This example was for a server. The same pattern applies on a client.
+
+How do I keep idle connections open?
+------------------------------------
+
+websockets sends pings at 20 seconds intervals to keep the connection open.
+
+It closes the connection if it doesn't get a pong within 20 seconds.
+
+You can adjust this behavior with ``ping_interval`` and ``ping_timeout``.
+
+See :doc:`../topics/timeouts` for details.
+
+How do I respond to pings?
+--------------------------
+
+If you are referring to Ping_ and Pong_ frames defined in the WebSocket
+protocol, don't bother, because websockets handles them for you.
+
+.. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2
+.. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3
+
+If you are connecting to a server that defines its own heartbeat at the
+application level, then you need to build that logic into your application.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/index.rst
new file mode 100644
index 0000000000..9d5b0d538a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/index.rst
@@ -0,0 +1,21 @@
+Frequently asked questions
+==========================
+
+.. currentmodule:: websockets
+
+.. admonition:: Many questions asked in websockets' issue tracker are really
+ about :mod:`asyncio`.
+ :class: seealso
+
+ Python's documentation about `developing with asyncio`_ is a good
+ complement.
+
+ .. _developing with asyncio: https://docs.python.org/3/library/asyncio-dev.html
+
+.. toctree::
+
+ server
+ client
+ common
+ asyncio
+ misc
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/misc.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/misc.rst
new file mode 100644
index 0000000000..ee5ad23728
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/misc.rst
@@ -0,0 +1,49 @@
+Miscellaneous
+=============
+
+.. currentmodule:: websockets
+
+Why do I get the error: ``module 'websockets' has no attribute '...'``?
+.......................................................................
+
+Often, this is because you created a script called ``websockets.py`` in your
+current working directory. Then ``import websockets`` imports this module
+instead of the websockets library.
+
+.. _real-import-paths:
+
+Why is the default implementation located in ``websockets.legacy``?
+...................................................................
+
+This is an artifact of websockets' history. For its first eight years, only the
+:mod:`asyncio` implementation existed. Then, the Sans-I/O implementation was
+added. Moving the code in a ``legacy`` submodule eased this refactoring and
+optimized maintainability.
+
+All public APIs were kept at their original locations. ``websockets.legacy``
+isn't a public API. It's only visible in the source code and in stack traces.
+There is no intent to deprecate this implementation — at least until a superior
+alternative exists.
+
+Why is websockets slower than another library in my benchmark?
+..............................................................
+
+Not all libraries are as feature-complete as websockets. For a fair benchmark,
+you should disable features that the other library doesn't provide. Typically,
+you may need to disable:
+
+* Compression: set ``compression=None``
+* Keepalive: set ``ping_interval=None``
+* UTF-8 decoding: send ``bytes`` rather than ``str``
+
+If websockets is still slower than another Python library, please file a bug.
+
+Are there ``onopen``, ``onmessage``, ``onerror``, and ``onclose`` callbacks?
+............................................................................
+
+No, there aren't.
+
+websockets provides high-level, coroutine-based APIs. Compared to callbacks,
+coroutines make it easier to manage control flow in concurrent code.
+
+If you prefer callback-based APIs, you should use another library.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/server.rst
new file mode 100644
index 0000000000..08b412d306
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/server.rst
@@ -0,0 +1,336 @@
+Server
+======
+
+.. currentmodule:: websockets
+
+Why does the server close the connection prematurely?
+-----------------------------------------------------
+
+Your connection handler exits prematurely. Wait for the work to be finished
+before returning.
+
+For example, if your handler has a structure similar to::
+
+ async def handler(websocket):
+ asyncio.create_task(do_some_work())
+
+change it to::
+
+ async def handler(websocket):
+ await do_some_work()
+
+Why does the server close the connection after one message?
+-----------------------------------------------------------
+
+Your connection handler exits after processing one message. Write a loop to
+process multiple messages.
+
+For example, if your handler looks like this::
+
+ async def handler(websocket):
+ print(websocket.recv())
+
+change it like this::
+
+ async def handler(websocket):
+ async for message in websocket:
+ print(message)
+
+*Don't feel bad if this happens to you — it's the most common question in
+websockets' issue tracker :-)*
+
+Why can only one client connect at a time?
+------------------------------------------
+
+Your connection handler blocks the event loop. Look for blocking calls.
+
+Any call that may take some time must be asynchronous.
+
+For example, this connection handler prevents the event loop from running during
+one second::
+
+ async def handler(websocket):
+ time.sleep(1)
+ ...
+
+Change it to::
+
+ async def handler(websocket):
+ await asyncio.sleep(1)
+ ...
+
+In addition, calling a coroutine doesn't guarantee that it will yield control to
+the event loop.
+
+For example, this connection handler blocks the event loop by sending messages
+continuously::
+
+ async def handler(websocket):
+ while True:
+ await websocket.send("firehose!")
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.send` completes synchronously as
+long as there's space in send buffers. The event loop never runs. (This pattern
+is uncommon in real-world applications. It occurs mostly in toy programs.)
+
+You can avoid the issue by yielding control to the event loop explicitly::
+
+ async def handler(websocket):
+ while True:
+ await websocket.send("firehose!")
+ await asyncio.sleep(0)
+
+All this is part of learning asyncio. It isn't specific to websockets.
+
+See also Python's documentation about `running blocking code`_.
+
+.. _running blocking code: https://docs.python.org/3/library/asyncio-dev.html#running-blocking-code
+
+.. _send-message-to-all-users:
+
+How do I send a message to all users?
+-------------------------------------
+
+Record all connections in a global variable::
+
+ CONNECTIONS = set()
+
+ async def handler(websocket):
+ CONNECTIONS.add(websocket)
+ try:
+ await websocket.wait_closed()
+ finally:
+ CONNECTIONS.remove(websocket)
+
+Then, call :func:`~websockets.broadcast`::
+
+ import websockets
+
+ def message_all(message):
+ websockets.broadcast(CONNECTIONS, message)
+
+If you're running multiple server processes, make sure you call ``message_all``
+in each process.
+
+.. _send-message-to-single-user:
+
+How do I send a message to a single user?
+-----------------------------------------
+
+Record connections in a global variable, keyed by user identifier::
+
+ CONNECTIONS = {}
+
+ async def handler(websocket):
+ user_id = ... # identify user in your app's context
+ CONNECTIONS[user_id] = websocket
+ try:
+ await websocket.wait_closed()
+ finally:
+ del CONNECTIONS[user_id]
+
+Then, call :meth:`~legacy.protocol.WebSocketCommonProtocol.send`::
+
+ async def message_user(user_id, message):
+ websocket = CONNECTIONS[user_id] # raises KeyError if user disconnected
+ await websocket.send(message) # may raise websockets.ConnectionClosed
+
+Add error handling according to the behavior you want if the user disconnected
+before the message could be sent.
+
+This example supports only one connection per user. To support concurrent
+connections by the same user, you can change ``CONNECTIONS`` to store a set of
+connections for each user.
+
+If you're running multiple server processes, call ``message_user`` in each
+process. The process managing the user's connection sends the message; other
+processes do nothing.
+
+When you reach a scale where server processes cannot keep up with the stream of
+all messages, you need a better architecture. For example, you could deploy an
+external publish / subscribe system such as Redis_. Server processes would
+subscribe their clients. Then, they would receive messages only for the
+connections that they're managing.
+
+.. _Redis: https://redis.io/
+
+How do I send a message to a channel, a topic, or some users?
+-------------------------------------------------------------
+
+websockets doesn't provide built-in publish / subscribe functionality.
+
+Record connections in a global variable, keyed by user identifier, as shown in
+:ref:`How do I send a message to a single user?<send-message-to-single-user>`
+
+Then, build the set of recipients and broadcast the message to them, as shown in
+:ref:`How do I send a message to all users?<send-message-to-all-users>`
+
+:doc:`../howto/django` contains a complete implementation of this pattern.
+
+Again, as you scale, you may reach the performance limits of a basic in-process
+implementation. You may need an external publish / subscribe system like Redis_.
+
+.. _Redis: https://redis.io/
+
+How do I pass arguments to the connection handler?
+--------------------------------------------------
+
+You can bind additional arguments to the connection handler with
+:func:`functools.partial`::
+
+ import asyncio
+ import functools
+ import websockets
+
+ async def handler(websocket, extra_argument):
+ ...
+
+ bound_handler = functools.partial(handler, extra_argument=42)
+ start_server = websockets.serve(bound_handler, ...)
+
+Another way to achieve this result is to define the ``handler`` coroutine in
+a scope where the ``extra_argument`` variable exists instead of injecting it
+through an argument.
+
+How do I access the request path?
+---------------------------------
+
+It is available in the :attr:`~server.WebSocketServerProtocol.path` attribute.
+
+You may route a connection to different handlers depending on the request path::
+
+ async def handler(websocket):
+ if websocket.path == "/blue":
+ await blue_handler(websocket)
+ elif websocket.path == "/green":
+ await green_handler(websocket)
+ else:
+ # No handler for this path; close the connection.
+ return
+
+You may also route the connection based on the first message received from the
+client, as shown in the :doc:`tutorial <../intro/tutorial2>`. When you want to
+authenticate the connection before routing it, this is usually more convenient.
+
+Generally speaking, there is far less emphasis on the request path in WebSocket
+servers than in HTTP servers. When a WebSocket server provides a single endpoint,
+it may ignore the request path entirely.
+
+How do I access HTTP headers?
+-----------------------------
+
+To access HTTP headers during the WebSocket handshake, you can override
+:attr:`~server.WebSocketServerProtocol.process_request`::
+
+ async def process_request(self, path, request_headers):
+ authorization = request_headers["Authorization"]
+
+Once the connection is established, HTTP headers are available in
+:attr:`~server.WebSocketServerProtocol.request_headers` and
+:attr:`~server.WebSocketServerProtocol.response_headers`::
+
+ async def handler(websocket):
+ authorization = websocket.request_headers["Authorization"]
+
+How do I set HTTP headers?
+--------------------------
+
+To set the ``Sec-WebSocket-Extensions`` or ``Sec-WebSocket-Protocol`` headers in
+the WebSocket handshake response, use the ``extensions`` or ``subprotocols``
+arguments of :func:`~server.serve`.
+
+To override the ``Server`` header, use the ``server_header`` argument. Set it to
+:obj:`None` to remove the header.
+
+To set other HTTP headers, use the ``extra_headers`` argument.
+
+How do I get the IP address of the client?
+------------------------------------------
+
+It's available in :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address`::
+
+ async def handler(websocket):
+ remote_ip = websocket.remote_address[0]
+
+How do I set the IP addresses that my server listens on?
+--------------------------------------------------------
+
+Use the ``host`` argument of :meth:`~asyncio.loop.create_server`::
+
+ await websockets.serve(handler, host="192.168.0.1", port=8080)
+
+:func:`~server.serve` accepts the same arguments as
+:meth:`~asyncio.loop.create_server`.
+
+What does ``OSError: [Errno 99] error while attempting to bind on address ('::1', 80, 0, 0): address not available`` mean?
+--------------------------------------------------------------------------------------------------------------------------
+
+You are calling :func:`~server.serve` without a ``host`` argument in a context
+where IPv6 isn't available.
+
+To listen only on IPv4, specify ``host="0.0.0.0"`` or ``family=socket.AF_INET``.
+
+Refer to the documentation of :meth:`~asyncio.loop.create_server` for details.
+
+How do I close a connection?
+----------------------------
+
+websockets takes care of closing the connection when the handler exits.
+
+How do I stop a server?
+-----------------------
+
+Exit the :func:`~server.serve` context manager.
+
+Here's an example that terminates cleanly when it receives SIGTERM on Unix:
+
+.. literalinclude:: ../../example/faq/shutdown_server.py
+ :emphasize-lines: 12-15,18
+
+How do I stop a server while keeping existing connections open?
+---------------------------------------------------------------
+
+Call the server's :meth:`~server.WebSocketServer.close` method with
+``close_connections=False``.
+
+Here's how to adapt the example just above::
+
+ async def server():
+ ...
+
+ server = await websockets.serve(echo, "localhost", 8765)
+ await stop
+ await server.close(close_connections=False)
+
+How do I implement a health check?
+----------------------------------
+
+Intercept WebSocket handshake requests with the
+:meth:`~server.WebSocketServerProtocol.process_request` hook.
+
+When a request is sent to the health check endpoint, treat is as an HTTP request
+and return a ``(status, headers, body)`` tuple, as in this example:
+
+.. literalinclude:: ../../example/faq/health_check_server.py
+ :emphasize-lines: 7-9,18
+
+How do I run HTTP and WebSocket servers on the same port?
+---------------------------------------------------------
+
+You don't.
+
+HTTP and WebSocket have widely different operational characteristics. Running
+them with the same server becomes inconvenient when you scale.
+
+Providing an HTTP server is out of scope for websockets. It only aims at
+providing a WebSocket server.
+
+There's limited support for returning HTTP responses with the
+:attr:`~server.WebSocketServerProtocol.process_request` hook.
+
+If you need more, pick an HTTP server and run it separately.
+
+Alternatively, pick an HTTP framework that builds on top of ``websockets`` to
+support WebSocket connections, like Sanic_.
+
+.. _Sanic: https://sanicframework.org/en/
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/autoreload.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/autoreload.rst
new file mode 100644
index 0000000000..fc736a5918
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/autoreload.rst
@@ -0,0 +1,31 @@
+Reload on code changes
+======================
+
+When developing a websockets server, you may run it locally to test changes.
+Unfortunately, whenever you want to try a new version of the code, you must
+stop the server and restart it, which slows down your development process.
+
+Web frameworks such as Django or Flask provide a development server that
+reloads the application automatically when you make code changes. There is no
+such functionality in websockets because it's designed for production rather
+than development.
+
+However, you can achieve the same result easily.
+
+Install watchdog_ with the ``watchmedo`` shell utility:
+
+.. code-block:: console
+
+ $ pip install 'watchdog[watchmedo]'
+
+.. _watchdog: https://pypi.org/project/watchdog/
+
+Run your server with ``watchmedo auto-restart``:
+
+.. code-block:: console
+
+ $ watchmedo auto-restart --pattern "*.py" --recursive --signal SIGTERM \
+ python app.py
+
+This example assumes that the server is defined in a script called ``app.py``.
+Adapt it as necessary.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst
new file mode 100644
index 0000000000..95b551f673
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst
@@ -0,0 +1,87 @@
+Cheat sheet
+===========
+
+.. currentmodule:: websockets
+
+Server
+------
+
+* Write a coroutine that handles a single connection. It receives a WebSocket
+ protocol instance and the URI path in argument.
+
+ * Call :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` and
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to receive and send
+ messages at any time.
+
+ * When :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` or
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.send` raises
+ :exc:`~exceptions.ConnectionClosed`, clean up and exit. If you started
+ other :class:`asyncio.Task`, terminate them before exiting.
+
+ * If you aren't awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`,
+ consider awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed`
+ to detect quickly when the connection is closed.
+
+ * You may :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` or
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` if you wish but it isn't
+ needed in general.
+
+* Create a server with :func:`~server.serve` which is similar to asyncio's
+ :meth:`~asyncio.loop.create_server`. You can also use it as an asynchronous
+ context manager.
+
+ * The server takes care of establishing connections, then lets the handler
+ execute the application logic, and finally closes the connection after the
+ handler exits normally or with an exception.
+
+ * For advanced customization, you may subclass
+ :class:`~server.WebSocketServerProtocol` and pass either this subclass or
+ a factory function as the ``create_protocol`` argument.
+
+Client
+------
+
+* Create a client with :func:`~client.connect` which is similar to asyncio's
+ :meth:`~asyncio.loop.create_connection`. You can also use it as an
+ asynchronous context manager.
+
+ * For advanced customization, you may subclass
+ :class:`~client.WebSocketClientProtocol` and pass either this subclass or
+ a factory function as the ``create_protocol`` argument.
+
+* Call :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` and
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to receive and send messages
+ at any time.
+
+* You may :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` or
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` if you wish but it isn't
+ needed in general.
+
+* If you aren't using :func:`~client.connect` as a context manager, call
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.close` to terminate the connection.
+
+.. _debugging:
+
+Debugging
+---------
+
+If you don't understand what websockets is doing, enable logging::
+
+ import logging
+ logger = logging.getLogger('websockets')
+ logger.setLevel(logging.DEBUG)
+ logger.addHandler(logging.StreamHandler())
+
+The logs contain:
+
+* Exceptions in the connection handler at the ``ERROR`` level
+* Exceptions in the opening or closing handshake at the ``INFO`` level
+* All frames at the ``DEBUG`` level — this can be very verbose
+
+If you're new to ``asyncio``, you will certainly encounter issues that are
+related to asynchronous programming in general rather than to websockets in
+particular. Fortunately Python's official documentation provides advice to
+`develop with asyncio`_. Check it out: it's invaluable!
+
+.. _develop with asyncio: https://docs.python.org/3/library/asyncio-dev.html
+
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/django.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/django.rst
new file mode 100644
index 0000000000..e3da0a878b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/django.rst
@@ -0,0 +1,294 @@
+Integrate with Django
+=====================
+
+If you're looking at adding real-time capabilities to a Django project with
+WebSocket, you have two main options.
+
+1. Using Django Channels_, a project adding WebSocket to Django, among other
+ features. This approach is fully supported by Django. However, it requires
+ switching to a new deployment architecture.
+
+2. Deploying a separate WebSocket server next to your Django project. This
+ technique is well suited when you need to add a small set of real-time
+ features — maybe a notification service — to an HTTP application.
+
+.. _Channels: https://channels.readthedocs.io/
+
+This guide shows how to implement the second technique with websockets. It
+assumes familiarity with Django.
+
+Authenticate connections
+------------------------
+
+Since the websockets server runs outside of Django, we need to integrate it
+with ``django.contrib.auth``.
+
+We will generate authentication tokens in the Django project. Then we will
+send them to the websockets server, where they will authenticate the user.
+
+Generating a token for the current user and making it available in the browser
+is up to you. You could render the token in a template or fetch it with an API
+call.
+
+Refer to the topic guide on :doc:`authentication <../topics/authentication>`
+for details on this design.
+
+Generate tokens
+...............
+
+We want secure, short-lived tokens containing the user ID. We'll rely on
+`django-sesame`_, a small library designed exactly for this purpose.
+
+.. _django-sesame: https://github.com/aaugustin/django-sesame
+
+Add django-sesame to the dependencies of your Django project, install it, and
+configure it in the settings of the project:
+
+.. code-block:: python
+
+ AUTHENTICATION_BACKENDS = [
+ "django.contrib.auth.backends.ModelBackend",
+ "sesame.backends.ModelBackend",
+ ]
+
+(If your project already uses another authentication backend than the default
+``"django.contrib.auth.backends.ModelBackend"``, adjust accordingly.)
+
+You don't need ``"sesame.middleware.AuthenticationMiddleware"``. It is for
+authenticating users in the Django server, while we're authenticating them in
+the websockets server.
+
+We'd like our tokens to be valid for 30 seconds. We expect web pages to load
+and to establish the WebSocket connection within this delay. Configure
+django-sesame accordingly in the settings of your Django project:
+
+.. code-block:: python
+
+ SESAME_MAX_AGE = 30
+
+If you expect your web site to load faster for all clients, a shorter lifespan
+is possible. However, in the context of this document, it would make manual
+testing more difficult.
+
+You could also enable single-use tokens. However, this would update the last
+login date of the user every time a WebSocket connection is established. This
+doesn't seem like a good idea, both in terms of behavior and in terms of
+performance.
+
+Now you can generate tokens in a ``django-admin shell`` as follows:
+
+.. code-block:: pycon
+
+ >>> from django.contrib.auth import get_user_model
+ >>> User = get_user_model()
+ >>> user = User.objects.get(username="<your username>")
+ >>> from sesame.utils import get_token
+ >>> get_token(user)
+ '<your token>'
+
+Keep this console open: since tokens expire after 30 seconds, you'll have to
+generate a new token every time you want to test connecting to the server.
+
+Validate tokens
+...............
+
+Let's move on to the websockets server.
+
+Add websockets to the dependencies of your Django project and install it.
+Indeed, we're going to reuse the environment of the Django project, so we can
+call its APIs in the websockets server.
+
+Now here's how to implement authentication.
+
+.. literalinclude:: ../../example/django/authentication.py
+
+Let's unpack this code.
+
+We're calling ``django.setup()`` before doing anything with Django because
+we're using Django in a `standalone script`_. This assumes that the
+``DJANGO_SETTINGS_MODULE`` environment variable is set to the Python path to
+your settings module.
+
+.. _standalone script: https://docs.djangoproject.com/en/stable/topics/settings/#calling-django-setup-is-required-for-standalone-django-usage
+
+The connection handler reads the first message received from the client, which
+is expected to contain a django-sesame token. Then it authenticates the user
+with ``get_user()``, the API for `authentication outside a view`_. If
+authentication fails, it closes the connection and exits.
+
+.. _authentication outside a view: https://django-sesame.readthedocs.io/en/stable/howto.html#outside-a-view
+
+When we call an API that makes a database query such as ``get_user()``, we
+wrap the call in :func:`~asyncio.to_thread`. Indeed, the Django ORM doesn't
+support asynchronous I/O. It would block the event loop if it didn't run in a
+separate thread. :func:`~asyncio.to_thread` is available since Python 3.9. In
+earlier versions, use :meth:`~asyncio.loop.run_in_executor` instead.
+
+Finally, we start a server with :func:`~websockets.server.serve`.
+
+We're ready to test!
+
+Save this code to a file called ``authentication.py``, make sure the
+``DJANGO_SETTINGS_MODULE`` environment variable is set properly, and start the
+websockets server:
+
+.. code-block:: console
+
+ $ python authentication.py
+
+Generate a new token — remember, they're only valid for 30 seconds — and use
+it to connect to your server. Paste your token and press Enter when you get a
+prompt:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:8888/
+ Connected to ws://localhost:8888/
+ > <your token>
+ < Hello <your username>!
+ Connection closed: 1000 (OK).
+
+It works!
+
+If you enter an expired or invalid token, authentication fails and the server
+closes the connection:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:8888/
+ Connected to ws://localhost:8888.
+ > not a token
+ Connection closed: 1011 (internal error) authentication failed.
+
+You can also test from a browser by generating a new token and running the
+following code in the JavaScript console of the browser:
+
+.. code-block:: javascript
+
+ websocket = new WebSocket("ws://localhost:8888/");
+ websocket.onopen = (event) => websocket.send("<your token>");
+ websocket.onmessage = (event) => console.log(event.data);
+
+If you don't want to import your entire Django project into the websockets
+server, you can build a separate Django project with ``django.contrib.auth``,
+``django-sesame``, a suitable ``User`` model, and a subset of the settings of
+the main project.
+
+Stream events
+-------------
+
+We can connect and authenticate but our server doesn't do anything useful yet!
+
+Let's send a message every time a user makes an action in the admin. This
+message will be broadcast to all users who can access the model on which the
+action was made. This may be used for showing notifications to other users.
+
+Many use cases for WebSocket with Django follow a similar pattern.
+
+Set up event bus
+................
+
+We need a event bus to enable communications between Django and websockets.
+Both sides connect permanently to the bus. Then Django writes events and
+websockets reads them. For the sake of simplicity, we'll rely on `Redis
+Pub/Sub`_.
+
+.. _Redis Pub/Sub: https://redis.io/topics/pubsub
+
+The easiest way to add Redis to a Django project is by configuring a cache
+backend with `django-redis`_. This library manages connections to Redis
+efficiently, persisting them between requests, and provides an API to access
+the Redis connection directly.
+
+.. _django-redis: https://github.com/jazzband/django-redis
+
+Install Redis, add django-redis to the dependencies of your Django project,
+install it, and configure it in the settings of the project:
+
+.. code-block:: python
+
+ CACHES = {
+ "default": {
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://127.0.0.1:6379/1",
+ },
+ }
+
+If you already have a default cache, add a new one with a different name and
+change ``get_redis_connection("default")`` in the code below to the same name.
+
+Publish events
+..............
+
+Now let's write events to the bus.
+
+Add the following code to a module that is imported when your Django project
+starts. Typically, you would put it in a ``signals.py`` module, which you
+would import in the ``AppConfig.ready()`` method of one of your apps:
+
+.. literalinclude:: ../../example/django/signals.py
+
+This code runs every time the admin saves a ``LogEntry`` object to keep track
+of a change. It extracts interesting data, serializes it to JSON, and writes
+an event to Redis.
+
+Let's check that it works:
+
+.. code-block:: console
+
+ $ redis-cli
+ 127.0.0.1:6379> SELECT 1
+ OK
+ 127.0.0.1:6379[1]> SUBSCRIBE events
+ Reading messages... (press Ctrl-C to quit)
+ 1) "subscribe"
+ 2) "events"
+ 3) (integer) 1
+
+Leave this command running, start the Django development server and make
+changes in the admin: add, modify, or delete objects. You should see
+corresponding events published to the ``"events"`` stream.
+
+Broadcast events
+................
+
+Now let's turn to reading events and broadcasting them to connected clients.
+We need to add several features:
+
+* Keep track of connected clients so we can broadcast messages.
+* Tell which content types the user has permission to view or to change.
+* Connect to the message bus and read events.
+* Broadcast these events to users who have corresponding permissions.
+
+Here's a complete implementation.
+
+.. literalinclude:: ../../example/django/notifications.py
+
+Since the ``get_content_types()`` function makes a database query, it is
+wrapped inside :func:`asyncio.to_thread()`. It runs once when each WebSocket
+connection is open; then its result is cached for the lifetime of the
+connection. Indeed, running it for each message would trigger database queries
+for all connected users at the same time, which would hurt the database.
+
+The connection handler merely registers the connection in a global variable,
+associated to the list of content types for which events should be sent to
+that connection, and waits until the client disconnects.
+
+The ``process_events()`` function reads events from Redis and broadcasts them
+to all connections that should receive them. We don't care much if a sending a
+notification fails — this happens when a connection drops between the moment
+we iterate on connections and the moment the corresponding message is sent —
+so we start a task with for each message and forget about it. Also, this means
+we're immediately ready to process the next event, even if it takes time to
+send a message to a slow client.
+
+Since Redis can publish a message to multiple subscribers, multiple instances
+of this server can safely run in parallel.
+
+Does it scale?
+--------------
+
+In theory, given enough servers, this design can scale to a hundred million
+clients, since Redis can handle ten thousand servers and each server can
+handle ten thousand clients. In practice, you would need a more scalable
+message bus before reaching that scale, due to the volume of messages.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/extensions.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/extensions.rst
new file mode 100644
index 0000000000..3c8a7d72a6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/extensions.rst
@@ -0,0 +1,30 @@
+Write an extension
+==================
+
+.. currentmodule:: websockets.extensions
+
+During the opening handshake, WebSocket clients and servers negotiate which
+extensions_ will be used with which parameters. Then each frame is processed
+by extensions before being sent or after being received.
+
+.. _extensions: https://www.rfc-editor.org/rfc/rfc6455.html#section-9
+
+As a consequence, writing an extension requires implementing several classes:
+
+* Extension Factory: it negotiates parameters and instantiates the extension.
+
+ Clients and servers require separate extension factories with distinct APIs.
+
+ Extension factories are the public API of an extension.
+
+* Extension: it decodes incoming frames and encodes outgoing frames.
+
+ If the extension is symmetrical, clients and servers can use the same
+ class.
+
+ Extensions are initialized by extension factories, so they don't need to be
+ part of the public API of an extension.
+
+websockets provides base classes for extension factories and extensions.
+See :class:`ClientExtensionFactory`, :class:`ServerExtensionFactory`,
+and :class:`Extension` for details.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/fly.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/fly.rst
new file mode 100644
index 0000000000..ed001a2aee
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/fly.rst
@@ -0,0 +1,177 @@
+Deploy to Fly
+================
+
+This guide describes how to deploy a websockets server to Fly_.
+
+.. _Fly: https://fly.io/
+
+.. admonition:: The free tier of Fly is sufficient for trying this guide.
+ :class: tip
+
+ The `free tier`__ include up to three small VMs. This guide uses only one.
+
+ __ https://fly.io/docs/about/pricing/
+
+We're going to deploy a very simple app. The process would be identical for a
+more realistic app.
+
+Create application
+------------------
+
+Here's the implementation of the app, an echo server. Save it in a file called
+``app.py``:
+
+.. literalinclude:: ../../example/deployment/fly/app.py
+ :language: python
+
+This app implements typical requirements for running on a Platform as a Service:
+
+* it provides a health check at ``/healthz``;
+* it closes connections and exits cleanly when it receives a ``SIGTERM`` signal.
+
+Create a ``requirements.txt`` file containing this line to declare a dependency
+on websockets:
+
+.. literalinclude:: ../../example/deployment/fly/requirements.txt
+ :language: text
+
+The app is ready. Let's deploy it!
+
+Deploy application
+------------------
+
+Follow the instructions__ to install the Fly CLI, if you haven't done that yet.
+
+__ https://fly.io/docs/hands-on/install-flyctl/
+
+Sign up or log in to Fly.
+
+Launch the app — you'll have to pick a different name because I'm already using
+``websockets-echo``:
+
+.. code-block:: console
+
+ $ fly launch
+ Creating app in ...
+ Scanning source code
+ Detected a Python app
+ Using the following build configuration:
+ Builder: paketobuildpacks/builder:base
+ ? App Name (leave blank to use an auto-generated name): websockets-echo
+ ? Select organization: ...
+ ? Select region: ...
+ Created app websockets-echo in organization ...
+ Wrote config file fly.toml
+ ? Would you like to set up a Postgresql database now? No
+ We have generated a simple Procfile for you. Modify it to fit your needs and run "fly deploy" to deploy your application.
+
+.. admonition:: This will build the image with a generic buildpack.
+ :class: tip
+
+ Fly can `build images`__ with a Dockerfile or a buildpack. Here, ``fly
+ launch`` configures a generic Paketo buildpack.
+
+ If you'd rather package the app with a Dockerfile, check out the guide to
+ :ref:`containerize an application <containerize-application>`.
+
+ __ https://fly.io/docs/reference/builders/
+
+Replace the auto-generated ``fly.toml`` with:
+
+.. literalinclude:: ../../example/deployment/fly/fly.toml
+ :language: toml
+
+This configuration:
+
+* listens on port 443, terminates TLS, and forwards to the app on port 8080;
+* declares a health check at ``/healthz``;
+* requests a ``SIGTERM`` for terminating the app.
+
+Replace the auto-generated ``Procfile`` with:
+
+.. literalinclude:: ../../example/deployment/fly/Procfile
+ :language: text
+
+This tells Fly how to run the app.
+
+Now you can deploy it:
+
+.. code-block:: console
+
+ $ fly deploy
+
+ ... lots of output...
+
+ ==> Monitoring deployment
+
+ 1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing]
+ --> v0 deployed successfully
+
+Validate deployment
+-------------------
+
+Let's confirm that your application is running as expected.
+
+Since it's a WebSocket server, you need a WebSocket client, such as the
+interactive client that comes with websockets.
+
+If you're currently building a websockets server, perhaps you're already in a
+virtualenv where websockets is installed. If not, you can install it in a new
+virtualenv as follows:
+
+.. code-block:: console
+
+ $ python -m venv websockets-client
+ $ . websockets-client/bin/activate
+ $ pip install websockets
+
+Connect the interactive client — you must replace ``websockets-echo`` with the
+name of your Fly app in this command:
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.fly.dev/
+ Connected to wss://websockets-echo.fly.dev/.
+ >
+
+Great! Your app is running!
+
+Once you're connected, you can send any message and the server will echo it,
+or press Ctrl-D to terminate the connection:
+
+.. code-block:: console
+
+ > Hello!
+ < Hello!
+ Connection closed: 1000 (OK).
+
+You can also confirm that your application shuts down gracefully.
+
+Connect an interactive client again — remember to replace ``websockets-echo``
+with your app:
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.fly.dev/
+ Connected to wss://websockets-echo.fly.dev/.
+ >
+
+In another shell, restart the app — again, replace ``websockets-echo`` with your
+app:
+
+.. code-block:: console
+
+ $ fly restart websockets-echo
+ websockets-echo is being restarted
+
+Go back to the first shell. The connection is closed with code 1001 (going
+away).
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.fly.dev/
+ Connected to wss://websockets-echo.fly.dev/.
+ Connection closed: 1001 (going away).
+
+If graceful shutdown wasn't working, the server wouldn't perform a closing
+handshake and the connection would be closed with code 1006 (abnormal closure).
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/haproxy.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/haproxy.rst
new file mode 100644
index 0000000000..fdaab04011
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/haproxy.rst
@@ -0,0 +1,61 @@
+Deploy behind HAProxy
+=====================
+
+This guide demonstrates a way to load balance connections across multiple
+websockets server processes running on the same machine with HAProxy_.
+
+We'll run server processes with Supervisor as described in :doc:`this guide
+<supervisor>`.
+
+.. _HAProxy: https://www.haproxy.org/
+
+Run server processes
+--------------------
+
+Save this app to ``app.py``:
+
+.. literalinclude:: ../../example/deployment/haproxy/app.py
+ :emphasize-lines: 24
+
+Each server process listens on a different port by extracting an incremental
+index from an environment variable set by Supervisor.
+
+Save this configuration to ``supervisord.conf``:
+
+.. literalinclude:: ../../example/deployment/haproxy/supervisord.conf
+
+This configuration runs four instances of the app.
+
+Install Supervisor and run it:
+
+.. code-block:: console
+
+ $ supervisord -c supervisord.conf -n
+
+Configure and run HAProxy
+-------------------------
+
+Here's a simple HAProxy configuration to load balance connections across four
+processes:
+
+.. literalinclude:: ../../example/deployment/haproxy/haproxy.cfg
+
+In the backend configuration, we set the load balancing method to
+``leastconn`` in order to balance the number of active connections across
+servers. This is best for long running connections.
+
+Save the configuration to ``haproxy.cfg``, install HAProxy, and run it:
+
+.. code-block:: console
+
+ $ haproxy -f haproxy.cfg
+
+You can confirm that HAProxy proxies connections properly:
+
+.. code-block:: console
+
+ $ PYTHONPATH=src python -m websockets ws://localhost:8080/
+ Connected to ws://localhost:8080/.
+ > Hello!
+ < Hello!
+ Connection closed: 1000 (OK).
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/heroku.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/heroku.rst
new file mode 100644
index 0000000000..a97d2e7ce0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/heroku.rst
@@ -0,0 +1,183 @@
+Deploy to Heroku
+================
+
+This guide describes how to deploy a websockets server to Heroku_. The same
+principles should apply to other Platform as a Service providers.
+
+.. _Heroku: https://www.heroku.com/
+
+.. admonition:: Heroku no longer offers a free tier.
+ :class: attention
+
+ When this tutorial was written, in September 2021, Heroku offered a free
+ tier where a websockets app could run at no cost. In November 2022, Heroku
+ removed the free tier, making it impossible to maintain this document. As a
+ consequence, it isn't updated anymore and may be removed in the future.
+
+We're going to deploy a very simple app. The process would be identical for a
+more realistic app.
+
+Create repository
+-----------------
+
+Deploying to Heroku requires a git repository. Let's initialize one:
+
+.. code-block:: console
+
+ $ mkdir websockets-echo
+ $ cd websockets-echo
+ $ git init -b main
+ Initialized empty Git repository in websockets-echo/.git/
+ $ git commit --allow-empty -m "Initial commit."
+ [main (root-commit) 1e7947d] Initial commit.
+
+Create application
+------------------
+
+Here's the implementation of the app, an echo server. Save it in a file called
+``app.py``:
+
+.. literalinclude:: ../../example/deployment/heroku/app.py
+ :language: python
+
+Heroku expects the server to `listen on a specific port`_, which is provided
+in the ``$PORT`` environment variable. The app reads it and passes it to
+:func:`~websockets.server.serve`.
+
+.. _listen on a specific port: https://devcenter.heroku.com/articles/preparing-a-codebase-for-heroku-deployment#4-listen-on-the-correct-port
+
+Heroku sends a ``SIGTERM`` signal to all processes when `shutting down a
+dyno`_. When the app receives this signal, it closes connections and exits
+cleanly.
+
+.. _shutting down a dyno: https://devcenter.heroku.com/articles/dynos#shutdown
+
+Create a ``requirements.txt`` file containing this line to declare a dependency
+on websockets:
+
+.. literalinclude:: ../../example/deployment/heroku/requirements.txt
+ :language: text
+
+Create a ``Procfile``.
+
+.. literalinclude:: ../../example/deployment/heroku/Procfile
+
+This tells Heroku how to run the app.
+
+Confirm that you created the correct files and commit them to git:
+
+.. code-block:: console
+
+ $ ls
+ Procfile app.py requirements.txt
+ $ git add .
+ $ git commit -m "Initial implementation."
+ [main 8418c62] Initial implementation.
+  3 files changed, 32 insertions(+)
+  create mode 100644 Procfile
+  create mode 100644 app.py
+  create mode 100644 requirements.txt
+
+The app is ready. Let's deploy it!
+
+Deploy application
+------------------
+
+Follow the instructions_ to install the Heroku CLI, if you haven't done that
+yet.
+
+.. _instructions: https://devcenter.heroku.com/articles/getting-started-with-python#set-up
+
+Sign up or log in to Heroku.
+
+Create a Heroku app — you'll have to pick a different name because I'm already
+using ``websockets-echo``:
+
+.. code-block:: console
+
+ $ heroku create websockets-echo
+ Creating ⬢ websockets-echo... done
+ https://websockets-echo.herokuapp.com/ | https://git.heroku.com/websockets-echo.git
+
+.. code-block:: console
+
+ $ git push heroku
+
+ ... lots of output...
+
+ remote: -----> Launching...
+ remote: Released v1
+ remote: https://websockets-echo.herokuapp.com/ deployed to Heroku
+ remote:
+ remote: Verifying deploy... done.
+ To https://git.heroku.com/websockets-echo.git
+  * [new branch] main -> main
+
+Validate deployment
+-------------------
+
+Let's confirm that your application is running as expected.
+
+Since it's a WebSocket server, you need a WebSocket client, such as the
+interactive client that comes with websockets.
+
+If you're currently building a websockets server, perhaps you're already in a
+virtualenv where websockets is installed. If not, you can install it in a new
+virtualenv as follows:
+
+.. code-block:: console
+
+ $ python -m venv websockets-client
+ $ . websockets-client/bin/activate
+ $ pip install websockets
+
+Connect the interactive client — you must replace ``websockets-echo`` with the
+name of your Heroku app in this command:
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.herokuapp.com/
+ Connected to wss://websockets-echo.herokuapp.com/.
+ >
+
+Great! Your app is running!
+
+Once you're connected, you can send any message and the server will echo it,
+or press Ctrl-D to terminate the connection:
+
+.. code-block:: console
+
+ > Hello!
+ < Hello!
+ Connection closed: 1000 (OK).
+
+You can also confirm that your application shuts down gracefully.
+
+Connect an interactive client again — remember to replace ``websockets-echo``
+with your app:
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.herokuapp.com/
+ Connected to wss://websockets-echo.herokuapp.com/.
+ >
+
+In another shell, restart the app — again, replace ``websockets-echo`` with your
+app:
+
+.. code-block:: console
+
+ $ heroku dyno:restart -a websockets-echo
+ Restarting dynos on ⬢ websockets-echo... done
+
+Go back to the first shell. The connection is closed with code 1001 (going
+away).
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.herokuapp.com/
+ Connected to wss://websockets-echo.herokuapp.com/.
+ Connection closed: 1001 (going away).
+
+If graceful shutdown wasn't working, the server wouldn't perform a closing
+handshake and the connection would be closed with code 1006 (abnormal closure).
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/index.rst
new file mode 100644
index 0000000000..ddbe67d3ae
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/index.rst
@@ -0,0 +1,56 @@
+How-to guides
+=============
+
+In a hurry? Check out these examples.
+
+.. toctree::
+ :titlesonly:
+
+ quickstart
+
+If you're stuck, perhaps you'll find the answer here.
+
+.. toctree::
+ :titlesonly:
+
+ cheatsheet
+ patterns
+ autoreload
+
+This guide will help you integrate websockets into a broader system.
+
+.. toctree::
+ :titlesonly:
+
+ django
+
+The WebSocket protocol makes provisions for extending or specializing its
+features, which websockets supports fully.
+
+.. toctree::
+ :titlesonly:
+
+ extensions
+
+.. _deployment-howto:
+
+Once your application is ready, learn how to deploy it on various platforms.
+
+.. toctree::
+ :titlesonly:
+
+ render
+ fly
+ heroku
+ kubernetes
+ supervisor
+ nginx
+ haproxy
+
+If you're integrating the Sans-I/O layer of websockets into a library, rather
+than building an application with websockets, follow this guide.
+
+.. toctree::
+ :maxdepth: 2
+
+ sansio
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/kubernetes.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/kubernetes.rst
new file mode 100644
index 0000000000..064a6ac4d5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/kubernetes.rst
@@ -0,0 +1,215 @@
+Deploy to Kubernetes
+====================
+
+This guide describes how to deploy a websockets server to Kubernetes_. It
+assumes familiarity with Docker and Kubernetes.
+
+We're going to deploy a simple app to a local Kubernetes cluster and to ensure
+that it scales as expected.
+
+In a more realistic context, you would follow your organization's practices
+for deploying to Kubernetes, but you would apply the same principles as far as
+websockets is concerned.
+
+.. _Kubernetes: https://kubernetes.io/
+
+.. _containerize-application:
+
+Containerize application
+------------------------
+
+Here's the app we're going to deploy. Save it in a file called
+``app.py``:
+
+.. literalinclude:: ../../example/deployment/kubernetes/app.py
+
+This is an echo server with one twist: every message blocks the server for
+100ms, which creates artificial starvation of CPU time. This makes it easier
+to saturate the server for load testing.
+
+The app exposes a health check on ``/healthz``. It also provides two other
+endpoints for testing purposes: ``/inemuri`` will make the app unresponsive
+for 10 seconds and ``/seppuku`` will terminate it.
+
+The quest for the perfect Python container image is out of scope of this
+guide, so we'll go for the simplest possible configuration instead:
+
+.. literalinclude:: ../../example/deployment/kubernetes/Dockerfile
+
+After saving this ``Dockerfile``, build the image:
+
+.. code-block:: console
+
+ $ docker build -t websockets-test:1.0 .
+
+Test your image by running:
+
+.. code-block:: console
+
+ $ docker run --name run-websockets-test --publish 32080:80 --rm \
+ websockets-test:1.0
+
+Then, in another shell, in a virtualenv where websockets is installed, connect
+to the app and check that it echoes anything you send:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:32080/
+ Connected to ws://localhost:32080/.
+ > Hey there!
+ < Hey there!
+ >
+
+Now, in yet another shell, stop the app with:
+
+.. code-block:: console
+
+ $ docker kill -s TERM run-websockets-test
+
+Going to the shell where you connected to the app, you can confirm that it
+shut down gracefully:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:32080/
+ Connected to ws://localhost:32080/.
+ > Hey there!
+ < Hey there!
+ Connection closed: 1001 (going away).
+
+If it didn't, you'd get code 1006 (abnormal closure).
+
+Deploy application
+------------------
+
+Configuring Kubernetes is even further beyond the scope of this guide, so
+we'll use a basic configuration for testing, with just one Service_ and one
+Deployment_:
+
+.. literalinclude:: ../../example/deployment/kubernetes/deployment.yaml
+
+For local testing, a service of type NodePort_ is good enough. For deploying
+to production, you would configure an Ingress_.
+
+.. _Service: https://kubernetes.io/docs/concepts/services-networking/service/
+.. _Deployment: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
+.. _NodePort: https://kubernetes.io/docs/concepts/services-networking/service/#nodeport
+.. _Ingress: https://kubernetes.io/docs/concepts/services-networking/ingress/
+
+After saving this to a file called ``deployment.yaml``, you can deploy:
+
+.. code-block:: console
+
+ $ kubectl apply -f deployment.yaml
+ service/websockets-test created
+ deployment.apps/websockets-test created
+
+Now you have a deployment with one pod running:
+
+.. code-block:: console
+
+ $ kubectl get deployment websockets-test
+ NAME READY UP-TO-DATE AVAILABLE AGE
+ websockets-test 1/1 1 1 10s
+ $ kubectl get pods -l app=websockets-test
+ NAME READY STATUS RESTARTS AGE
+ websockets-test-86b48f4bb7-nltfh 1/1 Running 0 10s
+
+You can connect to the service — press Ctrl-D to exit:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:32080/
+ Connected to ws://localhost:32080/.
+ Connection closed: 1000 (OK).
+
+Validate deployment
+-------------------
+
+First, let's ensure the liveness probe works by making the app unresponsive:
+
+.. code-block:: console
+
+ $ curl http://localhost:32080/inemuri
+ Sleeping for 10s
+
+Since we have only one pod, we know that this pod will go to sleep.
+
+The liveness probe is configured to run every second. By default, liveness
+probes time out after one second and have a threshold of three failures.
+Therefore Kubernetes should restart the pod after at most 5 seconds.
+
+Indeed, after a few seconds, the pod reports a restart:
+
+.. code-block:: console
+
+ $ kubectl get pods -l app=websockets-test
+ NAME READY STATUS RESTARTS AGE
+ websockets-test-86b48f4bb7-nltfh 1/1 Running 1 42s
+
+Next, let's take it one step further and crash the app:
+
+.. code-block:: console
+
+ $ curl http://localhost:32080/seppuku
+ Terminating
+
+The pod reports a second restart:
+
+.. code-block:: console
+
+ $ kubectl get pods -l app=websockets-test
+ NAME READY STATUS RESTARTS AGE
+ websockets-test-86b48f4bb7-nltfh 1/1 Running 2 72s
+
+All good — Kubernetes delivers on its promise to keep our app alive!
+
+Scale deployment
+----------------
+
+Of course, Kubernetes is for scaling. Let's scale — modestly — to 10 pods:
+
+.. code-block:: console
+
+ $ kubectl scale deployment.apps/websockets-test --replicas=10
+ deployment.apps/websockets-test scaled
+
+After a few seconds, we have 10 pods running:
+
+.. code-block:: console
+
+ $ kubectl get deployment websockets-test
+ NAME READY UP-TO-DATE AVAILABLE AGE
+ websockets-test 10/10 10 10 10m
+
+Now let's generate load. We'll use this script:
+
+.. literalinclude:: ../../example/deployment/kubernetes/benchmark.py
+
+We'll connect 500 clients in parallel, meaning 50 clients per pod, and have
+each client send 6 messages. Since the app blocks for 100ms before responding,
+if connections are perfectly distributed, we expect a total run time slightly
+over 50 * 6 * 0.1 = 30 seconds.
+
+Let's try it:
+
+.. code-block:: console
+
+ $ ulimit -n 512
+ $ time python benchmark.py 500 6
+ python benchmark.py 500 6 2.40s user 0.51s system 7% cpu 36.471 total
+
+A total runtime of 36 seconds is in the right ballpark. Repeating this
+experiment with other parameters shows roughly consistent results, with the
+high variability you'd expect from a quick benchmark without any effort to
+stabilize the test setup.
+
+Finally, we can scale back to one pod.
+
+.. code-block:: console
+
+ $ kubectl scale deployment.apps/websockets-test --replicas=1
+ deployment.apps/websockets-test scaled
+ $ kubectl get deployment websockets-test
+ NAME READY UP-TO-DATE AVAILABLE AGE
+ websockets-test 1/1 1 1 15m
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/nginx.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/nginx.rst
new file mode 100644
index 0000000000..30545fbc7d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/nginx.rst
@@ -0,0 +1,84 @@
+Deploy behind nginx
+===================
+
+This guide demonstrates a way to load balance connections across multiple
+websockets server processes running on the same machine with nginx_.
+
+We'll run server processes with Supervisor as described in :doc:`this guide
+<supervisor>`.
+
+.. _nginx: https://nginx.org/
+
+Run server processes
+--------------------
+
+Save this app to ``app.py``:
+
+.. literalinclude:: ../../example/deployment/nginx/app.py
+ :emphasize-lines: 21,23
+
+We'd like to nginx to connect to websockets servers via Unix sockets in order
+to avoid the overhead of TCP for communicating between processes running in
+the same OS.
+
+We start the app with :func:`~websockets.server.unix_serve`. Each server
+process listens on a different socket thanks to an environment variable set
+by Supervisor to a different value.
+
+Save this configuration to ``supervisord.conf``:
+
+.. literalinclude:: ../../example/deployment/nginx/supervisord.conf
+
+This configuration runs four instances of the app.
+
+Install Supervisor and run it:
+
+.. code-block:: console
+
+ $ supervisord -c supervisord.conf -n
+
+Configure and run nginx
+-----------------------
+
+Here's a simple nginx configuration to load balance connections across four
+processes:
+
+.. literalinclude:: ../../example/deployment/nginx/nginx.conf
+
+We set ``daemon off`` so we can run nginx in the foreground for testing.
+
+Then we combine the `WebSocket proxying`_ and `load balancing`_ guides:
+
+* The WebSocket protocol requires HTTP/1.1. We must set the HTTP protocol
+ version to 1.1, else nginx defaults to HTTP/1.0 for proxying.
+
+* The WebSocket handshake involves the ``Connection`` and ``Upgrade`` HTTP
+ headers. We must pass them to the upstream explicitly, else nginx drops
+ them because they're hop-by-hop headers.
+
+ We deviate from the `WebSocket proxying`_ guide because its example adds a
+ ``Connection: Upgrade`` header to every upstream request, even if the
+ original request didn't contain that header.
+
+* In the upstream configuration, we set the load balancing method to
+ ``least_conn`` in order to balance the number of active connections across
+ servers. This is best for long running connections.
+
+.. _WebSocket proxying: http://nginx.org/en/docs/http/websocket.html
+.. _load balancing: http://nginx.org/en/docs/http/load_balancing.html
+
+Save the configuration to ``nginx.conf``, install nginx, and run it:
+
+.. code-block:: console
+
+ $ nginx -c nginx.conf -p .
+
+You can confirm that nginx proxies connections properly:
+
+.. code-block:: console
+
+ $ PYTHONPATH=src python -m websockets ws://localhost:8080/
+ Connected to ws://localhost:8080/.
+ > Hello!
+ < Hello!
+ Connection closed: 1000 (OK).
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/patterns.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/patterns.rst
new file mode 100644
index 0000000000..c6f325d213
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/patterns.rst
@@ -0,0 +1,110 @@
+Patterns
+========
+
+.. currentmodule:: websockets
+
+Here are typical patterns for processing messages in a WebSocket server or
+client. You will certainly implement some of them in your application.
+
+This page gives examples of connection handlers for a server. However, they're
+also applicable to a client, simply by assuming that ``websocket`` is a
+connection created with :func:`~client.connect`.
+
+WebSocket connections are long-lived. You will usually write a loop to process
+several messages during the lifetime of a connection.
+
+Consumer
+--------
+
+To receive messages from the WebSocket connection::
+
+ async def consumer_handler(websocket):
+ async for message in websocket:
+ await consumer(message)
+
+In this example, ``consumer()`` is a coroutine implementing your business
+logic for processing a message received on the WebSocket connection. Each
+message may be :class:`str` or :class:`bytes`.
+
+Iteration terminates when the client disconnects.
+
+Producer
+--------
+
+To send messages to the WebSocket connection::
+
+ async def producer_handler(websocket):
+ while True:
+ message = await producer()
+ await websocket.send(message)
+
+In this example, ``producer()`` is a coroutine implementing your business
+logic for generating the next message to send on the WebSocket connection.
+Each message must be :class:`str` or :class:`bytes`.
+
+Iteration terminates when the client disconnects
+because :meth:`~server.WebSocketServerProtocol.send` raises a
+:exc:`~exceptions.ConnectionClosed` exception,
+which breaks out of the ``while True`` loop.
+
+Consumer and producer
+---------------------
+
+You can receive and send messages on the same WebSocket connection by
+combining the consumer and producer patterns. This requires running two tasks
+in parallel::
+
+ async def handler(websocket):
+ await asyncio.gather(
+ consumer_handler(websocket),
+ producer_handler(websocket),
+ )
+
+If a task terminates, :func:`~asyncio.gather` doesn't cancel the other task.
+This can result in a situation where the producer keeps running after the
+consumer finished, which may leak resources.
+
+Here's a way to exit and close the WebSocket connection as soon as a task
+terminates, after canceling the other task::
+
+ async def handler(websocket):
+ consumer_task = asyncio.create_task(consumer_handler(websocket))
+ producer_task = asyncio.create_task(producer_handler(websocket))
+ done, pending = await asyncio.wait(
+ [consumer_task, producer_task],
+ return_when=asyncio.FIRST_COMPLETED,
+ )
+ for task in pending:
+ task.cancel()
+
+Registration
+------------
+
+To keep track of currently connected clients, you can register them when they
+connect and unregister them when they disconnect::
+
+ connected = set()
+
+ async def handler(websocket):
+ # Register.
+ connected.add(websocket)
+ try:
+ # Broadcast a message to all connected clients.
+ websockets.broadcast(connected, "Hello!")
+ await asyncio.sleep(10)
+ finally:
+ # Unregister.
+ connected.remove(websocket)
+
+This example maintains the set of connected clients in memory. This works as
+long as you run a single process. It doesn't scale to multiple processes.
+
+Publish–subscribe
+-----------------
+
+If you plan to run multiple processes and you want to communicate updates
+between processes, then you must deploy a messaging system. You may find
+publish-subscribe functionality useful.
+
+A complete implementation of this idea with Redis is described in
+the :doc:`Django integration guide <../howto/django>`.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/quickstart.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/quickstart.rst
new file mode 100644
index 0000000000..ab870952c1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/quickstart.rst
@@ -0,0 +1,170 @@
+Quick start
+===========
+
+.. currentmodule:: websockets
+
+Here are a few examples to get you started quickly with websockets.
+
+Say "Hello world!"
+------------------
+
+Here's a WebSocket server.
+
+It receives a name from the client, sends a greeting, and closes the connection.
+
+.. literalinclude:: ../../example/quickstart/server.py
+ :caption: server.py
+ :language: python
+ :linenos:
+
+:func:`~server.serve` executes the connection handler coroutine ``hello()``
+once for each WebSocket connection. It closes the WebSocket connection when
+the handler returns.
+
+Here's a corresponding WebSocket client.
+
+It sends a name to the server, receives a greeting, and closes the connection.
+
+.. literalinclude:: ../../example/quickstart/client.py
+ :caption: client.py
+ :language: python
+ :linenos:
+
+Using :func:`~client.connect` as an asynchronous context manager ensures the
+WebSocket connection is closed.
+
+.. _secure-server-example:
+
+Encrypt connections
+-------------------
+
+Secure WebSocket connections improve confidentiality and also reliability
+because they reduce the risk of interference by bad proxies.
+
+The ``wss`` protocol is to ``ws`` what ``https`` is to ``http``. The
+connection is encrypted with TLS_ (Transport Layer Security). ``wss``
+requires certificates like ``https``.
+
+.. _TLS: https://developer.mozilla.org/en-US/docs/Web/Security/Transport_Layer_Security
+
+.. admonition:: TLS vs. SSL
+ :class: tip
+
+ TLS is sometimes referred to as SSL (Secure Sockets Layer). SSL was an
+ earlier encryption protocol; the name stuck.
+
+Here's how to adapt the server to encrypt connections. You must download
+:download:`localhost.pem <../../example/quickstart/localhost.pem>` and save it
+in the same directory as ``server_secure.py``.
+
+.. literalinclude:: ../../example/quickstart/server_secure.py
+ :caption: server_secure.py
+ :language: python
+ :linenos:
+
+Here's how to adapt the client similarly.
+
+.. literalinclude:: ../../example/quickstart/client_secure.py
+ :caption: client_secure.py
+ :language: python
+ :linenos:
+
+In this example, the client needs a TLS context because the server uses a
+self-signed certificate.
+
+When connecting to a secure WebSocket server with a valid certificate — any
+certificate signed by a CA that your Python installation trusts — you can
+simply pass ``ssl=True`` to :func:`~client.connect`.
+
+.. admonition:: Configure the TLS context securely
+ :class: attention
+
+ This example demonstrates the ``ssl`` argument with a TLS certificate shared
+ between the client and the server. This is a simplistic setup.
+
+ Please review the advice and security considerations in the documentation of
+ the :mod:`ssl` module to configure the TLS context securely.
+
+Connect from a browser
+----------------------
+
+The WebSocket protocol was invented for the web — as the name says!
+
+Here's how to connect to a WebSocket server from a browser.
+
+Run this script in a console:
+
+.. literalinclude:: ../../example/quickstart/show_time.py
+ :caption: show_time.py
+ :language: python
+ :linenos:
+
+Save this file as ``show_time.html``:
+
+.. literalinclude:: ../../example/quickstart/show_time.html
+ :caption: show_time.html
+ :language: html
+ :linenos:
+
+Save this file as ``show_time.js``:
+
+.. literalinclude:: ../../example/quickstart/show_time.js
+ :caption: show_time.js
+ :language: js
+ :linenos:
+
+Then, open ``show_time.html`` in several browsers. Clocks tick irregularly.
+
+Broadcast messages
+------------------
+
+Let's change the previous example to send the same timestamps to all browsers,
+instead of generating independent sequences for each client.
+
+Stop the previous script if it's still running and run this script in a console:
+
+.. literalinclude:: ../../example/quickstart/show_time_2.py
+ :caption: show_time_2.py
+ :language: python
+ :linenos:
+
+Refresh ``show_time.html`` in all browsers. Clocks tick in sync.
+
+Manage application state
+------------------------
+
+A WebSocket server can receive events from clients, process them to update the
+application state, and broadcast the updated state to all connected clients.
+
+Here's an example where any client can increment or decrement a counter. The
+concurrency model of :mod:`asyncio` guarantees that updates are serialized.
+
+Run this script in a console:
+
+.. literalinclude:: ../../example/quickstart/counter.py
+ :caption: counter.py
+ :language: python
+ :linenos:
+
+Save this file as ``counter.html``:
+
+.. literalinclude:: ../../example/quickstart/counter.html
+ :caption: counter.html
+ :language: html
+ :linenos:
+
+Save this file as ``counter.css``:
+
+.. literalinclude:: ../../example/quickstart/counter.css
+ :caption: counter.css
+ :language: css
+ :linenos:
+
+Save this file as ``counter.js``:
+
+.. literalinclude:: ../../example/quickstart/counter.js
+ :caption: counter.js
+ :language: js
+ :linenos:
+
+Then open ``counter.html`` file in several browsers and play with [+] and [-].
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/render.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/render.rst
new file mode 100644
index 0000000000..70bf8c376c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/render.rst
@@ -0,0 +1,172 @@
+Deploy to Render
+================
+
+This guide describes how to deploy a websockets server to Render_.
+
+.. _Render: https://render.com/
+
+.. admonition:: The free plan of Render is sufficient for trying this guide.
+ :class: tip
+
+ However, on a `free plan`__, connections are dropped after five minutes,
+ which is quite short for WebSocket application.
+
+ __ https://render.com/docs/free
+
+We're going to deploy a very simple app. The process would be identical for a
+more realistic app.
+
+Create repository
+-----------------
+
+Deploying to Render requires a git repository. Let's initialize one:
+
+.. code-block:: console
+
+ $ mkdir websockets-echo
+ $ cd websockets-echo
+ $ git init -b main
+ Initialized empty Git repository in websockets-echo/.git/
+ $ git commit --allow-empty -m "Initial commit."
+ [main (root-commit) 816c3b1] Initial commit.
+
+Render requires the git repository to be hosted at GitHub or GitLab.
+
+Sign up or log in to GitHub. Create a new repository named ``websockets-echo``.
+Don't enable any of the initialization options offered by GitHub. Then, follow
+instructions for pushing an existing repository from the command line.
+
+After pushing, refresh your repository's homepage on GitHub. You should see an
+empty repository with an empty initial commit.
+
+Create application
+------------------
+
+Here's the implementation of the app, an echo server. Save it in a file called
+``app.py``:
+
+.. literalinclude:: ../../example/deployment/render/app.py
+ :language: python
+
+This app implements requirements for `zero downtime deploys`_:
+
+* it provides a health check at ``/healthz``;
+* it closes connections and exits cleanly when it receives a ``SIGTERM`` signal.
+
+.. _zero downtime deploys: https://render.com/docs/deploys#zero-downtime-deploys
+
+Create a ``requirements.txt`` file containing this line to declare a dependency
+on websockets:
+
+.. literalinclude:: ../../example/deployment/render/requirements.txt
+ :language: text
+
+Confirm that you created the correct files and commit them to git:
+
+.. code-block:: console
+
+ $ ls
+ app.py requirements.txt
+ $ git add .
+ $ git commit -m "Initial implementation."
+ [main f26bf7f] Initial implementation.
+ 2 files changed, 37 insertions(+)
+ create mode 100644 app.py
+ create mode 100644 requirements.txt
+
+Push the changes to GitHub:
+
+.. code-block:: console
+
+ $ git push
+ ...
+ To github.com:<username>/websockets-echo.git
+ 816c3b1..f26bf7f main -> main
+
+The app is ready. Let's deploy it!
+
+Deploy application
+------------------
+
+Sign up or log in to Render.
+
+Create a new web service. Connect the git repository that you just created.
+
+Then, finalize the configuration of your app as follows:
+
+* **Name**: websockets-echo
+* **Start Command**: ``python app.py``
+
+If you're just experimenting, select the free plan. Create the web service.
+
+To configure the health check, go to Settings, scroll down to Health & Alerts,
+and set:
+
+* **Health Check Path**: /healthz
+
+This triggers a new deployment.
+
+Validate deployment
+-------------------
+
+Let's confirm that your application is running as expected.
+
+Since it's a WebSocket server, you need a WebSocket client, such as the
+interactive client that comes with websockets.
+
+If you're currently building a websockets server, perhaps you're already in a
+virtualenv where websockets is installed. If not, you can install it in a new
+virtualenv as follows:
+
+.. code-block:: console
+
+ $ python -m venv websockets-client
+ $ . websockets-client/bin/activate
+ $ pip install websockets
+
+Connect the interactive client — you must replace ``websockets-echo`` with the
+name of your Render app in this command:
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.onrender.com/
+ Connected to wss://websockets-echo.onrender.com/.
+ >
+
+Great! Your app is running!
+
+Once you're connected, you can send any message and the server will echo it,
+or press Ctrl-D to terminate the connection:
+
+.. code-block:: console
+
+ > Hello!
+ < Hello!
+ Connection closed: 1000 (OK).
+
+You can also confirm that your application shuts down gracefully when you deploy
+a new version. Due to limitations of Render's free plan, you must upgrade to a
+paid plan before you perform this test.
+
+Connect an interactive client again — remember to replace ``websockets-echo``
+with your app:
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.onrender.com/
+ Connected to wss://websockets-echo.onrender.com/.
+ >
+
+Trigger a new deployment with Manual Deploy > Deploy latest commit. When the
+deployment completes, the connection is closed with code 1001 (going away).
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.onrender.com/
+ Connected to wss://websockets-echo.onrender.com/.
+ Connection closed: 1001 (going away).
+
+If graceful shutdown wasn't working, the server wouldn't perform a closing
+handshake and the connection would be closed with code 1006 (abnormal closure).
+
+Remember to downgrade to a free plan if you upgraded just for testing this feature.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/sansio.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/sansio.rst
new file mode 100644
index 0000000000..d41519ff09
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/sansio.rst
@@ -0,0 +1,322 @@
+Integrate the Sans-I/O layer
+============================
+
+.. currentmodule:: websockets
+
+This guide explains how to integrate the `Sans-I/O`_ layer of websockets to
+add support for WebSocket in another library.
+
+.. _Sans-I/O: https://sans-io.readthedocs.io/
+
+As a prerequisite, you should decide how you will handle network I/O and
+asynchronous control flow.
+
+Your integration layer will provide an API for the application on one side,
+will talk to the network on the other side, and will rely on websockets to
+implement the protocol in the middle.
+
+.. image:: ../topics/data-flow.svg
+ :align: center
+
+Opening a connection
+--------------------
+
+Client-side
+...........
+
+If you're building a client, parse the URI you'd like to connect to::
+
+ from websockets.uri import parse_uri
+
+ wsuri = parse_uri("ws://example.com/")
+
+Open a TCP connection to ``(wsuri.host, wsuri.port)`` and perform a TLS
+handshake if ``wsuri.secure`` is :obj:`True`.
+
+Initialize a :class:`~client.ClientProtocol`::
+
+ from websockets.client import ClientProtocol
+
+ protocol = ClientProtocol(wsuri)
+
+Create a WebSocket handshake request
+with :meth:`~client.ClientProtocol.connect` and send it
+with :meth:`~client.ClientProtocol.send_request`::
+
+ request = protocol.connect()
+ protocol.send_request(request)
+
+Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to
+the network, as described in `Send data`_ below.
+
+Once you receive enough data, as explained in `Receive data`_ below, the first
+event returned by :meth:`~protocol.Protocol.events_received` is the WebSocket
+handshake response.
+
+When the handshake fails, the reason is available in
+:attr:`~client.ClientProtocol.handshake_exc`::
+
+ if protocol.handshake_exc is not None:
+ raise protocol.handshake_exc
+
+Else, the WebSocket connection is open.
+
+A WebSocket client API usually performs the handshake then returns a wrapper
+around the network socket and the :class:`~client.ClientProtocol`.
+
+Server-side
+...........
+
+If you're building a server, accept network connections from clients and
+perform a TLS handshake if desired.
+
+For each connection, initialize a :class:`~server.ServerProtocol`::
+
+ from websockets.server import ServerProtocol
+
+ protocol = ServerProtocol()
+
+Once you receive enough data, as explained in `Receive data`_ below, the first
+event returned by :meth:`~protocol.Protocol.events_received` is the WebSocket
+handshake request.
+
+Create a WebSocket handshake response
+with :meth:`~server.ServerProtocol.accept` and send it
+with :meth:`~server.ServerProtocol.send_response`::
+
+ response = protocol.accept(request)
+ protocol.send_response(response)
+
+Alternatively, you may reject the WebSocket handshake and return an HTTP
+response with :meth:`~server.ServerProtocol.reject`::
+
+ response = protocol.reject(status, explanation)
+ protocol.send_response(response)
+
+Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to
+the network, as described in `Send data`_ below.
+
+Even when you call :meth:`~server.ServerProtocol.accept`, the WebSocket
+handshake may fail if the request is incorrect or unsupported.
+
+When the handshake fails, the reason is available in
+:attr:`~server.ServerProtocol.handshake_exc`::
+
+ if protocol.handshake_exc is not None:
+ raise protocol.handshake_exc
+
+Else, the WebSocket connection is open.
+
+A WebSocket server API usually builds a wrapper around the network socket and
+the :class:`~server.ServerProtocol`. Then it invokes a connection handler that
+accepts the wrapper in argument.
+
+It may also provide a way to close all connections and to shut down the server
+gracefully.
+
+Going forwards, this guide focuses on handling an individual connection.
+
+From the network to the application
+-----------------------------------
+
+Go through the five steps below until you reach the end of the data stream.
+
+Receive data
+............
+
+When receiving data from the network, feed it to the protocol's
+:meth:`~protocol.Protocol.receive_data` method.
+
+When reaching the end of the data stream, call the protocol's
+:meth:`~protocol.Protocol.receive_eof` method.
+
+For example, if ``sock`` is a :obj:`~socket.socket`::
+
+ try:
+ data = sock.recv(65536)
+ except OSError: # socket closed
+ data = b""
+ if data:
+ protocol.receive_data(data)
+ else:
+ protocol.receive_eof()
+
+These methods aren't expected to raise exceptions — unless you call them again
+after calling :meth:`~protocol.Protocol.receive_eof`, which is an error.
+(If you get an exception, please file a bug!)
+
+Send data
+.........
+
+Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to
+the network::
+
+ for data in protocol.data_to_send():
+ if data:
+ sock.sendall(data)
+ else:
+ sock.shutdown(socket.SHUT_WR)
+
+The empty bytestring signals the end of the data stream. When you see it, you
+must half-close the TCP connection.
+
+Sending data right after receiving data is necessary because websockets
+responds to ping frames, close frames, and incorrect inputs automatically.
+
+Expect TCP connection to close
+..............................
+
+Closing a WebSocket connection normally involves a two-way WebSocket closing
+handshake. Then, regardless of whether the closure is normal or abnormal, the
+server starts the four-way TCP closing handshake. If the network fails at the
+wrong point, you can end up waiting until the TCP timeout, which is very long.
+
+To prevent dangling TCP connections when you expect the end of the data stream
+but you never reach it, call :meth:`~protocol.Protocol.close_expected`
+and, if it returns :obj:`True`, schedule closing the TCP connection after a
+short timeout::
+
+ # start a new execution thread to run this code
+ sleep(10)
+ sock.close() # does nothing if the socket is already closed
+
+If the connection is still open when the timeout elapses, closing the socket
+makes the execution thread that reads from the socket reach the end of the
+data stream, possibly with an exception.
+
+Close TCP connection
+....................
+
+If you called :meth:`~protocol.Protocol.receive_eof`, close the TCP
+connection now. This is a clean closure because the receive buffer is empty.
+
+After :meth:`~protocol.Protocol.receive_eof` signals the end of the read
+stream, :meth:`~protocol.Protocol.data_to_send` always signals the end of
+the write stream, unless it already ended. So, at this point, the TCP
+connection is already half-closed. The only reason for closing it now is to
+release resources related to the socket.
+
+Now you can exit the loop relaying data from the network to the application.
+
+Receive events
+..............
+
+Finally, call :meth:`~protocol.Protocol.events_received` to obtain events
+parsed from the data provided to :meth:`~protocol.Protocol.receive_data`::
+
+ events = connection.events_received()
+
+The first event will be the WebSocket opening handshake request or response.
+See `Opening a connection`_ above for details.
+
+All later events are WebSocket frames. There are two types of frames:
+
+* Data frames contain messages transferred over the WebSocket connections. You
+ should provide them to the application. See `Fragmentation`_ below for
+ how to reassemble messages from frames.
+* Control frames provide information about the connection's state. The main
+ use case is to expose an abstraction over ping and pong to the application.
+ Keep in mind that websockets responds to ping frames and close frames
+ automatically. Don't duplicate this functionality!
+
+From the application to the network
+-----------------------------------
+
+The connection object provides one method for each type of WebSocket frame.
+
+For sending a data frame:
+
+* :meth:`~protocol.Protocol.send_continuation`
+* :meth:`~protocol.Protocol.send_text`
+* :meth:`~protocol.Protocol.send_binary`
+
+These methods raise :exc:`~exceptions.ProtocolError` if you don't set
+the :attr:`FIN <websockets.frames.Frame.fin>` bit correctly in fragmented
+messages.
+
+For sending a control frame:
+
+* :meth:`~protocol.Protocol.send_close`
+* :meth:`~protocol.Protocol.send_ping`
+* :meth:`~protocol.Protocol.send_pong`
+
+:meth:`~protocol.Protocol.send_close` initiates the closing handshake.
+See `Closing a connection`_ below for details.
+
+If you encounter an unrecoverable error and you must fail the WebSocket
+connection, call :meth:`~protocol.Protocol.fail`.
+
+After any of the above, call :meth:`~protocol.Protocol.data_to_send` and
+send its output to the network, as shown in `Send data`_ above.
+
+If you called :meth:`~protocol.Protocol.send_close`
+or :meth:`~protocol.Protocol.fail`, you expect the end of the data
+stream. You should follow the process described in `Close TCP connection`_
+above in order to prevent dangling TCP connections.
+
+Closing a connection
+--------------------
+
+Under normal circumstances, when a server wants to close the TCP connection:
+
+* it closes the write side;
+* it reads until the end of the stream, because it expects the client to close
+ the read side;
+* it closes the socket.
+
+When a client wants to close the TCP connection:
+
+* it reads until the end of the stream, because it expects the server to close
+ the read side;
+* it closes the write side;
+* it closes the socket.
+
+Applying the rules described earlier in this document gives the intended
+result. As a reminder, the rules are:
+
+* When :meth:`~protocol.Protocol.data_to_send` returns the empty
+ bytestring, close the write side of the TCP connection.
+* When you reach the end of the read stream, close the TCP connection.
+* When :meth:`~protocol.Protocol.close_expected` returns :obj:`True`, if
+ you don't reach the end of the read stream quickly, close the TCP connection.
+
+Fragmentation
+-------------
+
+WebSocket messages may be fragmented. Since this is a protocol-level concern,
+you may choose to reassemble fragmented messages before handing them over to
+the application.
+
+To reassemble a message, read data frames until you get a frame where
+the :attr:`FIN <websockets.frames.Frame.fin>` bit is set, then concatenate
+the payloads of all frames.
+
+You will never receive an inconsistent sequence of frames because websockets
+raises a :exc:`~exceptions.ProtocolError` and fails the connection when this
+happens. However, you may receive an incomplete sequence if the connection
+drops in the middle of a fragmented message.
+
+Tips
+----
+
+Serialize operations
+....................
+
+The Sans-I/O layer expects to run sequentially. If your interact with it from
+multiple threads or coroutines, you must ensure correct serialization. This
+should happen automatically in a cooperative multitasking environment.
+
+However, you still have to make sure you don't break this property by
+accident. For example, serialize writes to the network
+when :meth:`~protocol.Protocol.data_to_send` returns multiple values to
+prevent concurrent writes from interleaving incorrectly.
+
+Avoid buffers
+.............
+
+The Sans-I/O layer doesn't do any buffering. It makes events available in
+:meth:`~protocol.Protocol.events_received` as soon as they're received.
+
+You should make incoming messages available to the application immediately and
+stop further processing until the application fetches them. This will usually
+result in the best performance.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/supervisor.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/supervisor.rst
new file mode 100644
index 0000000000..5eefc7711b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/supervisor.rst
@@ -0,0 +1,131 @@
+Deploy with Supervisor
+======================
+
+This guide proposes a simple way to deploy a websockets server directly on a
+Linux or BSD operating system.
+
+We'll configure Supervisor_ to run several server processes and to restart
+them if needed.
+
+.. _Supervisor: http://supervisord.org/
+
+We'll bind all servers to the same port. The OS will take care of balancing
+connections.
+
+Create and activate a virtualenv:
+
+.. code-block:: console
+
+ $ python -m venv supervisor-websockets
+ $ . supervisor-websockets/bin/activate
+
+Install websockets and Supervisor:
+
+.. code-block:: console
+
+ $ pip install websockets
+ $ pip install supervisor
+
+Save this app to a file called ``app.py``:
+
+.. literalinclude:: ../../example/deployment/supervisor/app.py
+
+This is an echo server with two features added for the purpose of this guide:
+
+* It shuts down gracefully when receiving a ``SIGTERM`` signal;
+* It enables the ``reuse_port`` option of :meth:`~asyncio.loop.create_server`,
+ which in turns sets ``SO_REUSEPORT`` on the accept socket.
+
+Save this Supervisor configuration to ``supervisord.conf``:
+
+.. literalinclude:: ../../example/deployment/supervisor/supervisord.conf
+
+This is the minimal configuration required to keep four instances of the app
+running, restarting them if they exit.
+
+Now start Supervisor in the foreground:
+
+.. code-block:: console
+
+ $ supervisord -c supervisord.conf -n
+ INFO Increased RLIMIT_NOFILE limit to 1024
+ INFO supervisord started with pid 43596
+ INFO spawned: 'websockets-test_00' with pid 43597
+ INFO spawned: 'websockets-test_01' with pid 43598
+ INFO spawned: 'websockets-test_02' with pid 43599
+ INFO spawned: 'websockets-test_03' with pid 43600
+ INFO success: websockets-test_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
+ INFO success: websockets-test_01 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
+ INFO success: websockets-test_02 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
+ INFO success: websockets-test_03 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
+
+In another shell, after activating the virtualenv, we can connect to the app —
+press Ctrl-D to exit:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:8080/
+ Connected to ws://localhost:8080/.
+ > Hello!
+ < Hello!
+ Connection closed: 1000 (OK).
+
+Look at the pid of an instance of the app in the logs and terminate it:
+
+.. code-block:: console
+
+ $ kill -TERM 43597
+
+The logs show that Supervisor restarted this instance:
+
+.. code-block:: console
+
+ INFO exited: websockets-test_00 (exit status 0; expected)
+ INFO spawned: 'websockets-test_00' with pid 43629
+ INFO success: websockets-test_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
+
+Now let's check what happens when we shut down Supervisor, but first let's
+establish a connection and leave it open:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:8080/
+ Connected to ws://localhost:8080/.
+ >
+
+Look at the pid of supervisord itself in the logs and terminate it:
+
+.. code-block:: console
+
+ $ kill -TERM 43596
+
+The logs show that Supervisor terminated all instances of the app before
+exiting:
+
+.. code-block:: console
+
+ WARN received SIGTERM indicating exit request
+ INFO waiting for websockets-test_00, websockets-test_01, websockets-test_02, websockets-test_03 to die
+ INFO stopped: websockets-test_02 (exit status 0)
+ INFO stopped: websockets-test_03 (exit status 0)
+ INFO stopped: websockets-test_01 (exit status 0)
+ INFO stopped: websockets-test_00 (exit status 0)
+
+And you can see that the connection to the app was closed gracefully:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:8080/
+ Connected to ws://localhost:8080/.
+ Connection closed: 1001 (going away).
+
+In this example, we've been sharing the same virtualenv for supervisor and
+websockets.
+
+In a real deployment, you would likely:
+
+* Install Supervisor with the package manager of the OS.
+* Create a virtualenv dedicated to your application.
+* Add ``environment=PATH="path/to/your/virtualenv/bin"`` in the Supervisor
+ configuration. Then ``python app.py`` runs in that virtualenv.
+
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst
new file mode 100644
index 0000000000..d9737db12a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst
@@ -0,0 +1,75 @@
+websockets
+==========
+
+|licence| |version| |pyversions| |tests| |docs| |openssf|
+
+.. |licence| image:: https://img.shields.io/pypi/l/websockets.svg
+ :target: https://pypi.python.org/pypi/websockets
+
+.. |version| image:: https://img.shields.io/pypi/v/websockets.svg
+ :target: https://pypi.python.org/pypi/websockets
+
+.. |pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg
+ :target: https://pypi.python.org/pypi/websockets
+
+.. |tests| image:: https://img.shields.io/github/checks-status/python-websockets/websockets/main?label=tests
+ :target: https://github.com/python-websockets/websockets/actions/workflows/tests.yml
+
+.. |docs| image:: https://img.shields.io/readthedocs/websockets.svg
+ :target: https://websockets.readthedocs.io/
+
+.. |openssf| image:: https://bestpractices.coreinfrastructure.org/projects/6475/badge
+ :target: https://bestpractices.coreinfrastructure.org/projects/6475
+
+websockets is a library for building WebSocket_ servers and clients in Python
+with a focus on correctness, simplicity, robustness, and performance.
+
+.. _WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
+
+It supports several network I/O and control flow paradigms:
+
+1. The default implementation builds upon :mod:`asyncio`, Python's standard
+ asynchronous I/O framework. It provides an elegant coroutine-based API. It's
+ ideal for servers that handle many clients concurrently.
+2. The :mod:`threading` implementation is a good alternative for clients,
+ especially if you aren't familiar with :mod:`asyncio`. It may also be used
+ for servers that don't need to serve many clients.
+3. The `Sans-I/O`_ implementation is designed for integrating in third-party
+ libraries, typically application servers, in addition being used internally
+ by websockets.
+
+.. _Sans-I/O: https://sans-io.readthedocs.io/
+
+Here's an echo server with the :mod:`asyncio` API:
+
+.. literalinclude:: ../example/echo.py
+
+Here's how a client sends and receives messages with the :mod:`threading` API:
+
+.. literalinclude:: ../example/hello.py
+
+Don't worry about the opening and closing handshakes, pings and pongs, or any
+other behavior described in the WebSocket specification. websockets takes care
+of this under the hood so you can focus on your application!
+
+Also, websockets provides an interactive client:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:8765/
+ Connected to ws://localhost:8765/.
+ > Hello world!
+ < Hello world!
+ Connection closed: 1000 (OK).
+
+Do you like it? :doc:`Let's dive in! <intro/index>`
+
+.. toctree::
+ :hidden:
+
+ intro/index
+ howto/index
+ faq/index
+ reference/index
+ topics/index
+ project/index
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/index.rst
new file mode 100644
index 0000000000..095262a207
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/index.rst
@@ -0,0 +1,46 @@
+Getting started
+===============
+
+.. currentmodule:: websockets
+
+Requirements
+------------
+
+websockets requires Python ≥ 3.8.
+
+.. admonition:: Use the most recent Python release
+ :class: tip
+
+ For each minor version (3.x), only the latest bugfix or security release
+ (3.x.y) is officially supported.
+
+It doesn't have any dependencies.
+
+.. _install:
+
+Installation
+------------
+
+Install websockets with:
+
+.. code-block:: console
+
+ $ pip install websockets
+
+Wheels are available for all platforms.
+
+Tutorial
+--------
+
+Learn how to build an real-time web application with websockets.
+
+.. toctree::
+
+ tutorial1
+ tutorial2
+ tutorial3
+
+In a hurry?
+-----------
+
+Look at the :doc:`quick start guide <../howto/quickstart>`.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial1.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial1.rst
new file mode 100644
index 0000000000..ff85003b58
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial1.rst
@@ -0,0 +1,591 @@
+Part 1 - Send & receive
+=======================
+
+.. currentmodule:: websockets
+
+In this tutorial, you're going to build a web-based `Connect Four`_ game.
+
+.. _Connect Four: https://en.wikipedia.org/wiki/Connect_Four
+
+The web removes the constraint of being in the same room for playing a game.
+Two players can connect over of the Internet, regardless of where they are,
+and play in their browsers.
+
+When a player makes a move, it should be reflected immediately on both sides.
+This is difficult to implement over HTTP due to the request-response style of
+the protocol.
+
+Indeed, there is no good way to be notified when the other player makes a
+move. Workarounds such as polling or long-polling introduce significant
+overhead.
+
+Enter `WebSocket <websocket>`_.
+
+The WebSocket protocol provides two-way communication between a browser and a
+server over a persistent connection. That's exactly what you need to exchange
+moves between players, via a server.
+
+.. admonition:: This is the first part of the tutorial.
+
+ * In this :doc:`first part <tutorial1>`, you will create a server and
+ connect one browser; you can play if you share the same browser.
+ * In the :doc:`second part <tutorial2>`, you will connect a second
+ browser; you can play from different browsers on a local network.
+ * In the :doc:`third part <tutorial3>`, you will deploy the game to the
+ web; you can play from any browser connected to the Internet.
+
+Prerequisites
+-------------
+
+This tutorial assumes basic knowledge of Python and JavaScript.
+
+If you're comfortable with :doc:`virtual environments <python:tutorial/venv>`,
+you can use one for this tutorial. Else, don't worry: websockets doesn't have
+any dependencies; it shouldn't create trouble in the default environment.
+
+If you haven't installed websockets yet, do it now:
+
+.. code-block:: console
+
+ $ pip install websockets
+
+Confirm that websockets is installed:
+
+.. code-block:: console
+
+ $ python -m websockets --version
+
+.. admonition:: This tutorial is written for websockets |release|.
+ :class: tip
+
+ If you installed another version, you should switch to the corresponding
+ version of the documentation.
+
+Download the starter kit
+------------------------
+
+Create a directory and download these three files:
+:download:`connect4.js <../../example/tutorial/start/connect4.js>`,
+:download:`connect4.css <../../example/tutorial/start/connect4.css>`,
+and :download:`connect4.py <../../example/tutorial/start/connect4.py>`.
+
+The JavaScript module, along with the CSS file, provides a web-based user
+interface. Here's its API.
+
+.. js:module:: connect4
+
+.. js:data:: PLAYER1
+
+ Color of the first player.
+
+.. js:data:: PLAYER2
+
+ Color of the second player.
+
+.. js:function:: createBoard(board)
+
+ Draw a board.
+
+ :param board: DOM element containing the board; must be initially empty.
+
+.. js:function:: playMove(board, player, column, row)
+
+ Play a move.
+
+ :param board: DOM element containing the board.
+ :param player: :js:data:`PLAYER1` or :js:data:`PLAYER2`.
+ :param column: between ``0`` and ``6``.
+ :param row: between ``0`` and ``5``.
+
+The Python module provides a class to record moves and tell when a player
+wins. Here's its API.
+
+.. module:: connect4
+
+.. data:: PLAYER1
+ :value: "red"
+
+ Color of the first player.
+
+.. data:: PLAYER2
+ :value: "yellow"
+
+ Color of the second player.
+
+.. class:: Connect4
+
+ A Connect Four game.
+
+ .. method:: play(player, column)
+
+ Play a move.
+
+ :param player: :data:`~connect4.PLAYER1` or :data:`~connect4.PLAYER2`.
+ :param column: between ``0`` and ``6``.
+ :returns: Row where the checker lands, between ``0`` and ``5``.
+ :raises RuntimeError: if the move is illegal.
+
+ .. attribute:: moves
+
+ List of moves played during this game, as ``(player, column, row)``
+ tuples.
+
+ .. attribute:: winner
+
+ :data:`~connect4.PLAYER1` or :data:`~connect4.PLAYER2` if they
+ won; :obj:`None` if the game is still ongoing.
+
+.. currentmodule:: websockets
+
+Bootstrap the web UI
+--------------------
+
+Create an ``index.html`` file next to ``connect4.js`` and ``connect4.css``
+with this content:
+
+.. literalinclude:: ../../example/tutorial/step1/index.html
+ :language: html
+
+This HTML page contains an empty ``<div>`` element where you will draw the
+Connect Four board. It loads a ``main.js`` script where you will write all
+your JavaScript code.
+
+Create a ``main.js`` file next to ``index.html``. In this script, when the
+page loads, draw the board:
+
+.. code-block:: javascript
+
+ import { createBoard, playMove } from "./connect4.js";
+
+ window.addEventListener("DOMContentLoaded", () => {
+ // Initialize the UI.
+ const board = document.querySelector(".board");
+ createBoard(board);
+ });
+
+Open a shell, navigate to the directory containing these files, and start an
+HTTP server:
+
+.. code-block:: console
+
+ $ python -m http.server
+
+Open http://localhost:8000/ in a web browser. The page displays an empty board
+with seven columns and six rows. You will play moves in this board later.
+
+Bootstrap the server
+--------------------
+
+Create an ``app.py`` file next to ``connect4.py`` with this content:
+
+.. code-block:: python
+
+ #!/usr/bin/env python
+
+ import asyncio
+
+ import websockets
+
+
+ async def handler(websocket):
+ while True:
+ message = await websocket.recv()
+ print(message)
+
+
+ async def main():
+ async with websockets.serve(handler, "", 8001):
+ await asyncio.Future() # run forever
+
+
+ if __name__ == "__main__":
+ asyncio.run(main())
+
+The entry point of this program is ``asyncio.run(main())``. It creates an
+asyncio event loop, runs the ``main()`` coroutine, and shuts down the loop.
+
+The ``main()`` coroutine calls :func:`~server.serve` to start a websockets
+server. :func:`~server.serve` takes three positional arguments:
+
+* ``handler`` is a coroutine that manages a connection. When a client
+ connects, websockets calls ``handler`` with the connection in argument.
+ When ``handler`` terminates, websockets closes the connection.
+* The second argument defines the network interfaces where the server can be
+ reached. Here, the server listens on all interfaces, so that other devices
+ on the same local network can connect.
+* The third argument is the port on which the server listens.
+
+Invoking :func:`~server.serve` as an asynchronous context manager, in an
+``async with`` block, ensures that the server shuts down properly when
+terminating the program.
+
+For each connection, the ``handler()`` coroutine runs an infinite loop that
+receives messages from the browser and prints them.
+
+Open a shell, navigate to the directory containing ``app.py``, and start the
+server:
+
+.. code-block:: console
+
+ $ python app.py
+
+This doesn't display anything. Hopefully the WebSocket server is running.
+Let's make sure that it works. You cannot test the WebSocket server with a
+web browser like you tested the HTTP server. However, you can test it with
+websockets' interactive client.
+
+Open another shell and run this command:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:8001/
+
+You get a prompt. Type a message and press "Enter". Switch to the shell where
+the server is running and check that the server received the message. Good!
+
+Exit the interactive client with Ctrl-C or Ctrl-D.
+
+Now, if you look at the console where you started the server, you can see the
+stack trace of an exception:
+
+.. code-block:: pytb
+
+ connection handler failed
+ Traceback (most recent call last):
+ ...
+ File "app.py", line 22, in handler
+ message = await websocket.recv()
+ ...
+ websockets.exceptions.ConnectionClosedOK: received 1000 (OK); then sent 1000 (OK)
+
+Indeed, the server was waiting for the next message
+with :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` when the client
+disconnected. When this happens, websockets raises
+a :exc:`~exceptions.ConnectionClosedOK` exception to let you know that you
+won't receive another message on this connection.
+
+This exception creates noise in the server logs, making it more difficult to
+spot real errors when you add functionality to the server. Catch it in the
+``handler()`` coroutine:
+
+.. code-block:: python
+
+ async def handler(websocket):
+ while True:
+ try:
+ message = await websocket.recv()
+ except websockets.ConnectionClosedOK:
+ break
+ print(message)
+
+Stop the server with Ctrl-C and start it again:
+
+.. code-block:: console
+
+ $ python app.py
+
+.. admonition:: You must restart the WebSocket server when you make changes.
+ :class: tip
+
+ The WebSocket server loads the Python code in ``app.py`` then serves every
+ WebSocket request with this version of the code. As a consequence,
+ changes to ``app.py`` aren't visible until you restart the server.
+
+ This is unlike the HTTP server that you started earlier with ``python -m
+ http.server``. For every request, this HTTP server reads the target file
+ and sends it. That's why changes are immediately visible.
+
+ It is possible to :doc:`restart the WebSocket server automatically
+ <../howto/autoreload>` but this isn't necessary for this tutorial.
+
+Try connecting and disconnecting the interactive client again.
+The :exc:`~exceptions.ConnectionClosedOK` exception doesn't appear anymore.
+
+This pattern is so common that websockets provides a shortcut for iterating
+over messages received on the connection until the client disconnects:
+
+.. code-block:: python
+
+ async def handler(websocket):
+ async for message in websocket:
+ print(message)
+
+Restart the server and check with the interactive client that its behavior
+didn't change.
+
+At this point, you bootstrapped a web application and a WebSocket server.
+Let's connect them.
+
+Transmit from browser to server
+-------------------------------
+
+In JavaScript, you open a WebSocket connection as follows:
+
+.. code-block:: javascript
+
+ const websocket = new WebSocket("ws://localhost:8001/");
+
+Before you exchange messages with the server, you need to decide their format.
+There is no universal convention for this.
+
+Let's use JSON objects with a ``type`` key identifying the type of the event
+and the rest of the object containing properties of the event.
+
+Here's an event describing a move in the middle slot of the board:
+
+.. code-block:: javascript
+
+ const event = {type: "play", column: 3};
+
+Here's how to serialize this event to JSON and send it to the server:
+
+.. code-block:: javascript
+
+ websocket.send(JSON.stringify(event));
+
+Now you have all the building blocks to send moves to the server.
+
+Add this function to ``main.js``:
+
+.. literalinclude:: ../../example/tutorial/step1/main.js
+ :language: js
+ :start-at: function sendMoves
+ :end-before: window.addEventListener
+
+``sendMoves()`` registers a listener for ``click`` events on the board. The
+listener figures out which column was clicked, builds a event of type
+``"play"``, serializes it, and sends it to the server.
+
+Modify the initialization to open the WebSocket connection and call the
+``sendMoves()`` function:
+
+.. code-block:: javascript
+
+ window.addEventListener("DOMContentLoaded", () => {
+ // Initialize the UI.
+ const board = document.querySelector(".board");
+ createBoard(board);
+ // Open the WebSocket connection and register event handlers.
+ const websocket = new WebSocket("ws://localhost:8001/");
+ sendMoves(board, websocket);
+ });
+
+Check that the HTTP server and the WebSocket server are still running. If you
+stopped them, here are the commands to start them again:
+
+.. code-block:: console
+
+ $ python -m http.server
+
+.. code-block:: console
+
+ $ python app.py
+
+Refresh http://localhost:8000/ in your web browser. Click various columns in
+the board. The server receives messages with the expected column number.
+
+There isn't any feedback in the board because you haven't implemented that
+yet. Let's do it.
+
+Transmit from server to browser
+-------------------------------
+
+In JavaScript, you receive WebSocket messages by listening to ``message``
+events. Here's how to receive a message from the server and deserialize it
+from JSON:
+
+.. code-block:: javascript
+
+ websocket.addEventListener("message", ({ data }) => {
+ const event = JSON.parse(data);
+ // do something with event
+ });
+
+You're going to need three types of messages from the server to the browser:
+
+.. code-block:: javascript
+
+ {type: "play", player: "red", column: 3, row: 0}
+ {type: "win", player: "red"}
+ {type: "error", message: "This slot is full."}
+
+The JavaScript code receiving these messages will dispatch events depending on
+their type and take appropriate action. For example, it will react to an
+event of type ``"play"`` by displaying the move on the board with
+the :js:func:`~connect4.playMove` function.
+
+Add this function to ``main.js``:
+
+.. literalinclude:: ../../example/tutorial/step1/main.js
+ :language: js
+ :start-at: function showMessage
+ :end-before: function sendMoves
+
+.. admonition:: Why does ``showMessage`` use ``window.setTimeout``?
+ :class: hint
+
+ When :js:func:`playMove` modifies the state of the board, the browser
+ renders changes asynchronously. Conversely, ``window.alert()`` runs
+ synchronously and blocks rendering while the alert is visible.
+
+ If you called ``window.alert()`` immediately after :js:func:`playMove`,
+ the browser could display the alert before rendering the move. You could
+ get a "Player red wins!" alert without seeing red's last move.
+
+ We're using ``window.alert()`` for simplicity in this tutorial. A real
+ application would display these messages in the user interface instead.
+ It wouldn't be vulnerable to this problem.
+
+Modify the initialization to call the ``receiveMoves()`` function:
+
+.. literalinclude:: ../../example/tutorial/step1/main.js
+ :language: js
+ :start-at: window.addEventListener
+
+At this point, the user interface should receive events properly. Let's test
+it by modifying the server to send some events.
+
+Sending an event from Python is quite similar to JavaScript:
+
+.. code-block:: python
+
+ event = {"type": "play", "player": "red", "column": 3, "row": 0}
+ await websocket.send(json.dumps(event))
+
+.. admonition:: Don't forget to serialize the event with :func:`json.dumps`.
+ :class: tip
+
+ Else, websockets raises ``TypeError: data is a dict-like object``.
+
+Modify the ``handler()`` coroutine in ``app.py`` as follows:
+
+.. code-block:: python
+
+ import json
+
+ from connect4 import PLAYER1, PLAYER2
+
+ async def handler(websocket):
+ for player, column, row in [
+ (PLAYER1, 3, 0),
+ (PLAYER2, 3, 1),
+ (PLAYER1, 4, 0),
+ (PLAYER2, 4, 1),
+ (PLAYER1, 2, 0),
+ (PLAYER2, 1, 0),
+ (PLAYER1, 5, 0),
+ ]:
+ event = {
+ "type": "play",
+ "player": player,
+ "column": column,
+ "row": row,
+ }
+ await websocket.send(json.dumps(event))
+ await asyncio.sleep(0.5)
+ event = {
+ "type": "win",
+ "player": PLAYER1,
+ }
+ await websocket.send(json.dumps(event))
+
+Restart the WebSocket server and refresh http://localhost:8000/ in your web
+browser. Seven moves appear at 0.5 second intervals. Then an alert announces
+the winner.
+
+Good! Now you know how to communicate both ways.
+
+Once you plug the game engine to process moves, you will have a fully
+functional game.
+
+Add the game logic
+------------------
+
+In the ``handler()`` coroutine, you're going to initialize a game:
+
+.. code-block:: python
+
+ from connect4 import Connect4
+
+ async def handler(websocket):
+ # Initialize a Connect Four game.
+ game = Connect4()
+
+ ...
+
+Then, you're going to iterate over incoming messages and take these steps:
+
+* parse an event of type ``"play"``, the only type of event that the user
+ interface sends;
+* play the move in the board with the :meth:`~connect4.Connect4.play` method,
+ alternating between the two players;
+* if :meth:`~connect4.Connect4.play` raises :exc:`RuntimeError` because the
+ move is illegal, send an event of type ``"error"``;
+* else, send an event of type ``"play"`` to tell the user interface where the
+ checker lands;
+* if the move won the game, send an event of type ``"win"``.
+
+Try to implement this by yourself!
+
+Keep in mind that you must restart the WebSocket server and reload the page in
+the browser when you make changes.
+
+When it works, you can play the game from a single browser, with players
+taking alternate turns.
+
+.. admonition:: Enable debug logs to see all messages sent and received.
+ :class: tip
+
+ Here's how to enable debug logs:
+
+ .. code-block:: python
+
+ import logging
+
+ logging.basicConfig(format="%(message)s", level=logging.DEBUG)
+
+If you're stuck, a solution is available at the bottom of this document.
+
+Summary
+-------
+
+In this first part of the tutorial, you learned how to:
+
+* build and run a WebSocket server in Python with :func:`~server.serve`;
+* receive a message in a connection handler
+ with :meth:`~server.WebSocketServerProtocol.recv`;
+* send a message in a connection handler
+ with :meth:`~server.WebSocketServerProtocol.send`;
+* iterate over incoming messages with ``async for
+ message in websocket: ...``;
+* open a WebSocket connection in JavaScript with the ``WebSocket`` API;
+* send messages in a browser with ``WebSocket.send()``;
+* receive messages in a browser by listening to ``message`` events;
+* design a set of events to be exchanged between the browser and the server.
+
+You can now play a Connect Four game in a browser, communicating over a
+WebSocket connection with a server where the game logic resides!
+
+However, the two players share a browser, so the constraint of being in the
+same room still applies.
+
+Move on to the :doc:`second part <tutorial2>` of the tutorial to break this
+constraint and play from separate browsers.
+
+Solution
+--------
+
+.. literalinclude:: ../../example/tutorial/step1/app.py
+ :caption: app.py
+ :language: python
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step1/index.html
+ :caption: index.html
+ :language: html
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step1/main.js
+ :caption: main.js
+ :language: js
+ :linenos:
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial2.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial2.rst
new file mode 100644
index 0000000000..5ac4ae9dd5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial2.rst
@@ -0,0 +1,565 @@
+Part 2 - Route & broadcast
+==========================
+
+.. currentmodule:: websockets
+
+.. admonition:: This is the second part of the tutorial.
+
+ * In the :doc:`first part <tutorial1>`, you created a server and
+ connected one browser; you could play if you shared the same browser.
+ * In this :doc:`second part <tutorial2>`, you will connect a second
+ browser; you can play from different browsers on a local network.
+ * In the :doc:`third part <tutorial3>`, you will deploy the game to the
+ web; you can play from any browser connected to the Internet.
+
+In the first part of the tutorial, you opened a WebSocket connection from a
+browser to a server and exchanged events to play moves. The state of the game
+was stored in an instance of the :class:`~connect4.Connect4` class,
+referenced as a local variable in the connection handler coroutine.
+
+Now you want to open two WebSocket connections from two separate browsers, one
+for each player, to the same server in order to play the same game. This
+requires moving the state of the game to a place where both connections can
+access it.
+
+Share game state
+----------------
+
+As long as you're running a single server process, you can share state by
+storing it in a global variable.
+
+.. admonition:: What if you need to scale to multiple server processes?
+ :class: hint
+
+ In that case, you must design a way for the process that handles a given
+ connection to be aware of relevant events for that client. This is often
+ achieved with a publish / subscribe mechanism.
+
+How can you make two connection handlers agree on which game they're playing?
+When the first player starts a game, you give it an identifier. Then, you
+communicate the identifier to the second player. When the second player joins
+the game, you look it up with the identifier.
+
+In addition to the game itself, you need to keep track of the WebSocket
+connections of the two players. Since both players receive the same events,
+you don't need to treat the two connections differently; you can store both
+in the same set.
+
+Let's sketch this in code.
+
+A module-level :class:`dict` enables lookups by identifier:
+
+.. code-block:: python
+
+ JOIN = {}
+
+When the first player starts the game, initialize and store it:
+
+.. code-block:: python
+
+ import secrets
+
+ async def handler(websocket):
+ ...
+
+ # Initialize a Connect Four game, the set of WebSocket connections
+ # receiving moves from this game, and secret access token.
+ game = Connect4()
+ connected = {websocket}
+
+ join_key = secrets.token_urlsafe(12)
+ JOIN[join_key] = game, connected
+
+ try:
+
+ ...
+
+ finally:
+ del JOIN[join_key]
+
+When the second player joins the game, look it up:
+
+.. code-block:: python
+
+ async def handler(websocket):
+ ...
+
+ join_key = ... # TODO
+
+ # Find the Connect Four game.
+ game, connected = JOIN[join_key]
+
+ # Register to receive moves from this game.
+ connected.add(websocket)
+ try:
+
+ ...
+
+ finally:
+ connected.remove(websocket)
+
+Notice how we're carefully cleaning up global state with ``try: ...
+finally: ...`` blocks. Else, we could leave references to games or
+connections in global state, which would cause a memory leak.
+
+In both connection handlers, you have a ``game`` pointing to the same
+:class:`~connect4.Connect4` instance, so you can interact with the game,
+and a ``connected`` set of connections, so you can send game events to
+both players as follows:
+
+.. code-block:: python
+
+ async def handler(websocket):
+
+ ...
+
+ for connection in connected:
+ await connection.send(json.dumps(event))
+
+ ...
+
+Perhaps you spotted a major piece missing from the puzzle. How does the second
+player obtain ``join_key``? Let's design new events to carry this information.
+
+To start a game, the first player sends an ``"init"`` event:
+
+.. code-block:: javascript
+
+ {type: "init"}
+
+The connection handler for the first player creates a game as shown above and
+responds with:
+
+.. code-block:: javascript
+
+ {type: "init", join: "<join_key>"}
+
+With this information, the user interface of the first player can create a
+link to ``http://localhost:8000/?join=<join_key>``. For the sake of simplicity,
+we will assume that the first player shares this link with the second player
+outside of the application, for example via an instant messaging service.
+
+To join the game, the second player sends a different ``"init"`` event:
+
+.. code-block:: javascript
+
+ {type: "init", join: "<join_key>"}
+
+The connection handler for the second player can look up the game with the
+join key as shown above. There is no need to respond.
+
+Let's dive into the details of implementing this design.
+
+Start a game
+------------
+
+We'll start with the initialization sequence for the first player.
+
+In ``main.js``, define a function to send an initialization event when the
+WebSocket connection is established, which triggers an ``open`` event:
+
+.. code-block:: javascript
+
+ function initGame(websocket) {
+ websocket.addEventListener("open", () => {
+ // Send an "init" event for the first player.
+ const event = { type: "init" };
+ websocket.send(JSON.stringify(event));
+ });
+ }
+
+Update the initialization sequence to call ``initGame()``:
+
+.. literalinclude:: ../../example/tutorial/step2/main.js
+ :language: js
+ :start-at: window.addEventListener
+
+In ``app.py``, define a new ``handler`` coroutine — keep a copy of the
+previous one to reuse it later:
+
+.. code-block:: python
+
+ import secrets
+
+
+ JOIN = {}
+
+
+ async def start(websocket):
+ # Initialize a Connect Four game, the set of WebSocket connections
+ # receiving moves from this game, and secret access token.
+ game = Connect4()
+ connected = {websocket}
+
+ join_key = secrets.token_urlsafe(12)
+ JOIN[join_key] = game, connected
+
+ try:
+ # Send the secret access token to the browser of the first player,
+ # where it'll be used for building a "join" link.
+ event = {
+ "type": "init",
+ "join": join_key,
+ }
+ await websocket.send(json.dumps(event))
+
+ # Temporary - for testing.
+ print("first player started game", id(game))
+ async for message in websocket:
+ print("first player sent", message)
+
+ finally:
+ del JOIN[join_key]
+
+
+ async def handler(websocket):
+ # Receive and parse the "init" event from the UI.
+ message = await websocket.recv()
+ event = json.loads(message)
+ assert event["type"] == "init"
+
+ # First player starts a new game.
+ await start(websocket)
+
+In ``index.html``, add an ``<a>`` element to display the link to share with
+the other player.
+
+.. code-block:: html
+
+ <body>
+ <div class="actions">
+ <a class="action join" href="">Join</a>
+ </div>
+ <!-- ... -->
+ </body>
+
+In ``main.js``, modify ``receiveMoves()`` to handle the ``"init"`` message and
+set the target of that link:
+
+.. code-block:: javascript
+
+ switch (event.type) {
+ case "init":
+ // Create link for inviting the second player.
+ document.querySelector(".join").href = "?join=" + event.join;
+ break;
+ // ...
+ }
+
+Restart the WebSocket server and reload http://localhost:8000/ in the browser.
+There's a link labeled JOIN below the board with a target that looks like
+http://localhost:8000/?join=95ftAaU5DJVP1zvb.
+
+The server logs say ``first player started game ...``. If you click the board,
+you see ``"play"`` events. There is no feedback in the UI, though, because
+you haven't restored the game logic yet.
+
+Before we get there, let's handle links with a ``join`` query parameter.
+
+Join a game
+-----------
+
+We'll now update the initialization sequence to account for the second
+player.
+
+In ``main.js``, update ``initGame()`` to send the join key in the ``"init"``
+message when it's in the URL:
+
+.. code-block:: javascript
+
+ function initGame(websocket) {
+ websocket.addEventListener("open", () => {
+ // Send an "init" event according to who is connecting.
+ const params = new URLSearchParams(window.location.search);
+ let event = { type: "init" };
+ if (params.has("join")) {
+ // Second player joins an existing game.
+ event.join = params.get("join");
+ } else {
+ // First player starts a new game.
+ }
+ websocket.send(JSON.stringify(event));
+ });
+ }
+
+In ``app.py``, update the ``handler`` coroutine to look for the join key in
+the ``"init"`` message, then load that game:
+
+.. code-block:: python
+
+ async def error(websocket, message):
+ event = {
+ "type": "error",
+ "message": message,
+ }
+ await websocket.send(json.dumps(event))
+
+
+ async def join(websocket, join_key):
+ # Find the Connect Four game.
+ try:
+ game, connected = JOIN[join_key]
+ except KeyError:
+ await error(websocket, "Game not found.")
+ return
+
+ # Register to receive moves from this game.
+ connected.add(websocket)
+ try:
+
+ # Temporary - for testing.
+ print("second player joined game", id(game))
+ async for message in websocket:
+ print("second player sent", message)
+
+ finally:
+ connected.remove(websocket)
+
+
+ async def handler(websocket):
+ # Receive and parse the "init" event from the UI.
+ message = await websocket.recv()
+ event = json.loads(message)
+ assert event["type"] == "init"
+
+ if "join" in event:
+ # Second player joins an existing game.
+ await join(websocket, event["join"])
+ else:
+ # First player starts a new game.
+ await start(websocket)
+
+Restart the WebSocket server and reload http://localhost:8000/ in the browser.
+
+Copy the link labeled JOIN and open it in another browser. You may also open
+it in another tab or another window of the same browser; however, that makes
+it a bit tricky to remember which one is the first or second player.
+
+.. admonition:: You must start a new game when you restart the server.
+ :class: tip
+
+ Since games are stored in the memory of the Python process, they're lost
+ when you stop the server.
+
+ Whenever you make changes to ``app.py``, you must restart the server,
+ create a new game in a browser, and join it in another browser.
+
+The server logs say ``first player started game ...`` and ``second player
+joined game ...``. The numbers match, proving that the ``game`` local
+variable in both connection handlers points to same object in the memory of
+the Python process.
+
+Click the board in either browser. The server receives ``"play"`` events from
+the corresponding player.
+
+In the initialization sequence, you're routing connections to ``start()`` or
+``join()`` depending on the first message received by the server. This is a
+common pattern in servers that handle different clients.
+
+.. admonition:: Why not use different URIs for ``start()`` and ``join()``?
+ :class: hint
+
+ Instead of sending an initialization event, you could encode the join key
+ in the WebSocket URI e.g. ``ws://localhost:8001/join/<join_key>``. The
+ WebSocket server would parse ``websocket.path`` and route the connection,
+ similar to how HTTP servers route requests.
+
+ When you need to send sensitive data like authentication credentials to
+ the server, sending it an event is considered more secure than encoding
+ it in the URI because URIs end up in logs.
+
+ For the purposes of this tutorial, both approaches are equivalent because
+ the join key comes from an HTTP URL. There isn't much at risk anyway!
+
+Now you can restore the logic for playing moves and you'll have a fully
+functional two-player game.
+
+Add the game logic
+------------------
+
+Once the initialization is done, the game is symmetrical, so you can write a
+single coroutine to process the moves of both players:
+
+.. code-block:: python
+
+ async def play(websocket, game, player, connected):
+ ...
+
+With such a coroutine, you can replace the temporary code for testing in
+``start()`` by:
+
+.. code-block:: python
+
+ await play(websocket, game, PLAYER1, connected)
+
+and in ``join()`` by:
+
+.. code-block:: python
+
+ await play(websocket, game, PLAYER2, connected)
+
+The ``play()`` coroutine will reuse much of the code you wrote in the first
+part of the tutorial.
+
+Try to implement this by yourself!
+
+Keep in mind that you must restart the WebSocket server, reload the page to
+start a new game with the first player, copy the JOIN link, and join the game
+with the second player when you make changes.
+
+When ``play()`` works, you can play the game from two separate browsers,
+possibly running on separate computers on the same local network.
+
+A complete solution is available at the bottom of this document.
+
+Watch a game
+------------
+
+Let's add one more feature: allow spectators to watch the game.
+
+The process for inviting a spectator can be the same as for inviting the
+second player. You will have to duplicate all the initialization logic:
+
+- declare a ``WATCH`` global variable similar to ``JOIN``;
+- generate a watch key when creating a game; it must be different from the
+ join key, or else a spectator could hijack a game by tweaking the URL;
+- include the watch key in the ``"init"`` event sent to the first player;
+- generate a WATCH link in the UI with a ``watch`` query parameter;
+- update the ``initGame()`` function to handle such links;
+- update the ``handler()`` coroutine to invoke a ``watch()`` coroutine for
+ spectators;
+- prevent ``sendMoves()`` from sending ``"play"`` events for spectators.
+
+Once the initialization sequence is done, watching a game is as simple as
+registering the WebSocket connection in the ``connected`` set in order to
+receive game events and doing nothing until the spectator disconnects. You
+can wait for a connection to terminate with
+:meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed`:
+
+.. code-block:: python
+
+ async def watch(websocket, watch_key):
+
+ ...
+
+ connected.add(websocket)
+ try:
+ await websocket.wait_closed()
+ finally:
+ connected.remove(websocket)
+
+The connection can terminate because the ``receiveMoves()`` function closed it
+explicitly after receiving a ``"win"`` event, because the spectator closed
+their browser, or because the network failed.
+
+Again, try to implement this by yourself.
+
+When ``watch()`` works, you can invite spectators to watch the game from other
+browsers, as long as they're on the same local network.
+
+As a further improvement, you may support adding spectators while a game is
+already in progress. This requires replaying moves that were played before
+the spectator was added to the ``connected`` set. Past moves are available in
+the :attr:`~connect4.Connect4.moves` attribute of the game.
+
+This feature is included in the solution proposed below.
+
+Broadcast
+---------
+
+When you need to send a message to the two players and to all spectators,
+you're using this pattern:
+
+.. code-block:: python
+
+ async def handler(websocket):
+
+ ...
+
+ for connection in connected:
+ await connection.send(json.dumps(event))
+
+ ...
+
+Since this is a very common pattern in WebSocket servers, websockets provides
+the :func:`broadcast` helper for this purpose:
+
+.. code-block:: python
+
+ async def handler(websocket):
+
+ ...
+
+ websockets.broadcast(connected, json.dumps(event))
+
+ ...
+
+Calling :func:`broadcast` once is more efficient than
+calling :meth:`~legacy.protocol.WebSocketCommonProtocol.send` in a loop.
+
+However, there's a subtle difference in behavior. Did you notice that there's
+no ``await`` in the second version? Indeed, :func:`broadcast` is a function,
+not a coroutine like :meth:`~legacy.protocol.WebSocketCommonProtocol.send`
+or :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`.
+
+It's quite obvious why :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`
+is a coroutine. When you want to receive the next message, you have to wait
+until the client sends it and the network transmits it.
+
+It's less obvious why :meth:`~legacy.protocol.WebSocketCommonProtocol.send` is
+a coroutine. If you send many messages or large messages, you could write
+data faster than the network can transmit it or the client can read it. Then,
+outgoing data will pile up in buffers, which will consume memory and may
+crash your application.
+
+To avoid this problem, :meth:`~legacy.protocol.WebSocketCommonProtocol.send`
+waits until the write buffer drains. By slowing down the application as
+necessary, this ensures that the server doesn't send data too quickly. This
+is called backpressure and it's useful for building robust systems.
+
+That said, when you're sending the same messages to many clients in a loop,
+applying backpressure in this way can become counterproductive. When you're
+broadcasting, you don't want to slow down everyone to the pace of the slowest
+clients; you want to drop clients that cannot keep up with the data stream.
+That's why :func:`broadcast` doesn't wait until write buffers drain.
+
+For our Connect Four game, there's no difference in practice: the total amount
+of data sent on a connection for a game of Connect Four is less than 64 KB,
+so the write buffer never fills up and backpressure never kicks in anyway.
+
+Summary
+-------
+
+In this second part of the tutorial, you learned how to:
+
+* configure a connection by exchanging initialization messages;
+* keep track of connections within a single server process;
+* wait until a client disconnects in a connection handler;
+* broadcast a message to many connections efficiently.
+
+You can now play a Connect Four game from separate browser, communicating over
+WebSocket connections with a server that synchronizes the game logic!
+
+However, the two players have to be on the same local network as the server,
+so the constraint of being in the same place still mostly applies.
+
+Head over to the :doc:`third part <tutorial3>` of the tutorial to deploy the
+game to the web and remove this constraint.
+
+Solution
+--------
+
+.. literalinclude:: ../../example/tutorial/step2/app.py
+ :caption: app.py
+ :language: python
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step2/index.html
+ :caption: index.html
+ :language: html
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step2/main.js
+ :caption: main.js
+ :language: js
+ :linenos:
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial3.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial3.rst
new file mode 100644
index 0000000000..6fdec113b2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial3.rst
@@ -0,0 +1,290 @@
+Part 3 - Deploy to the web
+==========================
+
+.. currentmodule:: websockets
+
+.. admonition:: This is the third part of the tutorial.
+
+ * In the :doc:`first part <tutorial1>`, you created a server and
+ connected one browser; you could play if you shared the same browser.
+ * In this :doc:`second part <tutorial2>`, you connected a second browser;
+ you could play from different browsers on a local network.
+ * In this :doc:`third part <tutorial3>`, you will deploy the game to the
+ web; you can play from any browser connected to the Internet.
+
+In the first and second parts of the tutorial, for local development, you ran
+an HTTP server on ``http://localhost:8000/`` with:
+
+.. code-block:: console
+
+ $ python -m http.server
+
+and a WebSocket server on ``ws://localhost:8001/`` with:
+
+.. code-block:: console
+
+ $ python app.py
+
+Now you want to deploy these servers on the Internet. There's a vast range of
+hosting providers to choose from. For the sake of simplicity, we'll rely on:
+
+* GitHub Pages for the HTTP server;
+* Heroku for the WebSocket server.
+
+Commit project to git
+---------------------
+
+Perhaps you committed your work to git while you were progressing through the
+tutorial. If you didn't, now is a good time, because GitHub and Heroku offer
+git-based deployment workflows.
+
+Initialize a git repository:
+
+.. code-block:: console
+
+ $ git init -b main
+ Initialized empty Git repository in websockets-tutorial/.git/
+ $ git commit --allow-empty -m "Initial commit."
+ [main (root-commit) ...] Initial commit.
+
+Add all files and commit:
+
+.. code-block:: console
+
+ $ git add .
+ $ git commit -m "Initial implementation of Connect Four game."
+ [main ...] Initial implementation of Connect Four game.
+ 6 files changed, 500 insertions(+)
+ create mode 100644 app.py
+ create mode 100644 connect4.css
+ create mode 100644 connect4.js
+ create mode 100644 connect4.py
+ create mode 100644 index.html
+ create mode 100644 main.js
+
+Prepare the WebSocket server
+----------------------------
+
+Before you deploy the server, you must adapt it to meet requirements of
+Heroku's runtime. This involves two small changes:
+
+1. Heroku expects the server to `listen on a specific port`_, provided in the
+ ``$PORT`` environment variable.
+
+2. Heroku sends a ``SIGTERM`` signal when `shutting down a dyno`_, which
+ should trigger a clean exit.
+
+.. _listen on a specific port: https://devcenter.heroku.com/articles/preparing-a-codebase-for-heroku-deployment#4-listen-on-the-correct-port
+
+.. _shutting down a dyno: https://devcenter.heroku.com/articles/dynos#shutdown
+
+Adapt the ``main()`` coroutine accordingly:
+
+.. code-block:: python
+
+ import os
+ import signal
+
+.. literalinclude:: ../../example/tutorial/step3/app.py
+ :pyobject: main
+
+To catch the ``SIGTERM`` signal, ``main()`` creates a :class:`~asyncio.Future`
+called ``stop`` and registers a signal handler that sets the result of this
+future. The value of the future doesn't matter; it's only for waiting for
+``SIGTERM``.
+
+Then, by using :func:`~server.serve` as a context manager and exiting the
+context when ``stop`` has a result, ``main()`` ensures that the server closes
+connections cleanly and exits on ``SIGTERM``.
+
+The app is now fully compatible with Heroku.
+
+Deploy the WebSocket server
+---------------------------
+
+Create a ``requirements.txt`` file with this content to install ``websockets``
+when building the image:
+
+.. literalinclude:: ../../example/tutorial/step3/requirements.txt
+ :language: text
+
+.. admonition:: Heroku treats ``requirements.txt`` as a signal to `detect a Python app`_.
+ :class: tip
+
+ That's why you don't need to declare that you need a Python runtime.
+
+.. _detect a Python app: https://devcenter.heroku.com/articles/python-support#recognizing-a-python-app
+
+Create a ``Procfile`` file with this content to configure the command for
+running the server:
+
+.. literalinclude:: ../../example/tutorial/step3/Procfile
+ :language: text
+
+Commit your changes:
+
+.. code-block:: console
+
+ $ git add .
+ $ git commit -m "Deploy to Heroku."
+ [main ...] Deploy to Heroku.
+ 3 files changed, 12 insertions(+), 2 deletions(-)
+ create mode 100644 Procfile
+ create mode 100644 requirements.txt
+
+Follow the `set-up instructions`_ to install the Heroku CLI and to log in, if
+you haven't done that yet.
+
+.. _set-up instructions: https://devcenter.heroku.com/articles/getting-started-with-python#set-up
+
+Create a Heroku app. You must choose a unique name and replace
+``websockets-tutorial`` by this name in the following command:
+
+.. code-block:: console
+
+ $ heroku create websockets-tutorial
+ Creating ⬢ websockets-tutorial... done
+ https://websockets-tutorial.herokuapp.com/ | https://git.heroku.com/websockets-tutorial.git
+
+If you reuse a name that someone else already uses, you will receive this
+error; if this happens, try another name:
+
+.. code-block:: console
+
+ $ heroku create websockets-tutorial
+ Creating ⬢ websockets-tutorial... !
+ ▸ Name websockets-tutorial is already taken
+
+Deploy by pushing the code to Heroku:
+
+.. code-block:: console
+
+ $ git push heroku
+
+ ... lots of output...
+
+ remote: Released v1
+ remote: https://websockets-tutorial.herokuapp.com/ deployed to Heroku
+ remote:
+ remote: Verifying deploy... done.
+ To https://git.heroku.com/websockets-tutorial.git
+ * [new branch] main -> main
+
+You can test the WebSocket server with the interactive client exactly like you
+did in the first part of the tutorial. Replace ``websockets-tutorial`` by the
+name of your app in the following command:
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-tutorial.herokuapp.com/
+ Connected to wss://websockets-tutorial.herokuapp.com/.
+ > {"type": "init"}
+ < {"type": "init", "join": "54ICxFae_Ip7TJE2", "watch": "634w44TblL5Dbd9a"}
+ Connection closed: 1000 (OK).
+
+It works!
+
+Prepare the web application
+---------------------------
+
+Before you deploy the web application, perhaps you're wondering how it will
+locate the WebSocket server? Indeed, at this point, its address is hard-coded
+in ``main.js``:
+
+.. code-block:: javascript
+
+ const websocket = new WebSocket("ws://localhost:8001/");
+
+You can take this strategy one step further by checking the address of the
+HTTP server and determining the address of the WebSocket server accordingly.
+
+Add this function to ``main.js``; replace ``python-websockets`` by your GitHub
+username and ``websockets-tutorial`` by the name of your app on Heroku:
+
+.. literalinclude:: ../../example/tutorial/step3/main.js
+ :language: js
+ :start-at: function getWebSocketServer
+ :end-before: function initGame
+
+Then, update the initialization to connect to this address instead:
+
+.. code-block:: javascript
+
+ const websocket = new WebSocket(getWebSocketServer());
+
+Commit your changes:
+
+.. code-block:: console
+
+ $ git add .
+ $ git commit -m "Configure WebSocket server address."
+ [main ...] Configure WebSocket server address.
+ 1 file changed, 11 insertions(+), 1 deletion(-)
+
+Deploy the web application
+--------------------------
+
+Go to GitHub and create a new repository called ``websockets-tutorial``.
+
+Push your code to this repository. You must replace ``python-websockets`` by
+your GitHub username in the following command:
+
+.. code-block:: console
+
+ $ git remote add origin git@github.com:python-websockets/websockets-tutorial.git
+ $ git push -u origin main
+ Enumerating objects: 11, done.
+ Counting objects: 100% (11/11), done.
+ Delta compression using up to 8 threads
+ Compressing objects: 100% (10/10), done.
+ Writing objects: 100% (11/11), 5.90 KiB | 2.95 MiB/s, done.
+ Total 11 (delta 0), reused 0 (delta 0), pack-reused 0
+ To github.com:<username>/websockets-tutorial.git
+ * [new branch] main -> main
+ Branch 'main' set up to track remote branch 'main' from 'origin'.
+
+Go back to GitHub, open the Settings tab of the repository and select Pages in
+the menu. Select the main branch as source and click Save. GitHub tells you
+that your site is published.
+
+Follow the link and start a game!
+
+Summary
+-------
+
+In this third part of the tutorial, you learned how to deploy a WebSocket
+application with Heroku.
+
+You can start a Connect Four game, send the JOIN link to a friend, and play
+over the Internet!
+
+Congratulations for completing the tutorial. Enjoy building real-time web
+applications with websockets!
+
+Solution
+--------
+
+.. literalinclude:: ../../example/tutorial/step3/app.py
+ :caption: app.py
+ :language: python
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step3/index.html
+ :caption: index.html
+ :language: html
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step3/main.js
+ :caption: main.js
+ :language: js
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step3/Procfile
+ :caption: Procfile
+ :language: text
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step3/requirements.txt
+ :caption: requirements.txt
+ :language: text
+ :linenos:
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/make.bat b/testing/web-platform/tests/tools/third_party/websockets/docs/make.bat
new file mode 100644
index 0000000000..922152e96a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/changelog.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/changelog.rst
new file mode 100644
index 0000000000..264e6e42d1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/changelog.rst
@@ -0,0 +1,1230 @@
+Changelog
+=========
+
+.. currentmodule:: websockets
+
+.. _backwards-compatibility policy:
+
+Backwards-compatibility policy
+------------------------------
+
+websockets is intended for production use. Therefore, stability is a goal.
+
+websockets also aims at providing the best API for WebSocket in Python.
+
+While we value stability, we value progress more. When an improvement requires
+changing a public API, we make the change and document it in this changelog.
+
+When possible with reasonable effort, we preserve backwards-compatibility for
+five years after the release that introduced the change.
+
+When a release contains backwards-incompatible API changes, the major version
+is increased, else the minor version is increased. Patch versions are only for
+fixing regressions shortly after a release.
+
+Only documented APIs are public. Undocumented, private APIs may change without
+notice.
+
+12.0
+----
+
+*October 21, 2023*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: websockets 12.0 requires Python ≥ 3.8.
+ :class: tip
+
+ websockets 11.0 is the last version supporting Python 3.7.
+
+Improvements
+............
+
+* Made convenience imports from ``websockets`` compatible with static code
+ analysis tools such as auto-completion in an IDE or type checking with mypy_.
+
+ .. _mypy: https://github.com/python/mypy
+
+* Accepted a plain :class:`int` where an :class:`~http.HTTPStatus` is expected.
+
+* Added :class:`~frames.CloseCode`.
+
+11.0.3
+------
+
+*May 7, 2023*
+
+Bug fixes
+.........
+
+* Fixed the :mod:`threading` implementation of servers on Windows.
+
+11.0.2
+------
+
+*April 18, 2023*
+
+Bug fixes
+.........
+
+* Fixed a deadlock in the :mod:`threading` implementation when closing a
+ connection without reading all messages.
+
+11.0.1
+------
+
+*April 6, 2023*
+
+Bug fixes
+.........
+
+* Restored the C extension in the source distribution.
+
+11.0
+----
+
+*April 2, 2023*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: The Sans-I/O implementation was moved.
+ :class: caution
+
+ Aliases provide compatibility for all previously public APIs according to
+ the `backwards-compatibility policy`_.
+
+ * The ``connection`` module was renamed to ``protocol``.
+
+ * The ``connection.Connection``, ``server.ServerConnection``, and
+ ``client.ClientConnection`` classes were renamed to ``protocol.Protocol``,
+ ``server.ServerProtocol``, and ``client.ClientProtocol``.
+
+.. admonition:: Sans-I/O protocol constructors now use keyword-only arguments.
+ :class: caution
+
+ If you instantiate :class:`~server.ServerProtocol` or
+ :class:`~client.ClientProtocol` directly, make sure you are using keyword
+ arguments.
+
+.. admonition:: Closing a connection without an empty close frame is OK.
+ :class: note
+
+ Receiving an empty close frame now results in
+ :exc:`~exceptions.ConnectionClosedOK` instead of
+ :exc:`~exceptions.ConnectionClosedError`.
+
+ As a consequence, calling ``WebSocket.close()`` without arguments in a
+ browser isn't reported as an error anymore.
+
+.. admonition:: :func:`~server.serve` times out on the opening handshake after 10 seconds by default.
+ :class: note
+
+ You can adjust the timeout with the ``open_timeout`` parameter. Set it to
+ :obj:`None` to disable the timeout entirely.
+
+New features
+............
+
+.. admonition:: websockets 11.0 introduces a implementation on top of :mod:`threading`.
+ :class: important
+
+ It may be more convenient if you don't need to manage many connections and
+ you're more comfortable with :mod:`threading` than :mod:`asyncio`.
+
+ It is particularly suited to client applications that establish only one
+ connection. It may be used for servers handling few connections.
+
+ See :func:`~sync.client.connect` and :func:`~sync.server.serve` for details.
+
+* Added ``open_timeout`` to :func:`~server.serve`.
+
+* Made it possible to close a server without closing existing connections.
+
+* Added :attr:`~server.ServerProtocol.select_subprotocol` to customize
+ negotiation of subprotocols in the Sans-I/O layer.
+
+Improvements
+............
+
+* Added platform-independent wheels.
+
+* Improved error handling in :func:`~websockets.broadcast`.
+
+* Set ``server_hostname`` automatically on TLS connections when providing a
+ ``sock`` argument to :func:`~sync.client.connect`.
+
+10.4
+----
+
+*October 25, 2022*
+
+New features
+............
+
+* Validated compatibility with Python 3.11.
+
+* Added the :attr:`~legacy.protocol.WebSocketCommonProtocol.latency` property to
+ protocols.
+
+* Changed :attr:`~legacy.protocol.WebSocketCommonProtocol.ping` to return the
+ latency of the connection.
+
+* Supported overriding or removing the ``User-Agent`` header in clients and the
+ ``Server`` header in servers.
+
+* Added deployment guides for more Platform as a Service providers.
+
+Improvements
+............
+
+* Improved FAQ.
+
+10.3
+----
+
+*April 17, 2022*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: The ``exception`` attribute of :class:`~http11.Request` and :class:`~http11.Response` is deprecated.
+ :class: note
+
+ Use the ``handshake_exc`` attribute of :class:`~server.ServerProtocol` and
+ :class:`~client.ClientProtocol` instead.
+
+ See :doc:`../howto/sansio` for details.
+
+Improvements
+............
+
+* Reduced noise in logs when :mod:`ssl` or :mod:`zlib` raise exceptions.
+
+10.2
+----
+
+*February 21, 2022*
+
+Improvements
+............
+
+* Made compression negotiation more lax for compatibility with Firefox.
+
+* Improved FAQ and quick start guide.
+
+Bug fixes
+.........
+
+* Fixed backwards-incompatibility in 10.1 for connection handlers created with
+ :func:`functools.partial`.
+
+* Avoided leaking open sockets when :func:`~client.connect` is canceled.
+
+10.1
+----
+
+*November 14, 2021*
+
+New features
+............
+
+* Added a tutorial.
+
+* Made the second parameter of connection handlers optional. It will be
+ deprecated in the next major release. The request path is available in
+ the :attr:`~legacy.protocol.WebSocketCommonProtocol.path` attribute of
+ the first argument.
+
+ If you implemented the connection handler of a server as::
+
+ async def handler(request, path):
+ ...
+
+ You should replace it by::
+
+ async def handler(request):
+ path = request.path # if handler() uses the path argument
+ ...
+
+* Added ``python -m websockets --version``.
+
+Improvements
+............
+
+* Added wheels for Python 3.10, PyPy 3.7, and for more platforms.
+
+* Reverted optimization of default compression settings for clients, mainly to
+ avoid triggering bugs in poorly implemented servers like `AWS API Gateway`_.
+
+ .. _AWS API Gateway: https://github.com/python-websockets/websockets/issues/1065
+
+* Mirrored the entire :class:`~asyncio.Server` API
+ in :class:`~server.WebSocketServer`.
+
+* Improved performance for large messages on ARM processors.
+
+* Documented how to auto-reload on code changes in development.
+
+Bug fixes
+.........
+
+* Avoided half-closing TCP connections that are already closed.
+
+10.0
+----
+
+*September 9, 2021*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: websockets 10.0 requires Python ≥ 3.7.
+ :class: tip
+
+ websockets 9.1 is the last version supporting Python 3.6.
+
+.. admonition:: The ``loop`` parameter is deprecated from all APIs.
+ :class: caution
+
+ This reflects a decision made in Python 3.8. See the release notes of
+ Python 3.10 for details.
+
+ The ``loop`` parameter is also removed
+ from :class:`~server.WebSocketServer`. This should be transparent.
+
+.. admonition:: :func:`~client.connect` times out after 10 seconds by default.
+ :class: note
+
+ You can adjust the timeout with the ``open_timeout`` parameter. Set it to
+ :obj:`None` to disable the timeout entirely.
+
+.. admonition:: The ``legacy_recv`` option is deprecated.
+ :class: note
+
+ See the release notes of websockets 3.0 for details.
+
+.. admonition:: The signature of :exc:`~exceptions.ConnectionClosed` changed.
+ :class: note
+
+ If you raise :exc:`~exceptions.ConnectionClosed` or a subclass, rather
+ than catch them when websockets raises them, you must change your code.
+
+.. admonition:: A ``msg`` parameter was added to :exc:`~exceptions.InvalidURI`.
+ :class: note
+
+ If you raise :exc:`~exceptions.InvalidURI`, rather than catch it when
+ websockets raises it, you must change your code.
+
+New features
+............
+
+.. admonition:: websockets 10.0 introduces a `Sans-I/O API
+ <https://sans-io.readthedocs.io/>`_ for easier integration
+ in third-party libraries.
+ :class: important
+
+ If you're integrating websockets in a library, rather than just using it,
+ look at the :doc:`Sans-I/O integration guide <../howto/sansio>`.
+
+* Added compatibility with Python 3.10.
+
+* Added :func:`~websockets.broadcast` to send a message to many clients.
+
+* Added support for reconnecting automatically by using
+ :func:`~client.connect` as an asynchronous iterator.
+
+* Added ``open_timeout`` to :func:`~client.connect`.
+
+* Documented how to integrate with `Django <https://www.djangoproject.com/>`_.
+
+* Documented how to deploy websockets in production, with several options.
+
+* Documented how to authenticate connections.
+
+* Documented how to broadcast messages to many connections.
+
+Improvements
+............
+
+* Improved logging. See the :doc:`logging guide <../topics/logging>`.
+
+* Optimized default compression settings to reduce memory usage.
+
+* Optimized processing of client-to-server messages when the C extension isn't
+ available.
+
+* Supported relative redirects in :func:`~client.connect`.
+
+* Handled TCP connection drops during the opening handshake.
+
+* Made it easier to customize authentication with
+ :meth:`~auth.BasicAuthWebSocketServerProtocol.check_credentials`.
+
+* Provided additional information in :exc:`~exceptions.ConnectionClosed`
+ exceptions.
+
+* Clarified several exceptions or log messages.
+
+* Restructured documentation.
+
+* Improved API documentation.
+
+* Extended FAQ.
+
+Bug fixes
+.........
+
+* Avoided a crash when receiving a ping while the connection is closing.
+
+9.1
+---
+
+*May 27, 2021*
+
+Security fix
+............
+
+.. admonition:: websockets 9.1 fixes a security issue introduced in 8.0.
+ :class: important
+
+ Version 8.0 was vulnerable to timing attacks on HTTP Basic Auth passwords
+ (`CVE-2021-33880`_).
+
+ .. _CVE-2021-33880: https://nvd.nist.gov/vuln/detail/CVE-2021-33880
+
+9.0.2
+-----
+
+*May 15, 2021*
+
+Bug fixes
+.........
+
+* Restored compatibility of ``python -m websockets`` with Python < 3.9.
+
+* Restored compatibility with mypy.
+
+9.0.1
+-----
+
+*May 2, 2021*
+
+Bug fixes
+.........
+
+* Fixed issues with the packaging of the 9.0 release.
+
+9.0
+---
+
+*May 1, 2021*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: Several modules are moved or deprecated.
+ :class: caution
+
+ Aliases provide compatibility for all previously public APIs according to
+ the `backwards-compatibility policy`_
+
+ * :class:`~datastructures.Headers` and
+ :exc:`~datastructures.MultipleValuesError` are moved from
+ ``websockets.http`` to :mod:`websockets.datastructures`. If you're using
+ them, you should adjust the import path.
+
+ * The ``client``, ``server``, ``protocol``, and ``auth`` modules were
+ moved from the ``websockets`` package to a ``websockets.legacy``
+ sub-package. Despite the name, they're still fully supported.
+
+ * The ``framing``, ``handshake``, ``headers``, ``http``, and ``uri``
+ modules in the ``websockets`` package are deprecated. These modules
+ provided low-level APIs for reuse by other projects, but they didn't
+ reach that goal. Keeping these APIs public makes it more difficult to
+ improve websockets.
+
+ These changes pave the path for a refactoring that should be a transparent
+ upgrade for most uses and facilitate integration by other projects.
+
+.. admonition:: Convenience imports from ``websockets`` are performed lazily.
+ :class: note
+
+ While Python supports this, tools relying on static code analysis don't.
+ This breaks auto-completion in an IDE or type checking with mypy_.
+
+ .. _mypy: https://github.com/python/mypy
+
+ If you depend on such tools, use the real import paths, which can be found
+ in the API documentation, for example::
+
+ from websockets.client import connect
+ from websockets.server import serve
+
+New features
+............
+
+* Added compatibility with Python 3.9.
+
+Improvements
+............
+
+* Added support for IRIs in addition to URIs.
+
+* Added close codes 1012, 1013, and 1014.
+
+* Raised an error when passing a :class:`dict` to
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.send`.
+
+* Improved error reporting.
+
+Bug fixes
+.........
+
+* Fixed sending fragmented, compressed messages.
+
+* Fixed ``Host`` header sent when connecting to an IPv6 address.
+
+* Fixed creating a client or a server with an existing Unix socket.
+
+* Aligned maximum cookie size with popular web browsers.
+
+* Ensured cancellation always propagates, even on Python versions where
+ :exc:`~asyncio.CancelledError` inherits :exc:`Exception`.
+
+8.1
+---
+
+*November 1, 2019*
+
+New features
+............
+
+* Added compatibility with Python 3.8.
+
+8.0.2
+-----
+
+*July 31, 2019*
+
+Bug fixes
+.........
+
+* Restored the ability to pass a socket with the ``sock`` parameter of
+ :func:`~server.serve`.
+
+* Removed an incorrect assertion when a connection drops.
+
+8.0.1
+-----
+
+*July 21, 2019*
+
+Bug fixes
+.........
+
+* Restored the ability to import ``WebSocketProtocolError`` from
+ ``websockets``.
+
+8.0
+---
+
+*July 7, 2019*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: websockets 8.0 requires Python ≥ 3.6.
+ :class: tip
+
+ websockets 7.0 is the last version supporting Python 3.4 and 3.5.
+
+.. admonition:: ``process_request`` is now expected to be a coroutine.
+ :class: note
+
+ If you're passing a ``process_request`` argument to
+ :func:`~server.serve` or :class:`~server.WebSocketServerProtocol`, or if
+ you're overriding
+ :meth:`~server.WebSocketServerProtocol.process_request` in a subclass,
+ define it with ``async def`` instead of ``def``. Previously, both were supported.
+
+ For backwards compatibility, functions are still accepted, but mixing
+ functions and coroutines won't work in some inheritance scenarios.
+
+.. admonition:: ``max_queue`` must be :obj:`None` to disable the limit.
+ :class: note
+
+ If you were setting ``max_queue=0`` to make the queue of incoming messages
+ unbounded, change it to ``max_queue=None``.
+
+.. admonition:: The ``host``, ``port``, and ``secure`` attributes
+ of :class:`~legacy.protocol.WebSocketCommonProtocol` are deprecated.
+ :class: note
+
+ Use :attr:`~legacy.protocol.WebSocketCommonProtocol.local_address` in
+ servers and
+ :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address` in clients
+ instead of ``host`` and ``port``.
+
+.. admonition:: ``WebSocketProtocolError`` is renamed
+ to :exc:`~exceptions.ProtocolError`.
+ :class: note
+
+ An alias provides backwards compatibility.
+
+.. admonition:: ``read_response()`` now returns the reason phrase.
+ :class: note
+
+ If you're using this low-level API, you must change your code.
+
+New features
+............
+
+* Added :func:`~auth.basic_auth_protocol_factory` to enforce HTTP
+ Basic Auth on the server side.
+
+* :func:`~client.connect` handles redirects from the server during the
+ handshake.
+
+* :func:`~client.connect` supports overriding ``host`` and ``port``.
+
+* Added :func:`~client.unix_connect` for connecting to Unix sockets.
+
+* Added support for asynchronous generators
+ in :meth:`~legacy.protocol.WebSocketCommonProtocol.send`
+ to generate fragmented messages incrementally.
+
+* Enabled readline in the interactive client.
+
+* Added type hints (:pep:`484`).
+
+* Added a FAQ to the documentation.
+
+* Added documentation for extensions.
+
+* Documented how to optimize memory usage.
+
+Improvements
+............
+
+* :meth:`~legacy.protocol.WebSocketCommonProtocol.send`,
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` support bytes-like
+ types :class:`bytearray` and :class:`memoryview` in addition to
+ :class:`bytes`.
+
+* Added :exc:`~exceptions.ConnectionClosedOK` and
+ :exc:`~exceptions.ConnectionClosedError` subclasses of
+ :exc:`~exceptions.ConnectionClosed` to tell apart normal connection
+ termination from errors.
+
+* Changed :meth:`WebSocketServer.close()
+ <server.WebSocketServer.close>` to perform a proper closing handshake
+ instead of failing the connection.
+
+* Improved error messages when HTTP parsing fails.
+
+* Improved API documentation.
+
+Bug fixes
+.........
+
+* Prevented spurious log messages about :exc:`~exceptions.ConnectionClosed`
+ exceptions in keepalive ping task. If you were using ``ping_timeout=None``
+ as a workaround, you can remove it.
+
+* Avoided a crash when a ``extra_headers`` callable returns :obj:`None`.
+
+7.0
+---
+
+*November 1, 2018*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: Keepalive is enabled by default.
+ :class: important
+
+ websockets now sends Ping frames at regular intervals and closes the
+ connection if it doesn't receive a matching Pong frame.
+ See :class:`~legacy.protocol.WebSocketCommonProtocol` for details.
+
+.. admonition:: Termination of connections by :meth:`WebSocketServer.close()
+ <server.WebSocketServer.close>` changes.
+ :class: caution
+
+ Previously, connections handlers were canceled. Now, connections are
+ closed with close code 1001 (going away).
+
+ From the perspective of the connection handler, this is the same as if the
+ remote endpoint was disconnecting. This removes the need to prepare for
+ :exc:`~asyncio.CancelledError` in connection handlers.
+
+ You can restore the previous behavior by adding the following line at the
+ beginning of connection handlers::
+
+ def handler(websocket, path):
+ closed = asyncio.ensure_future(websocket.wait_closed())
+ closed.add_done_callback(lambda task: task.cancel())
+
+.. admonition:: Calling :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`
+ concurrently raises a :exc:`RuntimeError`.
+ :class: note
+
+ Concurrent calls lead to non-deterministic behavior because there are no
+ guarantees about which coroutine will receive which message.
+
+.. admonition:: The ``timeout`` argument of :func:`~server.serve`
+ and :func:`~client.connect` is renamed to ``close_timeout`` .
+ :class: note
+
+ This prevents confusion with ``ping_timeout``.
+
+ For backwards compatibility, ``timeout`` is still supported.
+
+.. admonition:: The ``origins`` argument of :func:`~server.serve` changes.
+ :class: note
+
+ Include :obj:`None` in the list rather than ``''`` to allow requests that
+ don't contain an Origin header.
+
+.. admonition:: Pending pings aren't canceled when the connection is closed.
+ :class: note
+
+ A ping — as in ``ping = await websocket.ping()`` — for which no pong was
+ received yet used to be canceled when the connection is closed, so that
+ ``await ping`` raised :exc:`~asyncio.CancelledError`.
+
+ Now ``await ping`` raises :exc:`~exceptions.ConnectionClosed` like other
+ public APIs.
+
+New features
+............
+
+* Added ``process_request`` and ``select_subprotocol`` arguments to
+ :func:`~server.serve` and
+ :class:`~server.WebSocketServerProtocol` to facilitate customization of
+ :meth:`~server.WebSocketServerProtocol.process_request` and
+ :meth:`~server.WebSocketServerProtocol.select_subprotocol`.
+
+* Added support for sending fragmented messages.
+
+* Added the :meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed`
+ method to protocols.
+
+* Added an interactive client: ``python -m websockets <uri>``.
+
+Improvements
+............
+
+* Improved handling of multiple HTTP headers with the same name.
+
+* Improved error messages when a required HTTP header is missing.
+
+Bug fixes
+.........
+
+* Fixed a data loss bug in
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`:
+ canceling it at the wrong time could result in messages being dropped.
+
+6.0
+---
+
+*July 16, 2018*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: The :class:`~datastructures.Headers` class is introduced and
+ several APIs are updated to use it.
+ :class: caution
+
+ * The ``request_headers`` argument
+ of :meth:`~server.WebSocketServerProtocol.process_request` is now
+ a :class:`~datastructures.Headers` instead of
+ an ``http.client.HTTPMessage``.
+
+ * The ``request_headers`` and ``response_headers`` attributes of
+ :class:`~legacy.protocol.WebSocketCommonProtocol` are now
+ :class:`~datastructures.Headers` instead of ``http.client.HTTPMessage``.
+
+ * The ``raw_request_headers`` and ``raw_response_headers`` attributes of
+ :class:`~legacy.protocol.WebSocketCommonProtocol` are removed. Use
+ :meth:`~datastructures.Headers.raw_items` instead.
+
+ * Functions defined in the ``handshake`` module now receive
+ :class:`~datastructures.Headers` in argument instead of ``get_header``
+ or ``set_header`` functions. This affects libraries that rely on
+ low-level APIs.
+
+ * Functions defined in the ``http`` module now return HTTP headers as
+ :class:`~datastructures.Headers` instead of lists of ``(name, value)``
+ pairs.
+
+ Since :class:`~datastructures.Headers` and ``http.client.HTTPMessage``
+ provide similar APIs, much of the code dealing with HTTP headers won't
+ require changes.
+
+New features
+............
+
+* Added compatibility with Python 3.7.
+
+5.0.1
+-----
+
+*May 24, 2018*
+
+Bug fixes
+.........
+
+* Fixed a regression in 5.0 that broke some invocations of
+ :func:`~server.serve` and :func:`~client.connect`.
+
+5.0
+---
+
+*May 22, 2018*
+
+Security fix
+............
+
+.. admonition:: websockets 5.0 fixes a security issue introduced in 4.0.
+ :class: important
+
+ Version 4.0 was vulnerable to denial of service by memory exhaustion
+ because it didn't enforce ``max_size`` when decompressing compressed
+ messages (`CVE-2018-1000518`_).
+
+ .. _CVE-2018-1000518: https://nvd.nist.gov/vuln/detail/CVE-2018-1000518
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: A ``user_info`` field is added to the return value of
+ ``parse_uri`` and ``WebSocketURI``.
+ :class: note
+
+ If you're unpacking ``WebSocketURI`` into four variables, adjust your code
+ to account for that fifth field.
+
+New features
+............
+
+* :func:`~client.connect` performs HTTP Basic Auth when the URI contains
+ credentials.
+
+* :func:`~server.unix_serve` can be used as an asynchronous context
+ manager on Python ≥ 3.5.1.
+
+* Added the :attr:`~legacy.protocol.WebSocketCommonProtocol.closed` property
+ to protocols.
+
+* Added new examples in the documentation.
+
+Improvements
+............
+
+* Iterating on incoming messages no longer raises an exception when the
+ connection terminates with close code 1001 (going away).
+
+* A plain HTTP request now receives a 426 Upgrade Required response and
+ doesn't log a stack trace.
+
+* If a :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` doesn't receive a
+ pong, it's canceled when the connection is closed.
+
+* Reported the cause of :exc:`~exceptions.ConnectionClosed` exceptions.
+
+* Stopped logging stack traces when the TCP connection dies prematurely.
+
+* Prevented writing to a closing TCP connection during unclean shutdowns.
+
+* Made connection termination more robust to network congestion.
+
+* Prevented processing of incoming frames after failing the connection.
+
+* Updated documentation with new features from Python 3.6.
+
+* Improved several sections of the documentation.
+
+Bug fixes
+.........
+
+* Prevented :exc:`TypeError` due to missing close code on connection close.
+
+* Fixed a race condition in the closing handshake that raised
+ :exc:`~exceptions.InvalidState`.
+
+4.0.1
+-----
+
+*November 2, 2017*
+
+Bug fixes
+.........
+
+* Fixed issues with the packaging of the 4.0 release.
+
+4.0
+---
+
+*November 2, 2017*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: websockets 4.0 requires Python ≥ 3.4.
+ :class: tip
+
+ websockets 3.4 is the last version supporting Python 3.3.
+
+.. admonition:: Compression is enabled by default.
+ :class: important
+
+ In August 2017, Firefox and Chrome support the permessage-deflate
+ extension, but not Safari and IE.
+
+ Compression should improve performance but it increases RAM and CPU use.
+
+ If you want to disable compression, add ``compression=None`` when calling
+ :func:`~server.serve` or :func:`~client.connect`.
+
+.. admonition:: The ``state_name`` attribute of protocols is deprecated.
+ :class: note
+
+ Use ``protocol.state.name`` instead of ``protocol.state_name``.
+
+New features
+............
+
+* :class:`~legacy.protocol.WebSocketCommonProtocol` instances can be used as
+ asynchronous iterators on Python ≥ 3.6. They yield incoming messages.
+
+* Added :func:`~server.unix_serve` for listening on Unix sockets.
+
+* Added the :attr:`~server.WebSocketServer.sockets` attribute to the
+ return value of :func:`~server.serve`.
+
+* Allowed ``extra_headers`` to override ``Server`` and ``User-Agent`` headers.
+
+Improvements
+............
+
+* Reorganized and extended documentation.
+
+* Rewrote connection termination to increase robustness in edge cases.
+
+* Reduced verbosity of "Failing the WebSocket connection" logs.
+
+Bug fixes
+.........
+
+* Aborted connections if they don't close within the configured ``timeout``.
+
+* Stopped leaking pending tasks when :meth:`~asyncio.Task.cancel` is called on
+ a connection while it's being closed.
+
+3.4
+---
+
+*August 20, 2017*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: ``InvalidStatus`` is replaced
+ by :class:`~exceptions.InvalidStatusCode`.
+ :class: note
+
+ This exception is raised when :func:`~client.connect` receives an invalid
+ response status code from the server.
+
+New features
+............
+
+* :func:`~server.serve` can be used as an asynchronous context manager
+ on Python ≥ 3.5.1.
+
+* Added support for customizing handling of incoming connections with
+ :meth:`~server.WebSocketServerProtocol.process_request`.
+
+* Made read and write buffer sizes configurable.
+
+Improvements
+............
+
+* Renamed :func:`~server.serve` and :func:`~client.connect`'s
+ ``klass`` argument to ``create_protocol`` to reflect that it can also be a
+ callable. For backwards compatibility, ``klass`` is still supported.
+
+* Rewrote HTTP handling for simplicity and performance.
+
+* Added an optional C extension to speed up low-level operations.
+
+Bug fixes
+.........
+
+* Providing a ``sock`` argument to :func:`~client.connect` no longer
+ crashes.
+
+3.3
+---
+
+*March 29, 2017*
+
+New features
+............
+
+* Ensured compatibility with Python 3.6.
+
+Improvements
+............
+
+* Reduced noise in logs caused by connection resets.
+
+Bug fixes
+.........
+
+* Avoided crashing on concurrent writes on slow connections.
+
+3.2
+---
+
+*August 17, 2016*
+
+New features
+............
+
+* Added ``timeout``, ``max_size``, and ``max_queue`` arguments to
+ :func:`~client.connect` and :func:`~server.serve`.
+
+Improvements
+............
+
+* Made server shutdown more robust.
+
+3.1
+---
+
+*April 21, 2016*
+
+New features
+............
+
+* Added flow control for incoming data.
+
+Bug fixes
+.........
+
+* Avoided a warning when closing a connection before the opening handshake.
+
+3.0
+---
+
+*December 25, 2015*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` now
+ raises an exception when the connection is closed.
+ :class: caution
+
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` used to return
+ :obj:`None` when the connection was closed. This required checking the
+ return value of every call::
+
+ message = await websocket.recv()
+ if message is None:
+ return
+
+ Now it raises a :exc:`~exceptions.ConnectionClosed` exception instead.
+ This is more Pythonic. The previous code can be simplified to::
+
+ message = await websocket.recv()
+
+ When implementing a server, there's no strong reason to handle such
+ exceptions. Let them bubble up, terminate the handler coroutine, and the
+ server will simply ignore them.
+
+ In order to avoid stranding projects built upon an earlier version, the
+ previous behavior can be restored by passing ``legacy_recv=True`` to
+ :func:`~server.serve`, :func:`~client.connect`,
+ :class:`~server.WebSocketServerProtocol`, or
+ :class:`~client.WebSocketClientProtocol`.
+
+New features
+............
+
+* :func:`~client.connect` can be used as an asynchronous context
+ manager on Python ≥ 3.5.1.
+
+* :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` and
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` support data passed as
+ :class:`str` in addition to :class:`bytes`.
+
+* Made ``state_name`` attribute on protocols a public API.
+
+Improvements
+............
+
+* Updated documentation with ``await`` and ``async`` syntax from Python 3.5.
+
+* Worked around an :mod:`asyncio` bug affecting connection termination under
+ load.
+
+* Improved documentation.
+
+2.7
+---
+
+*November 18, 2015*
+
+New features
+............
+
+* Added compatibility with Python 3.5.
+
+Improvements
+............
+
+* Refreshed documentation.
+
+2.6
+---
+
+*August 18, 2015*
+
+New features
+............
+
+* Added ``local_address`` and ``remote_address`` attributes on protocols.
+
+* Closed open connections with code 1001 when a server shuts down.
+
+Bug fixes
+.........
+
+* Avoided TCP fragmentation of small frames.
+
+2.5
+---
+
+*July 28, 2015*
+
+New features
+............
+
+* Provided access to handshake request and response HTTP headers.
+
+* Allowed customizing handshake request and response HTTP headers.
+
+* Added support for running on a non-default event loop.
+
+Improvements
+............
+
+* Improved documentation.
+
+* Sent a 403 status code instead of 400 when request Origin isn't allowed.
+
+* Clarified that the closing handshake can be initiated by the client.
+
+* Set the close code and reason more consistently.
+
+* Strengthened connection termination.
+
+Bug fixes
+.........
+
+* Canceling :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` no longer
+ drops the next message.
+
+2.4
+---
+
+*January 31, 2015*
+
+New features
+............
+
+* Added support for subprotocols.
+
+* Added ``loop`` argument to :func:`~client.connect` and
+ :func:`~server.serve`.
+
+2.3
+---
+
+*November 3, 2014*
+
+Improvements
+............
+
+* Improved compliance of close codes.
+
+2.2
+---
+
+*July 28, 2014*
+
+New features
+............
+
+* Added support for limiting message size.
+
+2.1
+---
+
+*April 26, 2014*
+
+New features
+............
+
+* Added ``host``, ``port`` and ``secure`` attributes on protocols.
+
+* Added support for providing and checking Origin_.
+
+.. _Origin: https://www.rfc-editor.org/rfc/rfc6455.html#section-10.2
+
+2.0
+---
+
+*February 16, 2014*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: :meth:`~legacy.protocol.WebSocketCommonProtocol.send`,
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` are now coroutines.
+ :class: caution
+
+ They used to be functions.
+
+ Instead of::
+
+ websocket.send(message)
+
+ you must write::
+
+ await websocket.send(message)
+
+New features
+............
+
+* Added flow control for outgoing data.
+
+1.0
+---
+
+*November 14, 2013*
+
+New features
+............
+
+* Initial public release.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/contributing.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/contributing.rst
new file mode 100644
index 0000000000..020ed7ad85
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/contributing.rst
@@ -0,0 +1,66 @@
+Contributing
+============
+
+Thanks for taking the time to contribute to websockets!
+
+Code of Conduct
+---------------
+
+This project and everyone participating in it is governed by the `Code of
+Conduct`_. By participating, you are expected to uphold this code. Please
+report inappropriate behavior to aymeric DOT augustin AT fractalideas DOT com.
+
+.. _Code of Conduct: https://github.com/python-websockets/websockets/blob/main/CODE_OF_CONDUCT.md
+
+*(If I'm the person with the inappropriate behavior, please accept my
+apologies. I know I can mess up. I can't expect you to tell me, but if you
+choose to do so, I'll do my best to handle criticism constructively.
+-- Aymeric)*
+
+Contributions
+-------------
+
+Bug reports, patches and suggestions are welcome!
+
+Please open an issue_ or send a `pull request`_.
+
+Feedback about the documentation is especially valuable, as the primary author
+feels more confident about writing code than writing docs :-)
+
+If you're wondering why things are done in a certain way, the :doc:`design
+document <../topics/design>` provides lots of details about the internals of
+websockets.
+
+.. _issue: https://github.com/python-websockets/websockets/issues/new
+.. _pull request: https://github.com/python-websockets/websockets/compare/
+
+Questions
+---------
+
+GitHub issues aren't a good medium for handling questions. There are better
+places to ask questions, for example Stack Overflow.
+
+If you want to ask a question anyway, please make sure that:
+
+- it's a question about websockets and not about :mod:`asyncio`;
+- it isn't answered in the documentation;
+- it wasn't asked already.
+
+A good question can be written as a suggestion to improve the documentation.
+
+Cryptocurrency users
+--------------------
+
+websockets appears to be quite popular for interfacing with Bitcoin or other
+cryptocurrency trackers. I'm strongly opposed to Bitcoin's carbon footprint.
+
+I'm aware of efforts to build proof-of-stake models. I'll care once the total
+energy consumption of all cryptocurrencies drops to a non-bullshit level.
+
+You already negated all of humanity's efforts to develop renewable energy.
+Please stop heating the planet where my children will have to live.
+
+Since websockets is released under an open-source license, you can use it for
+any purpose you like. However, I won't spend any of my time to help you.
+
+I will summarily close issues related to Bitcoin or cryptocurrency in any way.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/index.rst
new file mode 100644
index 0000000000..459146345b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/index.rst
@@ -0,0 +1,12 @@
+About websockets
+================
+
+This is about websockets-the-project rather than websockets-the-software.
+
+.. toctree::
+ :titlesonly:
+
+ changelog
+ contributing
+ license
+ For enterprise <tidelift>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/license.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/license.rst
new file mode 100644
index 0000000000..0a3b8703d5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/license.rst
@@ -0,0 +1,4 @@
+License
+=======
+
+.. include:: ../../LICENSE
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/tidelift.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/tidelift.rst
new file mode 100644
index 0000000000..42100fade9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/tidelift.rst
@@ -0,0 +1,112 @@
+websockets for enterprise
+=========================
+
+Available as part of the Tidelift Subscription
+----------------------------------------------
+
+.. image:: ../_static/tidelift.png
+ :height: 150px
+ :width: 150px
+ :align: left
+
+Tidelift is working with the maintainers of websockets and thousands of other
+open source projects to deliver commercial support and maintenance for the
+open source dependencies you use to build your applications. Save time, reduce
+risk, and improve code health, while paying the maintainers of the exact
+dependencies you use.
+
+.. raw:: html
+
+ <style type="text/css">
+ .tidelift-links {
+ display: flex;
+ justify-content: center;
+ }
+ @media only screen and (max-width: 600px) {
+ .tidelift-links {
+ flex-direction: column;
+ }
+ }
+ .tidelift-links a {
+ border: thin solid #f6914d;
+ border-radius: 0.25em;
+ font-family: Verdana, sans-serif;
+ font-size: 15px;
+ margin: 0.5em 2em;
+ padding: 0.5em 2em;
+ text-align: center;
+ text-decoration: none;
+ text-transform: uppercase;
+ }
+ .tidelift-links a.tidelift-links__learn-more {
+ background-color: white;
+ color: #f6914d;
+ }
+ .tidelift-links a.tidelift-links__request-a-demo {
+ background-color: #f6914d;
+ color: white;
+ }
+ </style>
+
+ <div class="tidelift-links">
+ <a class="tidelift-links__learn-more" href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Learn more</a>
+ <a class="tidelift-links__request-a-demo" href="https://tidelift.com/subscription/request-a-demo?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Request a demo</a>
+ </div>
+
+Enterprise-ready open source software—managed for you
+-----------------------------------------------------
+
+The Tidelift Subscription is a managed open source subscription for
+application dependencies covering millions of open source projects across
+JavaScript, Python, Java, PHP, Ruby, .NET, and more.
+
+Your subscription includes:
+
+* **Security updates**
+
+ * Tidelift’s security response team coordinates patches for new breaking
+ security vulnerabilities and alerts immediately through a private channel,
+ so your software supply chain is always secure.
+
+* **Licensing verification and indemnification**
+
+ * Tidelift verifies license information to enable easy policy enforcement
+ and adds intellectual property indemnification to cover creators and users
+ in case something goes wrong. You always have a 100% up-to-date bill of
+ materials for your dependencies to share with your legal team, customers,
+ or partners.
+
+* **Maintenance and code improvement**
+
+ * Tidelift ensures the software you rely on keeps working as long as you
+ need it to work. Your managed dependencies are actively maintained and we
+ recruit additional maintainers where required.
+
+* **Package selection and version guidance**
+
+ * We help you choose the best open source packages from the start—and then
+ guide you through updates to stay on the best releases as new issues
+ arise.
+
+* **Roadmap input**
+
+ * Take a seat at the table with the creators behind the software you use.
+ Tidelift’s participating maintainers earn more income as their software is
+ used by more subscribers, so they’re interested in knowing what you need.
+
+* **Tooling and cloud integration**
+
+ * Tidelift works with GitHub, GitLab, BitBucket, and more. We support every
+ cloud platform (and other deployment targets, too).
+
+The end result? All of the capabilities you expect from commercial-grade
+software, for the full breadth of open source you use. That means less time
+grappling with esoteric open source trivia, and more time building your own
+applications—and your business.
+
+.. raw:: html
+
+ <div class="tidelift-links">
+ <a class="tidelift-links__learn-more" href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Learn more</a>
+ <a class="tidelift-links__request-a-demo" href="https://tidelift.com/subscription/request-a-demo?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Request a demo</a>
+ </div>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst
new file mode 100644
index 0000000000..5086015b7b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst
@@ -0,0 +1,64 @@
+Client (:mod:`asyncio`)
+=======================
+
+.. automodule:: websockets.client
+
+Opening a connection
+--------------------
+
+.. autofunction:: connect(uri, *, create_protocol=None, logger=None, compression="deflate", origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", open_timeout=10, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds)
+ :async:
+
+.. autofunction:: unix_connect(path, uri="ws://localhost/", *, create_protocol=None, logger=None, compression="deflate", origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", open_timeout=10, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds)
+ :async:
+
+Using a connection
+------------------
+
+.. autoclass:: WebSocketClientProtocol(*, logger=None, origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16)
+
+ .. automethod:: recv
+
+ .. automethod:: send
+
+ .. automethod:: close
+
+ .. automethod:: wait_closed
+
+ .. automethod:: ping
+
+ .. automethod:: pong
+
+ WebSocket connection objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: local_address
+
+ .. autoproperty:: remote_address
+
+ .. autoproperty:: open
+
+ .. autoproperty:: closed
+
+ .. autoattribute:: latency
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: path
+
+ .. autoattribute:: request_headers
+
+ .. autoattribute:: response_headers
+
+ .. autoattribute:: subprotocol
+
+ The following attributes are available after the closing handshake,
+ once the WebSocket connection is closed:
+
+ .. autoproperty:: close_code
+
+ .. autoproperty:: close_reason
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst
new file mode 100644
index 0000000000..dc7a54ee1a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst
@@ -0,0 +1,54 @@
+:orphan:
+
+Both sides (:mod:`asyncio`)
+===========================
+
+.. automodule:: websockets.legacy.protocol
+
+.. autoclass:: WebSocketCommonProtocol(*, logger=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16)
+
+ .. automethod:: recv
+
+ .. automethod:: send
+
+ .. automethod:: close
+
+ .. automethod:: wait_closed
+
+ .. automethod:: ping
+
+ .. automethod:: pong
+
+ WebSocket connection objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: local_address
+
+ .. autoproperty:: remote_address
+
+ .. autoproperty:: open
+
+ .. autoproperty:: closed
+
+ .. autoattribute:: latency
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: path
+
+ .. autoattribute:: request_headers
+
+ .. autoattribute:: response_headers
+
+ .. autoattribute:: subprotocol
+
+ The following attributes are available after the closing handshake,
+ once the WebSocket connection is closed:
+
+ .. autoproperty:: close_code
+
+ .. autoproperty:: close_reason
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst
new file mode 100644
index 0000000000..1063179162
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst
@@ -0,0 +1,113 @@
+Server (:mod:`asyncio`)
+=======================
+
+.. automodule:: websockets.server
+
+Starting a server
+-----------------
+
+.. autofunction:: serve(ws_handler, host=None, port=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds)
+ :async:
+
+.. autofunction:: unix_serve(ws_handler, path=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds)
+ :async:
+
+Stopping a server
+-----------------
+
+.. autoclass:: WebSocketServer
+
+ .. automethod:: close
+
+ .. automethod:: wait_closed
+
+ .. automethod:: get_loop
+
+ .. automethod:: is_serving
+
+ .. automethod:: start_serving
+
+ .. automethod:: serve_forever
+
+ .. autoattribute:: sockets
+
+Using a connection
+------------------
+
+.. autoclass:: WebSocketServerProtocol(ws_handler, ws_server, *, logger=None, origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16)
+
+ .. automethod:: recv
+
+ .. automethod:: send
+
+ .. automethod:: close
+
+ .. automethod:: wait_closed
+
+ .. automethod:: ping
+
+ .. automethod:: pong
+
+ You can customize the opening handshake in a subclass by overriding these methods:
+
+ .. automethod:: process_request
+
+ .. automethod:: select_subprotocol
+
+ WebSocket connection objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: local_address
+
+ .. autoproperty:: remote_address
+
+ .. autoproperty:: open
+
+ .. autoproperty:: closed
+
+ .. autoattribute:: latency
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: path
+
+ .. autoattribute:: request_headers
+
+ .. autoattribute:: response_headers
+
+ .. autoattribute:: subprotocol
+
+ The following attributes are available after the closing handshake,
+ once the WebSocket connection is closed:
+
+ .. autoproperty:: close_code
+
+ .. autoproperty:: close_reason
+
+
+Basic authentication
+--------------------
+
+.. automodule:: websockets.auth
+
+websockets supports HTTP Basic Authentication according to
+:rfc:`7235` and :rfc:`7617`.
+
+.. autofunction:: basic_auth_protocol_factory
+
+.. autoclass:: BasicAuthWebSocketServerProtocol
+
+ .. autoattribute:: realm
+
+ .. autoattribute:: username
+
+ .. automethod:: check_credentials
+
+Broadcast
+---------
+
+.. autofunction:: websockets.broadcast
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/datastructures.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/datastructures.rst
new file mode 100644
index 0000000000..ec02d42101
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/datastructures.rst
@@ -0,0 +1,66 @@
+Data structures
+===============
+
+WebSocket events
+----------------
+
+.. automodule:: websockets.frames
+
+ .. autoclass:: Frame
+
+ .. autoclass:: Opcode
+
+ .. autoattribute:: CONT
+ .. autoattribute:: TEXT
+ .. autoattribute:: BINARY
+ .. autoattribute:: CLOSE
+ .. autoattribute:: PING
+ .. autoattribute:: PONG
+
+ .. autoclass:: Close
+
+ .. autoclass:: CloseCode
+
+ .. autoattribute:: NORMAL_CLOSURE
+ .. autoattribute:: GOING_AWAY
+ .. autoattribute:: PROTOCOL_ERROR
+ .. autoattribute:: UNSUPPORTED_DATA
+ .. autoattribute:: NO_STATUS_RCVD
+ .. autoattribute:: ABNORMAL_CLOSURE
+ .. autoattribute:: INVALID_DATA
+ .. autoattribute:: POLICY_VIOLATION
+ .. autoattribute:: MESSAGE_TOO_BIG
+ .. autoattribute:: MANDATORY_EXTENSION
+ .. autoattribute:: INTERNAL_ERROR
+ .. autoattribute:: SERVICE_RESTART
+ .. autoattribute:: TRY_AGAIN_LATER
+ .. autoattribute:: BAD_GATEWAY
+ .. autoattribute:: TLS_HANDSHAKE
+
+HTTP events
+-----------
+
+.. automodule:: websockets.http11
+
+ .. autoclass:: Request
+
+ .. autoclass:: Response
+
+.. automodule:: websockets.datastructures
+
+ .. autoclass:: Headers
+
+ .. automethod:: get_all
+
+ .. automethod:: raw_items
+
+ .. autoexception:: MultipleValuesError
+
+URIs
+----
+
+.. automodule:: websockets.uri
+
+ .. autofunction:: parse_uri
+
+ .. autoclass:: WebSocketURI
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/exceptions.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/exceptions.rst
new file mode 100644
index 0000000000..907a650d20
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/exceptions.rst
@@ -0,0 +1,6 @@
+Exceptions
+==========
+
+.. automodule:: websockets.exceptions
+ :members:
+
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/extensions.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/extensions.rst
new file mode 100644
index 0000000000..a70f1b1e58
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/extensions.rst
@@ -0,0 +1,60 @@
+Extensions
+==========
+
+.. currentmodule:: websockets.extensions
+
+The WebSocket protocol supports extensions_.
+
+At the time of writing, there's only one `registered extension`_ with a public
+specification, WebSocket Per-Message Deflate.
+
+.. _extensions: https://www.rfc-editor.org/rfc/rfc6455.html#section-9
+.. _registered extension: https://www.iana.org/assignments/websocket/websocket.xhtml#extension-name
+
+Per-Message Deflate
+-------------------
+
+.. automodule:: websockets.extensions.permessage_deflate
+
+ :mod:`websockets.extensions.permessage_deflate` implements WebSocket
+ Per-Message Deflate.
+
+ This extension is specified in :rfc:`7692`.
+
+ Refer to the :doc:`topic guide on compression <../topics/compression>` to
+ learn more about tuning compression settings.
+
+ .. autoclass:: ClientPerMessageDeflateFactory
+
+ .. autoclass:: ServerPerMessageDeflateFactory
+
+Base classes
+------------
+
+.. automodule:: websockets.extensions
+
+ :mod:`websockets.extensions` defines base classes for implementing
+ extensions.
+
+ Refer to the :doc:`how-to guide on extensions <../howto/extensions>` to
+ learn more about writing an extension.
+
+ .. autoclass:: Extension
+
+ .. autoattribute:: name
+
+ .. automethod:: decode
+
+ .. automethod:: encode
+
+ .. autoclass:: ClientExtensionFactory
+
+ .. autoattribute:: name
+
+ .. automethod:: get_request_params
+
+ .. automethod:: process_response_params
+
+ .. autoclass:: ServerExtensionFactory
+
+ .. automethod:: process_request_params
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/features.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/features.rst
new file mode 100644
index 0000000000..98b3c0ddaf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/features.rst
@@ -0,0 +1,187 @@
+Features
+========
+
+.. currentmodule:: websockets
+
+Feature support matrices summarize which implementations support which features.
+
+.. raw:: html
+
+ <style>
+ .support-matrix-table { width: 100%; }
+ .support-matrix-table th:first-child { text-align: left; }
+ .support-matrix-table th:not(:first-child) { text-align: center; width: 15%; }
+ .support-matrix-table td:not(:first-child) { text-align: center; }
+ </style>
+
+.. |aio| replace:: :mod:`asyncio`
+.. |sync| replace:: :mod:`threading`
+.. |sans| replace:: `Sans-I/O`_
+.. _Sans-I/O: https://sans-io.readthedocs.io/
+
+Both sides
+----------
+
+.. table::
+ :class: support-matrix-table
+
+ +------------------------------------+--------+--------+--------+
+ | | |aio| | |sync| | |sans| |
+ +====================================+========+========+========+
+ | Perform the opening handshake | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Send a message | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Receive a message | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Iterate over received messages | ✅ | ✅ | ❌ |
+ +------------------------------------+--------+--------+--------+
+ | Send a fragmented message | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Receive a fragmented message after | ✅ | ✅ | ❌ |
+ | reassembly | | | |
+ +------------------------------------+--------+--------+--------+
+ | Receive a fragmented message frame | ❌ | ✅ | ✅ |
+ | by frame (`#479`_) | | | |
+ +------------------------------------+--------+--------+--------+
+ | Send a ping | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Respond to pings automatically | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Send a pong | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Perform the closing handshake | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Report close codes and reasons | ❌ | ✅ | ✅ |
+ | from both sides | | | |
+ +------------------------------------+--------+--------+--------+
+ | Compress messages (:rfc:`7692`) | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Tune memory usage for compression | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Negotiate extensions | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Implement custom extensions | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Negotiate a subprotocol | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Enforce security limits | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Log events | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Enforce opening timeout | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Enforce closing timeout | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Keepalive | ✅ | ❌ | — |
+ +------------------------------------+--------+--------+--------+
+ | Heartbeat | ✅ | ❌ | — |
+ +------------------------------------+--------+--------+--------+
+
+.. _#479: https://github.com/python-websockets/websockets/issues/479
+
+Server
+------
+
+.. table::
+ :class: support-matrix-table
+
+ +------------------------------------+--------+--------+--------+
+ | | |aio| | |sync| | |sans| |
+ +====================================+========+========+========+
+ | Listen on a TCP socket | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Listen on a Unix socket | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Listen using a preexisting socket | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Encrypt connection with TLS | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Close server on context exit | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Close connection on handler exit | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Shut down server gracefully | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Check ``Origin`` header | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Customize subprotocol selection | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Configure ``Server`` header | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Alter opening handshake request | ❌ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Alter opening handshake response | ❌ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Perform HTTP Basic Authentication | ✅ | ❌ | ❌ |
+ +------------------------------------+--------+--------+--------+
+ | Perform HTTP Digest Authentication | ❌ | ❌ | ❌ |
+ +------------------------------------+--------+--------+--------+
+ | Force HTTP response | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+
+Client
+------
+
+.. table::
+ :class: support-matrix-table
+
+ +------------------------------------+--------+--------+--------+
+ | | |aio| | |sync| | |sans| |
+ +====================================+========+========+========+
+ | Connect to a TCP socket | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Connect to a Unix socket | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Connect using a preexisting socket | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Encrypt connection with TLS | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Close connection on context exit | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Reconnect automatically | ✅ | ❌ | — |
+ +------------------------------------+--------+--------+--------+
+ | Configure ``Origin`` header | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Configure ``User-Agent`` header | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Alter opening handshake request | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Connect to non-ASCII IRIs | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Perform HTTP Basic Authentication | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Perform HTTP Digest Authentication | ❌ | ❌ | ❌ |
+ | (`#784`_) | | | |
+ +------------------------------------+--------+--------+--------+
+ | Follow HTTP redirects | ✅ | ❌ | — |
+ +------------------------------------+--------+--------+--------+
+ | Connect via a HTTP proxy (`#364`_) | ❌ | ❌ | — |
+ +------------------------------------+--------+--------+--------+
+ | Connect via a SOCKS5 proxy | ❌ | ❌ | — |
+ | (`#475`_) | | | |
+ +------------------------------------+--------+--------+--------+
+
+.. _#364: https://github.com/python-websockets/websockets/issues/364
+.. _#475: https://github.com/python-websockets/websockets/issues/475
+.. _#784: https://github.com/python-websockets/websockets/issues/784
+
+Known limitations
+-----------------
+
+There is no way to control compression of outgoing frames on a per-frame basis
+(`#538`_). If compression is enabled, all frames are compressed.
+
+.. _#538: https://github.com/python-websockets/websockets/issues/538
+
+The server doesn't check the Host header and respond with a HTTP 400 Bad Request
+if it is missing or invalid (`#1246`).
+
+.. _#1246: https://github.com/python-websockets/websockets/issues/1246
+
+The client API doesn't attempt to guarantee that there is no more than one
+connection to a given IP address in a CONNECTING state. This behavior is
+`mandated by RFC 6455`_. However, :func:`~client.connect()` isn't the right
+layer for enforcing this constraint. It's the caller's responsibility.
+
+.. _mandated by RFC 6455: https://www.rfc-editor.org/rfc/rfc6455.html#section-4.1
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/index.rst
new file mode 100644
index 0000000000..0b80f087a1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/index.rst
@@ -0,0 +1,90 @@
+API reference
+=============
+
+.. currentmodule:: websockets
+
+Features
+--------
+
+Check which implementations support which features and known limitations.
+
+.. toctree::
+ :titlesonly:
+
+ features
+
+
+:mod:`asyncio`
+--------------
+
+This is the default implementation. It's ideal for servers that handle many
+clients concurrently.
+
+.. toctree::
+ :titlesonly:
+
+ asyncio/server
+ asyncio/client
+
+:mod:`threading`
+----------------
+
+This alternative implementation can be a good choice for clients.
+
+.. toctree::
+ :titlesonly:
+
+ sync/server
+ sync/client
+
+`Sans-I/O`_
+-----------
+
+This layer is designed for integrating in third-party libraries, typically
+application servers.
+
+.. _Sans-I/O: https://sans-io.readthedocs.io/
+
+.. toctree::
+ :titlesonly:
+
+ sansio/server
+ sansio/client
+
+Extensions
+----------
+
+The Per-Message Deflate extension is built in. You may also define custom
+extensions.
+
+.. toctree::
+ :titlesonly:
+
+ extensions
+
+Shared
+------
+
+These low-level APIs are shared by all implementations.
+
+.. toctree::
+ :titlesonly:
+
+ datastructures
+ exceptions
+ types
+
+API stability
+-------------
+
+Public APIs documented in this API reference are subject to the
+:ref:`backwards-compatibility policy <backwards-compatibility policy>`.
+
+Anything that isn't listed in the API reference is a private API. There's no
+guarantees of behavior or backwards-compatibility for private APIs.
+
+Convenience imports
+-------------------
+
+For convenience, many public APIs can be imported directly from the
+``websockets`` package.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/client.rst
new file mode 100644
index 0000000000..09bafc7455
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/client.rst
@@ -0,0 +1,58 @@
+Client (`Sans-I/O`_)
+====================
+
+.. _Sans-I/O: https://sans-io.readthedocs.io/
+
+.. currentmodule:: websockets.client
+
+.. autoclass:: ClientProtocol(wsuri, origin=None, extensions=None, subprotocols=None, state=State.CONNECTING, max_size=2 ** 20, logger=None)
+
+ .. automethod:: receive_data
+
+ .. automethod:: receive_eof
+
+ .. automethod:: connect
+
+ .. automethod:: send_request
+
+ .. automethod:: send_continuation
+
+ .. automethod:: send_text
+
+ .. automethod:: send_binary
+
+ .. automethod:: send_close
+
+ .. automethod:: send_ping
+
+ .. automethod:: send_pong
+
+ .. automethod:: fail
+
+ .. automethod:: events_received
+
+ .. automethod:: data_to_send
+
+ .. automethod:: close_expected
+
+ WebSocket protocol objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: state
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: handshake_exc
+
+ The following attributes are available after the closing handshake,
+ once the WebSocket connection is closed:
+
+ .. autoproperty:: close_code
+
+ .. autoproperty:: close_reason
+
+ .. autoproperty:: close_exc
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/common.rst
new file mode 100644
index 0000000000..cd1ef3c63a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/common.rst
@@ -0,0 +1,64 @@
+:orphan:
+
+Both sides (`Sans-I/O`_)
+=========================
+
+.. _Sans-I/O: https://sans-io.readthedocs.io/
+
+.. automodule:: websockets.protocol
+
+.. autoclass:: Protocol(side, state=State.OPEN, max_size=2 ** 20, logger=None)
+
+ .. automethod:: receive_data
+
+ .. automethod:: receive_eof
+
+ .. automethod:: send_continuation
+
+ .. automethod:: send_text
+
+ .. automethod:: send_binary
+
+ .. automethod:: send_close
+
+ .. automethod:: send_ping
+
+ .. automethod:: send_pong
+
+ .. automethod:: fail
+
+ .. automethod:: events_received
+
+ .. automethod:: data_to_send
+
+ .. automethod:: close_expected
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: state
+
+ .. autoproperty:: close_code
+
+ .. autoproperty:: close_reason
+
+ .. autoproperty:: close_exc
+
+.. autoclass:: Side
+
+ .. autoattribute:: SERVER
+
+ .. autoattribute:: CLIENT
+
+.. autoclass:: State
+
+ .. autoattribute:: CONNECTING
+
+ .. autoattribute:: OPEN
+
+ .. autoattribute:: CLOSING
+
+ .. autoattribute:: CLOSED
+
+.. autodata:: SEND_EOF
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/server.rst
new file mode 100644
index 0000000000..d70df6277a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/server.rst
@@ -0,0 +1,62 @@
+Server (`Sans-I/O`_)
+====================
+
+.. _Sans-I/O: https://sans-io.readthedocs.io/
+
+.. currentmodule:: websockets.server
+
+.. autoclass:: ServerProtocol(origins=None, extensions=None, subprotocols=None, state=State.CONNECTING, max_size=2 ** 20, logger=None)
+
+ .. automethod:: receive_data
+
+ .. automethod:: receive_eof
+
+ .. automethod:: accept
+
+ .. automethod:: select_subprotocol
+
+ .. automethod:: reject
+
+ .. automethod:: send_response
+
+ .. automethod:: send_continuation
+
+ .. automethod:: send_text
+
+ .. automethod:: send_binary
+
+ .. automethod:: send_close
+
+ .. automethod:: send_ping
+
+ .. automethod:: send_pong
+
+ .. automethod:: fail
+
+ .. automethod:: events_received
+
+ .. automethod:: data_to_send
+
+ .. automethod:: close_expected
+
+ WebSocket protocol objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: state
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: handshake_exc
+
+ The following attributes are available after the closing handshake,
+ once the WebSocket connection is closed:
+
+ .. autoproperty:: close_code
+
+ .. autoproperty:: close_reason
+
+ .. autoproperty:: close_exc
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/client.rst
new file mode 100644
index 0000000000..6cccd6ec48
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/client.rst
@@ -0,0 +1,49 @@
+Client (:mod:`threading`)
+=========================
+
+.. automodule:: websockets.sync.client
+
+Opening a connection
+--------------------
+
+.. autofunction:: connect(uri, *, sock=None, ssl_context=None, server_hostname=None, origin=None, extensions=None, subprotocols=None, additional_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None)
+
+.. autofunction:: unix_connect(path, uri=None, *, sock=None, ssl_context=None, server_hostname=None, origin=None, extensions=None, subprotocols=None, additional_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None)
+
+Using a connection
+------------------
+
+.. autoclass:: ClientConnection
+
+ .. automethod:: __iter__
+
+ .. automethod:: recv
+
+ .. automethod:: recv_streaming
+
+ .. automethod:: send
+
+ .. automethod:: close
+
+ .. automethod:: ping
+
+ .. automethod:: pong
+
+ WebSocket connection objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: local_address
+
+ .. autoproperty:: remote_address
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: request
+
+ .. autoattribute:: response
+
+ .. autoproperty:: subprotocol
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/common.rst
new file mode 100644
index 0000000000..3dc6d4a509
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/common.rst
@@ -0,0 +1,41 @@
+:orphan:
+
+Both sides (:mod:`threading`)
+=============================
+
+.. automodule:: websockets.sync.connection
+
+.. autoclass:: Connection
+
+ .. automethod:: __iter__
+
+ .. automethod:: recv
+
+ .. automethod:: recv_streaming
+
+ .. automethod:: send
+
+ .. automethod:: close
+
+ .. automethod:: ping
+
+ .. automethod:: pong
+
+ WebSocket connection objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: local_address
+
+ .. autoproperty:: remote_address
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: request
+
+ .. autoattribute:: response
+
+ .. autoproperty:: subprotocol
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/server.rst
new file mode 100644
index 0000000000..35c112046a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/server.rst
@@ -0,0 +1,60 @@
+Server (:mod:`threading`)
+=========================
+
+.. automodule:: websockets.sync.server
+
+Creating a server
+-----------------
+
+.. autofunction:: serve(handler, host=None, port=None, *, sock=None, ssl_context=None, origins=None, extensions=None, subprotocols=None, select_subprotocol=None, process_request=None, process_response=None, server_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None)
+
+.. autofunction:: unix_serve(handler, path=None, *, sock=None, ssl_context=None, origins=None, extensions=None, subprotocols=None, select_subprotocol=None, process_request=None, process_response=None, server_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None)
+
+Running a server
+----------------
+
+.. autoclass:: WebSocketServer
+
+ .. automethod:: serve_forever
+
+ .. automethod:: shutdown
+
+ .. automethod:: fileno
+
+Using a connection
+------------------
+
+.. autoclass:: ServerConnection
+
+ .. automethod:: __iter__
+
+ .. automethod:: recv
+
+ .. automethod:: recv_streaming
+
+ .. automethod:: send
+
+ .. automethod:: close
+
+ .. automethod:: ping
+
+ .. automethod:: pong
+
+ WebSocket connection objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: local_address
+
+ .. autoproperty:: remote_address
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: request
+
+ .. autoattribute:: response
+
+ .. autoproperty:: subprotocol
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/types.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/types.rst
new file mode 100644
index 0000000000..9d3aa8310b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/types.rst
@@ -0,0 +1,24 @@
+Types
+=====
+
+.. automodule:: websockets.typing
+
+ .. autodata:: Data
+
+ .. autodata:: LoggerLike
+
+ .. autodata:: StatusLike
+
+ .. autodata:: Origin
+
+ .. autodata:: Subprotocol
+
+ .. autodata:: ExtensionName
+
+ .. autodata:: ExtensionParameter
+
+.. autodata:: websockets.protocol.Event
+
+.. autodata:: websockets.datastructures.HeadersLike
+
+.. autodata:: websockets.datastructures.SupportsKeysAndGetItem
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt b/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt
new file mode 100644
index 0000000000..bcd1d71143
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt
@@ -0,0 +1,8 @@
+furo
+sphinx
+sphinx-autobuild
+sphinx-copybutton
+sphinx-inline-tabs
+sphinxcontrib-spelling
+sphinxcontrib-trio
+sphinxext-opengraph
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt b/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt
new file mode 100644
index 0000000000..dfa7065e79
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt
@@ -0,0 +1,85 @@
+augustin
+auth
+autoscaler
+aymeric
+backend
+backoff
+backpressure
+balancer
+balancers
+bottlenecked
+bufferbloat
+bugfix
+buildpack
+bytestring
+bytestrings
+changelog
+coroutine
+coroutines
+cryptocurrencies
+cryptocurrency
+css
+ctrl
+deserialize
+django
+dev
+Dockerfile
+dyno
+formatter
+fractalideas
+gunicorn
+healthz
+html
+hypercorn
+iframe
+IPv
+istio
+iterable
+js
+keepalive
+KiB
+kubernetes
+lifecycle
+linkerd
+liveness
+lookups
+MiB
+mutex
+mypy
+nginx
+Paketo
+permessage
+pid
+procfile
+proxying
+py
+pythonic
+reconnection
+redis
+redistributions
+retransmit
+runtime
+scalable
+stateful
+subclasses
+subclassing
+submodule
+subpackages
+subprotocol
+subprotocols
+supervisord
+tidelift
+tls
+tox
+txt
+unregister
+uple
+uvicorn
+uvloop
+virtualenv
+WebSocket
+websocket
+websockets
+ws
+wsgi
+www
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.rst
new file mode 100644
index 0000000000..31bc8e6da8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.rst
@@ -0,0 +1,348 @@
+Authentication
+==============
+
+The WebSocket protocol was designed for creating web applications that need
+bidirectional communication between clients running in browsers and servers.
+
+In most practical use cases, WebSocket servers need to authenticate clients in
+order to route communications appropriately and securely.
+
+:rfc:`6455` stays elusive when it comes to authentication:
+
+ This protocol doesn't prescribe any particular way that servers can
+ authenticate clients during the WebSocket handshake. The WebSocket
+ server can use any client authentication mechanism available to a
+ generic HTTP server, such as cookies, HTTP authentication, or TLS
+ authentication.
+
+None of these three mechanisms works well in practice. Using cookies is
+cumbersome, HTTP authentication isn't supported by all mainstream browsers,
+and TLS authentication in a browser is an esoteric user experience.
+
+Fortunately, there are better alternatives! Let's discuss them.
+
+System design
+-------------
+
+Consider a setup where the WebSocket server is separate from the HTTP server.
+
+Most servers built with websockets to complement a web application adopt this
+design because websockets doesn't aim at supporting HTTP.
+
+The following diagram illustrates the authentication flow.
+
+.. image:: authentication.svg
+
+Assuming the current user is authenticated with the HTTP server (1), the
+application needs to obtain credentials from the HTTP server (2) in order to
+send them to the WebSocket server (3), who can check them against the database
+of user accounts (4).
+
+Usernames and passwords aren't a good choice of credentials here, if only
+because passwords aren't available in clear text in the database.
+
+Tokens linked to user accounts are a better choice. These tokens must be
+impossible to forge by an attacker. For additional security, they can be
+short-lived or even single-use.
+
+Sending credentials
+-------------------
+
+Assume the web application obtained authentication credentials, likely a
+token, from the HTTP server. There's four options for passing them to the
+WebSocket server.
+
+1. **Sending credentials as the first message in the WebSocket connection.**
+
+ This is fully reliable and the most secure mechanism in this discussion. It
+ has two minor downsides:
+
+ * Authentication is performed at the application layer. Ideally, it would
+ be managed at the protocol layer.
+
+ * Authentication is performed after the WebSocket handshake, making it
+ impossible to monitor authentication failures with HTTP response codes.
+
+2. **Adding credentials to the WebSocket URI in a query parameter.**
+
+ This is also fully reliable but less secure. Indeed, it has a major
+ downside:
+
+ * URIs end up in logs, which leaks credentials. Even if that risk could be
+ lowered with single-use tokens, it is usually considered unacceptable.
+
+ Authentication is still performed at the application layer but it can
+ happen before the WebSocket handshake, which improves separation of
+ concerns and enables responding to authentication failures with HTTP 401.
+
+3. **Setting a cookie on the domain of the WebSocket URI.**
+
+ Cookies are undoubtedly the most common and hardened mechanism for sending
+ credentials from a web application to a server. In an HTTP application,
+ credentials would be a session identifier or a serialized, signed session.
+
+ Unfortunately, when the WebSocket server runs on a different domain from
+ the web application, this idea bumps into the `Same-Origin Policy`_. For
+ security reasons, setting a cookie on a different origin is impossible.
+
+ The proper workaround consists in:
+
+ * creating a hidden iframe_ served from the domain of the WebSocket server
+ * sending the token to the iframe with postMessage_
+ * setting the cookie in the iframe
+
+ before opening the WebSocket connection.
+
+ Sharing a parent domain (e.g. example.com) between the HTTP server (e.g.
+ www.example.com) and the WebSocket server (e.g. ws.example.com) and setting
+ the cookie on that parent domain would work too.
+
+ However, the cookie would be shared with all subdomains of the parent
+ domain. For a cookie containing credentials, this is unacceptable.
+
+.. _Same-Origin Policy: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
+.. _iframe: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe
+.. _postMessage: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/postMessage
+
+4. **Adding credentials to the WebSocket URI in user information.**
+
+ Letting the browser perform HTTP Basic Auth is a nice idea in theory.
+
+ In practice it doesn't work due to poor support in browsers.
+
+ As of May 2021:
+
+ * Chrome 90 behaves as expected.
+
+ * Firefox 88 caches credentials too aggressively.
+
+ When connecting again to the same server with new credentials, it reuses
+ the old credentials, which may be expired, resulting in an HTTP 401. Then
+ the next connection succeeds. Perhaps errors clear the cache.
+
+ When tokens are short-lived or single-use, this bug produces an
+ interesting effect: every other WebSocket connection fails.
+
+ * Safari 14 ignores credentials entirely.
+
+Two other options are off the table:
+
+1. **Setting a custom HTTP header**
+
+ This would be the most elegant mechanism, solving all issues with the options
+ discussed above.
+
+ Unfortunately, it doesn't work because the `WebSocket API`_ doesn't support
+ `setting custom headers`_.
+
+.. _WebSocket API: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
+.. _setting custom headers: https://github.com/whatwg/html/issues/3062
+
+2. **Authenticating with a TLS certificate**
+
+ While this is suggested by the RFC, installing a TLS certificate is too far
+ from the mainstream experience of browser users. This could make sense in
+ high security contexts. I hope developers working on such projects don't
+ take security advice from the documentation of random open source projects.
+
+Let's experiment!
+-----------------
+
+The `experiments/authentication`_ directory demonstrates these techniques.
+
+Run the experiment in an environment where websockets is installed:
+
+.. _experiments/authentication: https://github.com/python-websockets/websockets/tree/main/experiments/authentication
+
+.. code-block:: console
+
+ $ python experiments/authentication/app.py
+ Running on http://localhost:8000/
+
+When you browse to the HTTP server at http://localhost:8000/ and you submit a
+username, the server creates a token and returns a testing web page.
+
+This page opens WebSocket connections to four WebSocket servers running on
+four different origins. It attempts to authenticate with the token in four
+different ways.
+
+First message
+.............
+
+As soon as the connection is open, the client sends a message containing the
+token:
+
+.. code-block:: javascript
+
+ const websocket = new WebSocket("ws://.../");
+ websocket.onopen = () => websocket.send(token);
+
+ // ...
+
+At the beginning of the connection handler, the server receives this message
+and authenticates the user. If authentication fails, the server closes the
+connection:
+
+.. code-block:: python
+
+ async def first_message_handler(websocket):
+ token = await websocket.recv()
+ user = get_user(token)
+ if user is None:
+ await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed")
+ return
+
+ ...
+
+Query parameter
+...............
+
+The client adds the token to the WebSocket URI in a query parameter before
+opening the connection:
+
+.. code-block:: javascript
+
+ const uri = `ws://.../?token=${token}`;
+ const websocket = new WebSocket(uri);
+
+ // ...
+
+The server intercepts the HTTP request, extracts the token and authenticates
+the user. If authentication fails, it returns an HTTP 401:
+
+.. code-block:: python
+
+ class QueryParamProtocol(websockets.WebSocketServerProtocol):
+ async def process_request(self, path, headers):
+ token = get_query_parameter(path, "token")
+ if token is None:
+ return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n"
+
+ user = get_user(token)
+ if user is None:
+ return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n"
+
+ self.user = user
+
+ async def query_param_handler(websocket):
+ user = websocket.user
+
+ ...
+
+Cookie
+......
+
+The client sets a cookie containing the token before opening the connection.
+
+The cookie must be set by an iframe loaded from the same origin as the
+WebSocket server. This requires passing the token to this iframe.
+
+.. code-block:: javascript
+
+ // in main window
+ iframe.contentWindow.postMessage(token, "http://...");
+
+ // in iframe
+ document.cookie = `token=${data}; SameSite=Strict`;
+
+ // in main window
+ const websocket = new WebSocket("ws://.../");
+
+ // ...
+
+This sequence must be synchronized between the main window and the iframe.
+This involves several events. Look at the full implementation for details.
+
+The server intercepts the HTTP request, extracts the token and authenticates
+the user. If authentication fails, it returns an HTTP 401:
+
+.. code-block:: python
+
+ class CookieProtocol(websockets.WebSocketServerProtocol):
+ async def process_request(self, path, headers):
+ # Serve iframe on non-WebSocket requests
+ ...
+
+ token = get_cookie(headers.get("Cookie", ""), "token")
+ if token is None:
+ return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n"
+
+ user = get_user(token)
+ if user is None:
+ return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n"
+
+ self.user = user
+
+ async def cookie_handler(websocket):
+ user = websocket.user
+
+ ...
+
+User information
+................
+
+The client adds the token to the WebSocket URI in user information before
+opening the connection:
+
+.. code-block:: javascript
+
+ const uri = `ws://token:${token}@.../`;
+ const websocket = new WebSocket(uri);
+
+ // ...
+
+Since HTTP Basic Auth is designed to accept a username and a password rather
+than a token, we send ``token`` as username and the token as password.
+
+The server intercepts the HTTP request, extracts the token and authenticates
+the user. If authentication fails, it returns an HTTP 401:
+
+.. code-block:: python
+
+ class UserInfoProtocol(websockets.BasicAuthWebSocketServerProtocol):
+ async def check_credentials(self, username, password):
+ if username != "token":
+ return False
+
+ user = get_user(password)
+ if user is None:
+ return False
+
+ self.user = user
+ return True
+
+ async def user_info_handler(websocket):
+ user = websocket.user
+
+ ...
+
+Machine-to-machine authentication
+---------------------------------
+
+When the WebSocket client is a standalone program rather than a script running
+in a browser, there are far fewer constraints. HTTP Authentication is the best
+solution in this scenario.
+
+To authenticate a websockets client with HTTP Basic Authentication
+(:rfc:`7617`), include the credentials in the URI:
+
+.. code-block:: python
+
+ async with websockets.connect(
+ f"wss://{username}:{password}@example.com",
+ ) as websocket:
+ ...
+
+(You must :func:`~urllib.parse.quote` ``username`` and ``password`` if they
+contain unsafe characters.)
+
+To authenticate a websockets client with HTTP Bearer Authentication
+(:rfc:`6750`), add a suitable ``Authorization`` header:
+
+.. code-block:: python
+
+ async with websockets.connect(
+ "wss://example.com",
+ extra_headers={"Authorization": f"Bearer {token}"}
+ ) as websocket:
+ ...
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.svg
new file mode 100644
index 0000000000..79b5fb8d56
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.svg
@@ -0,0 +1,63 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-514 74 644 380" width="644px" height="380px"><defs><style>@font-face{
+ font-family:"DIN Next";
+ font-weight: 400;
+ src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix");
+ src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 400;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix");
+ src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg");
+}
+@font-face{
+ font-weight: 500;
+ font-family:"DIN Next";
+ src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix");
+ src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 500;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix");
+ src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg");
+}
+@font-face{
+ font-weight: 700;
+ font-family:"DIN Next";
+ src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix");
+ src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 700;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix");
+ src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg");
+}
+
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff');
+ font-weight: 400;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff');
+ font-style: italic;
+ font-weight: 400;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff');
+ font-weight: 700;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff');
+ font-style: italic;
+ font-weight: 700;
+}
+* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><rect x="-504" y="228" width="192" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="228" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-504" ry="3" height="72"/><text y="240" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-492" font-family="DIN Next, sans-serif" height="48"><tspan x="-408" y="258" text-anchor="middle" style="white-space:pre;">HTTP</tspan><tspan x="-408" y="282" text-anchor="middle" style="white-space:pre;">server</tspan></text></g></g><g><g><rect x="-72" y="228" width="192" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="228" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-72" ry="3" height="72"/><text y="240" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-60" font-family="DIN Next, sans-serif" height="48"><tspan x="24" y="258" text-anchor="middle" style="white-space:pre;">WebSocket</tspan><tspan x="24" y="282" text-anchor="middle" style="white-space:pre;">server</tspan></text></g></g><g><g><rect x="-288" y="84" width="192" height="72" rx="3" ry="3" fill="#d9dde0"/><rect y="84" rx="3" stroke="#4B5C6B" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-288" ry="3" height="72"/><text y="96" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-276" font-family="DIN Next, sans-serif" height="48"><tspan x="-192" y="114" text-anchor="middle" style="white-space:pre;">web app</tspan><tspan x="-192" y="138" text-anchor="middle" style="white-space:pre;">in browser</tspan></text></g></g><g><g><path d="M-96,372c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 42.98074,-12 96,-12c53.01926,0 96,5.37259 96,12c0,6.62741 0,53.37259 0,60c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 0,-60 0,-60" fill="#e2cdf2"/><path d="M-96,372c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 42.98074,-12 96,-12c53.01926,0 96,5.37259 96,12c0,6.62741 0,53.37259 0,60c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 0,-60 0,-60" fill="none" stroke="#730FC3" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="396" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-276" font-family="DIN Next, sans-serif" height="24"><tspan x="-192" y="414" text-anchor="middle" style="white-space:pre;">user accounts</tspan></text></g></g><g><g><path d="M-400.46606,302.69069l37.26606,13.30931M-284.8,344l33.4991,11.96396" fill="none" stroke="#1AAE9F" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-252.34924,349.70583 -247.53394,357.30931 -256.07648,360.14213" fill="#1AAE9F" stroke="#1AAE9F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="318" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="160" xml:space="preserve" x="-404" font-family="DIN Next, sans-serif" height="24"><tspan x="-324" y="336" text-anchor="middle" style="white-space:pre;">(1) authenticate user</tspan></text></g></g><g><g><path d="M-247.35316,159.15135l-43.98017,18.84865M-356.66667,206l-40.30359,17.27297" fill="none" stroke="#1AAE9F" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-391.94549,227.14787 -400.64684,224.84865 -396.31086,216.96199" fill="#1AAE9F" stroke="#1AAE9F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="180" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="164" xml:space="preserve" x="-406" font-family="DIN Next, sans-serif" height="24"><tspan x="-324" y="198" text-anchor="middle" style="white-space:pre;">(2) obtain credentials</tspan></text></g></g><g><g><path d="M-136.64684,159.15135l43.98017,18.84865M-27.33333,206l40.30359,17.27297" fill="none" stroke="#2C88D9" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="12.31086,216.96199 16.64684,224.84865 7.94549,227.14787" fill="#2C88D9" stroke="#2C88D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="180" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="153" xml:space="preserve" x="-136.5" font-family="DIN Next, sans-serif" height="24"><tspan x="-60" y="198" text-anchor="middle" style="white-space:pre;">(3) send credentials</tspan></text></g></g><g><g><path d="M16.46606,302.69069l-37.26606,13.30931M-99.2,344l-33.4991,11.96396" fill="none" stroke="#2C88D9" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-127.92352,360.14213 -136.46606,357.30931 -131.65076,349.70583" fill="#2C88D9" stroke="#2C88D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="318" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="160" xml:space="preserve" x="-140" font-family="DIN Next, sans-serif" height="24"><tspan x="-60" y="336" text-anchor="middle" style="white-space:pre;">(4) authenticate user</tspan></text></g></g></svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/broadcast.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/broadcast.rst
new file mode 100644
index 0000000000..1acb372d4f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/broadcast.rst
@@ -0,0 +1,348 @@
+Broadcasting messages
+=====================
+
+.. currentmodule:: websockets
+
+
+.. admonition:: If you just want to send a message to all connected clients,
+ use :func:`broadcast`.
+ :class: tip
+
+ If you want to learn about its design in depth, continue reading this
+ document.
+
+WebSocket servers often send the same message to all connected clients or to a
+subset of clients for which the message is relevant.
+
+Let's explore options for broadcasting a message, explain the design
+of :func:`broadcast`, and discuss alternatives.
+
+For each option, we'll provide a connection handler called ``handler()`` and a
+function or coroutine called ``broadcast()`` that sends a message to all
+connected clients.
+
+Integrating them is left as an exercise for the reader. You could start with::
+
+ import asyncio
+ import websockets
+
+ async def handler(websocket):
+ ...
+
+ async def broadcast(message):
+ ...
+
+ async def broadcast_messages():
+ while True:
+ await asyncio.sleep(1)
+ message = ... # your application logic goes here
+ await broadcast(message)
+
+ async def main():
+ async with websockets.serve(handler, "localhost", 8765):
+ await broadcast_messages() # runs forever
+
+ if __name__ == "__main__":
+ asyncio.run(main())
+
+``broadcast_messages()`` must yield control to the event loop between each
+message, or else it will never let the server run. That's why it includes
+``await asyncio.sleep(1)``.
+
+A complete example is available in the `experiments/broadcast`_ directory.
+
+.. _experiments/broadcast: https://github.com/python-websockets/websockets/tree/main/experiments/broadcast
+
+The naive way
+-------------
+
+The most obvious way to send a message to all connected clients consists in
+keeping track of them and sending the message to each of them.
+
+Here's a connection handler that registers clients in a global variable::
+
+ CLIENTS = set()
+
+ async def handler(websocket):
+ CLIENTS.add(websocket)
+ try:
+ await websocket.wait_closed()
+ finally:
+ CLIENTS.remove(websocket)
+
+This implementation assumes that the client will never send any messages. If
+you'd rather not make this assumption, you can change::
+
+ await websocket.wait_closed()
+
+to::
+
+ async for _ in websocket:
+ pass
+
+Here's a coroutine that broadcasts a message to all clients::
+
+ async def broadcast(message):
+ for websocket in CLIENTS.copy():
+ try:
+ await websocket.send(message)
+ except websockets.ConnectionClosed:
+ pass
+
+There are two tricks in this version of ``broadcast()``.
+
+First, it makes a copy of ``CLIENTS`` before iterating it. Else, if a client
+connects or disconnects while ``broadcast()`` is running, the loop would fail
+with::
+
+ RuntimeError: Set changed size during iteration
+
+Second, it ignores :exc:`~exceptions.ConnectionClosed` exceptions because a
+client could disconnect between the moment ``broadcast()`` makes a copy of
+``CLIENTS`` and the moment it sends a message to this client. This is fine: a
+client that disconnected doesn't belongs to "all connected clients" anymore.
+
+The naive way can be very fast. Indeed, if all connections have enough free
+space in their write buffers, ``await websocket.send(message)`` writes the
+message and returns immediately, as it doesn't need to wait for the buffer to
+drain. In this case, ``broadcast()`` doesn't yield control to the event loop,
+which minimizes overhead.
+
+The naive way can also fail badly. If the write buffer of a connection reaches
+``write_limit``, ``broadcast()`` waits for the buffer to drain before sending
+the message to other clients. This can cause a massive drop in performance.
+
+As a consequence, this pattern works only when write buffers never fill up,
+which is usually outside of the control of the server.
+
+If you know for sure that you will never write more than ``write_limit`` bytes
+within ``ping_interval + ping_timeout``, then websockets will terminate slow
+connections before the write buffer has time to fill up.
+
+Don't set extreme ``write_limit``, ``ping_interval``, and ``ping_timeout``
+values to ensure that this condition holds. Set reasonable values and use the
+built-in :func:`broadcast` function instead.
+
+The concurrent way
+------------------
+
+The naive way didn't work well because it serialized writes, while the whole
+point of asynchronous I/O is to perform I/O concurrently.
+
+Let's modify ``broadcast()`` to send messages concurrently::
+
+ async def send(websocket, message):
+ try:
+ await websocket.send(message)
+ except websockets.ConnectionClosed:
+ pass
+
+ def broadcast(message):
+ for websocket in CLIENTS:
+ asyncio.create_task(send(websocket, message))
+
+We move the error handling logic in a new coroutine and we schedule
+a :class:`~asyncio.Task` to run it instead of executing it immediately.
+
+Since ``broadcast()`` no longer awaits coroutines, we can make it a function
+rather than a coroutine and do away with the copy of ``CLIENTS``.
+
+This version of ``broadcast()`` makes clients independent from one another: a
+slow client won't block others. As a side effect, it makes messages
+independent from one another.
+
+If you broadcast several messages, there is no strong guarantee that they will
+be sent in the expected order. Fortunately, the event loop runs tasks in the
+order in which they are created, so the order is correct in practice.
+
+Technically, this is an implementation detail of the event loop. However, it
+seems unlikely for an event loop to run tasks in an order other than FIFO.
+
+If you wanted to enforce the order without relying this implementation detail,
+you could be tempted to wait until all clients have received the message::
+
+ async def broadcast(message):
+ if CLIENTS: # asyncio.wait doesn't accept an empty list
+ await asyncio.wait([
+ asyncio.create_task(send(websocket, message))
+ for websocket in CLIENTS
+ ])
+
+However, this doesn't really work in practice. Quite often, it will block
+until the slowest client times out.
+
+Backpressure meets broadcast
+----------------------------
+
+At this point, it becomes apparent that backpressure, usually a good practice,
+doesn't work well when broadcasting a message to thousands of clients.
+
+When you're sending messages to a single client, you don't want to send them
+faster than the network can transfer them and the client accept them. This is
+why :meth:`~server.WebSocketServerProtocol.send` checks if the write buffer
+is full and, if it is, waits until it drain, giving the network and the
+client time to catch up. This provides backpressure.
+
+Without backpressure, you could pile up data in the write buffer until the
+server process runs out of memory and the operating system kills it.
+
+The :meth:`~server.WebSocketServerProtocol.send` API is designed to enforce
+backpressure by default. This helps users of websockets write robust programs
+even if they never heard about backpressure.
+
+For comparison, :class:`asyncio.StreamWriter` requires users to understand
+backpressure and to await :meth:`~asyncio.StreamWriter.drain` explicitly
+after each :meth:`~asyncio.StreamWriter.write`.
+
+When broadcasting messages, backpressure consists in slowing down all clients
+in an attempt to let the slowest client catch up. With thousands of clients,
+the slowest one is probably timing out and isn't going to receive the message
+anyway. So it doesn't make sense to synchronize with the slowest client.
+
+How do we avoid running out of memory when slow clients can't keep up with the
+broadcast rate, then? The most straightforward option is to disconnect them.
+
+If a client gets too far behind, eventually it reaches the limit defined by
+``ping_timeout`` and websockets terminates the connection. You can read the
+discussion of :doc:`keepalive and timeouts <./timeouts>` for details.
+
+How :func:`broadcast` works
+---------------------------
+
+The built-in :func:`broadcast` function is similar to the naive way. The main
+difference is that it doesn't apply backpressure.
+
+This provides the best performance by avoiding the overhead of scheduling and
+running one task per client.
+
+Also, when sending text messages, encoding to UTF-8 happens only once rather
+than once per client, providing a small performance gain.
+
+Per-client queues
+-----------------
+
+At this point, we deal with slow clients rather brutally: we disconnect then.
+
+Can we do better? For example, we could decide to skip or to batch messages,
+depending on how far behind a client is.
+
+To implement this logic, we can create a queue of messages for each client and
+run a task that gets messages from the queue and sends them to the client::
+
+ import asyncio
+
+ CLIENTS = set()
+
+ async def relay(queue, websocket):
+ while True:
+ # Implement custom logic based on queue.qsize() and
+ # websocket.transport.get_write_buffer_size() here.
+ message = await queue.get()
+ await websocket.send(message)
+
+ async def handler(websocket):
+ queue = asyncio.Queue()
+ relay_task = asyncio.create_task(relay(queue, websocket))
+ CLIENTS.add(queue)
+ try:
+ await websocket.wait_closed()
+ finally:
+ CLIENTS.remove(queue)
+ relay_task.cancel()
+
+Then we can broadcast a message by pushing it to all queues::
+
+ def broadcast(message):
+ for queue in CLIENTS:
+ queue.put_nowait(message)
+
+The queues provide an additional buffer between the ``broadcast()`` function
+and clients. This makes it easier to support slow clients without excessive
+memory usage because queued messages aren't duplicated to write buffers
+until ``relay()`` processes them.
+
+Publish–subscribe
+-----------------
+
+Can we avoid centralizing the list of connected clients in a global variable?
+
+If each client subscribes to a stream a messages, then broadcasting becomes as
+simple as publishing a message to the stream.
+
+Here's a message stream that supports multiple consumers::
+
+ class PubSub:
+ def __init__(self):
+ self.waiter = asyncio.Future()
+
+ def publish(self, value):
+ waiter, self.waiter = self.waiter, asyncio.Future()
+ waiter.set_result((value, self.waiter))
+
+ async def subscribe(self):
+ waiter = self.waiter
+ while True:
+ value, waiter = await waiter
+ yield value
+
+ __aiter__ = subscribe
+
+ PUBSUB = PubSub()
+
+The stream is implemented as a linked list of futures. It isn't necessary to
+synchronize consumers. They can read the stream at their own pace,
+independently from one another. Once all consumers read a message, there are
+no references left, therefore the garbage collector deletes it.
+
+The connection handler subscribes to the stream and sends messages::
+
+ async def handler(websocket):
+ async for message in PUBSUB:
+ await websocket.send(message)
+
+The broadcast function publishes to the stream::
+
+ def broadcast(message):
+ PUBSUB.publish(message)
+
+Like per-client queues, this version supports slow clients with limited memory
+usage. Unlike per-client queues, it makes it difficult to tell how far behind
+a client is. The ``PubSub`` class could be extended or refactored to provide
+this information.
+
+The ``for`` loop is gone from this version of the ``broadcast()`` function.
+However, there's still a ``for`` loop iterating on all clients hidden deep
+inside :mod:`asyncio`. When ``publish()`` sets the result of the ``waiter``
+future, :mod:`asyncio` loops on callbacks registered with this future and
+schedules them. This is how connection handlers receive the next value from
+the asynchronous iterator returned by ``subscribe()``.
+
+Performance considerations
+--------------------------
+
+The built-in :func:`broadcast` function sends all messages without yielding
+control to the event loop. So does the naive way when the network and clients
+are fast and reliable.
+
+For each client, a WebSocket frame is prepared and sent to the network. This
+is the minimum amount of work required to broadcast a message.
+
+It would be tempting to prepare a frame and reuse it for all connections.
+However, this isn't possible in general for two reasons:
+
+* Clients can negotiate different extensions. You would have to enforce the
+ same extensions with the same parameters. For example, you would have to
+ select some compression settings and reject clients that cannot support
+ these settings.
+
+* Extensions can be stateful, producing different encodings of the same
+ message depending on previous messages. For example, you would have to
+ disable context takeover to make compression stateless, resulting in poor
+ compression rates.
+
+All other patterns discussed above yield control to the event loop once per
+client because messages are sent by different tasks. This makes them slower
+than the built-in :func:`broadcast` function.
+
+There is no major difference between the performance of per-client queues and
+publish–subscribe.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/compression.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/compression.rst
new file mode 100644
index 0000000000..eaf99070db
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/compression.rst
@@ -0,0 +1,222 @@
+Compression
+===========
+
+.. currentmodule:: websockets.extensions.permessage_deflate
+
+Most WebSocket servers exchange JSON messages because they're convenient to
+parse and serialize in a browser. These messages contain text data and tend to
+be repetitive.
+
+This makes the stream of messages highly compressible. Enabling compression
+can reduce network traffic by more than 80%.
+
+There's a standard for compressing messages. :rfc:`7692` defines WebSocket
+Per-Message Deflate, a compression extension based on the Deflate_ algorithm.
+
+.. _Deflate: https://en.wikipedia.org/wiki/Deflate
+
+Configuring compression
+-----------------------
+
+:func:`~websockets.client.connect` and :func:`~websockets.server.serve` enable
+compression by default because the reduction in network bandwidth is usually
+worth the additional memory and CPU cost.
+
+If you want to disable compression, set ``compression=None``::
+
+ import websockets
+
+ websockets.connect(..., compression=None)
+
+ websockets.serve(..., compression=None)
+
+If you want to customize compression settings, you can enable the Per-Message
+Deflate extension explicitly with :class:`ClientPerMessageDeflateFactory` or
+:class:`ServerPerMessageDeflateFactory`::
+
+ import websockets
+ from websockets.extensions import permessage_deflate
+
+ websockets.connect(
+ ...,
+ extensions=[
+ permessage_deflate.ClientPerMessageDeflateFactory(
+ server_max_window_bits=11,
+ client_max_window_bits=11,
+ compress_settings={"memLevel": 4},
+ ),
+ ],
+ )
+
+ websockets.serve(
+ ...,
+ extensions=[
+ permessage_deflate.ServerPerMessageDeflateFactory(
+ server_max_window_bits=11,
+ client_max_window_bits=11,
+ compress_settings={"memLevel": 4},
+ ),
+ ],
+ )
+
+The Window Bits and Memory Level values in these examples reduce memory usage
+at the expense of compression rate.
+
+Compression settings
+--------------------
+
+When a client and a server enable the Per-Message Deflate extension, they
+negotiate two parameters to guarantee compatibility between compression and
+decompression. These parameters affect the trade-off between compression rate
+and memory usage for both sides.
+
+* **Context Takeover** means that the compression context is retained between
+ messages. In other words, compression is applied to the stream of messages
+ rather than to each message individually.
+
+ Context takeover should remain enabled to get good performance on
+ applications that send a stream of messages with similar structure,
+ that is, most applications.
+
+ This requires retaining the compression context and state between messages,
+ which increases the memory footprint of a connection.
+
+* **Window Bits** controls the size of the compression context. It must be
+ an integer between 9 (lowest memory usage) and 15 (best compression).
+ Setting it to 8 is possible but rejected by some versions of zlib.
+
+ On the server side, websockets defaults to 12. Specifically, the compression
+ window size (server to client) is always 12 while the decompression window
+ (client to server) size may be 12 or 15 depending on whether the client
+ supports configuring it.
+
+ On the client side, websockets lets the server pick a suitable value, which
+ has the same effect as defaulting to 15.
+
+:mod:`zlib` offers additional parameters for tuning compression. They control
+the trade-off between compression rate, memory usage, and CPU usage only for
+compressing. They're transparent for decompressing. Unless mentioned
+otherwise, websockets inherits defaults of :func:`~zlib.compressobj`.
+
+* **Memory Level** controls the size of the compression state. It must be an
+ integer between 1 (lowest memory usage) and 9 (best compression).
+
+ websockets defaults to 5. This is lower than zlib's default of 8. Not only
+ does a lower memory level reduce memory usage, but it can also increase
+ speed thanks to memory locality.
+
+* **Compression Level** controls the effort to optimize compression. It must
+ be an integer between 1 (lowest CPU usage) and 9 (best compression).
+
+* **Strategy** selects the compression strategy. The best choice depends on
+ the type of data being compressed.
+
+
+Tuning compression
+------------------
+
+For servers
+...........
+
+By default, websockets enables compression with conservative settings that
+optimize memory usage at the cost of a slightly worse compression rate:
+Window Bits = 12 and Memory Level = 5. This strikes a good balance for small
+messages that are typical of WebSocket servers.
+
+Here's how various compression settings affect memory usage of a single
+connection on a 64-bit system, as well a benchmark of compressed size and
+compression time for a corpus of small JSON documents.
+
+=========== ============ ============ ================ ================
+Window Bits Memory Level Memory usage Size vs. default Time vs. default
+=========== ============ ============ ================ ================
+15 8 322 KiB -4.0% +15%
+14 7 178 KiB -2.6% +10%
+13 6 106 KiB -1.4% +5%
+**12** **5** **70 KiB** **=** **=**
+11 4 52 KiB +3.7% -5%
+10 3 43 KiB +90% +50%
+9 2 39 KiB +160% +100%
+— — 19 KiB +452% —
+=========== ============ ============ ================ ================
+
+Window Bits and Memory Level don't have to move in lockstep. However, other
+combinations don't yield significantly better results than those shown above.
+
+Compressed size and compression time depend heavily on the kind of messages
+exchanged by the application so this example may not apply to your use case.
+
+You can adapt `compression/benchmark.py`_ by creating a list of typical
+messages and passing it to the ``_run`` function.
+
+Window Bits = 11 and Memory Level = 4 looks like the sweet spot in this table.
+
+websockets defaults to Window Bits = 12 and Memory Level = 5 to stay away from
+Window Bits = 10 or Memory Level = 3 where performance craters, raising doubts
+on what could happen at Window Bits = 11 and Memory Level = 4 on a different
+corpus.
+
+Defaults must be safe for all applications, hence a more conservative choice.
+
+.. _compression/benchmark.py: https://github.com/python-websockets/websockets/blob/main/experiments/compression/benchmark.py
+
+The benchmark focuses on compression because it's more expensive than
+decompression. Indeed, leaving aside small allocations, theoretical memory
+usage is:
+
+* ``(1 << (windowBits + 2)) + (1 << (memLevel + 9))`` for compression;
+* ``1 << windowBits`` for decompression.
+
+CPU usage is also higher for compression than decompression.
+
+While it's always possible for a server to use a smaller window size for
+compressing outgoing messages, using a smaller window size for decompressing
+incoming messages requires collaboration from clients.
+
+When a client doesn't support configuring the size of its compression window,
+websockets enables compression with the largest possible decompression window.
+In most use cases, this is more efficient than disabling compression both ways.
+
+If you are very sensitive to memory usage, you can reverse this behavior by
+setting the ``require_client_max_window_bits`` parameter of
+:class:`ServerPerMessageDeflateFactory` to ``True``.
+
+For clients
+...........
+
+By default, websockets enables compression with Memory Level = 5 but leaves
+the Window Bits setting up to the server.
+
+There's two good reasons and one bad reason for not optimizing the client side
+like the server side:
+
+1. If the maintainers of a server configured some optimized settings, we don't
+ want to override them with more restrictive settings.
+
+2. Optimizing memory usage doesn't matter very much for clients because it's
+ uncommon to open thousands of client connections in a program.
+
+3. On a more pragmatic note, some servers misbehave badly when a client
+ configures compression settings. `AWS API Gateway`_ is the worst offender.
+
+ .. _AWS API Gateway: https://github.com/python-websockets/websockets/issues/1065
+
+ Unfortunately, even though websockets is right and AWS is wrong, many users
+ jump to the conclusion that websockets doesn't work.
+
+ Until the ecosystem levels up, interoperability with buggy servers seems
+ more valuable than optimizing memory usage.
+
+
+Further reading
+---------------
+
+This `blog post by Ilya Grigorik`_ provides more details about how compression
+settings affect memory usage and how to optimize them.
+
+.. _blog post by Ilya Grigorik: https://www.igvita.com/2013/11/27/configuring-and-optimizing-websocket-compression/
+
+This `experiment by Peter Thorson`_ recommends Window Bits = 11 and Memory
+Level = 4 for optimizing memory usage.
+
+.. _experiment by Peter Thorson: https://mailarchive.ietf.org/arch/msg/hybi/F9t4uPufVEy8KBLuL36cZjCmM_Y/
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/data-flow.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/data-flow.svg
new file mode 100644
index 0000000000..22a198ed83
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/data-flow.svg
@@ -0,0 +1,63 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-382 -214 500 464" width="500px" height="464px"><defs><style>@font-face{
+ font-family:"DIN Next";
+ font-weight: 400;
+ src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix");
+ src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 400;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix");
+ src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg");
+}
+@font-face{
+ font-weight: 500;
+ font-family:"DIN Next";
+ src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix");
+ src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 500;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix");
+ src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg");
+}
+@font-face{
+ font-weight: 700;
+ font-family:"DIN Next";
+ src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix");
+ src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 700;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix");
+ src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg");
+}
+
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff');
+ font-weight: 400;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff');
+ font-style: italic;
+ font-weight: 400;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff');
+ font-weight: 700;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff');
+ font-style: italic;
+ font-weight: 700;
+}
+* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><rect x="-372" y="-12" width="480" height="72" rx="3" ry="3" fill="#f6d8dd"/><rect y="-12" rx="3" stroke="#D3455B" fill="none" stroke-linejoin="round" width="480" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="12" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="456" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-132" y="30" text-anchor="middle" style="white-space:pre;">Integration layer</tspan></text></g></g><g><g><rect x="-372" y="168" width="480" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="168" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="480" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="192" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="456" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-132" y="210" text-anchor="middle" style="white-space:pre;">Sans-I/O layer</tspan></text></g></g><g><g><rect x="-372" y="-192" width="216" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="-192" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="216" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="-168" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="192" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-264" y="-150" text-anchor="middle" style="white-space:pre;">Application</tspan></text></g></g><g><g><path d="M-324,-20v-20M-324,-92v-16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-329.54095,-104.9079 -324,-112 -318.45905,-104.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="79" xml:space="preserve" x="-363.5" font-family="DIN Next, sans-serif" height="48"><tspan x="-324" y="-72" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="-324" y="-48" text-anchor="middle" style="white-space:pre;">messages</tspan></text></g></g><g><g><path d="M-204,-112l0,20M-204,-40v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-198.45905,-27.0921 -204,-20 -209.54095,-27.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="79" xml:space="preserve" x="-243.5" font-family="DIN Next, sans-serif" height="48"><tspan x="-204" y="-72" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-204" y="-48" text-anchor="middle" style="white-space:pre;">messages</tspan></text></g></g><g><g><path d="M96.94345,-173.14258c4.53106,1.57532 8.08271,4.56716 9.84726,8.29515c1.76456,3.72799 1.59205,7.87526 -0.4783,11.4987c1.926,5.62898 0.07743,11.66356 -4.87418,15.91158c-4.95161,4.24802 -12.28578,6.09138 -19.33826,4.86046c-2.37232,7.09867 -9.03221,12.73751 -17.68526,14.97387c-8.65305,2.23636 -18.11659,0.76458 -25.13032,-3.90829c-8.39761,8.37233 -20.93327,13.315 -34.25257,13.5054c-13.3193,0.1904 -26.06267,-4.39089 -34.82012,-12.51799c-15.68591,7.18472 -35.58915,2.88148 -44.76245,-9.678c-7.38426,1.34031 -15.11657,-0.22307 -20.85552,-4.21672c-5.73895,-3.99365 -8.80965,-9.94793 -8.28226,-16.05983c-3.457,-2.81303 -4.97104,-6.82861 -4.04383,-10.72516c0.92721,-3.89655 4.17556,-7.16931 8.67598,-8.74119c1.08624,-6.50166 5.74572,-12.25693 12.67613,-15.65724c6.93042,-3.40031 15.38838,-4.08092 23.00992,-1.85161c7.11534,-8.80068 18.56556,-14.69575 31.42744,-16.1802c12.86188,-1.48445 25.89062,1.58538 35.76002,8.42577c6.64655,-4.34187 15.16482,-6.34654 23.65054,-5.56587c8.48572,0.78068 16.23125,4.28161 21.50507,9.72014c19.34894,-5.64384 40.71077,2.33211 47.97072,17.91102z" fill="#e3e6e9"/><path d="M96.94345,-173.14258c4.53106,1.57532 8.08271,4.56716 9.84726,8.29515c1.76456,3.72799 1.59205,7.87526 -0.4783,11.4987c1.926,5.62898 0.07743,11.66356 -4.87418,15.91158c-4.95161,4.24802 -12.28578,6.09138 -19.33826,4.86046c-2.37232,7.09867 -9.03221,12.73751 -17.68526,14.97387c-8.65305,2.23636 -18.11659,0.76458 -25.13032,-3.90829c-8.39761,8.37233 -20.93327,13.315 -34.25257,13.5054c-13.3193,0.1904 -26.06267,-4.39089 -34.82012,-12.51799c-15.68591,7.18472 -35.58915,2.88148 -44.76245,-9.678c-7.38426,1.34031 -15.11657,-0.22307 -20.85552,-4.21672c-5.73895,-3.99365 -8.80965,-9.94793 -8.28226,-16.05983c-3.457,-2.81303 -4.97104,-6.82861 -4.04383,-10.72516c0.92721,-3.89655 4.17556,-7.16931 8.67598,-8.74119c1.08624,-6.50166 5.74572,-12.25693 12.67613,-15.65724c6.93042,-3.40031 15.38838,-4.08092 23.00992,-1.85161c7.11534,-8.80068 18.56556,-14.69575 31.42744,-16.1802c12.86188,-1.48445 25.89062,1.58538 35.76002,8.42577c6.64655,-4.34187 15.16482,-6.34654 23.65054,-5.56587c8.48572,0.78068 16.23125,4.28161 21.50507,9.72014c19.34894,-5.64384 40.71077,2.33211 47.97072,17.91102z" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="-168" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="192" xml:space="preserve" x="-96" font-family="DIN Next, sans-serif" height="24"><tspan x="0" y="-150" text-anchor="middle" style="white-space:pre;">Network</tspan></text></g></g><g><g><path d="M-60,-20v-20M-60,-92v-15.53727" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-65.54095,-104.44518 -60,-111.53727 -54.45905,-104.44518" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="38" xml:space="preserve" x="-79" font-family="DIN Next, sans-serif" height="48"><tspan x="-60" y="-72" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-60" y="-48" text-anchor="middle" style="white-space:pre;">data</tspan></text></g></g><g><g><path d="M60,-108.78722v18.78722M60,-38v14" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="65.54095,-27.0921 60,-20 54.45905,-27.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-88" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="32" font-family="DIN Next, sans-serif" height="48"><tspan x="60" y="-70" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="60" y="-46" text-anchor="middle" style="white-space:pre;">data</tspan></text></g></g><g><g><path d="M60,68v20M60,140v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="65.54095,152.9079 60,160 54.45905,152.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="32" font-family="DIN Next, sans-serif" height="48"><tspan x="60" y="108" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="60" y="132" text-anchor="middle" style="white-space:pre;">bytes</tspan></text></g></g><g><g><path d="M-60,160v-20M-60,88v-16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-65.54095,75.0921 -60,68 -54.45905,75.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="42" xml:space="preserve" x="-81" font-family="DIN Next, sans-serif" height="48"><tspan x="-60" y="108" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-60" y="132" text-anchor="middle" style="white-space:pre;">bytes</tspan></text></g></g><g><g><path d="M-212,42" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-212,42 -212,42 -212,42" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><path d="M-204,68v20M-204,140v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-198.45905,152.9079 -204,160 -209.54095,152.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="52" xml:space="preserve" x="-230" font-family="DIN Next, sans-serif" height="48"><tspan x="-204" y="108" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-204" y="132" text-anchor="middle" style="white-space:pre;">events</tspan></text></g></g><g><g><path d="M-324,160l0,-18M-324,90v-14" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-329.54095,79.0921 -324,72 -318.45905,79.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="92" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="-352" font-family="DIN Next, sans-serif" height="48"><tspan x="-324" y="110" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="-324" y="134" text-anchor="middle" style="white-space:pre;">events</tspan></text></g></g></svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.rst
new file mode 100644
index 0000000000..2a1fe9a785
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.rst
@@ -0,0 +1,181 @@
+Deployment
+==========
+
+.. currentmodule:: websockets
+
+When you deploy your websockets server to production, at a high level, your
+architecture will almost certainly look like the following diagram:
+
+.. image:: deployment.svg
+
+The basic unit for scaling a websockets server is "one server process". Each
+blue box in the diagram represents one server process.
+
+There's more variation in routing. While the routing layer is shown as one big
+box, it is likely to involve several subsystems.
+
+When you design a deployment, your should consider two questions:
+
+1. How will I run the appropriate number of server processes?
+2. How will I route incoming connections to these processes?
+
+These questions are strongly related. There's a wide range of acceptable
+answers, depending on your goals and your constraints.
+
+You can find a few concrete examples in the :ref:`deployment how-to guides
+<deployment-howto>`.
+
+Running server processes
+------------------------
+
+How many processes do I need?
+.............................
+
+Typically, one server process will manage a few hundreds or thousands
+connections, depending on the frequency of messages and the amount of work
+they require.
+
+CPU and memory usage increase with the number of connections to the server.
+
+Often CPU is the limiting factor. If a server process goes to 100% CPU, then
+you reached the limit. How much headroom you want to keep is up to you.
+
+Once you know how many connections a server process can manage and how many
+connections you need to handle, you can calculate how many processes to run.
+
+You can also automate this calculation by configuring an autoscaler to keep
+CPU usage or connection count within acceptable limits.
+
+Don't scale with threads. Threads doesn't make sense for a server built with
+:mod:`asyncio`.
+
+How do I run processes?
+.......................
+
+Most solutions for running multiple instances of a server process fall into
+one of these three buckets:
+
+1. Running N processes on a platform:
+
+ * a Kubernetes Deployment
+
+ * its equivalent on a Platform as a Service provider
+
+2. Running N servers:
+
+ * an AWS Auto Scaling group, a GCP Managed instance group, etc.
+
+ * a fixed set of long-lived servers
+
+3. Running N processes on a server:
+
+ * preferably via a process manager or supervisor
+
+Option 1 is easiest of you have access to such a platform.
+
+Option 2 almost always combines with option 3.
+
+How do I start a process?
+.........................
+
+Run a Python program that invokes :func:`~server.serve`. That's it.
+
+Don't run an ASGI server such as Uvicorn, Hypercorn, or Daphne. They're
+alternatives to websockets, not complements.
+
+Don't run a WSGI server such as Gunicorn, Waitress, or mod_wsgi. They aren't
+designed to run WebSocket applications.
+
+Applications servers handle network connections and expose a Python API. You
+don't need one because websockets handles network connections directly.
+
+How do I stop a process?
+........................
+
+Process managers send the SIGTERM signal to terminate processes. Catch this
+signal and exit the server to ensure a graceful shutdown.
+
+Here's an example:
+
+.. literalinclude:: ../../example/faq/shutdown_server.py
+ :emphasize-lines: 12-15,18
+
+When exiting the context manager, :func:`~server.serve` closes all connections
+with code 1001 (going away). As a consequence:
+
+* If the connection handler is awaiting
+ :meth:`~server.WebSocketServerProtocol.recv`, it receives a
+ :exc:`~exceptions.ConnectionClosedOK` exception. It can catch the exception
+ and clean up before exiting.
+
+* Otherwise, it should be waiting on
+ :meth:`~server.WebSocketServerProtocol.wait_closed`, so it can receive the
+ :exc:`~exceptions.ConnectionClosedOK` exception and exit.
+
+This example is easily adapted to handle other signals.
+
+If you override the default signal handler for SIGINT, which raises
+:exc:`KeyboardInterrupt`, be aware that you won't be able to interrupt a
+program with Ctrl-C anymore when it's stuck in a loop.
+
+Routing connections
+-------------------
+
+What does routing involve?
+..........................
+
+Since the routing layer is directly exposed to the Internet, it should provide
+appropriate protection against threats ranging from Internet background noise
+to targeted attacks.
+
+You should always secure WebSocket connections with TLS. Since the routing
+layer carries the public domain name, it should terminate TLS connections.
+
+Finally, it must route connections to the server processes, balancing new
+connections across them.
+
+How do I route connections?
+...........................
+
+Here are typical solutions for load balancing, matched to ways of running
+processes:
+
+1. If you're running on a platform, it comes with a routing layer:
+
+ * a Kubernetes Ingress and Service
+
+ * a service mesh: Istio, Consul, Linkerd, etc.
+
+ * the routing mesh of a Platform as a Service
+
+2. If you're running N servers, you may load balance with:
+
+ * a cloud load balancer: AWS Elastic Load Balancing, GCP Cloud Load
+ Balancing, etc.
+
+ * A software load balancer: HAProxy, NGINX, etc.
+
+3. If you're running N processes on a server, you may load balance with:
+
+ * A software load balancer: HAProxy, NGINX, etc.
+
+ * The operating system — all processes listen on the same port
+
+You may trust the load balancer to handle encryption and to provide security.
+You may add another layer in front of the load balancer for these purposes.
+
+There are many possibilities. Don't add layers that you don't need, though.
+
+How do I implement a health check?
+..................................
+
+Load balancers need a way to check whether server processes are up and running
+to avoid routing connections to a non-functional backend.
+
+websockets provide minimal support for responding to HTTP requests with the
+:meth:`~server.WebSocketServerProtocol.process_request` hook.
+
+Here's an example:
+
+.. literalinclude:: ../../example/faq/health_check_server.py
+ :emphasize-lines: 7-9,18
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.svg
new file mode 100644
index 0000000000..cb948d8d6b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.svg
@@ -0,0 +1,63 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-490 -34 644 356" width="644px" height="356px"><defs><style>@font-face{
+ font-family:"DIN Next";
+ font-weight: 400;
+ src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix");
+ src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 400;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix");
+ src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg");
+}
+@font-face{
+ font-weight: 500;
+ font-family:"DIN Next";
+ src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix");
+ src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 500;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix");
+ src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg");
+}
+@font-face{
+ font-weight: 700;
+ font-family:"DIN Next";
+ src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix");
+ src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 700;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix");
+ src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg");
+}
+
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff');
+ font-weight: 400;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff');
+ font-style: italic;
+ font-weight: 400;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff');
+ font-weight: 700;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff');
+ font-style: italic;
+ font-weight: 700;
+}
+* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><path d="M132.40052,16.91125c8.20504,1.34238 13.19097,6.77278 11.13766,12.13055c-0.76086,3.6926 -4.09828,6.94687 -9.03771,8.81254c-4.93944,1.86567 -10.9238,2.13232 -16.20563,0.72209c-3.50792,6.79759 -12.12825,11.93728 -22.81409,13.60245c-10.68585,1.66516 -21.93194,-0.37877 -29.7633,-5.40937c-7.5447,11.32261 -23.72737,19.17936 -42.50646,20.63705c-18.77909,1.45769 -37.33189,-3.70278 -48.732,-13.55484c-11.00862,8.39145 -26.81112,13.46368 -43.71109,14.03023c-16.89997,0.56655 -33.41499,-3.42228 -45.68197,-11.03344c-12.09168,9.57066 -30.08677,15.12494 -49.08843,15.1514c-19.00166,0.02646 -37.03287,-5.47764 -49.18697,-15.01453c-32.22671,18.71922 -81.52408,17.01732 -110.54782,-3.81645c-9.61896,5.63808 -22.48701,8.30277 -35.32441,7.3149c-12.8374,-0.98787 -24.40407,-5.53286 -31.75198,-12.47659c-8.38039,4.12685 -19.07889,5.69428 -29.35634,4.30095c-10.27744,-1.39333 -19.13592,-5.61211 -24.30736,-11.5762c-6.03705,3.01428 -14.12058,3.62226 -21.04917,1.58316c-6.92858,-2.0391 -11.58448,-6.39632 -12.12376,-11.34603c-4.11078,-3.88947 -2.69127,-9.21224 3.19106,-11.96556c2.8597,-6.26181 10.13013,-11.25433 19.56423,-13.4345c9.43409,-2.18018 19.89581,-1.28549 28.15172,2.40755c4.56946,-5.60994 12.84561,-9.53066 22.43123,-10.62652c9.58562,-1.09585 19.40964,0.75561 26.62651,5.0181c13.21632,-10.89584 32.7134,-17.76363 53.91728,-18.99222c21.20388,-1.22858 42.24851,3.29017 58.19687,12.49616c11.75754,-9.7851 29.59785,-15.62692 48.64043,-15.92732c19.04258,-0.30041 37.29416,4.97204 49.76173,14.37498c12.30654,-11.80391 32.97339,-18.70238 54.83436,-18.30339c21.86097,0.39899 41.90535,8.04051 53.18277,20.27486c10.58585,-9.80304 28.36041,-15.18816 46.66019,-14.13654c18.29978,1.05162 34.36046,8.38113 42.16106,19.24077c7.63845,-6.15855 19.75125,-9.16677 31.72723,-7.87948c11.97598,1.2873 21.97263,6.67206 26.18437,14.10438c7.68618,-3.10406 17.06471,-3.86266 25.67861,-2.07706c8.6139,1.78561 15.60478,5.93747 19.14117,11.3679z" fill="#e3e6e9"/><path d="M132.40052,16.91125c8.20504,1.34238 13.19097,6.77278 11.13766,12.13055c-0.76086,3.6926 -4.09828,6.94687 -9.03771,8.81254c-4.93944,1.86567 -10.9238,2.13232 -16.20563,0.72209c-3.50792,6.79759 -12.12825,11.93728 -22.81409,13.60245c-10.68585,1.66516 -21.93194,-0.37877 -29.7633,-5.40937c-7.5447,11.32261 -23.72737,19.17936 -42.50646,20.63705c-18.77909,1.45769 -37.33189,-3.70278 -48.732,-13.55484c-11.00862,8.39145 -26.81112,13.46368 -43.71109,14.03023c-16.89997,0.56655 -33.41499,-3.42228 -45.68197,-11.03344c-12.09168,9.57066 -30.08677,15.12494 -49.08843,15.1514c-19.00166,0.02646 -37.03287,-5.47764 -49.18697,-15.01453c-32.22671,18.71922 -81.52408,17.01732 -110.54782,-3.81645c-9.61896,5.63808 -22.48701,8.30277 -35.32441,7.3149c-12.8374,-0.98787 -24.40407,-5.53286 -31.75198,-12.47659c-8.38039,4.12685 -19.07889,5.69428 -29.35634,4.30095c-10.27744,-1.39333 -19.13592,-5.61211 -24.30736,-11.5762c-6.03705,3.01428 -14.12058,3.62226 -21.04917,1.58316c-6.92858,-2.0391 -11.58448,-6.39632 -12.12376,-11.34603c-4.11078,-3.88947 -2.69127,-9.21224 3.19106,-11.96556c2.8597,-6.26181 10.13013,-11.25433 19.56423,-13.4345c9.43409,-2.18018 19.89581,-1.28549 28.15172,2.40755c4.56946,-5.60994 12.84561,-9.53066 22.43123,-10.62652c9.58562,-1.09585 19.40964,0.75561 26.62651,5.0181c13.21632,-10.89584 32.7134,-17.76363 53.91728,-18.99222c21.20388,-1.22858 42.24851,3.29017 58.19687,12.49616c11.75754,-9.7851 29.59785,-15.62692 48.64043,-15.92732c19.04258,-0.30041 37.29416,4.97204 49.76173,14.37498c12.30654,-11.80391 32.97339,-18.70238 54.83436,-18.30339c21.86097,0.39899 41.90535,8.04051 53.18277,20.27486c10.58585,-9.80304 28.36041,-15.18816 46.66019,-14.13654c18.29978,1.05162 34.36046,8.38113 42.16106,19.24077c7.63845,-6.15855 19.75125,-9.16677 31.72723,-7.87948c11.97598,1.2873 21.97263,6.67206 26.18437,14.10438c7.68618,-3.10406 17.06471,-3.86266 25.67861,-2.07706c8.6139,1.78561 15.60478,5.93747 19.14117,11.3679z" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="12" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="600" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-168" y="30" text-anchor="middle" style="white-space:pre;">Internet</tspan></text></g></g><g><g><rect x="-480" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="-480" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-408" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="-312" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="-312" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="-300" font-family="DIN Next, sans-serif" height="24"><tspan x="-240" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="0" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="0" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="12" font-family="DIN Next, sans-serif" height="24"><tspan x="72" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="-144" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-144" ry="1.5" height="36"/></g></g><g><g><rect x="-48" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-48" ry="1.5" height="36"/></g></g><g><g><rect x="-96" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-96" ry="1.5" height="36"/></g></g><g><g><rect x="-480" y="120" width="624" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="120" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="624" stroke-linecap="round" stroke-width="2" x="-480" ry="3" height="72"/><text y="144" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="600" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-168" y="162" text-anchor="middle" style="white-space:pre;">routing</tspan></text></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="199.99999999999997" stroke-linecap="round" stroke-width="4" x1="-240" y2="227.99999999999994" x2="-240"/><polygon points="-234.45905,224.9079 -240,232 -245.54095,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="199.99999999999997" stroke-linecap="round" stroke-width="4" x1="-407.99999999999994" y2="227.99999999999994" x2="-407.99999999999994"/><polygon points="-402.45905,224.9079 -408,232 -413.54095,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="200.00000000000003" stroke-linecap="round" stroke-width="4" x1="72" y2="228" x2="72"/><polygon points="77.54095,224.9079 72,232 66.45905,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="79.92376615249583" stroke-linecap="round" stroke-width="4" x1="-168" y2="107.99999999999994" x2="-168"/><polygon points="-162.45905,104.9079 -168,112 -173.54095,104.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g></svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst
new file mode 100644
index 0000000000..f164d29905
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst
@@ -0,0 +1,572 @@
+Design
+======
+
+.. currentmodule:: websockets
+
+This document describes the design of websockets. It assumes familiarity with
+the specification of the WebSocket protocol in :rfc:`6455`.
+
+It's primarily intended at maintainers. It may also be useful for users who
+wish to understand what happens under the hood.
+
+.. warning::
+
+ Internals described in this document may change at any time.
+
+ Backwards compatibility is only guaranteed for :doc:`public APIs
+ <../reference/index>`.
+
+Lifecycle
+---------
+
+State
+.....
+
+WebSocket connections go through a trivial state machine:
+
+- ``CONNECTING``: initial state,
+- ``OPEN``: when the opening handshake is complete,
+- ``CLOSING``: when the closing handshake is started,
+- ``CLOSED``: when the TCP connection is closed.
+
+Transitions happen in the following places:
+
+- ``CONNECTING -> OPEN``: in
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_open` which runs when
+ the :ref:`opening handshake <opening-handshake>` completes and the WebSocket
+ connection is established — not to be confused with
+ :meth:`~asyncio.BaseProtocol.connection_made` which runs when the TCP connection
+ is established;
+- ``OPEN -> CLOSING``: in
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.write_frame` immediately before
+ sending a close frame; since receiving a close frame triggers sending a
+ close frame, this does the right thing regardless of which side started the
+ :ref:`closing handshake <closing-handshake>`; also in
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` which duplicates
+ a few lines of code from ``write_close_frame()`` and ``write_frame()``;
+- ``* -> CLOSED``: in
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_lost` which is always
+ called exactly once when the TCP connection is closed.
+
+Coroutines
+..........
+
+The following diagram shows which coroutines are running at each stage of the
+connection lifecycle on the client side.
+
+.. image:: lifecycle.svg
+ :target: _images/lifecycle.svg
+
+The lifecycle is identical on the server side, except inversion of control
+makes the equivalent of :meth:`~client.connect` implicit.
+
+Coroutines shown in green are called by the application. Multiple coroutines
+may interact with the WebSocket connection concurrently.
+
+Coroutines shown in gray manage the connection. When the opening handshake
+succeeds, :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_open` starts
+two tasks:
+
+- :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` runs
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.transfer_data` which handles
+ incoming data and lets :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`
+ consume it. It may be canceled to terminate the connection. It never exits
+ with an exception other than :exc:`~asyncio.CancelledError`. See :ref:`data
+ transfer <data-transfer>` below.
+
+- :attr:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping_task` runs
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping` which sends Ping
+ frames at regular intervals and ensures that corresponding Pong frames are
+ received. It is canceled when the connection terminates. It never exits
+ with an exception other than :exc:`~asyncio.CancelledError`.
+
+- :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` runs
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.close_connection` which waits for
+ the data transfer to terminate, then takes care of closing the TCP
+ connection. It must not be canceled. It never exits with an exception. See
+ :ref:`connection termination <connection-termination>` below.
+
+Besides, :meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` starts
+the same :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` when
+the opening handshake fails, in order to close the TCP connection.
+
+Splitting the responsibilities between two tasks makes it easier to guarantee
+that websockets can terminate connections:
+
+- within a fixed timeout,
+- without leaking pending tasks,
+- without leaking open TCP connections,
+
+regardless of whether the connection terminates normally or abnormally.
+
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` completes when no
+more data will be received on the connection. Under normal circumstances, it
+exits after exchanging close frames.
+
+:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` completes when
+the TCP connection is closed.
+
+
+.. _opening-handshake:
+
+Opening handshake
+-----------------
+
+websockets performs the opening handshake when establishing a WebSocket
+connection. On the client side, :meth:`~client.connect` executes it
+before returning the protocol to the caller. On the server side, it's executed
+before passing the protocol to the ``ws_handler`` coroutine handling the
+connection.
+
+While the opening handshake is asymmetrical — the client sends an HTTP Upgrade
+request and the server replies with an HTTP Switching Protocols response —
+websockets aims at keeping the implementation of both sides consistent with
+one another.
+
+On the client side, :meth:`~client.WebSocketClientProtocol.handshake`:
+
+- builds an HTTP request based on the ``uri`` and parameters passed to
+ :meth:`~client.connect`;
+- writes the HTTP request to the network;
+- reads an HTTP response from the network;
+- checks the HTTP response, validates ``extensions`` and ``subprotocol``, and
+ configures the protocol accordingly;
+- moves to the ``OPEN`` state.
+
+On the server side, :meth:`~server.WebSocketServerProtocol.handshake`:
+
+- reads an HTTP request from the network;
+- calls :meth:`~server.WebSocketServerProtocol.process_request` which may
+ abort the WebSocket handshake and return an HTTP response instead; this
+ hook only makes sense on the server side;
+- checks the HTTP request, negotiates ``extensions`` and ``subprotocol``, and
+ configures the protocol accordingly;
+- builds an HTTP response based on the above and parameters passed to
+ :meth:`~server.serve`;
+- writes the HTTP response to the network;
+- moves to the ``OPEN`` state;
+- returns the ``path`` part of the ``uri``.
+
+The most significant asymmetry between the two sides of the opening handshake
+lies in the negotiation of extensions and, to a lesser extent, of the
+subprotocol. The server knows everything about both sides and decides what the
+parameters should be for the connection. The client merely applies them.
+
+If anything goes wrong during the opening handshake, websockets :ref:`fails
+the connection <connection-failure>`.
+
+
+.. _data-transfer:
+
+Data transfer
+-------------
+
+Symmetry
+........
+
+Once the opening handshake has completed, the WebSocket protocol enters the
+data transfer phase. This part is almost symmetrical. There are only two
+differences between a server and a client:
+
+- `client-to-server masking`_: the client masks outgoing frames; the server
+ unmasks incoming frames;
+- `closing the TCP connection`_: the server closes the connection immediately;
+ the client waits for the server to do it.
+
+.. _client-to-server masking: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.3
+.. _closing the TCP connection: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.1
+
+These differences are so minor that all the logic for `data framing`_, for
+`sending and receiving data`_ and for `closing the connection`_ is implemented
+in the same class, :class:`~legacy.protocol.WebSocketCommonProtocol`.
+
+.. _data framing: https://www.rfc-editor.org/rfc/rfc6455.html#section-5
+.. _sending and receiving data: https://www.rfc-editor.org/rfc/rfc6455.html#section-6
+.. _closing the connection: https://www.rfc-editor.org/rfc/rfc6455.html#section-7
+
+The :attr:`~legacy.protocol.WebSocketCommonProtocol.is_client` attribute tells which
+side a protocol instance is managing. This attribute is defined on the
+:attr:`~server.WebSocketServerProtocol` and
+:attr:`~client.WebSocketClientProtocol` classes.
+
+Data flow
+.........
+
+The following diagram shows how data flows between an application built on top
+of websockets and a remote endpoint. It applies regardless of which side is
+the server or the client.
+
+.. image:: protocol.svg
+ :target: _images/protocol.svg
+
+Public methods are shown in green, private methods in yellow, and buffers in
+orange. Methods related to connection termination are omitted; connection
+termination is discussed in another section below.
+
+Receiving data
+..............
+
+The left side of the diagram shows how websockets receives data.
+
+Incoming data is written to a :class:`~asyncio.StreamReader` in order to
+implement flow control and provide backpressure on the TCP connection.
+
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`, which is started
+when the WebSocket connection is established, processes this data.
+
+When it receives data frames, it reassembles fragments and puts the resulting
+messages in the :attr:`~legacy.protocol.WebSocketCommonProtocol.messages` queue.
+
+When it encounters a control frame:
+
+- if it's a close frame, it starts the closing handshake;
+- if it's a ping frame, it answers with a pong frame;
+- if it's a pong frame, it acknowledges the corresponding ping (unless it's an
+ unsolicited pong).
+
+Running this process in a task guarantees that control frames are processed
+promptly. Without such a task, websockets would depend on the application to
+drive the connection by having exactly one coroutine awaiting
+:meth:`~legacy.protocol.WebSocketCommonProtocol.recv` at any time. While this
+happens naturally in many use cases, it cannot be relied upon.
+
+Then :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` fetches the next message
+from the :attr:`~legacy.protocol.WebSocketCommonProtocol.messages` queue, with some
+complexity added for handling backpressure and termination correctly.
+
+Sending data
+............
+
+The right side of the diagram shows how websockets sends data.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.send` writes one or several data
+frames containing the message. While sending a fragmented message, concurrent
+calls to :meth:`~legacy.protocol.WebSocketCommonProtocol.send` are put on hold until
+all fragments are sent. This makes concurrent calls safe.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.ping` writes a ping frame and
+yields a :class:`~asyncio.Future` which will be completed when a matching pong
+frame is received.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.pong` writes a pong frame.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.close` writes a close frame and
+waits for the TCP connection to terminate.
+
+Outgoing data is written to a :class:`~asyncio.StreamWriter` in order to
+implement flow control and provide backpressure from the TCP connection.
+
+.. _closing-handshake:
+
+Closing handshake
+.................
+
+When the other side of the connection initiates the closing handshake,
+:meth:`~legacy.protocol.WebSocketCommonProtocol.read_message` receives a close
+frame while in the ``OPEN`` state. It moves to the ``CLOSING`` state, sends a
+close frame, and returns :obj:`None`, causing
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate.
+
+When this side of the connection initiates the closing handshake with
+:meth:`~legacy.protocol.WebSocketCommonProtocol.close`, it moves to the ``CLOSING``
+state and sends a close frame. When the other side sends a close frame,
+:meth:`~legacy.protocol.WebSocketCommonProtocol.read_message` receives it in the
+``CLOSING`` state and returns :obj:`None`, also causing
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate.
+
+If the other side doesn't send a close frame within the connection's close
+timeout, websockets :ref:`fails the connection <connection-failure>`.
+
+The closing handshake can take up to ``2 * close_timeout``: one
+``close_timeout`` to write a close frame and one ``close_timeout`` to receive
+a close frame.
+
+Then websockets terminates the TCP connection.
+
+
+.. _connection-termination:
+
+Connection termination
+----------------------
+
+:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`, which is
+started when the WebSocket connection is established, is responsible for
+eventually closing the TCP connection.
+
+First :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` waits
+for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate,
+which may happen as a result of:
+
+- a successful closing handshake: as explained above, this exits the infinite
+ loop in :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`;
+- a timeout while waiting for the closing handshake to complete: this cancels
+ :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`;
+- a protocol error, including connection errors: depending on the exception,
+ :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` :ref:`fails the
+ connection <connection-failure>` with a suitable code and exits.
+
+:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` is separate
+from :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to make it
+easier to implement the timeout on the closing handshake. Canceling
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` creates no risk
+of canceling :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`
+and failing to close the TCP connection, thus leaking resources.
+
+Then :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` cancels
+:meth:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping`. This task has no
+protocol compliance responsibilities. Terminating it to avoid leaking it is
+the only concern.
+
+Terminating the TCP connection can take up to ``2 * close_timeout`` on the
+server side and ``3 * close_timeout`` on the client side. Clients start by
+waiting for the server to close the connection, hence the extra
+``close_timeout``. Then both sides go through the following steps until the
+TCP connection is lost: half-closing the connection (only for non-TLS
+connections), closing the connection, aborting the connection. At this point
+the connection drops regardless of what happens on the network.
+
+
+.. _connection-failure:
+
+Connection failure
+------------------
+
+If the opening handshake doesn't complete successfully, websockets fails the
+connection by closing the TCP connection.
+
+Once the opening handshake has completed, websockets fails the connection by
+canceling :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`
+and sending a close frame if appropriate.
+
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` exits, unblocking
+:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`, which closes
+the TCP connection.
+
+
+.. _server-shutdown:
+
+Server shutdown
+---------------
+
+:class:`~websockets.server.WebSocketServer` closes asynchronously like
+:class:`asyncio.Server`. The shutdown happen in two steps:
+
+1. Stop listening and accepting new connections;
+2. Close established connections with close code 1001 (going away) or, if
+ the opening handshake is still in progress, with HTTP status code 503
+ (Service Unavailable).
+
+The first call to :class:`~websockets.server.WebSocketServer.close` starts a
+task that performs this sequence. Further calls are ignored. This is the
+easiest way to make :class:`~websockets.server.WebSocketServer.close` and
+:class:`~websockets.server.WebSocketServer.wait_closed` idempotent.
+
+
+.. _cancellation:
+
+Cancellation
+------------
+
+User code
+.........
+
+websockets provides a WebSocket application server. It manages connections and
+passes them to user-provided connection handlers. This is an *inversion of
+control* scenario: library code calls user code.
+
+If a connection drops, the corresponding handler should terminate. If the
+server shuts down, all connection handlers must terminate. Canceling
+connection handlers would terminate them.
+
+However, using cancellation for this purpose would require all connection
+handlers to handle it properly. For example, if a connection handler starts
+some tasks, it should catch :exc:`~asyncio.CancelledError`, terminate or
+cancel these tasks, and then re-raise the exception.
+
+Cancellation is tricky in :mod:`asyncio` applications, especially when it
+interacts with finalization logic. In the example above, what if a handler
+gets interrupted with :exc:`~asyncio.CancelledError` while it's finalizing
+the tasks it started, after detecting that the connection dropped?
+
+websockets considers that cancellation may only be triggered by the caller of
+a coroutine when it doesn't care about the results of that coroutine anymore.
+(Source: `Guido van Rossum <https://groups.google.com/forum/#!msg
+/python-tulip/LZQe38CR3bg/7qZ1p_q5yycJ>`_). Since connection handlers run
+arbitrary user code, websockets has no way of deciding whether that code is
+still doing something worth caring about.
+
+For these reasons, websockets never cancels connection handlers. Instead it
+expects them to detect when the connection is closed, execute finalization
+logic if needed, and exit.
+
+Conversely, cancellation isn't a concern for WebSocket clients because they
+don't involve inversion of control.
+
+Library
+.......
+
+Most :doc:`public APIs <../reference/index>` of websockets are coroutines.
+They may be canceled, for example if the user starts a task that calls these
+coroutines and cancels the task later. websockets must handle this situation.
+
+Cancellation during the opening handshake is handled like any other exception:
+the TCP connection is closed and the exception is re-raised. This can only
+happen on the client side. On the server side, the opening handshake is
+managed by websockets and nothing results in a cancellation.
+
+Once the WebSocket connection is established, internal tasks
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` and
+:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` mustn't get
+accidentally canceled if a coroutine that awaits them is canceled. In other
+words, they must be shielded from cancellation.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.recv` waits for the next message in
+the queue or for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`
+to terminate, whichever comes first. It relies on :func:`~asyncio.wait` for
+waiting on two futures in parallel. As a consequence, even though it's waiting
+on a :class:`~asyncio.Future` signaling the next message and on
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`, it doesn't
+propagate cancellation to them.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.ensure_open` is called by
+:meth:`~legacy.protocol.WebSocketCommonProtocol.send`,
+:meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and
+:meth:`~legacy.protocol.WebSocketCommonProtocol.pong`. When the connection state is
+``CLOSING``, it waits for
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` but shields it to
+prevent cancellation.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.close` waits for the data transfer
+task to terminate with :func:`~asyncio.timeout`. If it's canceled or if the
+timeout elapses, :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`
+is canceled, which is correct at this point.
+:meth:`~legacy.protocol.WebSocketCommonProtocol.close` then waits for
+:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` but shields it
+to prevent cancellation.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.close` and
+:meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` are the only
+places where :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` may
+be canceled.
+
+:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` starts by
+waiting for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`. It
+catches :exc:`~asyncio.CancelledError` to prevent a cancellation of
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` from propagating
+to :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`.
+
+.. _backpressure:
+
+Backpressure
+------------
+
+.. note::
+
+ This section discusses backpressure from the perspective of a server but
+ the concept applies to clients symmetrically.
+
+With a naive implementation, if a server receives inputs faster than it can
+process them, or if it generates outputs faster than it can send them, data
+accumulates in buffers, eventually causing the server to run out of memory and
+crash.
+
+The solution to this problem is backpressure. Any part of the server that
+receives inputs faster than it can process them and send the outputs
+must propagate that information back to the previous part in the chain.
+
+websockets is designed to make it easy to get backpressure right.
+
+For incoming data, websockets builds upon :class:`~asyncio.StreamReader` which
+propagates backpressure to its own buffer and to the TCP stream. Frames are
+parsed from the input stream and added to a bounded queue. If the queue fills
+up, parsing halts until the application reads a frame.
+
+For outgoing data, websockets builds upon :class:`~asyncio.StreamWriter` which
+implements flow control. If the output buffers grow too large, it waits until
+they're drained. That's why all APIs that write frames are asynchronous.
+
+Of course, it's still possible for an application to create its own unbounded
+buffers and break the backpressure. Be careful with queues.
+
+
+.. _buffers:
+
+Buffers
+-------
+
+.. note::
+
+ This section discusses buffers from the perspective of a server but it
+ applies to clients as well.
+
+An asynchronous systems works best when its buffers are almost always empty.
+
+For example, if a client sends data too fast for a server, the queue of
+incoming messages will be constantly full. The server will always be 32
+messages (by default) behind the client. This consumes memory and increases
+latency for no good reason. The problem is called bufferbloat.
+
+If buffers are almost always full and that problem cannot be solved by adding
+capacity — typically because the system is bottlenecked by the output and
+constantly regulated by backpressure — reducing the size of buffers minimizes
+negative consequences.
+
+By default websockets has rather high limits. You can decrease them according
+to your application's characteristics.
+
+Bufferbloat can happen at every level in the stack where there is a buffer.
+For each connection, the receiving side contains these buffers:
+
+- OS buffers: tuning them is an advanced optimization.
+- :class:`~asyncio.StreamReader` bytes buffer: the default limit is 64 KiB.
+ You can set another limit by passing a ``read_limit`` keyword argument to
+ :func:`~client.connect()` or :func:`~server.serve`.
+- Incoming messages :class:`~collections.deque`: its size depends both on
+ the size and the number of messages it contains. By default the maximum
+ UTF-8 encoded size is 1 MiB and the maximum number is 32. In the worst case,
+ after UTF-8 decoding, a single message could take up to 4 MiB of memory and
+ the overall memory consumption could reach 128 MiB. You should adjust these
+ limits by setting the ``max_size`` and ``max_queue`` keyword arguments of
+ :func:`~client.connect()` or :func:`~server.serve` according to your
+ application's requirements.
+
+For each connection, the sending side contains these buffers:
+
+- :class:`~asyncio.StreamWriter` bytes buffer: the default size is 64 KiB.
+ You can set another limit by passing a ``write_limit`` keyword argument to
+ :func:`~client.connect()` or :func:`~server.serve`.
+- OS buffers: tuning them is an advanced optimization.
+
+Concurrency
+-----------
+
+Awaiting any combination of :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`,
+:meth:`~legacy.protocol.WebSocketCommonProtocol.send`,
+:meth:`~legacy.protocol.WebSocketCommonProtocol.close`
+:meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, or
+:meth:`~legacy.protocol.WebSocketCommonProtocol.pong` concurrently is safe, including
+multiple calls to the same method, with one exception and one limitation.
+
+* **Only one coroutine can receive messages at a time.** This constraint
+ avoids non-deterministic behavior (and simplifies the implementation). If a
+ coroutine is awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`,
+ awaiting it again in another coroutine raises :exc:`RuntimeError`.
+
+* **Sending a fragmented message forces serialization.** Indeed, the WebSocket
+ protocol doesn't support multiplexing messages. If a coroutine is awaiting
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to send a fragmented message,
+ awaiting it again in another coroutine waits until the first call completes.
+ This will be transparent in many cases. It may be a concern if the
+ fragmented message is generated slowly by an asynchronous iterator.
+
+Receiving frames is independent from sending frames. This isolates
+:meth:`~legacy.protocol.WebSocketCommonProtocol.recv`, which receives frames, from
+the other methods, which send frames.
+
+While the connection is open, each frame is sent with a single write. Combined
+with the concurrency model of :mod:`asyncio`, this enforces serialization. The
+only other requirement is to prevent interleaving other data frames in the
+middle of a fragmented message.
+
+After the connection is closed, sending a frame raises
+:exc:`~websockets.exceptions.ConnectionClosed`, which is safe.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/index.rst
new file mode 100644
index 0000000000..120a3dd327
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/index.rst
@@ -0,0 +1,18 @@
+Topic guides
+============
+
+Get a deeper understanding of how websockets is built and why.
+
+.. toctree::
+ :titlesonly:
+
+ deployment
+ logging
+ authentication
+ broadcast
+ compression
+ timeouts
+ design
+ memory
+ security
+ performance
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle
new file mode 100644
index 0000000000..bd888dcf31
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle
@@ -0,0 +1,25 @@
+��][W�8~~���}�G�%��a�.�0KC��N�����J���i�i���ߒs�5n����cUI咾������}틌b/ ~Y�:Z�d��� ~Y}��u�Z�����?v����j#ߋ����������j�G#_�Z;���`��� �Vk�pU[=O�ћV���R���CE���p$���2[�M�U(f�{A��zN����ƅ��l;��E�W2�\�u���B�$r �M�њ]�X�lϱ��t��Ɏ"[]��'<�&H����D�x�������}�7ZS�5�B'�OL�9ɑl�fYOD'a��s'�h,[��-۹PE.7:��9��٧d�"������
+�i�zM�&_�L,���*�mߎ������2K,�;�L�$�� Y'��eF9�x�Y�Yj.y��ٗ�X�`�Gyq@�sE̮���c�1 �K��m;�b�G�7�0Se��|Ef�/Y��k����a���AP� WeJ�;#ہ,�����M��"=�0K��V�0c�?�m��IE<�����:��o(~cp�����*�0*󷯆2��=@�U�*��x�ȷ�:��W��¡9C�m~]��WϗݫQ����i�f�:� �2��%�N�w��M
+�����<^ k`2}�0H��~���+��F�'�/2�T�s��\kS��*WO7��<&&{�v��s��(�yF[x�������J�%&Eq­���Ց�jXX:% ƅ!���f`�[#�-D-$�y]�݌���&���)Lr;wf�kM[E��톗yMe��m�}���pV�V�ݎ���a�fu1M͗3a=�۹*��m?NҜ�k{���Q]�<79/POZ��rͶ�l�f����`�=�LkM#��[Y&�%8��*ܤ��s�X)������a��0Ma0
+6�2�B�JKb@i���i"!8����u�;��H�����py2:��ǝwݥ��nz=(�P�������E����������֫�w�b�����QlC�)&$g��n�n1*A�Ж���{sF��,؊��٤��� O����7�X�b� Ģ&C�04�ʽ�ۻ�0
+dtb��8�� �Bp�X ��h��&Q~M7��K��%*�]�%Sd/J��g���8�&���� m��8����g�#���K����>����+���w��(� ���d}+��wݟ���Q`��{�t�+�����8���)տz���\����~odGn/��8�_�Iz�:Bӓ���0RC�$��$ �$��N�$Pc(�׋;G�����78OdзU�C++�1�z��P����v!����/��Tx}[��
+���:���GQr�����H�� �O��g�=u�i�V�})3tF
+Q
+� ��%«��c/�u�gO���O�t�>^ǂ�����.�͏a�wLę���R�D��#F����I��@J���dfE>��s�X����t�(� �C0�"��V)8���aA�+h��kL˄��&LsMc\�b�b!L�xAn��D�z`��D|W��'�OH���"Ne�ځ��#�,np��֒�ӗQo��p��������v�Cӹ�w�'Øb�#bppO ��2����
+�Y�c�YF:�����j`������ �:�A�V
+� �nzD܂S�8�X�B��qk6�mp��q�d�0O�ʦ^L��s����*���7��h�=�0�i>'a�k(�қ�kZE�;��rKW>àG�֌$�ʑ�����2lQ]�hnRaLM�b�*_�|�E���e��(*d��2�����%4�c�sv���z����)���H:_�����ӊR���4���4��ҁ/�=b��b�
+u�`����]�jʔ���]
+n\�]]
+rW�WӸ�׸�b:������R�fg��
+E:f&5-�H�ݯ�7���
+Ͳ��[4��zՑ����-r��O�o��&�P�u�b���~-�9�H+���P�z�1�[��5�7��q=��iV��t5[��I-{���{�[^ۂ�C�'M��.sS��U�_��o�{O��I������O���{#ǰt�N̤�p<��rj��4�yA���ENi�eYwANw��9/9�z̽�GNw��97��Vq",A *M�~����N��"���}!������,��Ŀ�a��&��:�;�8�Zywy9�>��Z������/���o��1] �0��Fj�nh�ԗ�ԣ���DŽ)�q��9�T��
+N_�G=:<���.�TK�W��<7?ZSܓZ���-����V�YWy����0�e�w�Q�n���e��*{Y��۩la� �Q�
+��/n'�v�2>���&.����{g�}�KŤ#!��@ƕ���[��#܍��E��R��aq7����dDEm�ߺ�z�,�ɭ�����S�h�J~վ�Z��i���#���8����.j� m8N7�v�
+s�k/$%u�� �y�Nn���X�Uj�)mGv��K�5KRz?s7xAU7L*�jOo5{ �An�jV�l)xOD���R�)�L.Ñ�}l ��"�T�6]u���9��UT��?GŻ����O,�F�ZG�:fo{c��m�'�;�~&r�':�$�`C&�/��2M[PM��
+͋qs��
+�$����V����Y�}?��"����Y�I {a���^W��;��T��ޭ-h���������#��-���^ئ�o����=o��G �}��w���=z�?����mH��e�.��������a��� 4�~�/���,���'Q�2*7� 1mT��5�ON����/����ZLa���4���o��7�aj�5S��[
+U��D����H���?QA�S�7=E�E��V�Q ��Xw�H�=
+���m�Ix��G3����������w��j�&�(*�!ԩ�[��n��%���Yv��;���c��5ox����
+/�I����f5�^]�Ŧ|{E��T��8�&Lߝ��>MT���2�;��
+�)�*}���)�k�4k�j�z���l��y����<r���̾-�8��9[T����o��I��R d��i�O�`��J�T���L�±���\Ө)��p���ɩ�Ŝ��Х�k�F��w
+�Β���01�٭�����ס6W�� �Ǵj
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.svg
new file mode 100644
index 0000000000..0a9818d293
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="-14.3464565 112.653543 624.6929 372.69291" width="624.6929pt" height="372.69291pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.6.2 <dc:date>2018-07-29 15:25:34 +0000</dc:date></metadata><defs><font-face font-family="Courier New" font-size="12" panose-1="2 7 6 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="100.097656" slope="0" x-height="443.35938" cap-height="591.79688" ascent="832.51953" descent="-300.29297" font-weight="bold"><font-face-src><font-face-name name="CourierNewPS-BoldMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.8666667 0 L 0 0 M 0 -2.2 L 5.8666667 0 L 0 2.2" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Verdana" font-size="12" panose-1="2 11 6 4 3 5 4 4 2 4" units-per-em="1000" underline-position="-87.890625" underline-thickness="58.59375" slope="0" x-height="545.41016" cap-height="727.0508" ascent="1005.3711" descent="-209.96094" font-weight="500"><font-face-src><font-face-name name="Verdana"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><text transform="translate(19.173228 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="1.5138254" y="10" textLength="72.01172">CONNECTING</tspan></text><text transform="translate(160.90551 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="23.117341" y="10" textLength="28.804688">OPEN</tspan></text><text transform="translate(359.3307 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="12.315583" y="10" textLength="50.408203">CLOSING</tspan></text><text transform="translate(501.063 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="15.916169" y="10" textLength="43.20703">CLOSED</tspan></text><line x1="198.4252" y1="170.07874" x2="198.4252" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="396.8504" y1="170.07874" x2="396.8504" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="538.58267" y1="170.07874" x2="538.58267" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="56.692913" y1="170.07874" x2="56.692913" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><path d="M 240.94488 240.94488 L 411.02362 240.94488 C 418.85128 240.94488 425.19685 247.29045 425.19685 255.11811 L 425.19685 255.11811 C 425.19685 262.94577 418.85128 269.29134 411.02362 269.29134 L 240.94488 269.29134 C 233.11722 269.29134 226.77165 262.94577 226.77165 255.11811 L 226.77165 255.11811 C 226.77165 247.29045 233.11722 240.94488 240.94488 240.94488 Z" fill="#dadada"/><path d="M 240.94488 240.94488 L 411.02362 240.94488 C 418.85128 240.94488 425.19685 247.29045 425.19685 255.11811 L 425.19685 255.11811 C 425.19685 262.94577 418.85128 269.29134 411.02362 269.29134 L 240.94488 269.29134 C 233.11722 269.29134 226.77165 262.94577 226.77165 255.11811 L 226.77165 255.11811 C 226.77165 247.29045 233.11722 240.94488 240.94488 240.94488 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(226.77165 248.11811)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="52.40498" y="10" textLength="93.615234">transfer_data</tspan></text><path d="M 240.94488 354.3307 L 552.7559 354.3307 C 560.58356 354.3307 566.92913 360.67628 566.92913 368.50393 L 566.92913 368.50393 C 566.92913 376.3316 560.58356 382.67716 552.7559 382.67716 L 240.94488 382.67716 C 233.11722 382.67716 226.77165 376.3316 226.77165 368.50393 L 226.77165 368.50393 C 226.77165 360.67628 233.11722 354.3307 240.94488 354.3307 Z" fill="#dadada"/><path d="M 240.94488 354.3307 L 552.7559 354.3307 C 560.58356 354.3307 566.92913 360.67628 566.92913 368.50393 L 566.92913 368.50393 C 566.92913 376.3316 560.58356 382.67716 552.7559 382.67716 L 240.94488 382.67716 C 233.11722 382.67716 226.77165 376.3316 226.77165 368.50393 L 226.77165 368.50393 C 226.77165 360.67628 233.11722 354.3307 240.94488 354.3307 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(231.77165 361.50393)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="107.469364" y="10" textLength="115.21875">close_connection</tspan></text><path d="M 99.2126 184.25197 L 155.90551 184.25197 C 163.73317 184.25197 170.07874 190.59754 170.07874 198.4252 L 170.07874 198.4252 C 170.07874 206.25285 163.73317 212.59842 155.90551 212.59842 L 99.2126 212.59842 C 91.38494 212.59842 85.03937 206.25285 85.03937 198.4252 L 85.03937 198.4252 C 85.03937 190.59754 91.38494 184.25197 99.2126 184.25197 Z" fill="#6f6"/><path d="M 99.2126 184.25197 L 155.90551 184.25197 C 163.73317 184.25197 170.07874 190.59754 170.07874 198.4252 L 170.07874 198.4252 C 170.07874 206.25285 163.73317 212.59842 155.90551 212.59842 L 99.2126 212.59842 C 91.38494 212.59842 85.03937 206.25285 85.03937 198.4252 L 85.03937 198.4252 C 85.03937 190.59754 91.38494 184.25197 99.2126 184.25197 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 191.4252)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="12.315583" y="10" textLength="50.408203">connect</tspan></text><path d="M 240.94488 184.25197 L 496.063 184.25197 C 503.89065 184.25197 510.23622 190.59754 510.23622 198.4252 L 510.23622 198.4252 C 510.23622 206.25285 503.89065 212.59842 496.063 212.59842 L 240.94488 212.59842 C 233.11722 212.59842 226.77165 206.25285 226.77165 198.4252 L 226.77165 198.4252 C 226.77165 190.59754 233.11722 184.25197 240.94488 184.25197 Z" fill="#6f6"/><path d="M 240.94488 184.25197 L 496.063 184.25197 C 503.89065 184.25197 510.23622 190.59754 510.23622 198.4252 L 510.23622 198.4252 C 510.23622 206.25285 503.89065 212.59842 496.063 212.59842 L 240.94488 212.59842 C 233.11722 212.59842 226.77165 206.25285 226.77165 198.4252 L 226.77165 198.4252 C 226.77165 190.59754 233.11722 184.25197 240.94488 184.25197 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(231.77165 191.4252)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="17.912947" y="10" textLength="100.816406">recv / send / </tspan><tspan font-family="Courier New" font-size="12" font-weight="500" x="118.72935" y="10" textLength="93.615234">ping / pong /</tspan><tspan font-family="Courier New" font-size="12" font-weight="bold" x="212.34459" y="10" textLength="50.408203"> close </tspan></text><path d="M 170.07874 198.4252 L 183.97874 198.4252 L 198.4252 198.4252 L 198.4252 283.46457 L 198.4252 368.50393 L 212.87165 368.50393 L 215.37165 368.50393" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(75.86614 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="27.760296" y="12" textLength="52.083984">opening </tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="19.164593" y="27" textLength="58.02539">handshak</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="77.072796" y="27" textLength="7.1484375">e</tspan></text><text transform="translate(416.02362 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="19.182171" y="12" textLength="65.021484">connection</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="16.861858" y="27" textLength="69.66211">termination</tspan></text><text transform="translate(217.59842 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="41.03058" y="12" textLength="40.6875">data tr</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="81.507143" y="12" textLength="37.541016">ansfer</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="18.211245" y="27" textLength="116.625">&amp; closing handshak</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="134.71906" y="27" textLength="7.1484375">e</tspan></text><path d="M 425.19685 255.11811 L 439.09685 255.11811 L 453.5433 255.11811 L 453.5433 342.9307" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 240.94488 297.6378 L 411.02362 297.6378 C 418.85128 297.6378 425.19685 303.98336 425.19685 311.81102 L 425.19685 311.81102 C 425.19685 319.63868 418.85128 325.98425 411.02362 325.98425 L 240.94488 325.98425 C 233.11722 325.98425 226.77165 319.63868 226.77165 311.81102 L 226.77165 311.81102 C 226.77165 303.98336 233.11722 297.6378 240.94488 297.6378 Z" fill="#dadada"/><path d="M 240.94488 297.6378 L 411.02362 297.6378 C 418.85128 297.6378 425.19685 303.98336 425.19685 311.81102 L 425.19685 311.81102 C 425.19685 319.63868 418.85128 325.98425 411.02362 325.98425 L 240.94488 325.98425 C 233.11722 325.98425 226.77165 319.63868 226.77165 311.81102 L 226.77165 311.81102 C 226.77165 303.98336 233.11722 297.6378 240.94488 297.6378 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(226.77165 304.81102)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="48.804395" y="10" textLength="100.816406">keepalive_ping</tspan></text><line x1="198.4252" y1="255.11811" x2="214.62165" y2="255.11811" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="198.4252" y1="311.81102" x2="215.37165" y2="311.81102" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/></g></g></svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/logging.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/logging.rst
new file mode 100644
index 0000000000..e7abd96ce5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/logging.rst
@@ -0,0 +1,245 @@
+Logging
+=======
+
+.. currentmodule:: websockets
+
+Logs contents
+-------------
+
+When you run a WebSocket client, your code calls coroutines provided by
+websockets.
+
+If an error occurs, websockets tells you by raising an exception. For example,
+it raises a :exc:`~exceptions.ConnectionClosed` exception if the other side
+closes the connection.
+
+When you run a WebSocket server, websockets accepts connections, performs the
+opening handshake, runs the connection handler coroutine that you provided,
+and performs the closing handshake.
+
+Given this `inversion of control`_, if an error happens in the opening
+handshake or if the connection handler crashes, there is no way to raise an
+exception that you can handle.
+
+.. _inversion of control: https://en.wikipedia.org/wiki/Inversion_of_control
+
+Logs tell you about these errors.
+
+Besides errors, you may want to record the activity of the server.
+
+In a request/response protocol such as HTTP, there's an obvious way to record
+activity: log one event per request/response. Unfortunately, this solution
+doesn't work well for a bidirectional protocol such as WebSocket.
+
+Instead, when running as a server, websockets logs one event when a
+`connection is established`_ and another event when a `connection is
+closed`_.
+
+.. _connection is established: https://www.rfc-editor.org/rfc/rfc6455.html#section-4
+.. _connection is closed: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.4
+
+By default, websockets doesn't log an event for every message. That would be
+excessive for many applications exchanging small messages at a fast rate. If
+you need this level of detail, you could add logging in your own code.
+
+Finally, you can enable debug logs to get details about everything websockets
+is doing. This can be useful when developing clients as well as servers.
+
+See :ref:`log levels <log-levels>` below for a list of events logged by
+websockets logs at each log level.
+
+Configure logging
+-----------------
+
+websockets relies on the :mod:`logging` module from the standard library in
+order to maximize compatibility and integrate nicely with other libraries::
+
+ import logging
+
+websockets logs to the ``"websockets.client"`` and ``"websockets.server"``
+loggers.
+
+websockets doesn't provide a default logging configuration because
+requirements vary a lot depending on the environment.
+
+Here's a basic configuration for a server in production::
+
+ logging.basicConfig(
+ format="%(asctime)s %(message)s",
+ level=logging.INFO,
+ )
+
+Here's how to enable debug logs for development::
+
+ logging.basicConfig(
+ format="%(message)s",
+ level=logging.DEBUG,
+ )
+
+Furthermore, websockets adds a ``websocket`` attribute to log records, so you
+can include additional information about the current connection in logs.
+
+You could attempt to add information with a formatter::
+
+ # this doesn't work!
+ logging.basicConfig(
+ format="{asctime} {websocket.id} {websocket.remote_address[0]} {message}",
+ level=logging.INFO,
+ style="{",
+ )
+
+However, this technique runs into two problems:
+
+* The formatter applies to all records. It will crash if it receives a record
+ without a ``websocket`` attribute. For example, this happens when logging
+ that the server starts because there is no current connection.
+
+* Even with :meth:`str.format` style, you're restricted to attribute and index
+ lookups, which isn't enough to implement some fairly simple requirements.
+
+There's a better way. :func:`~client.connect` and :func:`~server.serve` accept
+a ``logger`` argument to override the default :class:`~logging.Logger`. You
+can set ``logger`` to a :class:`~logging.LoggerAdapter` that enriches logs.
+
+For example, if the server is behind a reverse
+proxy, :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address` gives
+the IP address of the proxy, which isn't useful. IP addresses of clients are
+provided in an HTTP header set by the proxy.
+
+Here's how to include them in logs, assuming they're in the
+``X-Forwarded-For`` header::
+
+ logging.basicConfig(
+ format="%(asctime)s %(message)s",
+ level=logging.INFO,
+ )
+
+ class LoggerAdapter(logging.LoggerAdapter):
+ """Add connection ID and client IP address to websockets logs."""
+ def process(self, msg, kwargs):
+ try:
+ websocket = kwargs["extra"]["websocket"]
+ except KeyError:
+ return msg, kwargs
+ xff = websocket.request_headers.get("X-Forwarded-For")
+ return f"{websocket.id} {xff} {msg}", kwargs
+
+ async with websockets.serve(
+ ...,
+ # Python < 3.10 requires passing None as the second argument.
+ logger=LoggerAdapter(logging.getLogger("websockets.server"), None),
+ ):
+ ...
+
+Logging to JSON
+---------------
+
+Even though :mod:`logging` predates structured logging, it's still possible to
+output logs as JSON with a bit of effort.
+
+First, we need a :class:`~logging.Formatter` that renders JSON:
+
+.. literalinclude:: ../../example/logging/json_log_formatter.py
+
+Then, we configure logging to apply this formatter::
+
+ handler = logging.StreamHandler()
+ handler.setFormatter(formatter)
+
+ logger = logging.getLogger()
+ logger.addHandler(handler)
+ logger.setLevel(logging.INFO)
+
+Finally, we populate the ``event_data`` custom attribute in log records with
+a :class:`~logging.LoggerAdapter`::
+
+ class LoggerAdapter(logging.LoggerAdapter):
+ """Add connection ID and client IP address to websockets logs."""
+ def process(self, msg, kwargs):
+ try:
+ websocket = kwargs["extra"]["websocket"]
+ except KeyError:
+ return msg, kwargs
+ kwargs["extra"]["event_data"] = {
+ "connection_id": str(websocket.id),
+ "remote_addr": websocket.request_headers.get("X-Forwarded-For"),
+ }
+ return msg, kwargs
+
+ async with websockets.serve(
+ ...,
+ # Python < 3.10 requires passing None as the second argument.
+ logger=LoggerAdapter(logging.getLogger("websockets.server"), None),
+ ):
+ ...
+
+Disable logging
+---------------
+
+If your application doesn't configure :mod:`logging`, Python outputs messages
+of severity ``WARNING`` and higher to :data:`~sys.stderr`. As a consequence,
+you will see a message and a stack trace if a connection handler coroutine
+crashes or if you hit a bug in websockets.
+
+If you want to disable this behavior for websockets, you can add
+a :class:`~logging.NullHandler`::
+
+ logging.getLogger("websockets").addHandler(logging.NullHandler())
+
+Additionally, if your application configures :mod:`logging`, you must disable
+propagation to the root logger, or else its handlers could output logs::
+
+ logging.getLogger("websockets").propagate = False
+
+Alternatively, you could set the log level to ``CRITICAL`` for the
+``"websockets"`` logger, as the highest level currently used is ``ERROR``::
+
+ logging.getLogger("websockets").setLevel(logging.CRITICAL)
+
+Or you could configure a filter to drop all messages::
+
+ logging.getLogger("websockets").addFilter(lambda record: None)
+
+.. _log-levels:
+
+Log levels
+----------
+
+Here's what websockets logs at each level.
+
+``ERROR``
+.........
+
+* Exceptions raised by connection handler coroutines in servers
+* Exceptions resulting from bugs in websockets
+
+``WARNING``
+...........
+
+* Failures in :func:`~websockets.broadcast`
+
+``INFO``
+........
+
+* Server starting and stopping
+* Server establishing and closing connections
+* Client reconnecting automatically
+
+``DEBUG``
+.........
+
+* Changes to the state of connections
+* Handshake requests and responses
+* All frames sent and received
+* Steps to close a connection
+* Keepalive pings and pongs
+* Errors handled transparently
+
+Debug messages have cute prefixes that make logs easier to scan:
+
+* ``>`` - send something
+* ``<`` - receive something
+* ``=`` - set connection state
+* ``x`` - shut down connection
+* ``%`` - manage pings and pongs
+* ``!`` - handle errors and timeouts
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/memory.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/memory.rst
new file mode 100644
index 0000000000..e44247a77c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/memory.rst
@@ -0,0 +1,48 @@
+Memory usage
+============
+
+.. currentmodule:: websockets
+
+In most cases, memory usage of a WebSocket server is proportional to the
+number of open connections. When a server handles thousands of connections,
+memory usage can become a bottleneck.
+
+Memory usage of a single connection is the sum of:
+
+1. the baseline amount of memory websockets requires for each connection,
+2. the amount of data held in buffers before the application processes it,
+3. any additional memory allocated by the application itself.
+
+Baseline
+--------
+
+Compression settings are the main factor affecting the baseline amount of
+memory used by each connection.
+
+With websockets' defaults, on the server side, a single connections uses
+70 KiB of memory.
+
+Refer to the :doc:`topic guide on compression <../topics/compression>` to
+learn more about tuning compression settings.
+
+Buffers
+-------
+
+Under normal circumstances, buffers are almost always empty.
+
+Under high load, if a server receives more messages than it can process,
+bufferbloat can result in excessive memory usage.
+
+By default websockets has generous limits. It is strongly recommended to adapt
+them to your application. When you call :func:`~server.serve`:
+
+- Set ``max_size`` (default: 1 MiB, UTF-8 encoded) to the maximum size of
+ messages your application generates.
+- Set ``max_queue`` (default: 32) to the maximum number of messages your
+ application expects to receive faster than it can process them. The queue
+ provides burst tolerance without slowing down the TCP connection.
+
+Furthermore, you can lower ``read_limit`` and ``write_limit`` (default:
+64 KiB) to reduce the size of buffers for incoming and outgoing data.
+
+The design document provides :ref:`more details about buffers <buffers>`.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/performance.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/performance.rst
new file mode 100644
index 0000000000..45e23b2390
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/performance.rst
@@ -0,0 +1,20 @@
+Performance
+===========
+
+Here are tips to optimize performance.
+
+uvloop
+------
+
+You can make a websockets application faster by running it with uvloop_.
+
+(This advice isn't specific to websockets. It applies to any :mod:`asyncio`
+application.)
+
+.. _uvloop: https://github.com/MagicStack/uvloop
+
+broadcast
+---------
+
+:func:`~websockets.broadcast` is the most efficient way to send a message to
+many clients.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.graffle b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.graffle
new file mode 100644
index 0000000000..04a9e7acb5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.graffle
@@ -0,0 +1,37 @@
+��]ks�6����
+���[Ӹ��q�t�n�xc'�v<ӡ%X�"U���f���J�x-J���ۉl�A��\q��_FQ�N�0�|�=����~2�Ꮟ���#���'�����ѯ/z�(�����g�_������Ǒ��}~��w����Q���}��q��Y����=??�C����!�v�d����5t�
+�A>x ���^|;���G�=��/�����~\��U<�_��o�b�z�ӧ���寗M��a?ȡ�Ӟ獂4
+�/�=���)��KFq8L���{��+
+NO#-���HJ�X(�P>��D�d����&y�7��$���Ӊ޽��,�2��p��Y؟�͞}Ff(��k�a|����o���b���=����2`�b?
+�f��$
+w�5y�|A9��<�S���"���1O/B�ɧ�������ip��b`oޖ�3:�����L�0ӿ%ɨÊ���A�6
+��b-\Nf1��W�hq����9o�J��(ދ�a�n���>}�bN�� zJ����'\*�d��p��0��y�7H�w��A��� �@E��~�5�$i����H�a��7��5Vm�<��Qpq��ƽ�����~���w�S飋q�qLS�-��?�8�s�|jIcng<3�c��ꊭ2�|<��X[e�I���O��un(,����>���猯J��|W�r�t�w_��.�C���R$�U�)�Qʐ���|I)�Xq�}�J� �ǂbd\}�
+��5#��JH�+�S /o�N+�4f5;�yy�v|i��\ڑ���K �Ҵ2���[�4�Z�Ϣi�gJo��W<���A��B|DY!C�DGA��ن�1�g�! ϯ���Κ@\g���* *Ʃ���1��+*��1�=)�/��1�2#�$�UT(.a�()a��Q&��>RJ�oߚ��¾�g�X6����$
+u�F��rԙ���\,,\��;N�(�&޸U��������g�5>�+�%o�劀/�2T}c3�
+�� +�h�q�A�i~���8���!&���~�K������=�z|
++7?��O���(�4>>�i�sԫ��|�~%)4��q����a�ul~9�&>���/������?��8���1&Ň������ U c �M[�����7L� ��Ad����Y��ӠZz��x����q��N�`��o~������"��+7t�J�K"|��`R��t��:t���"�v�{p�@��4?K�IDς�xlF`S�K�/]66�œ���'9<��WT16b�OC>�o) �p����y�Խ|�\~��f�Ad�������Ǖd�*�K�|�f�ՍB_�#5 �n� �M�P�kZm�N��ᵲ��������a
+.���f�̸=���*v,�����٤?�a�!BV�c_����?gYk��y��"�2���P�J���&�4f�/<N�""���Ķ�9����V�Oa׾*�|)�pG��BQ�)�P��%yO� �6}����Xs�ږkY��$��
+�><�DǏn�U��� tS&�5fQS{��a�����H� F�X>�v=��f����ĩ�v6�-�p+��5L8·�*�j]��%l-�� )@G�m�ȅ�eqΰ5|ykǍ���8Y�;B�M���"&����S˜����֎;F\��M
+��� +#.i��������Xu:)�ڛggnqS淍��``mb)}čJ�6�m��$W0H�" 1_��P�,�
+��W��!{�
+�}���m��l�����j{ɖ�@1,i��1�@n;\|�!_�Ta!�tr�H;ϒh�=�TŤNhl�� c��WV���֋��O�|m�yqUY$�7��
+��(�$'��sՄ�鏘}V�_��CKKl�K�w�ӎ1V�_h5�[��������D�f�wwCH��bE'&Z�fѢ(���e[�և\�4�
+wr��X�'VA�1���@�v�mp�F3
+֐�I'�K:�b��A�p?%�B.��xx�b�h�XI��¤s)�:���rb��'�X�W,�� {>%DA%gJ:�L�2��A���<`�L���ʀ��B�|ݐ?S�C�(�e�0
+r�OT�d�A-K�z����IM�@��w&u����6�4��+�u�,�c�����1]�����XpJ|�̨X��,��n3�$�c�{�+�Z�Y#�����š�ԝ�-����4�hl�� uO�ϖ.�+��yɱ�}���t'י�u���N�ui}���x�\殺
+��|A}_ N)�;�V.Q�"�3��8ج
+��[����1Y���pmKҟW�߅7��\_��n]� ��E�\t�E�pt-����][K����!7�{�{F�T�3�8�fG�:��<�b�>�Ew`|��G�+0�[���[�GZ��:�sK�����;�|P
+0����_V=�/���=�L�Fj��Jj�M�E�=�VqYر�
+��x;͗������u�4�e�j����F�Z3�m�f���:��1t�S{���HgY0���MB�6,||R�h9�L��u,A�\��f���Q`N}�*��AA�|y �Z�H*�m�B�7��#�杫���*�I�R��0��u�8�i]�u�]���ǹ��sm���:L��Xv=a��B��u�,���&���7��]�C���YE��ݺ�D�#��SX��_�xh`[U�z��OX��Y\_$����֊{��\�H�� O���RM 偙!1�����C�mtmR�pv5��
+:o�c=��� ��L��:�r�iy$�P����0��<��HR%WBZ�6�=�'8��J�"���d���,�R��И�������3.��/�X��0׿'sm���m[����N�� ,u�»�ݩ�ypC'�wǻ�;�aw��u���t�=�Y�9�s@瀮#Н\䷈s������}�܄m`G\P�&tn‡�/1�>K�ޠ�Њ�ͤV��m8�v�;�u8��p��47��Am=�i��HK�:�uH����&���g��N�a��ʠ,�ԫ"*PJ�h��
+�x������Aׇ��jٕ%��Ʒd��[��5��y�8_#�f �#�j�N+8vi���� s�.���0���W�1\�a`>G�pj�bk|�mk�8���xz��J�y�~��cE��\wJ��$#�4)@�Y�;׽�!��*��G�Ŵ��IX�n��曳��7߶
+��7L|SHh+�v�Hh�2,���ۥ���p
+�Զ�n��"ugQ��,|eJ�տj�Rw�r����?�-aXT`�HܽJY���-����O�X��A8ɚ 3u胪L�l�c�<��A{/�w���&v�ᙧ0��:���� �4+lֻ��j�#U�`�j���G����s�֐���}��[�X�µ���� ��˟��R)����q�e�e�)
+�q)�,�����X��T�LF��M�n�,�ܒKzsn�N��݉�eq ��afw�3�aƲ�#��:��*��� ��WU�KeA��G �K0�i���1��qRA����D�l5��&����ܮ&���}MB�٘+ՏH�%y��R},F�*u�qJ�`0�(�}��"�8��D��N$߁�ֻ+���s"��H����̷��(��ڜ���\���b��Zw�$ҫ���}]��]r��q0o@�%ð���6�lV�i�JKm�-�J�VK7 �C��8�i
+%��>ɒ�'0�����G}rX|���FI|�&9�at�I5\���]T8����K�Ψp��=N�s*�}B�T��\�t<�l��
+���8�<ʠ31���������dՖ],����Ijm>��֐|�ܓ�̴Z�۳���q0Ύ��/�jt�ڈ�yC+��?�Yx�����X��9�C�P;BP�۴A����z|����(����L��B��;���5�h|�odJ�&Y���mz�I~�Z�uX����Y�a|���n�8�6���K|/GA���+2W�a�O~ ��a��ء|�ւ�P�V]VZA�|ø9�A�7� O�ވ�:�����6���9'��nX�_a��y2;�W�
+����{��6�Ѭ@����R`g���\�K2OgF��қ�� �v�������Y�nf�q�����.�]�t�{{��$�ø��
+�s�'z����9��v���ӹsM��5}�:�nn�7�ϒ<OF��0,-�RTk��i�y��g�3\��bM��2I�?ͮ.w��]�F}���=�����9��#F��Ϣ�^�L������ዳ�����'���}�{��=�����h���GG��?~������b�����Kt������Z��ez �\���i�-�O?����N��1l�MŒ�ݦ�n��W�����%\H0�TuÜ�f��ӟA�iK܇����m������ut_���E������Ä�����rLy��W*�^��iMu�J��4g*#`�Q�7�o?�?YEQ���N] ø���^�ī:hwEc�q_���}�1������q�WM������0#���'i
+3Q�dSO���/f&���).�q�;�X�xZ��2@��"�' �������A�)��X<��n-�r�^�I��rռ]\dyz`��(8y�
+�� Jkj�L4���
+z-�|<n �"�G�ݝ�)�4��$��V��!���Y����ba��,��|/e0j��s�<}�������
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.svg
new file mode 100644
index 0000000000..51bfd982be
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 624.34646 822.34646" width="624.34646pt" height="822.34646pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.6.2 <dc:date>2019-07-07 08:38:24 +0000</dc:date></metadata><defs><font-face font-family="Verdana" font-size="12" panose-1="2 11 6 4 3 5 4 4 2 4" units-per-em="1000" underline-position="-87.890625" underline-thickness="58.59375" slope="0" x-height="545.41016" cap-height="727.0508" ascent="1005.3711" descent="-209.96094" font-weight="500"><font-face-src><font-face-name name="Verdana"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 6 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="100.097656" slope="0" x-height="443.35938" cap-height="591.79688" ascent="832.51953" descent="-300.29297" font-weight="bold"><font-face-src><font-face-name name="CourierNewPS-BoldMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="10" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.8666667 0 L 0 0 M 0 -2.2 L 5.8666667 0 L 0 2.2" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><radialGradient cx="0" cy="0" r="1" id="Gradient" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="white"/><stop offset="1" stop-color="#a5a5a5"/></radialGradient><radialGradient id="Obj_Gradient" xl:href="#Gradient" gradientTransform="translate(311.81102 708.6614) scale(145.75703)"/><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker_2" viewBox="-1 -6 14 12" markerWidth="14" markerHeight="12" color="black"><g><path d="M 12 0 L 0 0 M 0 -4.5 L 12 0 L 0 4.5" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker_3" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.9253333 0 L 0 0 M 0 -2.222 L 5.9253333 0 L 0 2.222" fill="none" stroke="currentColor" stroke-width="1"/></g></marker></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><rect fill="white" width="1314" height="1698"/><g><title>Layer 1</title><rect x="28.346457" y="765.35433" width="566.92913" height="28.346457" fill="#6cf"/><rect x="28.346457" y="765.35433" width="566.92913" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 772.02755)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="228.50753" y="12" textLength="99.91406">remote endpoint</tspan></text><rect x="28.346457" y="85.03937" width="566.92913" height="566.92913" fill="white"/><rect x="28.346457" y="85.03937" width="566.92913" height="566.92913" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 90.03937)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="243.79171" y="12" textLength="51.333984">websock</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="295.00851" y="12" textLength="18.128906">ets</tspan><tspan font-family="Courier New" font-size="12" font-weight="500" x="195.65109" y="25" textLength="165.62695">WebSocketCommonProtocol</tspan></text><rect x="28.346457" y="28.346457" width="566.92913" height="28.346457" fill="#6f6"/><rect x="28.346457" y="28.346457" width="566.92913" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 35.019685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="230.0046" y="12" textLength="96.91992">application logic</tspan></text><path d="M 102.047243 586.77165 L 238.11023 586.77165 C 247.49858 586.77165 255.11811 596.93102 255.11811 609.4488 C 255.11811 621.9666 247.49858 632.12598 238.11023 632.12598 L 102.047243 632.12598 C 92.658897 632.12598 85.03937 621.9666 85.03937 609.4488 C 85.03937 596.93102 92.658897 586.77165 102.047243 586.77165" fill="#fc6"/><path d="M 102.047243 586.77165 L 238.11023 586.77165 C 247.49858 586.77165 255.11811 596.93102 255.11811 609.4488 C 255.11811 621.9666 247.49858 632.12598 238.11023 632.12598 L 102.047243 632.12598 C 92.658897 632.12598 85.03937 621.9666 85.03937 609.4488 C 85.03937 596.93102 92.658897 586.77165 102.047243 586.77165 M 238.11023 586.77165 C 228.72189 586.77165 221.10236 596.93102 221.10236 609.4488 C 221.10236 621.9666 228.72189 632.12598 238.11023 632.12598" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(125.33071 596.9488)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="14.896484" y="10" textLength="43.20703">reader</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x=".49414062" y="22" textLength="72.01172">StreamReader</tspan></text><path d="M 385.5118 586.77165 L 521.5748 586.77165 C 530.96315 586.77165 538.58267 596.93102 538.58267 609.4488 C 538.58267 621.9666 530.96315 632.12598 521.5748 632.12598 L 385.5118 632.12598 C 376.12346 632.12598 368.50393 621.9666 368.50393 609.4488 C 368.50393 596.93102 376.12346 586.77165 385.5118 586.77165" fill="#fc6"/><path d="M 385.5118 586.77165 L 521.5748 586.77165 C 530.96315 586.77165 538.58267 596.93102 538.58267 609.4488 C 538.58267 621.9666 530.96315 632.12598 521.5748 632.12598 L 385.5118 632.12598 C 376.12346 632.12598 368.50393 621.9666 368.50393 609.4488 C 368.50393 596.93102 376.12346 586.77165 385.5118 586.77165 M 521.5748 586.77165 C 512.18645 586.77165 504.56693 596.93102 504.56693 609.4488 C 504.56693 621.9666 512.18645 632.12598 521.5748 632.12598" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(408.79527 596.9488)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="14.896484" y="10" textLength="43.20703">writer</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x=".49414062" y="22" textLength="72.01172">StreamWriter</tspan></text><path d="M 481.88976 419.52756 L 481.88976 374.17323 C 481.88976 371.04378 469.19055 368.50393 453.5433 368.50393 C 437.89606 368.50393 425.19685 371.04378 425.19685 374.17323 L 425.19685 419.52756 C 425.19685 422.657 437.89606 425.19685 453.5433 425.19685 C 469.19055 425.19685 481.88976 422.657 481.88976 419.52756" fill="#fecc66"/><path d="M 481.88976 419.52756 L 481.88976 374.17323 C 481.88976 371.04378 469.19055 368.50393 453.5433 368.50393 C 437.89606 368.50393 425.19685 371.04378 425.19685 374.17323 L 425.19685 419.52756 C 425.19685 422.657 437.89606 425.19685 453.5433 425.19685 C 469.19055 425.19685 481.88976 422.657 481.88976 419.52756 M 481.88976 374.17323 C 481.88976 377.30267 469.19055 379.84252 453.5433 379.84252 C 437.89606 379.84252 425.19685 377.30267 425.19685 374.17323" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(429.19685 387.18504)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="6.343527" y="10" textLength="36.00586">pings</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="12.3445034" y="22" textLength="24.003906">dict</tspan></text><path d="M 85.039413 283.46457 L 255.11806 283.46457 C 270.7734 283.46457 283.46457 296.15573 283.46457 311.81107 L 283.46457 481.88972 C 283.46457 497.54506 270.7734 510.23622 255.11806 510.23622 L 85.039413 510.23622 C 69.384074 510.23622 56.692913 497.54506 56.692913 481.88972 L 56.692913 311.81107 C 56.692913 296.15573 69.384074 283.46457 85.039413 283.46457 Z" fill="#dadada"/><path d="M 85.039413 283.46457 L 255.11806 283.46457 C 270.7734 283.46457 283.46457 296.15573 283.46457 311.81107 L 283.46457 481.88972 C 283.46457 497.54506 270.7734 510.23622 255.11806 510.23622 L 85.039413 510.23622 C 69.384074 510.23622 56.692913 497.54506 56.692913 481.88972 L 56.692913 311.81107 C 56.692913 296.15573 69.384074 283.46457 85.039413 283.46457 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(61.692913 288.46457)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="43.57528" y="10" textLength="129.62109">transfer_data_task</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="96.383873" y="22" textLength="24.003906">Task</tspan></text><path d="M 297.6378 765.35433 L 297.6378 609.4488 L 255.11811 609.4488 L 269.01811 609.4488 L 266.51811 609.4488" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 368.50393 609.4488 L 354.60393 609.4488 L 325.98425 609.4488 L 325.98425 753.95433" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 207.03401 712.3154 C 161.22047 708.6614 179.48976 677.90097 252.5726 683.1496 C 259.35307 672.91835 344.33858 674.579 343.783 683.1496 C 397.0715 672.1877 465.17102 694.04553 419.49354 705.00744 C 474.30425 710.32206 418.80189 738.9565 373.8189 734.17322 C 370.2189 742.14584 289.80283 744.9358 282.74457 734.17322 C 237.20882 745.66715 142.25953 727.9946 207.03401 712.3154 Z" fill="url(#Obj_Gradient)"/><path d="M 207.03401 712.3154 C 161.22047 708.6614 179.48976 677.90097 252.5726 683.1496 C 259.35307 672.91835 344.33858 674.579 343.783 683.1496 C 397.0715 672.1877 465.17102 694.04553 419.49354 705.00744 C 474.30425 710.32206 418.80189 738.9565 373.8189 734.17322 C 370.2189 742.14584 289.80283 744.9358 282.74457 734.17322 C 237.20882 745.66715 142.25953 727.9946 207.03401 712.3154 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(217.59842 701.1614)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="69.81416" y="12" textLength="48.796875">network</tspan></text><rect x="85.03937" y="453.5433" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="453.5433" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 460.71653)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="44.03351" y="10" textLength="72.01172">read_frame</tspan></text><rect x="85.03937" y="396.8504" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="396.8504" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 404.02362)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="26.03058" y="10" textLength="108.01758">read_data_frame</tspan></text><rect x="85.03937" y="340.15748" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="340.15748" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 347.3307)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="36.832338" y="10" textLength="86.41406">read_message</tspan></text><text transform="translate(178.07874 490.563)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="30.004883">bytes</tspan></text><text transform="translate(178.07874 433.87008)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="36.00586">frames</tspan></text><text transform="translate(178.07874 371.67716)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="24.003906">data</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="19" textLength="36.00586">frames</tspan></text><rect x="368.50393" y="510.23622" width="170.07874" height="28.346457" fill="#ff6"/><rect x="368.50393" y="510.23622" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 517.40945)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="40.432924" y="10" textLength="79.21289">write_frame</tspan></text><path d="M 85.03937 609.4488 L 71.13937 609.4488 L 56.692913 609.4488 L 56.692913 595.2756 L 56.692913 566.92913 L 113.385826 566.92913 L 170.07874 566.92913 L 170.07874 495.78976 L 170.07874 494.03976" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 453.5433 539.33267 L 453.5433 552.48267 L 453.5433 566.92913 L 510.23622 566.92913 L 569.76378 566.92913 L 569.76378 595.2756 L 569.76378 609.4488 L 552.48267 609.4488 L 549.98267 609.4488" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="453.5433" x2="170.07874" y2="437.34685" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="396.8504" x2="170.07874" y2="380.65393" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 102.047243 204.09449 L 238.11023 204.09449 C 247.49858 204.09449 255.11811 214.25386 255.11811 226.77165 C 255.11811 239.28945 247.49858 249.44882 238.11023 249.44882 L 102.047243 249.44882 C 92.658897 249.44882 85.03937 239.28945 85.03937 226.77165 C 85.03937 214.25386 92.658897 204.09449 102.047243 204.09449" fill="#fc6"/><path d="M 102.047243 204.09449 L 238.11023 204.09449 C 247.49858 204.09449 255.11811 214.25386 255.11811 226.77165 C 255.11811 239.28945 247.49858 249.44882 238.11023 249.44882 L 102.047243 249.44882 C 92.658897 249.44882 85.03937 239.28945 85.03937 226.77165 C 85.03937 214.25386 92.658897 204.09449 102.047243 204.09449 M 238.11023 204.09449 C 228.72189 204.09449 221.10236 214.25386 221.10236 226.77165 C 221.10236 239.28945 228.72189 249.44882 238.11023 249.44882" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(132.33071 214.27165)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x=".1953125" y="10" textLength="57.609375">messages</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="13.997559" y="22" textLength="30.004883">deque</tspan></text><path d="M 255.11811 354.3307 L 269.01811 354.3307 L 297.6378 354.3307 L 297.6378 328.8189 L 297.6378 226.77165 L 269.01811 226.77165 L 266.51811 226.77165" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><rect x="85.03937" y="141.73228" width="170.07874" height="28.346457" fill="#cf6"/><rect x="85.03937" y="141.73228" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="65.637026" y="10" textLength="28.804688">recv</tspan></text><path d="M 85.03937 226.77165 L 71.13937 226.77165 L 42.519685 226.77165 L 42.519685 209.76378 L 42.519685 155.90551 L 71.13937 155.90551 L 73.63937 155.90551" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="141.73228" x2="170.07874" y2="68.092913" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="453.5433" y1="56.692913" x2="453.5433" y2="130.33228" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="467.71653" y1="56.692913" x2="467.71653" y2="187.8752" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="481.88976" y1="56.692913" x2="481.88976" y2="244.56811" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="496.063" y1="56.692913" x2="496.063" y2="300.32302" marker-end="url(#StickArrow_Marker_3)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><rect x="368.50393" y="141.73228" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="141.73228" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="65.637026" y="10" textLength="28.804688">send</tspan></text><rect x="368.50393" y="198.4252" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="198.4252" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(373.50393 205.59842)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="65.637026" y="10" textLength="28.804688">ping</tspan></text><rect x="368.50393" y="255.11811" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="255.11811" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(373.50393 262.29134)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="65.637026" y="10" textLength="28.804688">pong</tspan></text><rect x="368.50393" y="311.81102" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="311.81102" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 318.98425)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="62.03644" y="10" textLength="36.00586">close</tspan></text><path d="M 538.58267 155.90551 L 552.48267 155.90551 L 566.92913 155.90551 L 566.92913 481.88976 L 453.5433 481.88976 L 453.5433 496.33622 L 453.5433 498.08622" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="538.58267" y1="212.59842" x2="566.92913" y2="212.59842" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="538.58267" y1="269.29134" x2="566.92913" y2="269.29134" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="538.58267" y1="325.98425" x2="566.92913" y2="325.98425" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 255.86811 411.02362 L 262.61811 411.02362 L 340.15748 411.02362 L 340.15748 481.88976 L 453.5433 481.88976" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(291.94527 399.02362)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="42.006836">control</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="21" textLength="36.00586">frames</tspan></text><line x1="340.15748" y1="411.02362" x2="414.64685" y2="411.02362" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><path d="M 368.50393 212.59842 L 361.75393 212.59842 L 340.15748 212.59842 L 340.15748 340.15748 L 340.15748 382.67716" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(461.5433 547.2559)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="30.004883">bytes</tspan></text><text transform="translate(461.5433 490.563)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="36.00586">frames</tspan></text><line x1="340.15748" y1="382.67716" x2="414.64685" y2="382.67716" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/></g></g></svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/security.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/security.rst
new file mode 100644
index 0000000000..d3dec21bd1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/security.rst
@@ -0,0 +1,41 @@
+Security
+========
+
+Encryption
+----------
+
+For production use, a server should require encrypted connections.
+
+See this example of :ref:`encrypting connections with TLS
+<secure-server-example>`.
+
+Memory usage
+------------
+
+.. warning::
+
+ An attacker who can open an arbitrary number of connections will be able
+ to perform a denial of service by memory exhaustion. If you're concerned
+ by denial of service attacks, you must reject suspicious connections
+ before they reach websockets, typically in a reverse proxy.
+
+With the default settings, opening a connection uses 70 KiB of memory.
+
+Sending some highly compressed messages could use up to 128 MiB of memory with
+an amplification factor of 1000 between network traffic and memory usage.
+
+Configuring a server to :doc:`optimize memory usage <memory>` will improve
+security in addition to improving performance.
+
+Other limits
+------------
+
+websockets implements additional limits on the amount of data it accepts in
+order to minimize exposure to security vulnerabilities.
+
+In the opening handshake, websockets limits the number of HTTP headers to 256
+and the size of an individual header to 4096 bytes. These limits are 10 to 20
+times larger than what's expected in standard use cases. They're hard-coded.
+
+If you need to change these limits, you can monkey-patch the constants in
+``websockets.http11``.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/timeouts.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/timeouts.rst
new file mode 100644
index 0000000000..633fc1ab43
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/timeouts.rst
@@ -0,0 +1,116 @@
+Timeouts
+========
+
+.. currentmodule:: websockets
+
+Long-lived connections
+----------------------
+
+Since the WebSocket protocol is intended for real-time communications over
+long-lived connections, it is desirable to ensure that connections don't
+break, and if they do, to report the problem quickly.
+
+Connections can drop as a consequence of temporary network connectivity issues,
+which are very common, even within data centers.
+
+Furthermore, WebSocket builds on top of HTTP/1.1 where connections are
+short-lived, even with ``Connection: keep-alive``. Typically, HTTP/1.1
+infrastructure closes idle connections after 30 to 120 seconds.
+
+As a consequence, proxies may terminate WebSocket connections prematurely when
+no message was exchanged in 30 seconds.
+
+.. _keepalive:
+
+Keepalive in websockets
+-----------------------
+
+To avoid these problems, websockets runs a keepalive and heartbeat mechanism
+based on WebSocket Ping_ and Pong_ frames, which are designed for this purpose.
+
+.. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2
+.. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3
+
+It loops through these steps:
+
+1. Wait 20 seconds.
+2. Send a Ping frame.
+3. Receive a corresponding Pong frame within 20 seconds.
+
+If the Pong frame isn't received, websockets considers the connection broken and
+closes it.
+
+This mechanism serves two purposes:
+
+1. It creates a trickle of traffic so that the TCP connection isn't idle and
+ network infrastructure along the path keeps it open ("keepalive").
+2. It detects if the connection drops or becomes so slow that it's unusable in
+ practice ("heartbeat"). In that case, it terminates the connection and your
+ application gets a :exc:`~exceptions.ConnectionClosed` exception.
+
+Timings are configurable with the ``ping_interval`` and ``ping_timeout``
+arguments of :func:`~client.connect` and :func:`~server.serve`. Shorter values
+will detect connection drops faster but they will increase network traffic and
+they will be more sensitive to latency.
+
+Setting ``ping_interval`` to :obj:`None` disables the whole keepalive and
+heartbeat mechanism.
+
+Setting ``ping_timeout`` to :obj:`None` disables only timeouts. This enables
+keepalive, to keep idle connections open, and disables heartbeat, to support large
+latency spikes.
+
+.. admonition:: Why doesn't websockets rely on TCP keepalive?
+ :class: hint
+
+ TCP keepalive is disabled by default on most operating systems. When
+ enabled, the default interval is two hours or more, which is far too much.
+
+Keepalive in browsers
+---------------------
+
+Browsers don't enable a keepalive mechanism like websockets by default. As a
+consequence, they can fail to notice that a WebSocket connection is broken for
+an extended period of time, until the TCP connection times out.
+
+In this scenario, the ``WebSocket`` object in the browser doesn't fire a
+``close`` event. If you have a reconnection mechanism, it doesn't kick in
+because it believes that the connection is still working.
+
+If your browser-based app mysteriously and randomly fails to receive events,
+this is a likely cause. You need a keepalive mechanism in the browser to avoid
+this scenario.
+
+Unfortunately, the WebSocket API in browsers doesn't expose the native Ping and
+Pong functionality in the WebSocket protocol. You have to roll your own in the
+application layer.
+
+Latency issues
+--------------
+
+Latency between a client and a server may increase for two reasons:
+
+* Network connectivity is poor. When network packets are lost, TCP attempts to
+ retransmit them, which manifests as latency. Excessive packet loss makes
+ the connection unusable in practice. At some point, timing out is a
+ reasonable choice.
+
+* Traffic is high. For example, if a client sends messages on the connection
+ faster than a server can process them, this manifests as latency as well,
+ because data is waiting in flight, mostly in OS buffers.
+
+ If the server is more than 20 seconds behind, it doesn't see the Pong before
+ the default timeout elapses. As a consequence, it closes the connection.
+ This is a reasonable choice to prevent overload.
+
+ If traffic spikes cause unwanted timeouts and you're confident that the server
+ will catch up eventually, you can increase ``ping_timeout`` or you can set it
+ to :obj:`None` to disable heartbeat entirely.
+
+ The same reasoning applies to situations where the server sends more traffic
+ than the client can accept.
+
+The latency measured during the last exchange of Ping and Pong frames is
+available in the :attr:`~legacy.protocol.WebSocketCommonProtocol.latency`
+attribute. Alternatively, you can measure the latency at any time with the
+:attr:`~legacy.protocol.WebSocketCommonProtocol.ping` method.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/Procfile b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/Procfile
new file mode 100644
index 0000000000..2e35818f67
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/Procfile
@@ -0,0 +1 @@
+web: python app.py
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/app.py
new file mode 100644
index 0000000000..4ca34d23bb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/app.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+
+import asyncio
+import http
+import signal
+
+import websockets
+
+
+async def echo(websocket):
+ async for message in websocket:
+ await websocket.send(message)
+
+
+async def health_check(path, request_headers):
+ if path == "/healthz":
+ return http.HTTPStatus.OK, [], b"OK\n"
+
+
+async def main():
+ # Set the stop condition when receiving SIGTERM.
+ loop = asyncio.get_running_loop()
+ stop = loop.create_future()
+ loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
+
+ async with websockets.serve(
+ echo,
+ host="",
+ port=8080,
+ process_request=health_check,
+ ):
+ await stop
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/fly.toml b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/fly.toml
new file mode 100644
index 0000000000..5290072ed2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/fly.toml
@@ -0,0 +1,16 @@
+app = "websockets-echo"
+kill_signal = "SIGTERM"
+
+[build]
+ builder = "paketobuildpacks/builder:base"
+
+[[services]]
+ internal_port = 8080
+ protocol = "tcp"
+
+ [[services.http_checks]]
+ path = "/healthz"
+
+ [[services.ports]]
+ handlers = ["tls", "http"]
+ port = 443
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/requirements.txt b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/requirements.txt
new file mode 100644
index 0000000000..14774b465e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/requirements.txt
@@ -0,0 +1 @@
+websockets
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/app.py
new file mode 100644
index 0000000000..360479b8eb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/app.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+
+import asyncio
+import os
+import signal
+
+import websockets
+
+
+async def echo(websocket):
+ async for message in websocket:
+ await websocket.send(message)
+
+
+async def main():
+ # Set the stop condition when receiving SIGTERM.
+ loop = asyncio.get_running_loop()
+ stop = loop.create_future()
+ loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
+
+ async with websockets.serve(
+ echo,
+ host="localhost",
+ port=8000 + int(os.environ["SUPERVISOR_PROCESS_NAME"][-2:]),
+ ):
+ await stop
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/haproxy.cfg b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/haproxy.cfg
new file mode 100644
index 0000000000..e63727d1c0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/haproxy.cfg
@@ -0,0 +1,17 @@
+defaults
+ mode http
+ timeout connect 10s
+ timeout client 30s
+ timeout server 30s
+
+frontend websocket
+ bind localhost:8080
+ default_backend websocket
+
+backend websocket
+ balance leastconn
+ server websockets-test_00 localhost:8000
+ server websockets-test_01 localhost:8001
+ server websockets-test_02 localhost:8002
+ server websockets-test_03 localhost:8003
+
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/supervisord.conf b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/supervisord.conf
new file mode 100644
index 0000000000..76a664d91b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/supervisord.conf
@@ -0,0 +1,7 @@
+[supervisord]
+
+[program:websockets-test]
+command = python app.py
+process_name = %(program_name)s_%(process_num)02d
+numprocs = 4
+autorestart = true
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/Procfile b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/Procfile
new file mode 100644
index 0000000000..2e35818f67
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/Procfile
@@ -0,0 +1 @@
+web: python app.py
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/app.py
new file mode 100644
index 0000000000..d4ba3edb51
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/app.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+
+import asyncio
+import signal
+import os
+
+import websockets
+
+
+async def echo(websocket):
+ async for message in websocket:
+ await websocket.send(message)
+
+
+async def main():
+ # Set the stop condition when receiving SIGTERM.
+ loop = asyncio.get_running_loop()
+ stop = loop.create_future()
+ loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
+
+ async with websockets.serve(
+ echo,
+ host="",
+ port=int(os.environ["PORT"]),
+ ):
+ await stop
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/requirements.txt b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/requirements.txt
new file mode 100644
index 0000000000..14774b465e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/requirements.txt
@@ -0,0 +1 @@
+websockets
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/Dockerfile b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/Dockerfile
new file mode 100644
index 0000000000..83ed8722c0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/Dockerfile
@@ -0,0 +1,7 @@
+FROM python:3.9-alpine
+
+RUN pip install websockets
+
+COPY app.py .
+
+CMD ["python", "app.py"]
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/app.py
new file mode 100644
index 0000000000..a8bcef6881
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/app.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+
+import asyncio
+import http
+import signal
+import sys
+import time
+
+import websockets
+
+
+async def slow_echo(websocket):
+ async for message in websocket:
+ # Block the event loop! This allows saturating a single asyncio
+ # process without opening an impractical number of connections.
+ time.sleep(0.1) # 100ms
+ await websocket.send(message)
+
+
+async def health_check(path, request_headers):
+ if path == "/healthz":
+ return http.HTTPStatus.OK, [], b"OK\n"
+ if path == "/inemuri":
+ loop = asyncio.get_running_loop()
+ loop.call_later(1, time.sleep, 10)
+ return http.HTTPStatus.OK, [], b"Sleeping for 10s\n"
+ if path == "/seppuku":
+ loop = asyncio.get_running_loop()
+ loop.call_later(1, sys.exit, 69)
+ return http.HTTPStatus.OK, [], b"Terminating\n"
+
+
+async def main():
+ # Set the stop condition when receiving SIGTERM.
+ loop = asyncio.get_running_loop()
+ stop = loop.create_future()
+ loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
+
+ async with websockets.serve(
+ slow_echo,
+ host="",
+ port=80,
+ process_request=health_check,
+ ):
+ await stop
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/benchmark.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/benchmark.py
new file mode 100644
index 0000000000..22ee4c5bd7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/benchmark.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+
+import asyncio
+import sys
+import websockets
+
+
+URI = "ws://localhost:32080"
+
+
+async def run(client_id, messages):
+ async with websockets.connect(URI) as websocket:
+ for message_id in range(messages):
+ await websocket.send(f"{client_id}:{message_id}")
+ await websocket.recv()
+
+
+async def benchmark(clients, messages):
+ await asyncio.wait([
+ asyncio.create_task(run(client_id, messages))
+ for client_id in range(clients)
+ ])
+
+
+if __name__ == "__main__":
+ clients, messages = int(sys.argv[1]), int(sys.argv[2])
+ asyncio.run(benchmark(clients, messages))
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/deployment.yaml b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/deployment.yaml
new file mode 100644
index 0000000000..ba58dd62bf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/deployment.yaml
@@ -0,0 +1,35 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: websockets-test
+spec:
+ type: NodePort
+ ports:
+ - port: 80
+ nodePort: 32080
+ selector:
+ app: websockets-test
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: websockets-test
+spec:
+ selector:
+ matchLabels:
+ app: websockets-test
+ template:
+ metadata:
+ labels:
+ app: websockets-test
+ spec:
+ containers:
+ - name: websockets-test
+ image: websockets-test:1.0
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: 80
+ periodSeconds: 1
+ ports:
+ - containerPort: 80
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/app.py
new file mode 100644
index 0000000000..24e6089756
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/app.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+
+import asyncio
+import os
+import signal
+
+import websockets
+
+
+async def echo(websocket):
+ async for message in websocket:
+ await websocket.send(message)
+
+
+async def main():
+ # Set the stop condition when receiving SIGTERM.
+ loop = asyncio.get_running_loop()
+ stop = loop.create_future()
+ loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
+
+ async with websockets.unix_serve(
+ echo,
+ path=f"{os.environ['SUPERVISOR_PROCESS_NAME']}.sock",
+ ):
+ await stop
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/nginx.conf b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/nginx.conf
new file mode 100644
index 0000000000..67aa0086d5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/nginx.conf
@@ -0,0 +1,25 @@
+daemon off;
+
+events {
+}
+
+http {
+ server {
+ listen localhost:8080;
+
+ location / {
+ proxy_http_version 1.1;
+ proxy_pass http://websocket;
+ proxy_set_header Connection $http_connection;
+ proxy_set_header Upgrade $http_upgrade;
+ }
+ }
+
+ upstream websocket {
+ least_conn;
+ server unix:websockets-test_00.sock;
+ server unix:websockets-test_01.sock;
+ server unix:websockets-test_02.sock;
+ server unix:websockets-test_03.sock;
+ }
+}
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/supervisord.conf b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/supervisord.conf
new file mode 100644
index 0000000000..76a664d91b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/supervisord.conf
@@ -0,0 +1,7 @@
+[supervisord]
+
+[program:websockets-test]
+command = python app.py
+process_name = %(program_name)s_%(process_num)02d
+numprocs = 4
+autorestart = true
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/render/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/render/app.py
new file mode 100644
index 0000000000..4ca34d23bb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/render/app.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+
+import asyncio
+import http
+import signal
+
+import websockets
+
+
+async def echo(websocket):
+ async for message in websocket:
+ await websocket.send(message)
+
+
+async def health_check(path, request_headers):
+ if path == "/healthz":
+ return http.HTTPStatus.OK, [], b"OK\n"
+
+
+async def main():
+ # Set the stop condition when receiving SIGTERM.
+ loop = asyncio.get_running_loop()
+ stop = loop.create_future()
+ loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
+
+ async with websockets.serve(
+ echo,
+ host="",
+ port=8080,
+ process_request=health_check,
+ ):
+ await stop
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/render/requirements.txt b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/render/requirements.txt
new file mode 100644
index 0000000000..14774b465e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/render/requirements.txt
@@ -0,0 +1 @@
+websockets
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/supervisor/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/supervisor/app.py
new file mode 100644
index 0000000000..bf61983ef7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/supervisor/app.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+
+import asyncio
+import signal
+
+import websockets
+
+
+async def echo(websocket):
+ async for message in websocket:
+ await websocket.send(message)
+
+
+async def main():
+ # Set the stop condition when receiving SIGTERM.
+ loop = asyncio.get_running_loop()
+ stop = loop.create_future()
+ loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
+
+ async with websockets.serve(
+ echo,
+ host="",
+ port=8080,
+ reuse_port=True,
+ ):
+ await stop
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/supervisor/supervisord.conf b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/supervisor/supervisord.conf
new file mode 100644
index 0000000000..76a664d91b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/supervisor/supervisord.conf
@@ -0,0 +1,7 @@
+[supervisord]
+
+[program:websockets-test]
+command = python app.py
+process_name = %(program_name)s_%(process_num)02d
+numprocs = 4
+autorestart = true
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/django/authentication.py b/testing/web-platform/tests/tools/third_party/websockets/example/django/authentication.py
new file mode 100644
index 0000000000..f6dad0f55e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/django/authentication.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+
+import asyncio
+
+import django
+import websockets
+
+django.setup()
+
+from sesame.utils import get_user
+from websockets.frames import CloseCode
+
+
+async def handler(websocket):
+ sesame = await websocket.recv()
+ user = await asyncio.to_thread(get_user, sesame)
+ if user is None:
+ await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed")
+ return
+
+ await websocket.send(f"Hello {user}!")
+
+
+async def main():
+ async with websockets.serve(handler, "localhost", 8888):
+ await asyncio.Future() # run forever
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/django/notifications.py b/testing/web-platform/tests/tools/third_party/websockets/example/django/notifications.py
new file mode 100644
index 0000000000..3a9ed10cf0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/django/notifications.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+
+import asyncio
+import json
+
+import aioredis
+import django
+import websockets
+
+django.setup()
+
+from django.contrib.contenttypes.models import ContentType
+from sesame.utils import get_user
+from websockets.frames import CloseCode
+
+
+CONNECTIONS = {}
+
+
+def get_content_types(user):
+ """Return the set of IDs of content types visible by user."""
+ # This does only three database queries because Django caches
+ # all permissions on the first call to user.has_perm(...).
+ return {
+ ct.id
+ for ct in ContentType.objects.all()
+ if user.has_perm(f"{ct.app_label}.view_{ct.model}")
+ or user.has_perm(f"{ct.app_label}.change_{ct.model}")
+ }
+
+
+async def handler(websocket):
+ """Authenticate user and register connection in CONNECTIONS."""
+ sesame = await websocket.recv()
+ user = await asyncio.to_thread(get_user, sesame)
+ if user is None:
+ await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed")
+ return
+
+ ct_ids = await asyncio.to_thread(get_content_types, user)
+ CONNECTIONS[websocket] = {"content_type_ids": ct_ids}
+ try:
+ await websocket.wait_closed()
+ finally:
+ del CONNECTIONS[websocket]
+
+
+async def process_events():
+ """Listen to events in Redis and process them."""
+ redis = aioredis.from_url("redis://127.0.0.1:6379/1")
+ pubsub = redis.pubsub()
+ await pubsub.subscribe("events")
+ async for message in pubsub.listen():
+ if message["type"] != "message":
+ continue
+ payload = message["data"].decode()
+ # Broadcast event to all users who have permissions to see it.
+ event = json.loads(payload)
+ recipients = (
+ websocket
+ for websocket, connection in CONNECTIONS.items()
+ if event["content_type_id"] in connection["content_type_ids"]
+ )
+ websockets.broadcast(recipients, payload)
+
+
+async def main():
+ async with websockets.serve(handler, "localhost", 8888):
+ await process_events() # runs forever
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/django/signals.py b/testing/web-platform/tests/tools/third_party/websockets/example/django/signals.py
new file mode 100644
index 0000000000..6dc827f72d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/django/signals.py
@@ -0,0 +1,23 @@
+import json
+
+from django.contrib.admin.models import LogEntry
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+from django_redis import get_redis_connection
+
+
+@receiver(post_save, sender=LogEntry)
+def publish_event(instance, **kwargs):
+ event = {
+ "model": instance.content_type.name,
+ "object": instance.object_repr,
+ "message": instance.get_change_message(),
+ "timestamp": instance.action_time.isoformat(),
+ "user": str(instance.user),
+ "content_type_id": instance.content_type_id,
+ "object_id": instance.object_id,
+ }
+ connection = get_redis_connection("default")
+ payload = json.dumps(event)
+ connection.publish("events", payload)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/echo.py b/testing/web-platform/tests/tools/third_party/websockets/example/echo.py
new file mode 100644
index 0000000000..2e47e52d94
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/echo.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+
+import asyncio
+from websockets.server import serve
+
+async def echo(websocket):
+ async for message in websocket:
+ await websocket.send(message)
+
+async def main():
+ async with serve(echo, "localhost", 8765):
+ await asyncio.Future() # run forever
+
+asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/faq/health_check_server.py b/testing/web-platform/tests/tools/third_party/websockets/example/faq/health_check_server.py
new file mode 100644
index 0000000000..7b8bded772
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/faq/health_check_server.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+
+import asyncio
+import http
+import websockets
+
+async def health_check(path, request_headers):
+ if path == "/healthz":
+ return http.HTTPStatus.OK, [], b"OK\n"
+
+async def echo(websocket):
+ async for message in websocket:
+ await websocket.send(message)
+
+async def main():
+ async with websockets.serve(
+ echo, "localhost", 8765,
+ process_request=health_check,
+ ):
+ await asyncio.Future() # run forever
+
+asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/faq/shutdown_client.py b/testing/web-platform/tests/tools/third_party/websockets/example/faq/shutdown_client.py
new file mode 100644
index 0000000000..539dd0304a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/faq/shutdown_client.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+import asyncio
+import signal
+import websockets
+
+async def client():
+ uri = "ws://localhost:8765"
+ async with websockets.connect(uri) as websocket:
+ # Close the connection when receiving SIGTERM.
+ loop = asyncio.get_running_loop()
+ loop.add_signal_handler(
+ signal.SIGTERM, loop.create_task, websocket.close())
+
+ # Process messages received on the connection.
+ async for message in websocket:
+ ...
+
+asyncio.run(client())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/faq/shutdown_server.py b/testing/web-platform/tests/tools/third_party/websockets/example/faq/shutdown_server.py
new file mode 100644
index 0000000000..1bcc9c90ba
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/faq/shutdown_server.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+
+import asyncio
+import signal
+import websockets
+
+async def echo(websocket):
+ async for message in websocket:
+ await websocket.send(message)
+
+async def server():
+ # Set the stop condition when receiving SIGTERM.
+ loop = asyncio.get_running_loop()
+ stop = loop.create_future()
+ loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
+
+ async with websockets.serve(echo, "localhost", 8765):
+ await stop
+
+asyncio.run(server())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/hello.py b/testing/web-platform/tests/tools/third_party/websockets/example/hello.py
new file mode 100644
index 0000000000..a3ce0699ee
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/hello.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+
+import asyncio
+from websockets.sync.client import connect
+
+def hello():
+ with connect("ws://localhost:8765") as websocket:
+ websocket.send("Hello world!")
+ message = websocket.recv()
+ print(f"Received: {message}")
+
+hello()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/legacy/basic_auth_client.py b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/basic_auth_client.py
new file mode 100644
index 0000000000..164732152f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/basic_auth_client.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+
+# WS client example with HTTP Basic Authentication
+
+import asyncio
+import websockets
+
+async def hello():
+ uri = "ws://mary:p@ssw0rd@localhost:8765"
+ async with websockets.connect(uri) as websocket:
+ greeting = await websocket.recv()
+ print(greeting)
+
+asyncio.run(hello())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/legacy/basic_auth_server.py b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/basic_auth_server.py
new file mode 100644
index 0000000000..d2efeb7e53
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/basic_auth_server.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+
+# Server example with HTTP Basic Authentication over TLS
+
+import asyncio
+import websockets
+
+async def hello(websocket):
+ greeting = f"Hello {websocket.username}!"
+ await websocket.send(greeting)
+
+async def main():
+ async with websockets.serve(
+ hello, "localhost", 8765,
+ create_protocol=websockets.basic_auth_protocol_factory(
+ realm="example", credentials=("mary", "p@ssw0rd")
+ ),
+ ):
+ await asyncio.Future() # run forever
+
+asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/legacy/unix_client.py b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/unix_client.py
new file mode 100644
index 0000000000..9261567303
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/unix_client.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+# WS client example connecting to a Unix socket
+
+import asyncio
+import os.path
+import websockets
+
+async def hello():
+ socket_path = os.path.join(os.path.dirname(__file__), "socket")
+ async with websockets.unix_connect(socket_path) as websocket:
+ name = input("What's your name? ")
+ await websocket.send(name)
+ print(f">>> {name}")
+
+ greeting = await websocket.recv()
+ print(f"<<< {greeting}")
+
+asyncio.run(hello())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/legacy/unix_server.py b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/unix_server.py
new file mode 100644
index 0000000000..335039c351
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/unix_server.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+
+# WS server example listening on a Unix socket
+
+import asyncio
+import os.path
+import websockets
+
+async def hello(websocket):
+ name = await websocket.recv()
+ print(f"<<< {name}")
+
+ greeting = f"Hello {name}!"
+
+ await websocket.send(greeting)
+ print(f">>> {greeting}")
+
+async def main():
+ socket_path = os.path.join(os.path.dirname(__file__), "socket")
+ async with websockets.unix_serve(hello, socket_path):
+ await asyncio.Future() # run forever
+
+asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/logging/json_log_formatter.py b/testing/web-platform/tests/tools/third_party/websockets/example/logging/json_log_formatter.py
new file mode 100644
index 0000000000..b8fc8d6dc9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/logging/json_log_formatter.py
@@ -0,0 +1,33 @@
+import json
+import logging
+import datetime
+
+class JSONFormatter(logging.Formatter):
+ """
+ Render logs as JSON.
+
+ To add details to a log record, store them in a ``event_data``
+ custom attribute. This dict is merged into the event.
+
+ """
+ def __init__(self):
+ pass # override logging.Formatter constructor
+
+ def format(self, record):
+ event = {
+ "timestamp": self.getTimestamp(record.created),
+ "message": record.getMessage(),
+ "level": record.levelname,
+ "logger": record.name,
+ }
+ event_data = getattr(record, "event_data", None)
+ if event_data:
+ event.update(event_data)
+ if record.exc_info:
+ event["exc_info"] = self.formatException(record.exc_info)
+ if record.stack_info:
+ event["stack_info"] = self.formatStack(record.stack_info)
+ return json.dumps(event)
+
+ def getTimestamp(self, created):
+ return datetime.datetime.utcfromtimestamp(created).isoformat()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/client.py b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/client.py
new file mode 100644
index 0000000000..8d588c2b0e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/client.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+
+import asyncio
+import websockets
+
+async def hello():
+ uri = "ws://localhost:8765"
+ async with websockets.connect(uri) as websocket:
+ name = input("What's your name? ")
+
+ await websocket.send(name)
+ print(f">>> {name}")
+
+ greeting = await websocket.recv()
+ print(f"<<< {greeting}")
+
+if __name__ == "__main__":
+ asyncio.run(hello())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/client_secure.py b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/client_secure.py
new file mode 100644
index 0000000000..f4b39f2b83
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/client_secure.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+
+import asyncio
+import pathlib
+import ssl
+import websockets
+
+ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+localhost_pem = pathlib.Path(__file__).with_name("localhost.pem")
+ssl_context.load_verify_locations(localhost_pem)
+
+async def hello():
+ uri = "wss://localhost:8765"
+ async with websockets.connect(uri, ssl=ssl_context) as websocket:
+ name = input("What's your name? ")
+
+ await websocket.send(name)
+ print(f">>> {name}")
+
+ greeting = await websocket.recv()
+ print(f"<<< {greeting}")
+
+if __name__ == "__main__":
+ asyncio.run(hello())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.css b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.css
new file mode 100644
index 0000000000..e1f4b77148
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.css
@@ -0,0 +1,33 @@
+body {
+ font-family: "Courier New", sans-serif;
+ text-align: center;
+}
+.buttons {
+ font-size: 4em;
+ display: flex;
+ justify-content: center;
+}
+.button, .value {
+ line-height: 1;
+ padding: 2rem;
+ margin: 2rem;
+ border: medium solid;
+ min-height: 1em;
+ min-width: 1em;
+}
+.button {
+ cursor: pointer;
+ user-select: none;
+}
+.minus {
+ color: red;
+}
+.plus {
+ color: green;
+}
+.value {
+ min-width: 2em;
+}
+.state {
+ font-size: 2em;
+}
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.html b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.html
new file mode 100644
index 0000000000..2e3433bd21
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>WebSocket demo</title>
+ <link href="counter.css" rel="stylesheet">
+ </head>
+ <body>
+ <div class="buttons">
+ <div class="minus button">-</div>
+ <div class="value">?</div>
+ <div class="plus button">+</div>
+ </div>
+ <div class="state">
+ <span class="users">?</span> online
+ </div>
+ <script src="counter.js"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.js b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.js
new file mode 100644
index 0000000000..37d892a28b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.js
@@ -0,0 +1,26 @@
+window.addEventListener("DOMContentLoaded", () => {
+ const websocket = new WebSocket("ws://localhost:6789/");
+
+ document.querySelector(".minus").addEventListener("click", () => {
+ websocket.send(JSON.stringify({ action: "minus" }));
+ });
+
+ document.querySelector(".plus").addEventListener("click", () => {
+ websocket.send(JSON.stringify({ action: "plus" }));
+ });
+
+ websocket.onmessage = ({ data }) => {
+ const event = JSON.parse(data);
+ switch (event.type) {
+ case "value":
+ document.querySelector(".value").textContent = event.value;
+ break;
+ case "users":
+ const users = `${event.count} user${event.count == 1 ? "" : "s"}`;
+ document.querySelector(".users").textContent = users;
+ break;
+ default:
+ console.error("unsupported event", event);
+ }
+ };
+});
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.py b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.py
new file mode 100644
index 0000000000..566e12965e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+
+import asyncio
+import json
+import logging
+import websockets
+
+logging.basicConfig()
+
+USERS = set()
+
+VALUE = 0
+
+def users_event():
+ return json.dumps({"type": "users", "count": len(USERS)})
+
+def value_event():
+ return json.dumps({"type": "value", "value": VALUE})
+
+async def counter(websocket):
+ global USERS, VALUE
+ try:
+ # Register user
+ USERS.add(websocket)
+ websockets.broadcast(USERS, users_event())
+ # Send current state to user
+ await websocket.send(value_event())
+ # Manage state changes
+ async for message in websocket:
+ event = json.loads(message)
+ if event["action"] == "minus":
+ VALUE -= 1
+ websockets.broadcast(USERS, value_event())
+ elif event["action"] == "plus":
+ VALUE += 1
+ websockets.broadcast(USERS, value_event())
+ else:
+ logging.error("unsupported event: %s", event)
+ finally:
+ # Unregister user
+ USERS.remove(websocket)
+ websockets.broadcast(USERS, users_event())
+
+async def main():
+ async with websockets.serve(counter, "localhost", 6789):
+ await asyncio.Future() # run forever
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/localhost.pem b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/localhost.pem
new file mode 100644
index 0000000000..f9a30ba8f6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/localhost.pem
@@ -0,0 +1,48 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDG8iDak4UBpurI
+TWjSfqJ0YVG/S56nhswehupCaIzu0xQ8wqPSs36h5t1jMexJPZfvwyvFjcV+hYpj
+LMM0wMJPx9oBQEe0bsmlC66e8aF0UpSQw1aVfYoxA9BejgEyrFNE7cRbQNYFEb/5
+3HfqZKdEQA2fgQSlZ0RTRmLrD+l72iO5o2xl5bttXpqYZB2XOkyO79j/xWdu9zFE
+sgZJ5ysWbqoRAGgnxjdYYr9DARd8bIE/hN3SW7mDt5v4LqCIhGn1VmrwtT3d5AuG
+QPz4YEbm0t6GOlmFjIMYH5Y7pALRVfoJKRj6DGNIR1JicL+wqLV66kcVnj8WKbla
+20i7fR7NAgMBAAECggEAG5yvgqbG5xvLqlFUIyMAWTbIqcxNEONcoUAIc38fUGZr
+gKNjKXNQOBha0dG0AdZSqCxmftzWdGEEfA9SaJf4YCpUz6ekTB60Tfv5GIZg6kwr
+4ou6ELWD4Jmu6fC7qdTRGdgGUMQG8F0uT/eRjS67KHXbbi/x/SMAEK7MO+PRfCbj
++JGzS9Ym9mUweINPotgjHdDGwwd039VWYS+9A+QuNK27p3zq4hrWRb4wshSC8fKy
+oLoe4OQt81aowpX9k6mAU6N8vOmP8/EcQHYC+yFIIDZB2EmDP07R1LUEH3KJnzo7
+plCK1/kYPhX0a05cEdTpXdKa74AlvSRkS11sGqfUAQKBgQDj1SRv0AUGsHSA0LWx
+a0NT1ZLEXCG0uqgdgh0sTqIeirQsPROw3ky4lH5MbjkfReArFkhHu3M6KoywEPxE
+wanSRh/t1qcNjNNZUvFoUzAKVpb33RLkJppOTVEWPt+wtyDlfz1ZAXzMV66tACrx
+H2a3v0ZWUz6J+x/dESH5TTNL4QKBgQDfirmknp408pwBE+bulngKy0QvU09En8H0
+uvqr8q4jCXqJ1tXon4wsHg2yF4Fa37SCpSmvONIDwJvVWkkYLyBHKOns/fWCkW3n
+hIcYx0q2jgcoOLU0uoaM9ArRXhIxoWqV/KGkQzN+3xXC1/MxZ5OhyxBxfPCPIYIN
+YN3M1t/QbQKBgDImhsC+D30rdlmsl3IYZFed2ZKznQ/FTqBANd+8517FtWdPgnga
+VtUCitKUKKrDnNafLwXrMzAIkbNn6b/QyWrp2Lln2JnY9+TfpxgJx7de3BhvZ2sl
+PC4kQsccy+yAQxOBcKWY+Dmay251bP5qpRepWPhDlq6UwqzMyqev4KzBAoGAWDMi
+IEO9ZGK9DufNXCHeZ1PgKVQTmJ34JxmHQkTUVFqvEKfFaq1Y3ydUfAouLa7KSCnm
+ko42vuhGFB41bOdbMvh/o9RoBAZheNGfhDVN002ioUoOpSlbYU4A3q7hOtfXeCpf
+lLI3JT3cFi6ic8HMTDAU4tJLEA5GhATOPr4hPNkCgYB8jTYGcLvoeFaLEveg0kS2
+cz6ZXGLJx5m1AOQy5g9FwGaW+10lr8TF2k3AldwoiwX0R6sHAf/945aGU83ms5v9
+PB9/x66AYtSRUos9MwB4y1ur4g6FiXZUBgTJUqzz2nehPCyGjYhh49WucjszqcjX
+chS1bKZOY+1knWq8xj5Qyg==
+-----END PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIDTTCCAjWgAwIBAgIJAOjte6l+03jvMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV
+BAYTAkZSMQ4wDAYDVQQHDAVQYXJpczEZMBcGA1UECgwQQXltZXJpYyBBdWd1c3Rp
+bjESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE4MDUwNTE2NTkyOVoYDzIwNjAwNTA0
+MTY1OTI5WjBMMQswCQYDVQQGEwJGUjEOMAwGA1UEBwwFUGFyaXMxGTAXBgNVBAoM
+EEF5bWVyaWMgQXVndXN0aW4xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAMbyINqThQGm6shNaNJ+onRhUb9LnqeGzB6G
+6kJojO7TFDzCo9KzfqHm3WMx7Ek9l+/DK8WNxX6FimMswzTAwk/H2gFAR7RuyaUL
+rp7xoXRSlJDDVpV9ijED0F6OATKsU0TtxFtA1gURv/ncd+pkp0RADZ+BBKVnRFNG
+YusP6XvaI7mjbGXlu21emphkHZc6TI7v2P/FZ273MUSyBknnKxZuqhEAaCfGN1hi
+v0MBF3xsgT+E3dJbuYO3m/guoIiEafVWavC1Pd3kC4ZA/PhgRubS3oY6WYWMgxgf
+ljukAtFV+gkpGPoMY0hHUmJwv7CotXrqRxWePxYpuVrbSLt9Hs0CAwEAAaMwMC4w
+LAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0G
+CSqGSIb3DQEBCwUAA4IBAQC9TsTxTEvqHPUS6sfvF77eG0D6HLOONVN91J+L7LiX
+v3bFeS1xbUS6/wIxZi5EnAt/te5vaHk/5Q1UvznQP4j2gNoM6lH/DRkSARvRitVc
+H0qN4Xp2Yk1R9VEx4ZgArcyMpI+GhE4vJRx1LE/hsuAzw7BAdsTt9zicscNg2fxO
+3ao/eBcdaC6n9aFYdE6CADMpB1lCX2oWNVdj6IavQLu7VMc+WJ3RKncwC9th+5OP
+ISPvkVZWf25rR2STmvvb0qEm3CZjk4Xd7N+gxbKKUvzEgPjrLSWzKKJAWHjCLugI
+/kQqhpjWVlTbtKzWz5bViqCjSbrIPpU2MgG9AUV9y3iV
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/server.py b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/server.py
new file mode 100644
index 0000000000..31b1829729
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/server.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+
+import asyncio
+import websockets
+
+async def hello(websocket):
+ name = await websocket.recv()
+ print(f"<<< {name}")
+
+ greeting = f"Hello {name}!"
+
+ await websocket.send(greeting)
+ print(f">>> {greeting}")
+
+async def main():
+ async with websockets.serve(hello, "localhost", 8765):
+ await asyncio.Future() # run forever
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/server_secure.py b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/server_secure.py
new file mode 100644
index 0000000000..de41d30dc0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/server_secure.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+
+import asyncio
+import pathlib
+import ssl
+import websockets
+
+async def hello(websocket):
+ name = await websocket.recv()
+ print(f"<<< {name}")
+
+ greeting = f"Hello {name}!"
+
+ await websocket.send(greeting)
+ print(f">>> {greeting}")
+
+ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+localhost_pem = pathlib.Path(__file__).with_name("localhost.pem")
+ssl_context.load_cert_chain(localhost_pem)
+
+async def main():
+ async with websockets.serve(hello, "localhost", 8765, ssl=ssl_context):
+ await asyncio.Future() # run forever
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.html b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.html
new file mode 100644
index 0000000000..b1c93b141d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>WebSocket demo</title>
+ </head>
+ <body>
+ <script src="show_time.js"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.js b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.js
new file mode 100644
index 0000000000..26bed7ec9e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.js
@@ -0,0 +1,12 @@
+window.addEventListener("DOMContentLoaded", () => {
+ const messages = document.createElement("ul");
+ document.body.appendChild(messages);
+
+ const websocket = new WebSocket("ws://localhost:5678/");
+ websocket.onmessage = ({ data }) => {
+ const message = document.createElement("li");
+ const content = document.createTextNode(data);
+ message.appendChild(content);
+ messages.appendChild(message);
+ };
+});
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.py b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.py
new file mode 100644
index 0000000000..a83078e8a9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+import asyncio
+import datetime
+import random
+import websockets
+
+async def show_time(websocket):
+ while True:
+ message = datetime.datetime.utcnow().isoformat() + "Z"
+ await websocket.send(message)
+ await asyncio.sleep(random.random() * 2 + 1)
+
+async def main():
+ async with websockets.serve(show_time, "localhost", 5678):
+ await asyncio.Future() # run forever
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time_2.py b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time_2.py
new file mode 100644
index 0000000000..08e87f5931
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time_2.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+
+import asyncio
+import datetime
+import random
+import websockets
+
+CONNECTIONS = set()
+
+async def register(websocket):
+ CONNECTIONS.add(websocket)
+ try:
+ await websocket.wait_closed()
+ finally:
+ CONNECTIONS.remove(websocket)
+
+async def show_time():
+ while True:
+ message = datetime.datetime.utcnow().isoformat() + "Z"
+ websockets.broadcast(CONNECTIONS, message)
+ await asyncio.sleep(random.random() * 2 + 1)
+
+async def main():
+ async with websockets.serve(register, "localhost", 5678):
+ await show_time()
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.css b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.css
new file mode 100644
index 0000000000..27f0baf6e4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.css
@@ -0,0 +1,105 @@
+/* General layout */
+
+body {
+ background-color: white;
+ display: flex;
+ flex-direction: column-reverse;
+ justify-content: center;
+ align-items: center;
+ margin: 0;
+ min-height: 100vh;
+}
+
+/* Action buttons */
+
+.actions {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-evenly;
+ align-items: flex-end;
+ width: 720px;
+ height: 100px;
+}
+
+.action {
+ color: darkgray;
+ font-family: "Helvetica Neue", sans-serif;
+ font-size: 20px;
+ line-height: 20px;
+ font-weight: 300;
+ text-align: center;
+ text-decoration: none;
+ text-transform: uppercase;
+ padding: 20px;
+ width: 120px;
+}
+
+.action:hover {
+ background-color: darkgray;
+ color: white;
+ font-weight: 700;
+}
+
+.action[href=""] {
+ display: none;
+}
+
+/* Connect Four board */
+
+.board {
+ background-color: blue;
+ display: flex;
+ flex-direction: row;
+ padding: 0 10px;
+ position: relative;
+}
+
+.board::before,
+.board::after {
+ background-color: blue;
+ content: "";
+ height: 720px;
+ width: 20px;
+ position: absolute;
+}
+
+.board::before {
+ left: -20px;
+}
+
+.board::after {
+ right: -20px;
+}
+
+.column {
+ display: flex;
+ flex-direction: column-reverse;
+ padding: 10px;
+}
+
+.cell {
+ border-radius: 50%;
+ width: 80px;
+ height: 80px;
+ margin: 10px 0;
+}
+
+.empty {
+ background-color: white;
+}
+
+.column:hover .empty {
+ background-color: lightgray;
+}
+
+.column:hover .empty ~ .empty {
+ background-color: white;
+}
+
+.red {
+ background-color: red;
+}
+
+.yellow {
+ background-color: yellow;
+}
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.js b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.js
new file mode 100644
index 0000000000..cb5eb9fa27
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.js
@@ -0,0 +1,45 @@
+const PLAYER1 = "red";
+
+const PLAYER2 = "yellow";
+
+function createBoard(board) {
+ // Inject stylesheet.
+ const linkElement = document.createElement("link");
+ linkElement.href = import.meta.url.replace(".js", ".css");
+ linkElement.rel = "stylesheet";
+ document.head.append(linkElement);
+ // Generate board.
+ for (let column = 0; column < 7; column++) {
+ const columnElement = document.createElement("div");
+ columnElement.className = "column";
+ columnElement.dataset.column = column;
+ for (let row = 0; row < 6; row++) {
+ const cellElement = document.createElement("div");
+ cellElement.className = "cell empty";
+ cellElement.dataset.column = column;
+ columnElement.append(cellElement);
+ }
+ board.append(columnElement);
+ }
+}
+
+function playMove(board, player, column, row) {
+ // Check values of arguments.
+ if (player !== PLAYER1 && player !== PLAYER2) {
+ throw new Error(`player must be ${PLAYER1} or ${PLAYER2}.`);
+ }
+ const columnElement = board.querySelectorAll(".column")[column];
+ if (columnElement === undefined) {
+ throw new RangeError("column must be between 0 and 6.");
+ }
+ const cellElement = columnElement.querySelectorAll(".cell")[row];
+ if (cellElement === undefined) {
+ throw new RangeError("row must be between 0 and 5.");
+ }
+ // Place checker in cell.
+ if (!cellElement.classList.replace("empty", player)) {
+ throw new Error("cell must be empty.");
+ }
+}
+
+export { PLAYER1, PLAYER2, createBoard, playMove };
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.py b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.py
new file mode 100644
index 0000000000..0a61e7c7ee
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.py
@@ -0,0 +1,62 @@
+__all__ = ["PLAYER1", "PLAYER2", "Connect4"]
+
+PLAYER1, PLAYER2 = "red", "yellow"
+
+
+class Connect4:
+ """
+ A Connect Four game.
+
+ Play moves with :meth:`play`.
+
+ Get past moves with :attr:`moves`.
+
+ Check for a victory with :attr:`winner`.
+
+ """
+
+ def __init__(self):
+ self.moves = []
+ self.top = [0 for _ in range(7)]
+ self.winner = None
+
+ @property
+ def last_player(self):
+ """
+ Player who played the last move.
+
+ """
+ return PLAYER1 if len(self.moves) % 2 else PLAYER2
+
+ @property
+ def last_player_won(self):
+ """
+ Whether the last move is winning.
+
+ """
+ b = sum(1 << (8 * column + row) for _, column, row in self.moves[::-2])
+ return any(b & b >> v & b >> 2 * v & b >> 3 * v for v in [1, 7, 8, 9])
+
+ def play(self, player, column):
+ """
+ Play a move in a column.
+
+ Returns the row where the checker lands.
+
+ Raises :exc:`RuntimeError` if the move is illegal.
+
+ """
+ if player == self.last_player:
+ raise RuntimeError("It isn't your turn.")
+
+ row = self.top[column]
+ if row == 6:
+ raise RuntimeError("This slot is full.")
+
+ self.moves.append((player, column, row))
+ self.top[column] += 1
+
+ if self.winner is None and self.last_player_won:
+ self.winner = self.last_player
+
+ return row
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/favicon.ico b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/favicon.ico
new file mode 100644
index 0000000000..602c14e4eb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/favicon.ico
@@ -0,0 +1,2 @@
+ h& ��( >��-=��v=��`@��B��U=���;���;���;���B��#J��VE���A���=���<���;���<���R��WN���I���E���E��%>���;���;���;���=��*\��VU���Q���M���N��$K��cE���A���=���D���`����q9 ^���]���Z���U��$R��dM���I���F���g��j�j1��i0��k1Cf��b���_��#[��eU���Q���N���R���t8w�k2��i0��k19d��J]���Y���U���R���|>s�v:��r7��k3�����k2u�j50f��da���]���^����Gk�~@��z>��w<Ҧs@�l2��q��zE�h��;p��ēN��I���C��Bѳ�@�x<��o5��M��o7�ȔOJ��M���J���F(�A��z=��v:��p5ɪw3ϘS%ŒP���L���JﳂC��~A��{>ɪwDʖS�ŒP���M���J���CŻ�D͗UBƔPyÏMY̙f����������������( @ <��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��U��U��U��U��U��U��U��U��U��B��<���<���<���;���<���<��M<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��A��3<���;���;���;���;���;���;���;���U��U��U��U��U��U��U��U��U��U��U��U��U��U��U��>��>��>��>��>��>��>��F��3A���?���>���;���;���;���;���;���;���<���<��<��<��<��<��<��<��<��<��<��<��<��<��<��=��=��=��=��=��=��J��4F���D���B���?���>���<���;���;���;���;���;���>��->��>��>��>��>��>��>��>��>��>��>��>��>��<��<��<��<��<��M��5J���G���E���C���B���@���=���<���;���;���;���;���=���=��=��=��=��=��=��=��=��=��=��=��=��=��U��U��U��U��R��5N���K���I���G���E���C���B���B��A��'<���;���;���;���;���;���<��Y<��<��<��<��<��<��<��<��<��<��<��<��<��<��U��6S���Q���N���M���K���I���H���H��H��B��U@���>���<���;���;���;���;���<���U��U��U��U��U��U��U��U��U��U���j2�j2Z��6V���T���R���P���O���M���L���L��L��H��JE���A���@���=���<���;���;���;���;���<���<��<��<��<��<��<��<��<��<���k2^��1Z���X���V���T���S���P���P���P��P��K��KH���F���C���B���@���=���<���@���a���}�k��zOŜj2$�j2�j2�j2�j2�j2�j2�j2�j2�i1_���^���\���Z���X���V���V���V��V��M��LL���I���G���F���C���B���@����m��i1��i0��i0��i0��k2u�k2�k2�k2�k2�k2�k2�k2�k2�k1b���_���]���[���Z���Z���Z��Z��T��LR���N���M���J���I���H���G��sG���r6/�j0��i0��i0��i0��i1��i1�i1�i1�i1�i1�i1�i1�i1�k1d���a���`���]���]���]��]��V��MU���R���Q���N���M���K���J��rJ��J���r7p�l2��j1��i0��i0��k1��k1�k1�k1�k1�k1�k1�k1�k1��U���f��sa���a��_a��a��\��NW���V���T���R���P���O���M��qM��M���t8e�q6��o4��l2��j1��i0��k1X�k1�k1�k1�k1�k1�k1�k1�k1��U��U��U��U��U��Ub��F^���\���Z���X���W���T���T��pT��T���{<f�w;��s8��q6��o5��k2��j1�q9 �q9�q9�j2W��U��U��U��U��U�i0�i0�i0�i0�i0�i0a���_���^���[���Z���X���W��pW��W���~@g�z>��x<��v:��s8��r7��p5��l2B�l2�l2�j1}�i0��j0���U��U��U��U�i0�i0�i0�i0�i0���d���a���`���^���[���\��o\��\����Eg��B��~@��z=��y<��v:��t9��r7S�r7�r7�k3~�i0��|J��i0��i0��i0�i0�i0�j1�j1�j1�j1�j1�j1g���c���a���_���_��n_��_����JS��E���C���B��|?��z=��x<��v;R�v;�v;�n4�l2���\�����è���i0�i0�i0�i0�k1�k1�k1�k1�k1�k1���i��ff���d��Bd��d��ĝN
+��I���H���E���C��A��|?��~?Q�~?�~?�t8�q6��n4��t=�Ѽ���N��j1љj1�j1�j1�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5M`��J���H���G���D���C���BQ��B��B�z>��t9��s8��q6��m3��l3��k1��k19�k1�k1�k1�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5ÐO���M���J���H���G���G^��G��G�}?��z=��y<��u9��s8��o5��o4��o5>�o5�o5�o5�o5�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;ƑO�ÐO���L���J���I���G2��G��C���B��|?��z=��v:��u9��t7��s5>�s5�s5�s5�s5�s5�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?ʕTtőO�ÐN���M���I���I귆Gʵ�D���C��~@��|?��z=��x<�u;=�u;�u;�u;�u;�u;�u;��D��D��D��D��D��D��D��D��D��D��D��D̙W#ɖS�ŒP�ÐN���K���J���H���F���D���C��}@��|?�}?=�}?�}?�}?�}?�}?�}?�}?��I��I��I��I��I��I��I��I��I��I��I��I��I̘T�ǓQ�őO�ÏN���L���J���I���F���D���C�D<��D��D��D��D��D��D��D��D’U’U’U’U’U’U’U’U’U’U’U’U’U��U̗T�ǔQ�ŒP�ÏN���K���J���H���F鶄I8��I��I��I��I��I��I��I��I��I˘WOȓQ�œP�ÑN���Lھ�L�’U’U’U’U’U’U’U’U’U’U’U�����������������?��?����������0��`������@������������ ��0 ����0�� ��?�����������������������
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/app.py
new file mode 100644
index 0000000000..3b0fbd7868
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/app.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+
+import asyncio
+import itertools
+import json
+
+import websockets
+
+from connect4 import PLAYER1, PLAYER2, Connect4
+
+
+async def handler(websocket):
+ # Initialize a Connect Four game.
+ game = Connect4()
+
+ # Players take alternate turns, using the same browser.
+ turns = itertools.cycle([PLAYER1, PLAYER2])
+ player = next(turns)
+
+ async for message in websocket:
+ # Parse a "play" event from the UI.
+ event = json.loads(message)
+ assert event["type"] == "play"
+ column = event["column"]
+
+ try:
+ # Play the move.
+ row = game.play(player, column)
+ except RuntimeError as exc:
+ # Send an "error" event if the move was illegal.
+ event = {
+ "type": "error",
+ "message": str(exc),
+ }
+ await websocket.send(json.dumps(event))
+ continue
+
+ # Send a "play" event to update the UI.
+ event = {
+ "type": "play",
+ "player": player,
+ "column": column,
+ "row": row,
+ }
+ await websocket.send(json.dumps(event))
+
+ # If move is winning, send a "win" event.
+ if game.winner is not None:
+ event = {
+ "type": "win",
+ "player": game.winner,
+ }
+ await websocket.send(json.dumps(event))
+
+ # Alternate turns.
+ player = next(turns)
+
+
+async def main():
+ async with websockets.serve(handler, "", 8001):
+ await asyncio.Future() # run forever
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/index.html b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/index.html
new file mode 100644
index 0000000000..8e38e89922
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/index.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Connect Four</title>
+ </head>
+ <body>
+ <div class="board"></div>
+ <script src="main.js" type="module"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/main.js b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/main.js
new file mode 100644
index 0000000000..dd28f9a6a8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/main.js
@@ -0,0 +1,53 @@
+import { createBoard, playMove } from "./connect4.js";
+
+function showMessage(message) {
+ window.setTimeout(() => window.alert(message), 50);
+}
+
+function receiveMoves(board, websocket) {
+ websocket.addEventListener("message", ({ data }) => {
+ const event = JSON.parse(data);
+ switch (event.type) {
+ case "play":
+ // Update the UI with the move.
+ playMove(board, event.player, event.column, event.row);
+ break;
+ case "win":
+ showMessage(`Player ${event.player} wins!`);
+ // No further messages are expected; close the WebSocket connection.
+ websocket.close(1000);
+ break;
+ case "error":
+ showMessage(event.message);
+ break;
+ default:
+ throw new Error(`Unsupported event type: ${event.type}.`);
+ }
+ });
+}
+
+function sendMoves(board, websocket) {
+ // When clicking a column, send a "play" event for a move in that column.
+ board.addEventListener("click", ({ target }) => {
+ const column = target.dataset.column;
+ // Ignore clicks outside a column.
+ if (column === undefined) {
+ return;
+ }
+ const event = {
+ type: "play",
+ column: parseInt(column, 10),
+ };
+ websocket.send(JSON.stringify(event));
+ });
+}
+
+window.addEventListener("DOMContentLoaded", () => {
+ // Initialize the UI.
+ const board = document.querySelector(".board");
+ createBoard(board);
+ // Open the WebSocket connection and register event handlers.
+ const websocket = new WebSocket("ws://localhost:8001/");
+ receiveMoves(board, websocket);
+ sendMoves(board, websocket);
+});
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/app.py
new file mode 100644
index 0000000000..2693d4304d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/app.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python
+
+import asyncio
+import json
+import secrets
+
+import websockets
+
+from connect4 import PLAYER1, PLAYER2, Connect4
+
+
+JOIN = {}
+
+WATCH = {}
+
+
+async def error(websocket, message):
+ """
+ Send an error message.
+
+ """
+ event = {
+ "type": "error",
+ "message": message,
+ }
+ await websocket.send(json.dumps(event))
+
+
+async def replay(websocket, game):
+ """
+ Send previous moves.
+
+ """
+ # Make a copy to avoid an exception if game.moves changes while iteration
+ # is in progress. If a move is played while replay is running, moves will
+ # be sent out of order but each move will be sent once and eventually the
+ # UI will be consistent.
+ for player, column, row in game.moves.copy():
+ event = {
+ "type": "play",
+ "player": player,
+ "column": column,
+ "row": row,
+ }
+ await websocket.send(json.dumps(event))
+
+
+async def play(websocket, game, player, connected):
+ """
+ Receive and process moves from a player.
+
+ """
+ async for message in websocket:
+ # Parse a "play" event from the UI.
+ event = json.loads(message)
+ assert event["type"] == "play"
+ column = event["column"]
+
+ try:
+ # Play the move.
+ row = game.play(player, column)
+ except RuntimeError as exc:
+ # Send an "error" event if the move was illegal.
+ await error(websocket, str(exc))
+ continue
+
+ # Send a "play" event to update the UI.
+ event = {
+ "type": "play",
+ "player": player,
+ "column": column,
+ "row": row,
+ }
+ websockets.broadcast(connected, json.dumps(event))
+
+ # If move is winning, send a "win" event.
+ if game.winner is not None:
+ event = {
+ "type": "win",
+ "player": game.winner,
+ }
+ websockets.broadcast(connected, json.dumps(event))
+
+
+async def start(websocket):
+ """
+ Handle a connection from the first player: start a new game.
+
+ """
+ # Initialize a Connect Four game, the set of WebSocket connections
+ # receiving moves from this game, and secret access tokens.
+ game = Connect4()
+ connected = {websocket}
+
+ join_key = secrets.token_urlsafe(12)
+ JOIN[join_key] = game, connected
+
+ watch_key = secrets.token_urlsafe(12)
+ WATCH[watch_key] = game, connected
+
+ try:
+ # Send the secret access tokens to the browser of the first player,
+ # where they'll be used for building "join" and "watch" links.
+ event = {
+ "type": "init",
+ "join": join_key,
+ "watch": watch_key,
+ }
+ await websocket.send(json.dumps(event))
+ # Receive and process moves from the first player.
+ await play(websocket, game, PLAYER1, connected)
+ finally:
+ del JOIN[join_key]
+ del WATCH[watch_key]
+
+
+async def join(websocket, join_key):
+ """
+ Handle a connection from the second player: join an existing game.
+
+ """
+ # Find the Connect Four game.
+ try:
+ game, connected = JOIN[join_key]
+ except KeyError:
+ await error(websocket, "Game not found.")
+ return
+
+ # Register to receive moves from this game.
+ connected.add(websocket)
+ try:
+ # Send the first move, in case the first player already played it.
+ await replay(websocket, game)
+ # Receive and process moves from the second player.
+ await play(websocket, game, PLAYER2, connected)
+ finally:
+ connected.remove(websocket)
+
+
+async def watch(websocket, watch_key):
+ """
+ Handle a connection from a spectator: watch an existing game.
+
+ """
+ # Find the Connect Four game.
+ try:
+ game, connected = WATCH[watch_key]
+ except KeyError:
+ await error(websocket, "Game not found.")
+ return
+
+ # Register to receive moves from this game.
+ connected.add(websocket)
+ try:
+ # Send previous moves, in case the game already started.
+ await replay(websocket, game)
+ # Keep the connection open, but don't receive any messages.
+ await websocket.wait_closed()
+ finally:
+ connected.remove(websocket)
+
+
+async def handler(websocket):
+ """
+ Handle a connection and dispatch it according to who is connecting.
+
+ """
+ # Receive and parse the "init" event from the UI.
+ message = await websocket.recv()
+ event = json.loads(message)
+ assert event["type"] == "init"
+
+ if "join" in event:
+ # Second player joins an existing game.
+ await join(websocket, event["join"])
+ elif "watch" in event:
+ # Spectator watches an existing game.
+ await watch(websocket, event["watch"])
+ else:
+ # First player starts a new game.
+ await start(websocket)
+
+
+async def main():
+ async with websockets.serve(handler, "", 8001):
+ await asyncio.Future() # run forever
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/index.html b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/index.html
new file mode 100644
index 0000000000..1a16f72a25
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Connect Four</title>
+ </head>
+ <body>
+ <div class="actions">
+ <a class="action new" href="/">New</a>
+ <a class="action join" href="">Join</a>
+ <a class="action watch" href="">Watch</a>
+ </div>
+ <div class="board"></div>
+ <script src="main.js" type="module"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/main.js b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/main.js
new file mode 100644
index 0000000000..d38a0140ac
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/main.js
@@ -0,0 +1,83 @@
+import { createBoard, playMove } from "./connect4.js";
+
+function initGame(websocket) {
+ websocket.addEventListener("open", () => {
+ // Send an "init" event according to who is connecting.
+ const params = new URLSearchParams(window.location.search);
+ let event = { type: "init" };
+ if (params.has("join")) {
+ // Second player joins an existing game.
+ event.join = params.get("join");
+ } else if (params.has("watch")) {
+ // Spectator watches an existing game.
+ event.watch = params.get("watch");
+ } else {
+ // First player starts a new game.
+ }
+ websocket.send(JSON.stringify(event));
+ });
+}
+
+function showMessage(message) {
+ window.setTimeout(() => window.alert(message), 50);
+}
+
+function receiveMoves(board, websocket) {
+ websocket.addEventListener("message", ({ data }) => {
+ const event = JSON.parse(data);
+ switch (event.type) {
+ case "init":
+ // Create links for inviting the second player and spectators.
+ document.querySelector(".join").href = "?join=" + event.join;
+ document.querySelector(".watch").href = "?watch=" + event.watch;
+ break;
+ case "play":
+ // Update the UI with the move.
+ playMove(board, event.player, event.column, event.row);
+ break;
+ case "win":
+ showMessage(`Player ${event.player} wins!`);
+ // No further messages are expected; close the WebSocket connection.
+ websocket.close(1000);
+ break;
+ case "error":
+ showMessage(event.message);
+ break;
+ default:
+ throw new Error(`Unsupported event type: ${event.type}.`);
+ }
+ });
+}
+
+function sendMoves(board, websocket) {
+ // Don't send moves for a spectator watching a game.
+ const params = new URLSearchParams(window.location.search);
+ if (params.has("watch")) {
+ return;
+ }
+
+ // When clicking a column, send a "play" event for a move in that column.
+ board.addEventListener("click", ({ target }) => {
+ const column = target.dataset.column;
+ // Ignore clicks outside a column.
+ if (column === undefined) {
+ return;
+ }
+ const event = {
+ type: "play",
+ column: parseInt(column, 10),
+ };
+ websocket.send(JSON.stringify(event));
+ });
+}
+
+window.addEventListener("DOMContentLoaded", () => {
+ // Initialize the UI.
+ const board = document.querySelector(".board");
+ createBoard(board);
+ // Open the WebSocket connection and register event handlers.
+ const websocket = new WebSocket("ws://localhost:8001/");
+ initGame(websocket);
+ receiveMoves(board, websocket);
+ sendMoves(board, websocket);
+});
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/Procfile b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/Procfile
new file mode 100644
index 0000000000..2e35818f67
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/Procfile
@@ -0,0 +1 @@
+web: python app.py
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/app.py
new file mode 100644
index 0000000000..c2ee020d20
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/app.py
@@ -0,0 +1,198 @@
+#!/usr/bin/env python
+
+import asyncio
+import json
+import os
+import secrets
+import signal
+
+import websockets
+
+from connect4 import PLAYER1, PLAYER2, Connect4
+
+
+JOIN = {}
+
+WATCH = {}
+
+
+async def error(websocket, message):
+ """
+ Send an error message.
+
+ """
+ event = {
+ "type": "error",
+ "message": message,
+ }
+ await websocket.send(json.dumps(event))
+
+
+async def replay(websocket, game):
+ """
+ Send previous moves.
+
+ """
+ # Make a copy to avoid an exception if game.moves changes while iteration
+ # is in progress. If a move is played while replay is running, moves will
+ # be sent out of order but each move will be sent once and eventually the
+ # UI will be consistent.
+ for player, column, row in game.moves.copy():
+ event = {
+ "type": "play",
+ "player": player,
+ "column": column,
+ "row": row,
+ }
+ await websocket.send(json.dumps(event))
+
+
+async def play(websocket, game, player, connected):
+ """
+ Receive and process moves from a player.
+
+ """
+ async for message in websocket:
+ # Parse a "play" event from the UI.
+ event = json.loads(message)
+ assert event["type"] == "play"
+ column = event["column"]
+
+ try:
+ # Play the move.
+ row = game.play(player, column)
+ except RuntimeError as exc:
+ # Send an "error" event if the move was illegal.
+ await error(websocket, str(exc))
+ continue
+
+ # Send a "play" event to update the UI.
+ event = {
+ "type": "play",
+ "player": player,
+ "column": column,
+ "row": row,
+ }
+ websockets.broadcast(connected, json.dumps(event))
+
+ # If move is winning, send a "win" event.
+ if game.winner is not None:
+ event = {
+ "type": "win",
+ "player": game.winner,
+ }
+ websockets.broadcast(connected, json.dumps(event))
+
+
+async def start(websocket):
+ """
+ Handle a connection from the first player: start a new game.
+
+ """
+ # Initialize a Connect Four game, the set of WebSocket connections
+ # receiving moves from this game, and secret access tokens.
+ game = Connect4()
+ connected = {websocket}
+
+ join_key = secrets.token_urlsafe(12)
+ JOIN[join_key] = game, connected
+
+ watch_key = secrets.token_urlsafe(12)
+ WATCH[watch_key] = game, connected
+
+ try:
+ # Send the secret access tokens to the browser of the first player,
+ # where they'll be used for building "join" and "watch" links.
+ event = {
+ "type": "init",
+ "join": join_key,
+ "watch": watch_key,
+ }
+ await websocket.send(json.dumps(event))
+ # Receive and process moves from the first player.
+ await play(websocket, game, PLAYER1, connected)
+ finally:
+ del JOIN[join_key]
+ del WATCH[watch_key]
+
+
+async def join(websocket, join_key):
+ """
+ Handle a connection from the second player: join an existing game.
+
+ """
+ # Find the Connect Four game.
+ try:
+ game, connected = JOIN[join_key]
+ except KeyError:
+ await error(websocket, "Game not found.")
+ return
+
+ # Register to receive moves from this game.
+ connected.add(websocket)
+ try:
+ # Send the first move, in case the first player already played it.
+ await replay(websocket, game)
+ # Receive and process moves from the second player.
+ await play(websocket, game, PLAYER2, connected)
+ finally:
+ connected.remove(websocket)
+
+
+async def watch(websocket, watch_key):
+ """
+ Handle a connection from a spectator: watch an existing game.
+
+ """
+ # Find the Connect Four game.
+ try:
+ game, connected = WATCH[watch_key]
+ except KeyError:
+ await error(websocket, "Game not found.")
+ return
+
+ # Register to receive moves from this game.
+ connected.add(websocket)
+ try:
+ # Send previous moves, in case the game already started.
+ await replay(websocket, game)
+ # Keep the connection open, but don't receive any messages.
+ await websocket.wait_closed()
+ finally:
+ connected.remove(websocket)
+
+
+async def handler(websocket):
+ """
+ Handle a connection and dispatch it according to who is connecting.
+
+ """
+ # Receive and parse the "init" event from the UI.
+ message = await websocket.recv()
+ event = json.loads(message)
+ assert event["type"] == "init"
+
+ if "join" in event:
+ # Second player joins an existing game.
+ await join(websocket, event["join"])
+ elif "watch" in event:
+ # Spectator watches an existing game.
+ await watch(websocket, event["watch"])
+ else:
+ # First player starts a new game.
+ await start(websocket)
+
+
+async def main():
+ # Set the stop condition when receiving SIGTERM.
+ loop = asyncio.get_running_loop()
+ stop = loop.create_future()
+ loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
+
+ port = int(os.environ.get("PORT", "8001"))
+ async with websockets.serve(handler, "", port):
+ await stop
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/index.html b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/index.html
new file mode 100644
index 0000000000..1a16f72a25
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Connect Four</title>
+ </head>
+ <body>
+ <div class="actions">
+ <a class="action new" href="/">New</a>
+ <a class="action join" href="">Join</a>
+ <a class="action watch" href="">Watch</a>
+ </div>
+ <div class="board"></div>
+ <script src="main.js" type="module"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/main.js b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/main.js
new file mode 100644
index 0000000000..3000fa2f78
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/main.js
@@ -0,0 +1,93 @@
+import { createBoard, playMove } from "./connect4.js";
+
+function getWebSocketServer() {
+ if (window.location.host === "python-websockets.github.io") {
+ return "wss://websockets-tutorial.herokuapp.com/";
+ } else if (window.location.host === "localhost:8000") {
+ return "ws://localhost:8001/";
+ } else {
+ throw new Error(`Unsupported host: ${window.location.host}`);
+ }
+}
+
+function initGame(websocket) {
+ websocket.addEventListener("open", () => {
+ // Send an "init" event according to who is connecting.
+ const params = new URLSearchParams(window.location.search);
+ let event = { type: "init" };
+ if (params.has("join")) {
+ // Second player joins an existing game.
+ event.join = params.get("join");
+ } else if (params.has("watch")) {
+ // Spectator watches an existing game.
+ event.watch = params.get("watch");
+ } else {
+ // First player starts a new game.
+ }
+ websocket.send(JSON.stringify(event));
+ });
+}
+
+function showMessage(message) {
+ window.setTimeout(() => window.alert(message), 50);
+}
+
+function receiveMoves(board, websocket) {
+ websocket.addEventListener("message", ({ data }) => {
+ const event = JSON.parse(data);
+ switch (event.type) {
+ case "init":
+ // Create links for inviting the second player and spectators.
+ document.querySelector(".join").href = "?join=" + event.join;
+ document.querySelector(".watch").href = "?watch=" + event.watch;
+ break;
+ case "play":
+ // Update the UI with the move.
+ playMove(board, event.player, event.column, event.row);
+ break;
+ case "win":
+ showMessage(`Player ${event.player} wins!`);
+ // No further messages are expected; close the WebSocket connection.
+ websocket.close(1000);
+ break;
+ case "error":
+ showMessage(event.message);
+ break;
+ default:
+ throw new Error(`Unsupported event type: ${event.type}.`);
+ }
+ });
+}
+
+function sendMoves(board, websocket) {
+ // Don't send moves for a spectator watching a game.
+ const params = new URLSearchParams(window.location.search);
+ if (params.has("watch")) {
+ return;
+ }
+
+ // When clicking a column, send a "play" event for a move in that column.
+ board.addEventListener("click", ({ target }) => {
+ const column = target.dataset.column;
+ // Ignore clicks outside a column.
+ if (column === undefined) {
+ return;
+ }
+ const event = {
+ type: "play",
+ column: parseInt(column, 10),
+ };
+ websocket.send(JSON.stringify(event));
+ });
+}
+
+window.addEventListener("DOMContentLoaded", () => {
+ // Initialize the UI.
+ const board = document.querySelector(".board");
+ createBoard(board);
+ // Open the WebSocket connection and register event handlers.
+ const websocket = new WebSocket(getWebSocketServer());
+ initGame(websocket);
+ receiveMoves(board, websocket);
+ sendMoves(board, websocket);
+});
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/requirements.txt b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/requirements.txt
new file mode 100644
index 0000000000..14774b465e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/requirements.txt
@@ -0,0 +1 @@
+websockets
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/app.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/app.py
new file mode 100644
index 0000000000..039e21174b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/app.py
@@ -0,0 +1,226 @@
+#!/usr/bin/env python
+
+import asyncio
+import http
+import http.cookies
+import pathlib
+import signal
+import urllib.parse
+import uuid
+
+import websockets
+from websockets.frames import CloseCode
+
+
+# User accounts database
+
+USERS = {}
+
+
+def create_token(user, lifetime=1):
+ """Create token for user and delete it once its lifetime is over."""
+ token = uuid.uuid4().hex
+ USERS[token] = user
+ asyncio.get_running_loop().call_later(lifetime, USERS.pop, token)
+ return token
+
+
+def get_user(token):
+ """Find user authenticated by token or return None."""
+ return USERS.get(token)
+
+
+# Utilities
+
+
+def get_cookie(raw, key):
+ cookie = http.cookies.SimpleCookie(raw)
+ morsel = cookie.get(key)
+ if morsel is not None:
+ return morsel.value
+
+
+def get_query_param(path, key):
+ query = urllib.parse.urlparse(path).query
+ params = urllib.parse.parse_qs(query)
+ values = params.get(key, [])
+ if len(values) == 1:
+ return values[0]
+
+
+# Main HTTP server
+
+CONTENT_TYPES = {
+ ".css": "text/css",
+ ".html": "text/html; charset=utf-8",
+ ".ico": "image/x-icon",
+ ".js": "text/javascript",
+}
+
+
+async def serve_html(path, request_headers):
+ user = get_query_param(path, "user")
+ path = urllib.parse.urlparse(path).path
+ if path == "/":
+ if user is None:
+ page = "index.html"
+ else:
+ page = "test.html"
+ else:
+ page = path[1:]
+
+ try:
+ template = pathlib.Path(__file__).with_name(page)
+ except ValueError:
+ pass
+ else:
+ if template.is_file():
+ headers = {"Content-Type": CONTENT_TYPES[template.suffix]}
+ body = template.read_bytes()
+ if user is not None:
+ token = create_token(user)
+ body = body.replace(b"TOKEN", token.encode())
+ return http.HTTPStatus.OK, headers, body
+
+ return http.HTTPStatus.NOT_FOUND, {}, b"Not found\n"
+
+
+async def noop_handler(websocket):
+ pass
+
+
+# Send credentials as the first message in the WebSocket connection
+
+
+async def first_message_handler(websocket):
+ token = await websocket.recv()
+ user = get_user(token)
+ if user is None:
+ await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed")
+ return
+
+ await websocket.send(f"Hello {user}!")
+ message = await websocket.recv()
+ assert message == f"Goodbye {user}."
+
+
+# Add credentials to the WebSocket URI in a query parameter
+
+
+class QueryParamProtocol(websockets.WebSocketServerProtocol):
+ async def process_request(self, path, headers):
+ token = get_query_param(path, "token")
+ if token is None:
+ return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n"
+
+ user = get_user(token)
+ if user is None:
+ return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n"
+
+ self.user = user
+
+
+async def query_param_handler(websocket):
+ user = websocket.user
+
+ await websocket.send(f"Hello {user}!")
+ message = await websocket.recv()
+ assert message == f"Goodbye {user}."
+
+
+# Set a cookie on the domain of the WebSocket URI
+
+
+class CookieProtocol(websockets.WebSocketServerProtocol):
+ async def process_request(self, path, headers):
+ if "Upgrade" not in headers:
+ template = pathlib.Path(__file__).with_name(path[1:])
+ headers = {"Content-Type": CONTENT_TYPES[template.suffix]}
+ body = template.read_bytes()
+ return http.HTTPStatus.OK, headers, body
+
+ token = get_cookie(headers.get("Cookie", ""), "token")
+ if token is None:
+ return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n"
+
+ user = get_user(token)
+ if user is None:
+ return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n"
+
+ self.user = user
+
+
+async def cookie_handler(websocket):
+ user = websocket.user
+
+ await websocket.send(f"Hello {user}!")
+ message = await websocket.recv()
+ assert message == f"Goodbye {user}."
+
+
+# Adding credentials to the WebSocket URI in user information
+
+
+class UserInfoProtocol(websockets.BasicAuthWebSocketServerProtocol):
+ async def check_credentials(self, username, password):
+ if username != "token":
+ return False
+
+ user = get_user(password)
+ if user is None:
+ return False
+
+ self.user = user
+ return True
+
+
+async def user_info_handler(websocket):
+ user = websocket.user
+
+ await websocket.send(f"Hello {user}!")
+ message = await websocket.recv()
+ assert message == f"Goodbye {user}."
+
+
+# Start all five servers
+
+
+async def main():
+ # Set the stop condition when receiving SIGINT or SIGTERM.
+ loop = asyncio.get_running_loop()
+ stop = loop.create_future()
+ loop.add_signal_handler(signal.SIGINT, stop.set_result, None)
+ loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
+
+ async with websockets.serve(
+ noop_handler,
+ host="",
+ port=8000,
+ process_request=serve_html,
+ ), websockets.serve(
+ first_message_handler,
+ host="",
+ port=8001,
+ ), websockets.serve(
+ query_param_handler,
+ host="",
+ port=8002,
+ create_protocol=QueryParamProtocol,
+ ), websockets.serve(
+ cookie_handler,
+ host="",
+ port=8003,
+ create_protocol=CookieProtocol,
+ ), websockets.serve(
+ user_info_handler,
+ host="",
+ port=8004,
+ create_protocol=UserInfoProtocol,
+ ):
+ print("Running on http://localhost:8000/")
+ await stop
+ print("\rExiting")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie.html b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie.html
new file mode 100644
index 0000000000..ca17358fd0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Cookie | WebSocket Authentication</title>
+ <link href="style.css" rel="stylesheet">
+ </head>
+ <body class="test">
+ <p class="test">[??] Cookie</p>
+ <p class="ok">[OK] Cookie</p>
+ <p class="ko">[KO] Cookie</p>
+ <script src="script.js"></script>
+ <script src="cookie.js"></script>
+ <iframe src="http://localhost:8003/cookie_iframe.html" style="display: none;"></iframe>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie.js b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie.js
new file mode 100644
index 0000000000..2cca34fcbb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie.js
@@ -0,0 +1,23 @@
+// send token to iframe
+window.addEventListener("DOMContentLoaded", () => {
+ const iframe = document.querySelector("iframe");
+ iframe.addEventListener("load", () => {
+ iframe.contentWindow.postMessage(token, "http://localhost:8003");
+ });
+});
+
+// once iframe has set cookie, open WebSocket connection
+window.addEventListener("message", ({ origin }) => {
+ if (origin !== "http://localhost:8003") {
+ return;
+ }
+
+ const websocket = new WebSocket("ws://localhost:8003/");
+
+ websocket.onmessage = ({ data }) => {
+ // event.data is expected to be "Hello <user>!"
+ websocket.send(`Goodbye ${data.slice(6, -1)}.`);
+ };
+
+ runTest(websocket);
+});
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.html b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.html
new file mode 100644
index 0000000000..9f49ebb9a0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Cookie iframe | WebSocket Authentication</title>
+ </head>
+ <body>
+ <script src="cookie_iframe.js"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.js b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.js
new file mode 100644
index 0000000000..2d2e692e8d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.js
@@ -0,0 +1,9 @@
+// receive token from the parent window, set cookie and notify parent
+window.addEventListener("message", ({ origin, data }) => {
+ if (origin !== "http://localhost:8000") {
+ return;
+ }
+
+ document.cookie = `token=${data}; SameSite=Strict`;
+ window.parent.postMessage("", "http://localhost:8000");
+});
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/first_message.html b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/first_message.html
new file mode 100644
index 0000000000..4dc511a176
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/first_message.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>First message | WebSocket Authentication</title>
+ <link href="style.css" rel="stylesheet">
+ </head>
+ <body class="test">
+ <p class="test">[??] First message</p>
+ <p class="ok">[OK] First message</p>
+ <p class="ko">[KO] First message</p>
+ <script src="script.js"></script>
+ <script src="first_message.js"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/first_message.js b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/first_message.js
new file mode 100644
index 0000000000..1acf048baf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/first_message.js
@@ -0,0 +1,11 @@
+window.addEventListener("DOMContentLoaded", () => {
+ const websocket = new WebSocket("ws://localhost:8001/");
+ websocket.onopen = () => websocket.send(token);
+
+ websocket.onmessage = ({ data }) => {
+ // event.data is expected to be "Hello <user>!"
+ websocket.send(`Goodbye ${data.slice(6, -1)}.`);
+ };
+
+ runTest(websocket);
+});
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/index.html b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/index.html
new file mode 100644
index 0000000000..c37deef270
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>WebSocket Authentication</title>
+ <link href="style.css" rel="stylesheet">
+ </head>
+ <body>
+ <form method="GET">
+ <input name="user" placeholder="username">
+ </form>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/query_param.html b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/query_param.html
new file mode 100644
index 0000000000..27aa454a40
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/query_param.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Query parameter | WebSocket Authentication</title>
+ <link href="style.css" rel="stylesheet">
+ </head>
+ <body class="test">
+ <p class="test">[??] Query parameter</p>
+ <p class="ok">[OK] Query parameter</p>
+ <p class="ko">[KO] Query parameter</p>
+ <script src="script.js"></script>
+ <script src="query_param.js"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/query_param.js b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/query_param.js
new file mode 100644
index 0000000000..6a54d0b6ca
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/query_param.js
@@ -0,0 +1,11 @@
+window.addEventListener("DOMContentLoaded", () => {
+ const uri = `ws://localhost:8002/?token=${token}`;
+ const websocket = new WebSocket(uri);
+
+ websocket.onmessage = ({ data }) => {
+ // event.data is expected to be "Hello <user>!"
+ websocket.send(`Goodbye ${data.slice(6, -1)}.`);
+ };
+
+ runTest(websocket);
+});
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/script.js b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/script.js
new file mode 100644
index 0000000000..ec4e5e6709
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/script.js
@@ -0,0 +1,51 @@
+var token = window.parent.token;
+
+function getExpectedEvents() {
+ return [
+ {
+ type: "open",
+ },
+ {
+ type: "message",
+ data: `Hello ${window.parent.user}!`,
+ },
+ {
+ type: "close",
+ code: 1000,
+ reason: "",
+ wasClean: true,
+ },
+ ];
+}
+
+function isEqual(expected, actual) {
+ // good enough for our purposes here!
+ return JSON.stringify(expected) === JSON.stringify(actual);
+}
+
+function testStep(expected, actual) {
+ if (isEqual(expected, actual)) {
+ document.body.className = "ok";
+ } else if (isEqual(expected.slice(0, actual.length), actual)) {
+ document.body.className = "test";
+ } else {
+ document.body.className = "ko";
+ }
+}
+
+function runTest(websocket) {
+ const expected = getExpectedEvents();
+ var actual = [];
+ websocket.addEventListener("open", ({ type }) => {
+ actual.push({ type });
+ testStep(expected, actual);
+ });
+ websocket.addEventListener("message", ({ type, data }) => {
+ actual.push({ type, data });
+ testStep(expected, actual);
+ });
+ websocket.addEventListener("close", ({ type, code, reason, wasClean }) => {
+ actual.push({ type, code, reason, wasClean });
+ testStep(expected, actual);
+ });
+}
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/style.css b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/style.css
new file mode 100644
index 0000000000..6e3918ccae
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/style.css
@@ -0,0 +1,69 @@
+/* page layout */
+
+body {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ margin: 0;
+ height: 100vh;
+}
+div.title, iframe {
+ width: 100vw;
+ height: 20vh;
+ border: none;
+}
+div.title {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+h1, p {
+ margin: 0;
+ width: 24em;
+}
+
+/* text style */
+
+h1, input, p {
+ font-family: monospace;
+ font-size: 3em;
+}
+input {
+ color: #333;
+ border: 3px solid #999;
+ padding: 1em;
+}
+input:focus {
+ border-color: #333;
+ outline: none;
+}
+input::placeholder {
+ color: #999;
+ opacity: 1;
+}
+
+/* test results */
+
+body.test {
+ background-color: #666;
+ color: #fff;
+}
+body.ok {
+ background-color: #090;
+ color: #fff;
+}
+body.ko {
+ background-color: #900;
+ color: #fff;
+}
+body > p {
+ display: none;
+}
+body > p.title,
+body.test > p.test,
+body.ok > p.ok,
+body.ko > p.ko {
+ display: block;
+}
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/test.html b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/test.html
new file mode 100644
index 0000000000..3883d6a39e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/test.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>WebSocket Authentication</title>
+ <link href="style.css" rel="stylesheet">
+ </head>
+ <body data-token="TOKEN">
+ <div class="title"><h1>WebSocket Authentication</h1></div>
+ <iframe src="first_message.html"></iframe>
+ <iframe src="query_param.html"></iframe>
+ <iframe src="cookie.html"></iframe>
+ <iframe src="user_info.html"></iframe>
+ <script src="test.js"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/test.js b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/test.js
new file mode 100644
index 0000000000..428830ff31
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/test.js
@@ -0,0 +1,6 @@
+// for connecting to WebSocket servers
+var token = document.body.dataset.token;
+
+// for test assertions only
+const params = new URLSearchParams(window.location.search);
+var user = params.get("user");
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/user_info.html b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/user_info.html
new file mode 100644
index 0000000000..7b9c99c730
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/user_info.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>User information | WebSocket Authentication</title>
+ <link href="style.css" rel="stylesheet">
+ </head>
+ <body class="test">
+ <p class="test">[??] User information</p>
+ <p class="ok">[OK] User information</p>
+ <p class="ko">[KO] User information</p>
+ <script src="script.js"></script>
+ <script src="user_info.js"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/user_info.js b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/user_info.js
new file mode 100644
index 0000000000..1dab2ce4c1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/user_info.js
@@ -0,0 +1,11 @@
+window.addEventListener("DOMContentLoaded", () => {
+ const uri = `ws://token:${token}@localhost:8004/`;
+ const websocket = new WebSocket(uri);
+
+ websocket.onmessage = ({ data }) => {
+ // event.data is expected to be "Hello <user>!"
+ websocket.send(`Goodbye ${data.slice(6, -1)}.`);
+ };
+
+ runTest(websocket);
+});
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/broadcast/clients.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/broadcast/clients.py
new file mode 100644
index 0000000000..fe39dfe051
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/broadcast/clients.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+
+import asyncio
+import statistics
+import sys
+import time
+
+import websockets
+
+
+LATENCIES = {}
+
+
+async def log_latency(interval):
+ while True:
+ await asyncio.sleep(interval)
+ p = statistics.quantiles(LATENCIES.values(), n=100)
+ print(f"clients = {len(LATENCIES)}")
+ print(
+ f"p50 = {p[49] / 1e6:.1f}ms, "
+ f"p95 = {p[94] / 1e6:.1f}ms, "
+ f"p99 = {p[98] / 1e6:.1f}ms"
+ )
+ print()
+
+
+async def client():
+ try:
+ async with websockets.connect(
+ "ws://localhost:8765",
+ ping_timeout=None,
+ ) as websocket:
+ async for msg in websocket:
+ client_time = time.time_ns()
+ server_time = int(msg[:19].decode())
+ LATENCIES[websocket] = client_time - server_time
+ except Exception as exc:
+ print(exc)
+
+
+async def main(count, interval):
+ asyncio.create_task(log_latency(interval))
+ clients = []
+ for _ in range(count):
+ clients.append(asyncio.create_task(client()))
+ await asyncio.sleep(0.001) # 1ms between each connection
+ await asyncio.wait(clients)
+
+
+if __name__ == "__main__":
+ try:
+ count = int(sys.argv[1])
+ interval = float(sys.argv[2])
+ except Exception as exc:
+ print(f"Usage: {sys.argv[0]} count interval")
+ print(" Connect <count> clients e.g. 1000")
+ print(" Report latency every <interval> seconds e.g. 1")
+ print()
+ print(exc)
+ else:
+ asyncio.run(main(count, interval))
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/broadcast/server.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/broadcast/server.py
new file mode 100644
index 0000000000..9c9907b7f9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/broadcast/server.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python
+
+import asyncio
+import functools
+import os
+import sys
+import time
+
+import websockets
+
+
+CLIENTS = set()
+
+
+async def send(websocket, message):
+ try:
+ await websocket.send(message)
+ except websockets.ConnectionClosed:
+ pass
+
+
+async def relay(queue, websocket):
+ while True:
+ message = await queue.get()
+ await websocket.send(message)
+
+
+class PubSub:
+ def __init__(self):
+ self.waiter = asyncio.Future()
+
+ def publish(self, value):
+ waiter, self.waiter = self.waiter, asyncio.Future()
+ waiter.set_result((value, self.waiter))
+
+ async def subscribe(self):
+ waiter = self.waiter
+ while True:
+ value, waiter = await waiter
+ yield value
+
+ __aiter__ = subscribe
+
+
+PUBSUB = PubSub()
+
+
+async def handler(websocket, method=None):
+ if method in ["default", "naive", "task", "wait"]:
+ CLIENTS.add(websocket)
+ try:
+ await websocket.wait_closed()
+ finally:
+ CLIENTS.remove(websocket)
+ elif method == "queue":
+ queue = asyncio.Queue()
+ relay_task = asyncio.create_task(relay(queue, websocket))
+ CLIENTS.add(queue)
+ try:
+ await websocket.wait_closed()
+ finally:
+ CLIENTS.remove(queue)
+ relay_task.cancel()
+ elif method == "pubsub":
+ async for message in PUBSUB:
+ await websocket.send(message)
+ else:
+ raise NotImplementedError(f"unsupported method: {method}")
+
+
+async def broadcast(method, size, delay):
+ """Broadcast messages at regular intervals."""
+ load_average = 0
+ time_average = 0
+ pc1, pt1 = time.perf_counter_ns(), time.process_time_ns()
+ await asyncio.sleep(delay)
+ while True:
+ print(f"clients = {len(CLIENTS)}")
+ pc0, pt0 = time.perf_counter_ns(), time.process_time_ns()
+ load_average = 0.9 * load_average + 0.1 * (pt0 - pt1) / (pc0 - pc1)
+ print(
+ f"load = {(pt0 - pt1) / (pc0 - pc1) * 100:.1f}% / "
+ f"average = {load_average * 100:.1f}%, "
+ f"late = {(pc0 - pc1 - delay * 1e9) / 1e6:.1f} ms"
+ )
+ pc1, pt1 = pc0, pt0
+
+ assert size > 20
+ message = str(time.time_ns()).encode() + b" " + os.urandom(size - 20)
+
+ if method == "default":
+ websockets.broadcast(CLIENTS, message)
+ elif method == "naive":
+ # Since the loop can yield control, make a copy of CLIENTS
+ # to avoid: RuntimeError: Set changed size during iteration
+ for websocket in CLIENTS.copy():
+ await send(websocket, message)
+ elif method == "task":
+ for websocket in CLIENTS:
+ asyncio.create_task(send(websocket, message))
+ elif method == "wait":
+ if CLIENTS: # asyncio.wait doesn't accept an empty list
+ await asyncio.wait(
+ [
+ asyncio.create_task(send(websocket, message))
+ for websocket in CLIENTS
+ ]
+ )
+ elif method == "queue":
+ for queue in CLIENTS:
+ queue.put_nowait(message)
+ elif method == "pubsub":
+ PUBSUB.publish(message)
+ else:
+ raise NotImplementedError(f"unsupported method: {method}")
+
+ pc2 = time.perf_counter_ns()
+ wait = delay + (pc1 - pc2) / 1e9
+ time_average = 0.9 * time_average + 0.1 * (pc2 - pc1)
+ print(
+ f"broadcast = {(pc2 - pc1) / 1e6:.1f}ms / "
+ f"average = {time_average / 1e6:.1f}ms, "
+ f"wait = {wait * 1e3:.1f}ms"
+ )
+ await asyncio.sleep(wait)
+ print()
+
+
+async def main(method, size, delay):
+ async with websockets.serve(
+ functools.partial(handler, method=method),
+ "localhost",
+ 8765,
+ compression=None,
+ ping_timeout=None,
+ ):
+ await broadcast(method, size, delay)
+
+
+if __name__ == "__main__":
+ try:
+ method = sys.argv[1]
+ assert method in ["default", "naive", "task", "wait", "queue", "pubsub"]
+ size = int(sys.argv[2])
+ delay = float(sys.argv[3])
+ except Exception as exc:
+ print(f"Usage: {sys.argv[0]} method size delay")
+ print(" Start a server broadcasting messages with <method> e.g. naive")
+ print(" Send a payload of <size> bytes every <delay> seconds")
+ print()
+ print(exc)
+ else:
+ asyncio.run(main(method, size, delay))
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/benchmark.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/benchmark.py
new file mode 100644
index 0000000000..c5b13c8fa3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/benchmark.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python
+
+import getpass
+import json
+import pickle
+import subprocess
+import sys
+import time
+import zlib
+
+
+CORPUS_FILE = "corpus.pkl"
+
+REPEAT = 10
+
+WB, ML = 12, 5 # defaults used as a reference
+
+
+def _corpus():
+ OAUTH_TOKEN = getpass.getpass("OAuth Token? ")
+ COMMIT_API = (
+ f'curl -H "Authorization: token {OAUTH_TOKEN}" '
+ f"https://api.github.com/repos/python-websockets/websockets/git/commits/:sha"
+ )
+
+ commits = []
+
+ head = subprocess.check_output("git rev-parse HEAD", shell=True).decode().strip()
+ todo = [head]
+ seen = set()
+
+ while todo:
+ sha = todo.pop(0)
+ commit = subprocess.check_output(COMMIT_API.replace(":sha", sha), shell=True)
+ commits.append(commit)
+ seen.add(sha)
+ for parent in json.loads(commit)["parents"]:
+ sha = parent["sha"]
+ if sha not in seen and sha not in todo:
+ todo.append(sha)
+ time.sleep(1) # rate throttling
+
+ return commits
+
+
+def corpus():
+ data = _corpus()
+ with open(CORPUS_FILE, "wb") as handle:
+ pickle.dump(data, handle)
+
+
+def _run(data):
+ size = {}
+ duration = {}
+
+ for wbits in range(9, 16):
+ size[wbits] = {}
+ duration[wbits] = {}
+
+ for memLevel in range(1, 10):
+ encoder = zlib.compressobj(wbits=-wbits, memLevel=memLevel)
+ encoded = []
+
+ t0 = time.perf_counter()
+
+ for _ in range(REPEAT):
+ for item in data:
+ if isinstance(item, str):
+ item = item.encode("utf-8")
+ # Taken from PerMessageDeflate.encode
+ item = encoder.compress(item) + encoder.flush(zlib.Z_SYNC_FLUSH)
+ if item.endswith(b"\x00\x00\xff\xff"):
+ item = item[:-4]
+ encoded.append(item)
+
+ t1 = time.perf_counter()
+
+ size[wbits][memLevel] = sum(len(item) for item in encoded)
+ duration[wbits][memLevel] = (t1 - t0) / REPEAT
+
+ raw_size = sum(len(item) for item in data)
+
+ print("=" * 79)
+ print("Compression ratio")
+ print("=" * 79)
+ print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in range(1, 10)]))
+ for wbits in range(9, 16):
+ print(
+ "\t".join(
+ [str(wbits)]
+ + [
+ f"{100 * (1 - size[wbits][memLevel] / raw_size):.1f}%"
+ for memLevel in range(1, 10)
+ ]
+ )
+ )
+ print("=" * 79)
+ print()
+
+ print("=" * 79)
+ print("CPU time")
+ print("=" * 79)
+ print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in range(1, 10)]))
+ for wbits in range(9, 16):
+ print(
+ "\t".join(
+ [str(wbits)]
+ + [
+ f"{1000 * duration[wbits][memLevel]:.1f}ms"
+ for memLevel in range(1, 10)
+ ]
+ )
+ )
+ print("=" * 79)
+ print()
+
+ print("=" * 79)
+ print(f"Size vs. {WB} \\ {ML}")
+ print("=" * 79)
+ print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in range(1, 10)]))
+ for wbits in range(9, 16):
+ print(
+ "\t".join(
+ [str(wbits)]
+ + [
+ f"{100 * (size[wbits][memLevel] / size[WB][ML] - 1):.1f}%"
+ for memLevel in range(1, 10)
+ ]
+ )
+ )
+ print("=" * 79)
+ print()
+
+ print("=" * 79)
+ print(f"Time vs. {WB} \\ {ML}")
+ print("=" * 79)
+ print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in range(1, 10)]))
+ for wbits in range(9, 16):
+ print(
+ "\t".join(
+ [str(wbits)]
+ + [
+ f"{100 * (duration[wbits][memLevel] / duration[WB][ML] - 1):.1f}%"
+ for memLevel in range(1, 10)
+ ]
+ )
+ )
+ print("=" * 79)
+ print()
+
+
+def run():
+ with open(CORPUS_FILE, "rb") as handle:
+ data = pickle.load(handle)
+ _run(data)
+
+
+try:
+ run = globals()[sys.argv[1]]
+except (KeyError, IndexError):
+ print(f"Usage: {sys.argv[0]} [corpus|run]")
+else:
+ run()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/client.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/client.py
new file mode 100644
index 0000000000..3ee19ddc59
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/client.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+
+import asyncio
+import statistics
+import tracemalloc
+
+import websockets
+from websockets.extensions import permessage_deflate
+
+
+CLIENTS = 20
+INTERVAL = 1 / 10 # seconds
+
+WB, ML = 12, 5
+
+MEM_SIZE = []
+
+
+async def client(client):
+ # Space out connections to make them sequential.
+ await asyncio.sleep(client * INTERVAL)
+
+ tracemalloc.start()
+
+ async with websockets.connect(
+ "ws://localhost:8765",
+ extensions=[
+ permessage_deflate.ClientPerMessageDeflateFactory(
+ server_max_window_bits=WB,
+ client_max_window_bits=WB,
+ compress_settings={"memLevel": ML},
+ )
+ ],
+ ) as ws:
+ await ws.send("hello")
+ await ws.recv()
+
+ await ws.send(b"hello")
+ await ws.recv()
+
+ MEM_SIZE.append(tracemalloc.get_traced_memory()[0])
+ tracemalloc.stop()
+
+ # Hold connection open until the end of the test.
+ await asyncio.sleep(CLIENTS * INTERVAL)
+
+
+async def clients():
+ await asyncio.gather(*[client(client) for client in range(CLIENTS + 1)])
+
+
+asyncio.run(clients())
+
+
+# First connection incurs non-representative setup costs.
+del MEM_SIZE[0]
+
+print(f"µ = {statistics.mean(MEM_SIZE) / 1024:.1f} KiB")
+print(f"σ = {statistics.stdev(MEM_SIZE) / 1024:.1f} KiB")
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/server.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/server.py
new file mode 100644
index 0000000000..8d1ee3cd7c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/server.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+
+import asyncio
+import os
+import signal
+import statistics
+import tracemalloc
+
+import websockets
+from websockets.extensions import permessage_deflate
+
+
+CLIENTS = 20
+INTERVAL = 1 / 10 # seconds
+
+WB, ML = 12, 5
+
+MEM_SIZE = []
+
+
+async def handler(ws):
+ msg = await ws.recv()
+ await ws.send(msg)
+
+ msg = await ws.recv()
+ await ws.send(msg)
+
+ MEM_SIZE.append(tracemalloc.get_traced_memory()[0])
+ tracemalloc.stop()
+
+ tracemalloc.start()
+
+ # Hold connection open until the end of the test.
+ await asyncio.sleep(CLIENTS * INTERVAL)
+
+
+async def server():
+ loop = asyncio.get_running_loop()
+ stop = loop.create_future()
+
+ # Set the stop condition when receiving SIGTERM.
+ print("Stop the server with:")
+ print(f"kill -TERM {os.getpid()}")
+ print()
+ loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
+
+ async with websockets.serve(
+ handler,
+ "localhost",
+ 8765,
+ extensions=[
+ permessage_deflate.ServerPerMessageDeflateFactory(
+ server_max_window_bits=WB,
+ client_max_window_bits=WB,
+ compress_settings={"memLevel": ML},
+ )
+ ],
+ ):
+ tracemalloc.start()
+ await stop
+
+
+asyncio.run(server())
+
+
+# First connection may incur non-representative setup costs.
+del MEM_SIZE[0]
+
+print(f"µ = {statistics.mean(MEM_SIZE) / 1024:.1f} KiB")
+print(f"σ = {statistics.stdev(MEM_SIZE) / 1024:.1f} KiB")
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/parse_frames.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/parse_frames.py
new file mode 100644
index 0000000000..e3acbe3c20
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/parse_frames.py
@@ -0,0 +1,101 @@
+"""Benchark parsing WebSocket frames."""
+
+import subprocess
+import sys
+import timeit
+
+from websockets.extensions.permessage_deflate import PerMessageDeflate
+from websockets.frames import Frame, Opcode
+from websockets.streams import StreamReader
+
+
+# 256kB of text, compressible by about 70%.
+text = subprocess.check_output(["git", "log", "8dd8e410"], text=True)
+
+
+def get_frame(size):
+ repeat, remainder = divmod(size, 256 * 1024)
+ payload = repeat * text + text[:remainder]
+ return Frame(Opcode.TEXT, payload.encode(), True)
+
+
+def parse_frame(data, count, mask, extensions):
+ reader = StreamReader()
+ for _ in range(count):
+ reader.feed_data(data)
+ parser = Frame.parse(
+ reader.read_exact,
+ mask=mask,
+ extensions=extensions,
+ )
+ try:
+ next(parser)
+ except StopIteration:
+ pass
+ else:
+ assert False, "parser should return frame"
+ reader.feed_eof()
+ assert reader.at_eof(), "parser should consume all data"
+
+
+def run_benchmark(size, count, compression=False, number=100):
+ if compression:
+ extensions = [PerMessageDeflate(True, True, 12, 12, {"memLevel": 5})]
+ else:
+ extensions = []
+ globals = {
+ "get_frame": get_frame,
+ "parse_frame": parse_frame,
+ "extensions": extensions,
+ }
+ sppf = (
+ min(
+ timeit.repeat(
+ f"parse_frame(data, {count}, mask=True, extensions=extensions)",
+ f"data = get_frame({size})"
+ f".serialize(mask=True, extensions=extensions)",
+ number=number,
+ globals=globals,
+ )
+ )
+ / number
+ / count
+ * 1_000_000
+ )
+ cppf = (
+ min(
+ timeit.repeat(
+ f"parse_frame(data, {count}, mask=False, extensions=extensions)",
+ f"data = get_frame({size})"
+ f".serialize(mask=False, extensions=extensions)",
+ number=number,
+ globals=globals,
+ )
+ )
+ / number
+ / count
+ * 1_000_000
+ )
+ print(f"{size}\t{compression}\t{sppf:.2f}\t{cppf:.2f}")
+
+
+if __name__ == "__main__":
+ print("Sizes are in bytes. Times are in µs per frame.", file=sys.stderr)
+ print("Run `tabs -16` for clean output. Pipe stdout to TSV for saving.")
+ print(file=sys.stderr)
+
+ print("size\tcompression\tserver\tclient")
+ run_benchmark(size=8, count=1000, compression=False)
+ run_benchmark(size=60, count=1000, compression=False)
+ run_benchmark(size=500, count=1000, compression=False)
+ run_benchmark(size=4_000, count=1000, compression=False)
+ run_benchmark(size=30_000, count=200, compression=False)
+ run_benchmark(size=250_000, count=100, compression=False)
+ run_benchmark(size=2_000_000, count=20, compression=False)
+
+ run_benchmark(size=8, count=1000, compression=True)
+ run_benchmark(size=60, count=1000, compression=True)
+ run_benchmark(size=500, count=200, compression=True)
+ run_benchmark(size=4_000, count=100, compression=True)
+ run_benchmark(size=30_000, count=20, compression=True)
+ run_benchmark(size=250_000, count=10, compression=True)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/parse_handshake.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/parse_handshake.py
new file mode 100644
index 0000000000..af5a4ecae2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/parse_handshake.py
@@ -0,0 +1,102 @@
+"""Benchark parsing WebSocket handshake requests."""
+
+# The parser for responses is designed similarly and should perform similarly.
+
+import sys
+import timeit
+
+from websockets.http11 import Request
+from websockets.streams import StreamReader
+
+
+CHROME_HANDSHAKE = (
+ b"GET / HTTP/1.1\r\n"
+ b"Host: localhost:5678\r\n"
+ b"Connection: Upgrade\r\n"
+ b"Pragma: no-cache\r\n"
+ b"Cache-Control: no-cache\r\n"
+ b"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
+ b"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36\r\n"
+ b"Upgrade: websocket\r\n"
+ b"Origin: null\r\n"
+ b"Sec-WebSocket-Version: 13\r\n"
+ b"Accept-Encoding: gzip, deflate, br\r\n"
+ b"Accept-Language: en-GB,en;q=0.9,en-US;q=0.8,fr;q=0.7\r\n"
+ b"Sec-WebSocket-Key: ebkySAl+8+e6l5pRKTMkyQ==\r\n"
+ b"Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n"
+ b"\r\n"
+)
+
+FIREFOX_HANDSHAKE = (
+ b"GET / HTTP/1.1\r\n"
+ b"Host: localhost:5678\r\n"
+ b"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) "
+ b"Gecko/20100101 Firefox/111.0\r\n"
+ b"Accept: */*\r\n"
+ b"Accept-Language: en-US,en;q=0.7,fr-FR;q=0.3\r\n"
+ b"Accept-Encoding: gzip, deflate, br\r\n"
+ b"Sec-WebSocket-Version: 13\r\n"
+ b"Origin: null\r\n"
+ b"Sec-WebSocket-Extensions: permessage-deflate\r\n"
+ b"Sec-WebSocket-Key: 1PuS+hnb+0AXsL7z2hNAhw==\r\n"
+ b"Connection: keep-alive, Upgrade\r\n"
+ b"Sec-Fetch-Dest: websocket\r\n"
+ b"Sec-Fetch-Mode: websocket\r\n"
+ b"Sec-Fetch-Site: cross-site\r\n"
+ b"Pragma: no-cache\r\n"
+ b"Cache-Control: no-cache\r\n"
+ b"Upgrade: websocket\r\n"
+ b"\r\n"
+)
+
+WEBSOCKETS_HANDSHAKE = (
+ b"GET / HTTP/1.1\r\n"
+ b"Host: localhost:8765\r\n"
+ b"Upgrade: websocket\r\n"
+ b"Connection: Upgrade\r\n"
+ b"Sec-WebSocket-Key: 9c55e0/siQ6tJPCs/QR8ZA==\r\n"
+ b"Sec-WebSocket-Version: 13\r\n"
+ b"Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n"
+ b"User-Agent: Python/3.11 websockets/11.0\r\n"
+ b"\r\n"
+)
+
+
+def parse_handshake(handshake):
+ reader = StreamReader()
+ reader.feed_data(handshake)
+ parser = Request.parse(reader.read_line)
+ try:
+ next(parser)
+ except StopIteration:
+ pass
+ else:
+ assert False, "parser should return request"
+ reader.feed_eof()
+ assert reader.at_eof(), "parser should consume all data"
+
+
+def run_benchmark(name, handshake, number=10000):
+ ph = (
+ min(
+ timeit.repeat(
+ "parse_handshake(handshake)",
+ number=number,
+ globals={"parse_handshake": parse_handshake, "handshake": handshake},
+ )
+ )
+ / number
+ * 1_000_000
+ )
+ print(f"{name}\t{len(handshake)}\t{ph:.1f}")
+
+
+if __name__ == "__main__":
+ print("Sizes are in bytes. Times are in µs per frame.", file=sys.stderr)
+ print("Run `tabs -16` for clean output. Pipe stdout to TSV for saving.")
+ print(file=sys.stderr)
+
+ print("client\tsize\ttime")
+ run_benchmark("Chrome", CHROME_HANDSHAKE)
+ run_benchmark("Firefox", FIREFOX_HANDSHAKE)
+ run_benchmark("websockets", WEBSOCKETS_HANDSHAKE)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/streams.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/streams.py
new file mode 100644
index 0000000000..ca24a59834
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/streams.py
@@ -0,0 +1,301 @@
+"""
+Benchmark two possible implementations of a stream reader.
+
+The difference lies in the data structure that buffers incoming data:
+
+* ``ByteArrayStreamReader`` uses a ``bytearray``;
+* ``BytesDequeStreamReader`` uses a ``deque[bytes]``.
+
+``ByteArrayStreamReader`` is faster for streaming small frames, which is the
+standard use case of websockets, likely due to its simple implementation and
+to ``bytearray`` being fast at appending data and removing data at the front
+(https://hg.python.org/cpython/rev/499a96611baa).
+
+``BytesDequeStreamReader`` is faster for large frames and for bursts, likely
+because it copies payloads only once, while ``ByteArrayStreamReader`` copies
+them twice.
+
+"""
+
+
+import collections
+import os
+import timeit
+
+
+# Implementations
+
+
+class ByteArrayStreamReader:
+ def __init__(self):
+ self.buffer = bytearray()
+ self.eof = False
+
+ def readline(self):
+ n = 0 # number of bytes to read
+ p = 0 # number of bytes without a newline
+ while True:
+ n = self.buffer.find(b"\n", p) + 1
+ if n > 0:
+ break
+ p = len(self.buffer)
+ yield
+ r = self.buffer[:n]
+ del self.buffer[:n]
+ return r
+
+ def readexactly(self, n):
+ assert n >= 0
+ while len(self.buffer) < n:
+ yield
+ r = self.buffer[:n]
+ del self.buffer[:n]
+ return r
+
+ def feed_data(self, data):
+ self.buffer += data
+
+ def feed_eof(self):
+ self.eof = True
+
+ def at_eof(self):
+ return self.eof and not self.buffer
+
+
+class BytesDequeStreamReader:
+ def __init__(self):
+ self.buffer = collections.deque()
+ self.eof = False
+
+ def readline(self):
+ b = []
+ while True:
+ # Read next chunk
+ while True:
+ try:
+ c = self.buffer.popleft()
+ except IndexError:
+ yield
+ else:
+ break
+ # Handle chunk
+ n = c.find(b"\n") + 1
+ if n == len(c):
+ # Read exactly enough data
+ b.append(c)
+ break
+ elif n > 0:
+ # Read too much data
+ b.append(c[:n])
+ self.buffer.appendleft(c[n:])
+ break
+ else: # n == 0
+ # Need to read more data
+ b.append(c)
+ return b"".join(b)
+
+ def readexactly(self, n):
+ if n == 0:
+ return b""
+ b = []
+ while True:
+ # Read next chunk
+ while True:
+ try:
+ c = self.buffer.popleft()
+ except IndexError:
+ yield
+ else:
+ break
+ # Handle chunk
+ n -= len(c)
+ if n == 0:
+ # Read exactly enough data
+ b.append(c)
+ break
+ elif n < 0:
+ # Read too much data
+ b.append(c[:n])
+ self.buffer.appendleft(c[n:])
+ break
+ else: # n >= 0
+ # Need to read more data
+ b.append(c)
+ return b"".join(b)
+
+ def feed_data(self, data):
+ self.buffer.append(data)
+
+ def feed_eof(self):
+ self.eof = True
+
+ def at_eof(self):
+ return self.eof and not self.buffer
+
+
+# Tests
+
+
+class Protocol:
+ def __init__(self, StreamReader):
+ self.reader = StreamReader()
+ self.events = []
+ # Start parser coroutine
+ self.parser = self.run_parser()
+ next(self.parser)
+
+ def run_parser(self):
+ while True:
+ frame = yield from self.reader.readexactly(2)
+ self.events.append(frame)
+ frame = yield from self.reader.readline()
+ self.events.append(frame)
+
+ def data_received(self, data):
+ self.reader.feed_data(data)
+ next(self.parser) # run parser until more data is needed
+ events, self.events = self.events, []
+ return events
+
+
+def run_test(StreamReader):
+ proto = Protocol(StreamReader)
+
+ actual = proto.data_received(b"a")
+ expected = []
+ assert actual == expected, f"{actual} != {expected}"
+
+ actual = proto.data_received(b"b")
+ expected = [b"ab"]
+ assert actual == expected, f"{actual} != {expected}"
+
+ actual = proto.data_received(b"c")
+ expected = []
+ assert actual == expected, f"{actual} != {expected}"
+
+ actual = proto.data_received(b"\n")
+ expected = [b"c\n"]
+ assert actual == expected, f"{actual} != {expected}"
+
+ actual = proto.data_received(b"efghi\njklmn")
+ expected = [b"ef", b"ghi\n", b"jk"]
+ assert actual == expected, f"{actual} != {expected}"
+
+
+# Benchmarks
+
+
+def get_frame_packets(size, packet_size=None):
+ if size < 126:
+ frame = bytes([138, size])
+ elif size < 65536:
+ frame = bytes([138, 126]) + bytes(divmod(size, 256))
+ else:
+ size1, size2 = divmod(size, 65536)
+ frame = (
+ bytes([138, 127]) + bytes(divmod(size1, 256)) + bytes(divmod(size2, 256))
+ )
+ frame += os.urandom(size)
+ if packet_size is None:
+ return [frame]
+ else:
+ packets = []
+ while frame:
+ packets.append(frame[:packet_size])
+ frame = frame[packet_size:]
+ return packets
+
+
+def benchmark_stream(StreamReader, packets, size, count):
+ reader = StreamReader()
+ for _ in range(count):
+ for packet in packets:
+ reader.feed_data(packet)
+ yield from reader.readexactly(2)
+ if size >= 65536:
+ yield from reader.readexactly(4)
+ elif size >= 126:
+ yield from reader.readexactly(2)
+ yield from reader.readexactly(size)
+ reader.feed_eof()
+ assert reader.at_eof()
+
+
+def benchmark_burst(StreamReader, packets, size, count):
+ reader = StreamReader()
+ for _ in range(count):
+ for packet in packets:
+ reader.feed_data(packet)
+ reader.feed_eof()
+ for _ in range(count):
+ yield from reader.readexactly(2)
+ if size >= 65536:
+ yield from reader.readexactly(4)
+ elif size >= 126:
+ yield from reader.readexactly(2)
+ yield from reader.readexactly(size)
+ assert reader.at_eof()
+
+
+def run_benchmark(size, count, packet_size=None, number=1000):
+ stmt = f"list(benchmark(StreamReader, packets, {size}, {count}))"
+ setup = f"packets = get_frame_packets({size}, {packet_size})"
+ context = globals()
+
+ context["StreamReader"] = context["ByteArrayStreamReader"]
+ context["benchmark"] = context["benchmark_stream"]
+ bas = min(timeit.repeat(stmt, setup, number=number, globals=context))
+ context["benchmark"] = context["benchmark_burst"]
+ bab = min(timeit.repeat(stmt, setup, number=number, globals=context))
+
+ context["StreamReader"] = context["BytesDequeStreamReader"]
+ context["benchmark"] = context["benchmark_stream"]
+ bds = min(timeit.repeat(stmt, setup, number=number, globals=context))
+ context["benchmark"] = context["benchmark_burst"]
+ bdb = min(timeit.repeat(stmt, setup, number=number, globals=context))
+
+ print(
+ f"Frame size = {size} bytes, "
+ f"frame count = {count}, "
+ f"packet size = {packet_size}"
+ )
+ print(f"* ByteArrayStreamReader (stream): {bas / number * 1_000_000:.1f}µs")
+ print(
+ f"* BytesDequeStreamReader (stream): "
+ f"{bds / number * 1_000_000:.1f}µs ({(bds / bas - 1) * 100:+.1f}%)"
+ )
+ print(f"* ByteArrayStreamReader (burst): {bab / number * 1_000_000:.1f}µs")
+ print(
+ f"* BytesDequeStreamReader (burst): "
+ f"{bdb / number * 1_000_000:.1f}µs ({(bdb / bab - 1) * 100:+.1f}%)"
+ )
+ print()
+
+
+if __name__ == "__main__":
+ run_test(ByteArrayStreamReader)
+ run_test(BytesDequeStreamReader)
+
+ run_benchmark(size=8, count=1000)
+ run_benchmark(size=60, count=1000)
+ run_benchmark(size=500, count=500)
+ run_benchmark(size=4_000, count=200)
+ run_benchmark(size=30_000, count=100)
+ run_benchmark(size=250_000, count=50)
+ run_benchmark(size=2_000_000, count=20)
+
+ run_benchmark(size=4_000, count=200, packet_size=1024)
+ run_benchmark(size=30_000, count=100, packet_size=1024)
+ run_benchmark(size=250_000, count=50, packet_size=1024)
+ run_benchmark(size=2_000_000, count=20, packet_size=1024)
+
+ run_benchmark(size=30_000, count=100, packet_size=4096)
+ run_benchmark(size=250_000, count=50, packet_size=4096)
+ run_benchmark(size=2_000_000, count=20, packet_size=4096)
+
+ run_benchmark(size=30_000, count=100, packet_size=16384)
+ run_benchmark(size=250_000, count=50, packet_size=16384)
+ run_benchmark(size=2_000_000, count=20, packet_size=16384)
+
+ run_benchmark(size=250_000, count=50, packet_size=65536)
+ run_benchmark(size=2_000_000, count=20, packet_size=65536)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_http11_request_parser.py b/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_http11_request_parser.py
new file mode 100644
index 0000000000..59e0cea0f4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_http11_request_parser.py
@@ -0,0 +1,42 @@
+import sys
+
+import atheris
+
+
+with atheris.instrument_imports():
+ from websockets.exceptions import SecurityError
+ from websockets.http11 import Request
+ from websockets.streams import StreamReader
+
+
+def test_one_input(data):
+ reader = StreamReader()
+ reader.feed_data(data)
+ reader.feed_eof()
+
+ parser = Request.parse(
+ reader.read_line,
+ )
+
+ try:
+ next(parser)
+ except StopIteration as exc:
+ assert isinstance(exc.value, Request)
+ return # input accepted
+ except (
+ EOFError, # connection is closed without a full HTTP request
+ SecurityError, # request exceeds a security limit
+ ValueError, # request isn't well formatted
+ ):
+ return # input rejected with a documented exception
+
+ raise RuntimeError("parsing didn't complete")
+
+
+def main():
+ atheris.Setup(sys.argv, test_one_input)
+ atheris.Fuzz()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_http11_response_parser.py b/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_http11_response_parser.py
new file mode 100644
index 0000000000..6906720a49
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_http11_response_parser.py
@@ -0,0 +1,44 @@
+import sys
+
+import atheris
+
+
+with atheris.instrument_imports():
+ from websockets.exceptions import SecurityError
+ from websockets.http11 import Response
+ from websockets.streams import StreamReader
+
+
+def test_one_input(data):
+ reader = StreamReader()
+ reader.feed_data(data)
+ reader.feed_eof()
+
+ parser = Response.parse(
+ reader.read_line,
+ reader.read_exact,
+ reader.read_to_eof,
+ )
+ try:
+ next(parser)
+ except StopIteration as exc:
+ assert isinstance(exc.value, Response)
+ return # input accepted
+ except (
+ EOFError, # connection is closed without a full HTTP response
+ SecurityError, # response exceeds a security limit
+ LookupError, # response isn't well formatted
+ ValueError, # response isn't well formatted
+ ):
+ return # input rejected with a documented exception
+
+ raise RuntimeError("parsing didn't complete")
+
+
+def main():
+ atheris.Setup(sys.argv, test_one_input)
+ atheris.Fuzz()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_websocket_parser.py b/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_websocket_parser.py
new file mode 100644
index 0000000000..1509a3549d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_websocket_parser.py
@@ -0,0 +1,51 @@
+import sys
+
+import atheris
+
+
+with atheris.instrument_imports():
+ from websockets.exceptions import PayloadTooBig, ProtocolError
+ from websockets.frames import Frame
+ from websockets.streams import StreamReader
+
+
+def test_one_input(data):
+ fdp = atheris.FuzzedDataProvider(data)
+ mask = fdp.ConsumeBool()
+ max_size_enabled = fdp.ConsumeBool()
+ max_size = fdp.ConsumeInt(4)
+ payload = fdp.ConsumeBytes(atheris.ALL_REMAINING)
+
+ reader = StreamReader()
+ reader.feed_data(payload)
+ reader.feed_eof()
+
+ parser = Frame.parse(
+ reader.read_exact,
+ mask=mask,
+ max_size=max_size if max_size_enabled else None,
+ )
+
+ try:
+ next(parser)
+ except StopIteration as exc:
+ assert isinstance(exc.value, Frame)
+ return # input accepted
+ except (
+ EOFError, # connection is closed without a full WebSocket frame
+ UnicodeDecodeError, # frame contains invalid UTF-8
+ PayloadTooBig, # frame's payload size exceeds ``max_size``
+ ProtocolError, # frame contains incorrect values
+ ):
+ return # input rejected with a documented exception
+
+ raise RuntimeError("parsing didn't complete")
+
+
+def main():
+ atheris.Setup(sys.argv, test_one_input)
+ atheris.Fuzz()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/favicon.ico b/testing/web-platform/tests/tools/third_party/websockets/logo/favicon.ico
new file mode 100644
index 0000000000..602c14e4eb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/logo/favicon.ico
@@ -0,0 +1,2 @@
+ h& ��( >��-=��v=��`@��B��U=���;���;���;���B��#J��VE���A���=���<���;���<���R��WN���I���E���E��%>���;���;���;���=��*\��VU���Q���M���N��$K��cE���A���=���D���`����q9 ^���]���Z���U��$R��dM���I���F���g��j�j1��i0��k1Cf��b���_��#[��eU���Q���N���R���t8w�k2��i0��k19d��J]���Y���U���R���|>s�v:��r7��k3�����k2u�j50f��da���]���^����Gk�~@��z>��w<Ҧs@�l2��q��zE�h��;p��ēN��I���C��Bѳ�@�x<��o5��M��o7�ȔOJ��M���J���F(�A��z=��v:��p5ɪw3ϘS%ŒP���L���JﳂC��~A��{>ɪwDʖS�ŒP���M���J���CŻ�D͗UBƔPyÏMY̙f����������������( @ <��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��U��U��U��U��U��U��U��U��U��B��<���<���<���;���<���<��M<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��A��3<���;���;���;���;���;���;���;���U��U��U��U��U��U��U��U��U��U��U��U��U��U��U��>��>��>��>��>��>��>��F��3A���?���>���;���;���;���;���;���;���<���<��<��<��<��<��<��<��<��<��<��<��<��<��<��=��=��=��=��=��=��J��4F���D���B���?���>���<���;���;���;���;���;���>��->��>��>��>��>��>��>��>��>��>��>��>��>��<��<��<��<��<��M��5J���G���E���C���B���@���=���<���;���;���;���;���=���=��=��=��=��=��=��=��=��=��=��=��=��=��U��U��U��U��R��5N���K���I���G���E���C���B���B��A��'<���;���;���;���;���;���<��Y<��<��<��<��<��<��<��<��<��<��<��<��<��<��U��6S���Q���N���M���K���I���H���H��H��B��U@���>���<���;���;���;���;���<���U��U��U��U��U��U��U��U��U��U���j2�j2Z��6V���T���R���P���O���M���L���L��L��H��JE���A���@���=���<���;���;���;���;���<���<��<��<��<��<��<��<��<��<���k2^��1Z���X���V���T���S���P���P���P��P��K��KH���F���C���B���@���=���<���@���a���}�k��zOŜj2$�j2�j2�j2�j2�j2�j2�j2�j2�i1_���^���\���Z���X���V���V���V��V��M��LL���I���G���F���C���B���@����m��i1��i0��i0��i0��k2u�k2�k2�k2�k2�k2�k2�k2�k2�k1b���_���]���[���Z���Z���Z��Z��T��LR���N���M���J���I���H���G��sG���r6/�j0��i0��i0��i0��i1��i1�i1�i1�i1�i1�i1�i1�i1�k1d���a���`���]���]���]��]��V��MU���R���Q���N���M���K���J��rJ��J���r7p�l2��j1��i0��i0��k1��k1�k1�k1�k1�k1�k1�k1�k1��U���f��sa���a��_a��a��\��NW���V���T���R���P���O���M��qM��M���t8e�q6��o4��l2��j1��i0��k1X�k1�k1�k1�k1�k1�k1�k1�k1��U��U��U��U��U��Ub��F^���\���Z���X���W���T���T��pT��T���{<f�w;��s8��q6��o5��k2��j1�q9 �q9�q9�j2W��U��U��U��U��U�i0�i0�i0�i0�i0�i0a���_���^���[���Z���X���W��pW��W���~@g�z>��x<��v:��s8��r7��p5��l2B�l2�l2�j1}�i0��j0���U��U��U��U�i0�i0�i0�i0�i0���d���a���`���^���[���\��o\��\����Eg��B��~@��z=��y<��v:��t9��r7S�r7�r7�k3~�i0��|J��i0��i0��i0�i0�i0�j1�j1�j1�j1�j1�j1g���c���a���_���_��n_��_����JS��E���C���B��|?��z=��x<��v;R�v;�v;�n4�l2���\�����è���i0�i0�i0�i0�k1�k1�k1�k1�k1�k1���i��ff���d��Bd��d��ĝN
+��I���H���E���C��A��|?��~?Q�~?�~?�t8�q6��n4��t=�Ѽ���N��j1љj1�j1�j1�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5M`��J���H���G���D���C���BQ��B��B�z>��t9��s8��q6��m3��l3��k1��k19�k1�k1�k1�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5ÐO���M���J���H���G���G^��G��G�}?��z=��y<��u9��s8��o5��o4��o5>�o5�o5�o5�o5�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;ƑO�ÐO���L���J���I���G2��G��C���B��|?��z=��v:��u9��t7��s5>�s5�s5�s5�s5�s5�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?ʕTtőO�ÐN���M���I���I귆Gʵ�D���C��~@��|?��z=��x<�u;=�u;�u;�u;�u;�u;�u;��D��D��D��D��D��D��D��D��D��D��D��D̙W#ɖS�ŒP�ÐN���K���J���H���F���D���C��}@��|?�}?=�}?�}?�}?�}?�}?�}?�}?��I��I��I��I��I��I��I��I��I��I��I��I��I̘T�ǓQ�őO�ÏN���L���J���I���F���D���C�D<��D��D��D��D��D��D��D��D’U’U’U’U’U’U’U’U’U’U’U’U’U��U̗T�ǔQ�ŒP�ÏN���K���J���H���F鶄I8��I��I��I��I��I��I��I��I��I˘WOȓQ�œP�ÑN���Lھ�L�’U’U’U’U’U’U’U’U’U’U’U�����������������?��?����������0��`������@������������ ��0 ����0�� ��?�����������������������
diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/github-social-preview.html b/testing/web-platform/tests/tools/third_party/websockets/logo/github-social-preview.html
new file mode 100644
index 0000000000..7f2b45badb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/logo/github-social-preview.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>GitHub social preview</title>
+ <style>
+ body {
+ background-color: black;
+ color: white;
+ font-family: -apple-system;
+ font-size: 36px;
+ font-weight: 100;
+ text-align: center;
+ }
+ p.screenshot {
+ background-color: white;
+ box-sizing: border-box;
+ width: 1280px;
+ height: 640px;
+ margin: 40px auto;
+ padding: 40px;
+ }
+ p.screenshot.x2 {
+ width: 640px;
+ height: 320px;
+ padding: 20px;
+ }
+ p.screenshot img {
+ height: 100%;
+ }
+ </style>
+ </head>
+ <body>
+ <p>Take a screenshot of this DOM node to make a PNG.</p>
+ <p>For 2x DPI screens.</p>
+ <p class="screenshot x2"><img src="vertical.svg" alt="preview @ 2x"></p>
+ <p>For regular screens.</p>
+ <p class="screenshot"><img src="vertical.svg" alt="preview"></p>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/horizontal.svg b/testing/web-platform/tests/tools/third_party/websockets/logo/horizontal.svg
new file mode 100644
index 0000000000..ee872dc478
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/logo/horizontal.svg
@@ -0,0 +1,31 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="256" viewBox="0 0 1024 256">
+ <linearGradient id="w" x1="0" y1="0" x2="0.1667" y2="0.6667">
+ <stop offset="0%" stop-color="#ffe873" />
+ <stop offset="100%" stop-color="#ffd43b" />
+ </linearGradient>
+ <linearGradient id="s" x1="0" y1="0" x2="0.1667" y2="0.6667">
+ <stop offset="0%" stop-color="#5a9fd4" />
+ <stop offset="100%" stop-color="#306998" />
+ </linearGradient>
+<g>
+ <path fill="url(#w)" d="m 151.60708,154.81618 c -0.43704,0.0747 -0.88656,0.12978 -1.35572,0.14933 -2.45813,0.0764 -4.25357,-0.58665 -5.82335,-2.15107 l -8.89246,-8.85942 -11.23464,-11.19805 -36.040757,-35.919452 c -3.43568,-3.42217 -7.332485,-5.347474 -11.589626,-5.723468 -2.229803,-0.198219 -4.473877,0.03111 -6.640354,0.675545 -3.242133,0.944875 -6.135526,2.664848 -8.593662,5.116366 -3.834369,3.819499 -5.86349,8.414979 -5.875977,13.287799 -0.06065,4.95281 1.951523,9.60074 5.808192,13.44424 l 55.622894,55.43648 c 1.82219,1.84175 2.65971,3.79549 2.63384,6.14568 l 0.004,0.208 c 0.0527,2.43196 -0.75991,4.34571 -2.6267,6.20612 -1.78028,1.77598 -3.8094,2.65241 -6.30945,2.75552 -2.45814,0.0764 -4.25446,-0.58844 -5.82514,-2.15286 L 48.702551,136.2618 c -5.214172,-5.19459 -11.702899,-6.98745 -18.22998,-5.04881 -3.245701,0.9431 -6.135527,2.66307 -8.595446,5.11459 -3.83437,3.82127 -5.865275,8.41676 -5.875978,13.28957 -0.05619,4.95281 1.951524,9.60252 5.806409,13.4478 l 58.10689,57.90577 c 8.319842,8.29143 19.340421,11.9376 32.743314,10.83806 12.57967,-1.02043 23.02317,-5.5848 31.03441,-13.57313 7.51265,-7.4861 11.96423,-16.35175 13.28695,-26.42537 10.47206,-1.68264 19.29494,-6.04524 26.27512,-13.00158 4.01364,-3.99994 7.14963,-8.3972 9.40531,-13.16157 -14.15569,-0.39911 -28.23645,-4.00972 -41.05247,-10.83095 z" />
+ <path fill="url(#s)" d="m 196.96038,146.11854 c 0.10259,-12.84514 -4.43017,-23.98541 -13.50635,-33.1346 L 147.57292,77.225374 c -0.24349,-0.240885 -0.46469,-0.487992 -0.68678,-0.744877 -1.48416,-1.739529 -2.18788,-3.583056 -2.21018,-5.807022 -0.0259,-2.470184 0.84911,-4.508375 2.7605,-6.407902 1.91406,-1.909304 3.8531,-2.737735 6.36564,-2.684403 2.53662,0.024 4.62728,0.943097 6.57257,2.881734 l 60.59178,60.384846 12.11408,-12.06914 c 1.12203,-0.90755 1.95777,-1.76887 2.87823,-2.93418 5.91879,-7.51544 5.26947,-18.272609 -1.51003,-25.02895 L 187.20456,37.727314 c -9.19393,-9.157192 -20.36703,-13.776677 -33.16789,-13.7269 -12.94266,-0.05067 -24.14163,4.548375 -33.28739,13.662901 -9.02892,8.996307 -13.64015,19.93925 -13.7008,32.487501 l -0.004,0.14222 c -0.002,0.167998 -0.005,0.336884 -0.005,0.506659 -0.091,12.232701 4.10729,22.95787 12.48154,31.881285 0.40226,0.43022 0.80274,0.85777 1.22283,1.27821 l 35.75088,35.62612 c 1.88909,1.88174 2.71769,3.79638 2.69361,6.20968 l 0.003,0.20977 c 0.0527,2.43197 -0.76081,4.34571 -2.6276,6.20791 -1.44759,1.43909 -3.06286,2.27818 -4.9564,2.60262 12.81601,6.82123 26.89677,10.43184 41.05246,10.83362 2.80598,-5.92525 4.2509,-12.41848 4.29906,-19.43526 z" />
+ <path fill="#ffffff" d="m 215.68093,93.181574 c 2.84701,-2.838179 7.46359,-2.836401 10.30883,0 2.84433,2.834623 2.84612,7.435446 -0.002,10.270956 -2.84345,2.83818 -7.46271,2.83818 -10.30704,0 -2.84524,-2.83551 -2.84791,-7.435444 0,-10.270956 z" />
+ </g>
+ <g>
+ <g fill="#ffd43b">
+ <path d="m 271.62046,177.33313 c 0,4.1637 1.46619,7.71227 4.39858,10.64361 2.9324,2.93556 6.48202,4.40069 10.64783,4.40069 4.16475,0 7.71438,-1.46513 10.64572,-4.40069 2.93344,-2.93134 4.40069,-6.47991 4.40069,-10.64361 v -35.00332 c 0,-2.12345 0.7647,-3.95198 2.29514,-5.48032 1.53045,-1.53256 3.35793,-2.29831 5.48349,-2.29831 h 0.12745 c 2.16664,0 3.972,0.76575 5.41923,2.29831 1.53045,1.52834 2.2962,3.35793 2.2962,5.48032 v 35.00332 c 0,4.1637 1.4662,7.71227 4.40069,10.64361 2.93134,2.93556 6.47886,4.40069 10.64572,4.40069 4.20794,0 7.77758,-1.46513 10.70997,-4.40069 2.93345,-2.93134 4.40069,-6.47991 4.40069,-10.64361 v -35.00332 c 0,-2.12345 0.76365,-3.95198 2.29515,-5.48032 1.44302,-1.53256 3.25049,-2.29831 5.41924,-2.29831 h 0.1264 c 2.12661,0 3.95409,0.76575 5.48349,2.29831 1.48831,1.52834 2.23194,3.35793 2.23194,5.48032 v 35.00332 c 0,8.45696 -2.9977,15.68261 -8.98887,21.67484 -5.99329,5.99224 -13.21999,8.98993 -21.67695,8.98993 -10.11696,0 -17.7239,-3.35583 -22.82609,-10.07272 -5.14222,6.71689 -12.77234,10.07272 -22.88719,10.07272 -8.45801,0 -15.68471,-2.99769 -21.67695,-8.98993 C 258.9998,193.01574 256,185.79113 256,177.33313 v -35.00332 c 0,-2.12345 0.76575,-3.95198 2.29619,-5.48032 1.5294,-1.53256 3.33581,-2.29831 5.41924,-2.29831 h 0.1917 c 2.08238,0 3.88774,0.76575 5.42029,2.29831 1.52834,1.52834 2.29409,3.35793 2.29409,5.48032 v 35.00332 z" />
+ <path d="m 443.95216,155.97534 c 0.51085,1.06173 0.7668,2.14346 0.7668,3.25048 0,0.8932 -0.16957,1.78536 -0.50979,2.67854 -0.72363,1.99707 -2.08343,3.4422 -4.0805,4.33434 -5.95114,2.67854 -13.77085,6.20711 -23.46228,10.58463 -12.02871,5.43924 -19.08477,8.64866 -21.16715,9.62823 3.22943,4.07944 8.26737,6.11863 15.11067,6.11863 4.5471,0 8.67077,-1.33769 12.36786,-4.01625 3.61283,-2.63534 6.14286,-6.03541 7.58798,-10.20227 1.23342,-3.48538 3.69815,-5.22754 7.39524,-5.22754 2.6343,0 4.7388,1.10702 6.31138,3.31369 0.97746,1.36193 1.46619,2.78598 1.46619,4.27325 0,0.8932 -0.16958,1.80641 -0.50874,2.74069 -2.50791,7.26988 -6.90861,13.13573 -13.19681,17.59961 -6.37563,4.63031 -13.51702,6.94757 -21.4231,6.94757 -10.11591,0 -18.76563,-3.58965 -25.94809,-10.7742 -7.18351,-7.18353 -10.77527,-15.83219 -10.77527,-25.9502 0,-10.11591 3.59176,-18.76351 10.77527,-25.95019 7.18142,-7.1814 15.83218,-10.77422 25.94809,-10.77422 7.30885,0 13.98257,1.99916 20.01904,5.99223 5.99118,3.91512 10.43296,9.05524 13.32321,15.43298 z m -33.34331,-5.67836 c -5.86583,0 -10.86059,2.06343 -14.98322,6.18604 -4.08049,4.12473 -6.12073,9.11949 -6.12073,14.98322 v 0.44661 l 35.63951,-16.00282 c -3.1441,-3.73817 -7.99035,-5.61305 -14.53556,-5.61305 z" />
+ <path d="m 465.12141,108.41246 c 2.08238,0 3.88775,0.74469 5.41924,2.23194 1.53045,1.52834 2.29619,3.35793 2.29619,5.48244 v 24.79998 c 4.80202,-4.24796 11.83701,-6.37564 21.10185,-6.37564 10.11591,0 18.76561,3.59177 25.94914,10.77422 7.18245,7.18563 10.77527,15.83429 10.77527,25.9502 0,10.11695 -3.59282,18.76561 -10.77527,25.95018 C 512.70536,204.41035 504.05566,208 493.93869,208 c -10.11696,0 -18.74349,-3.56964 -25.88382,-10.71207 -7.18457,-7.09504 -10.7974,-15.70262 -10.83954,-25.82063 v -55.33941 c 0,-2.12556 0.76576,-3.95409 2.29621,-5.48243 1.52939,-1.48727 3.3358,-2.23196 5.41924,-2.23196 h 0.19063 z m 28.81622,41.88452 c -5.86477,0 -10.85953,2.06343 -14.9832,6.18604 -4.0784,4.12473 -6.11969,9.11949 -6.11969,14.98322 0,5.8237 2.04129,10.79633 6.11969,14.91896 4.12367,4.12263 9.11737,6.18393 14.9832,6.18393 5.82371,0 10.79635,-2.0613 14.92002,-6.18393 4.12051,-4.12263 6.18288,-9.09526 6.18288,-14.91896 0,-5.86267 -2.06237,-10.85849 -6.18288,-14.98322 -4.12367,-4.12261 -9.09525,-6.18604 -14.92002,-6.18604 z" />
+ </g>
+ <g fill="#306998">
+ <path d="m 561.26467,150.17375 c -1.87066,0 -3.44325,0.6362 -4.71773,1.9107 -1.27556,1.31872 -1.91281,2.89025 -1.91281,4.71773 0,2.5511 1.23237,4.46389 3.69919,5.73733 0.84898,0.46872 4.39859,1.53045 10.64678,3.18834 5.05795,1.44619 8.81825,3.33686 11.28296,5.67413 3.52963,3.35898 5.29179,8.14097 5.29179,14.34703 0,6.11862 -2.16769,11.36829 -6.50203,15.74581 -4.37857,4.33644 -9.62823,6.50308 -15.74791,6.50308 h -16.64005 c -2.08448,0 -3.88879,-0.76365 -5.42029,-2.29621 -1.53045,-1.44407 -2.2962,-3.25048 -2.2962,-5.41712 v -0.12953 c 0,-2.12345 0.76575,-3.95198 2.2962,-5.48243 1.53044,-1.53045 3.33581,-2.29619 5.42029,-2.29619 h 17.2773 c 1.8696,0 3.44324,-0.6362 4.71773,-1.9107 1.27556,-1.27554 1.91281,-2.84707 1.91281,-4.71774 0,-2.33937 -1.21131,-4.10366 -3.63285,-5.29073 -0.63723,-0.30018 -4.20898,-1.36192 -10.71208,-3.18834 -5.05899,-1.48725 -8.82139,-3.44535 -11.28611,-5.8669 -3.52856,-3.44217 -5.29075,-8.30949 -5.29075,-14.5998 0,-6.12073 2.16876,-11.34721 6.50414,-15.68261 4.37648,-4.37752 9.62718,-6.56839 15.74687,-6.56839 h 11.73166 c 2.12452,0 3.95304,0.76575 5.48349,2.29831 1.52939,1.52834 2.29515,3.35793 2.29515,5.48032 v 0.12745 c 0,2.16876 -0.76576,3.97622 -2.29515,5.4203 -1.53045,1.52834 -3.35897,2.29619 -5.48349,2.29619 z" />
+ <path d="m 630.5677,134.55118 c 10.1159,0 18.76456,3.59177 25.94912,10.77422 7.18246,7.18563 10.77422,15.83429 10.77422,25.9502 0,10.11695 -3.59176,18.76561 -10.77422,25.95018 C 649.33331,204.40929 640.6836,208 630.5677,208 c -10.11592,0 -18.76563,-3.58965 -25.9481,-10.77422 -7.18351,-7.18351 -10.77526,-15.83217 -10.77526,-25.95018 0,-10.11591 3.59175,-18.76352 10.77526,-25.9502 7.18247,-7.18245 15.83218,-10.77422 25.9481,-10.77422 z m 0,15.7458 c -5.86585,0 -10.86059,2.06343 -14.98322,6.18604 -4.08155,4.12473 -6.12178,9.11949 -6.12178,14.98322 0,5.8237 2.04023,10.79633 6.12178,14.91896 4.12263,4.12263 9.11632,6.18393 14.98322,6.18393 5.82264,0 10.79527,-2.0613 14.91896,-6.18393 4.12261,-4.12263 6.18393,-9.09526 6.18393,-14.91896 0,-5.86267 -2.06132,-10.85849 -6.18393,-14.98322 -4.12369,-4.12261 -9.09527,-6.18604 -14.91896,-6.18604 z" />
+ <path d="m 724.0345,136.27333 c 3.61388,1.14811 5.4203,3.61282 5.4203,7.39523 v 0.32125 c 0,2.59008 -1.04278,4.65138 -3.12516,6.18394 -1.44512,1.01854 -2.93343,1.52834 -4.46178,1.52834 -0.80894,0 -1.63789,-0.12745 -2.48684,-0.38235 -2.08344,-0.67938 -4.23007,-1.02276 -6.43883,-1.02276 -5.86585,0 -10.86165,2.06343 -14.98322,6.18604 -4.08154,4.12473 -6.12074,9.11949 -6.12074,14.98322 0,5.8237 2.0392,10.79633 6.12074,14.91896 4.12157,4.12263 9.11633,6.18393 14.98322,6.18393 2.20982,0 4.35645,-0.33915 6.43883,-1.02065 0.80683,-0.25489 1.61471,-0.38234 2.42259,-0.38234 1.57046,0 3.08197,0.5119 4.52709,1.53254 2.08238,1.52835 3.12514,3.61283 3.12514,6.24819 0,3.74027 -1.80746,6.205 -5.42028,7.39524 -3.56964,1.10491 -7.26673,1.65579 -11.09232,1.65579 -10.11591,0 -18.76562,-3.58965 -25.95019,-10.77423 -7.1814,-7.18351 -10.77422,-15.83217 -10.77422,-25.95018 0,-10.11592 3.59176,-18.76352 10.77422,-25.9502 7.18351,-7.1814 15.83322,-10.77422 25.95019,-10.77422 3.82348,0.002 7.52162,0.57827 11.09126,1.72426 z" />
+ <path d="m 748.19829,108.41246 c 2.08132,0 3.88773,0.74469 5.42029,2.23194 1.5294,1.52834 2.29514,3.35793 2.29514,5.48244 v 44.18284 h 2.42259 c 5.44031,0 10.17805,-1.80642 14.21852,-5.4203 3.95198,-3.61283 6.20394,-8.07461 6.75693,-13.38852 0.25491,-1.99705 1.10597,-3.63494 2.5511,-4.90837 1.44408,-1.35982 3.16517,-2.04131 5.16328,-2.04131 h 0.19066 c 2.25405,0 4.14578,0.85212 5.67517,2.55109 1.36087,1.48727 2.04026,3.20942 2.04026,5.16329 0,0.25491 -0.0222,0.53298 -0.0632,0.82895 -1.02064,10.66889 -5.10115,18.65923 -12.24147,23.97103 3.73922,2.29831 7.18246,6.18604 10.32973,11.66849 3.27155,5.65306 4.90944,11.75483 4.90944,18.29688 v 3.25471 c 0,2.16664 -0.7668,3.972 -2.29515,5.41713 -1.53255,1.53256 -3.33791,2.29619 -5.4203,2.29619 h -0.1917 c -2.08342,0 -3.88879,-0.76363 -5.41922,-2.29619 -1.53045,-1.44408 -2.29514,-3.25049 -2.29514,-5.41713 v -3.25471 c -0.0442,-5.77629 -2.10555,-10.73102 -6.185,-14.85575 -4.12367,-4.07944 -9.09736,-6.11863 -14.91896,-6.11863 h -5.22754 v 24.22804 c 0,2.16664 -0.76574,3.97199 -2.29514,5.41712 -1.5315,1.53256 -3.33897,2.29621 -5.42028,2.29621 h -0.19381 c -2.08237,0 -3.88668,-0.76365 -5.41819,-2.29621 -1.52939,-1.44407 -2.29515,-3.25048 -2.29515,-5.41712 v -84.15879 c 0,-2.12556 0.76576,-3.95408 2.29515,-5.48243 1.53045,-1.48727 3.33582,-2.23195 5.41819,-2.23195 h 0.19381 z" />
+ <path d="m 876.85801,155.97534 c 0.5098,1.06173 0.76469,2.14346 0.76469,3.25048 0,0.8932 -0.17063,1.78536 -0.50874,2.67854 -0.72362,1.99707 -2.08342,3.4422 -4.08049,4.33434 -5.95115,2.67854 -13.77191,6.20711 -23.46229,10.58463 -12.02869,5.43924 -19.08476,8.64866 -21.16715,9.62823 3.22838,4.07944 8.26632,6.11863 15.11066,6.11863 4.54606,0 8.66973,-1.33769 12.36893,-4.01625 3.61176,-2.63534 6.14075,-6.03541 7.58587,-10.20227 1.23238,-3.48538 3.6992,-5.22754 7.39524,-5.22754 2.63536,0 4.73985,1.10702 6.31348,3.31369 0.97536,1.36193 1.46515,2.78598 1.46515,4.27325 0,0.8932 -0.16958,1.80641 -0.5098,2.74069 -2.50791,7.26988 -6.9065,13.13573 -13.19681,17.59961 -6.37563,4.63031 -13.51598,6.94757 -21.42206,6.94757 -10.1159,0 -18.76561,-3.58965 -25.94808,-10.7742 -7.18351,-7.18353 -10.77526,-15.83219 -10.77526,-25.9502 0,-10.11591 3.59175,-18.76351 10.77526,-25.95019 7.18141,-7.1814 15.83218,-10.77422 25.94808,-10.77422 7.30887,0 13.98364,1.99916 20.01906,5.99223 5.99223,3.91512 10.43294,9.05524 13.32426,15.43298 z m -33.34436,-5.67836 c -5.86479,0 -10.86059,2.06343 -14.98322,6.18604 -4.08049,4.12473 -6.12074,9.11949 -6.12074,14.98322 v 0.44661 l 35.63952,-16.00282 c -3.14516,-3.73817 -7.99034,-5.61305 -14.53556,-5.61305 z" />
+ <path d="m 898.02411,108.41246 c 2.08238,0 3.88879,0.74469 5.42028,2.23194 1.52939,1.52834 2.29515,3.35793 2.29515,5.48244 v 18.42434 h 9.56398 c 2.08237,0 3.88772,0.76575 5.42028,2.29831 1.5294,1.52834 2.29304,3.35793 2.29304,5.48032 v 0.12745 c 0,2.16876 -0.76364,3.97621 -2.29304,5.4203 -1.5315,1.52834 -3.33791,2.29619 -5.42028,2.29619 h -9.56398 v 37.80405 c 0,1.23446 0.42343,2.27724 1.27554,3.12514 0.85002,0.85212 1.9128,1.27555 3.1873,1.27555 h 5.10114 c 2.08237,0 3.88772,0.76574 5.42028,2.29619 1.5294,1.53045 2.29304,3.35898 2.29304,5.48243 v 0.12954 c 0,2.16664 -0.76364,3.97199 -2.29304,5.41711 C 919.1923,207.23635 917.38589,208 915.30352,208 h -5.10114 c -5.52563,0 -10.26442,-1.95387 -14.21746,-5.86478 -3.91196,-3.95198 -5.86479,-8.67078 -5.86479,-14.15532 v -71.85095 c 0,-2.12558 0.7647,-3.9541 2.29515,-5.48245 1.53045,-1.48725 3.33686,-2.23193 5.41924,-2.23193 h 0.18959 z" />
+ <path d="m 951.70877,150.17375 c -1.87066,0 -3.44324,0.6362 -4.71773,1.9107 -1.27556,1.31872 -1.91281,2.89025 -1.91281,4.71773 0,2.5511 1.23238,4.46389 3.69711,5.73733 0.8521,0.46872 4.40067,1.53045 10.64886,3.18834 5.05691,1.44619 8.81825,3.33686 11.28402,5.67413 3.52751,3.35898 5.2918,8.14097 5.2918,14.34703 0,6.11862 -2.16876,11.36829 -6.5031,15.74581 -4.37752,4.33644 -9.62822,6.50308 -15.74789,6.50308 h -16.64007 c -2.08342,0 -3.88879,-0.76365 -5.42028,-2.29621 -1.53045,-1.44407 -2.2941,-3.25048 -2.2941,-5.41712 v -0.12953 c 0,-2.12345 0.76365,-3.95198 2.2941,-5.48243 1.53045,-1.53045 3.33686,-2.29619 5.42028,-2.29619 h 17.2773 c 1.86962,0 3.4443,-0.6362 4.71775,-1.9107 1.27554,-1.27554 1.91279,-2.84707 1.91279,-4.71774 0,-2.33937 -1.2113,-4.10366 -3.63283,-5.29073 -0.63936,-0.30018 -4.209,-1.36192 -10.71208,-3.18834 -5.05901,-1.48725 -8.8214,-3.44535 -11.28613,-5.8669 -3.52856,-3.44217 -5.29073,-8.30949 -5.29073,-14.5998 0,-6.12073 2.16875,-11.34721 6.50413,-15.68261 4.37647,-4.37752 9.62718,-6.56839 15.74791,-6.56839 h 11.73063 c 2.1266,0 3.95304,0.76575 5.48243,2.29831 1.53045,1.52834 2.29514,3.35793 2.29514,5.48032 v 0.12745 c 0,2.16876 -0.76469,3.97622 -2.29514,5.4203 -1.52939,1.52834 -3.35687,2.29619 -5.48243,2.29619 z" />
+ </g>
+ </g>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/icon.html b/testing/web-platform/tests/tools/third_party/websockets/logo/icon.html
new file mode 100644
index 0000000000..6a71ec23bc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/logo/icon.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Icon</title>
+ <style>
+ body {
+ font-family: -apple-system;
+ font-size: 24px;
+ font-weight: 100;
+ text-align: center;
+ }
+ </style>
+ </head>
+ <body>
+ <p>Take a screenshot of these DOM nodes to2x make a PNG.</p>
+ <p><img src="icon.svg" height="8" width="8" alt="8x8 / 16x16 @ 2x"></p>
+ <p><img src="icon.svg" height="16" width="16" alt="16x16 / 32x32 @ 2x"></p>
+ <p><img src="icon.svg" height="32" width="32" alt="32x32 / 32x32 @ 2x"></p>
+ <p><img src="icon.svg" height="32" width="32" alt="32x32 / 64x64 @ 2x"></p>
+ <p><img src="icon.svg" height="64" width="64" alt="64x64 / 128x128 @ 2x"></p>
+ <p><img src="icon.svg" height="128" width="128" alt="128x128 / 256x256 @ 2x"></p>
+ <p><img src="icon.svg" height="256" width="256" alt="256x256 / 512x512 @ 2x"></p>
+ <p><img src="icon.svg" height="512" width="512" alt="512x512 / 1024x1024 @ 2x"></p>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/icon.svg b/testing/web-platform/tests/tools/third_party/websockets/logo/icon.svg
new file mode 100644
index 0000000000..cb760940aa
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/logo/icon.svg
@@ -0,0 +1,15 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
+ <linearGradient id="w" x1="0" y1="0" x2="0.6667" y2="0.6667">
+ <stop offset="0%" stop-color="#ffe873" />
+ <stop offset="100%" stop-color="#ffd43b" />
+ </linearGradient>
+ <linearGradient id="s" x1="0" y1="0" x2="0.6667" y2="0.6667">
+ <stop offset="0%" stop-color="#5a9fd4" />
+ <stop offset="100%" stop-color="#306998" />
+ </linearGradient>
+ <g>
+ <path fill="url(#w)" d="m 151.60708,154.81618 c -0.43704,0.0747 -0.88656,0.12978 -1.35572,0.14933 -2.45813,0.0764 -4.25357,-0.58665 -5.82335,-2.15107 l -8.89246,-8.85942 -11.23464,-11.19805 -36.040757,-35.919452 c -3.43568,-3.42217 -7.332485,-5.347474 -11.589626,-5.723468 -2.229803,-0.198219 -4.473877,0.03111 -6.640354,0.675545 -3.242133,0.944875 -6.135526,2.664848 -8.593662,5.116366 -3.834369,3.819499 -5.86349,8.414979 -5.875977,13.287799 -0.06065,4.95281 1.951523,9.60074 5.808192,13.44424 l 55.622894,55.43648 c 1.82219,1.84175 2.65971,3.79549 2.63384,6.14568 l 0.004,0.208 c 0.0527,2.43196 -0.75991,4.34571 -2.6267,6.20612 -1.78028,1.77598 -3.8094,2.65241 -6.30945,2.75552 -2.45814,0.0764 -4.25446,-0.58844 -5.82514,-2.15286 L 48.702551,136.2618 c -5.214172,-5.19459 -11.702899,-6.98745 -18.22998,-5.04881 -3.245701,0.9431 -6.135527,2.66307 -8.595446,5.11459 -3.83437,3.82127 -5.865275,8.41676 -5.875978,13.28957 -0.05619,4.95281 1.951524,9.60252 5.806409,13.4478 l 58.10689,57.90577 c 8.319842,8.29143 19.340421,11.9376 32.743314,10.83806 12.57967,-1.02043 23.02317,-5.5848 31.03441,-13.57313 7.51265,-7.4861 11.96423,-16.35175 13.28695,-26.42537 10.47206,-1.68264 19.29494,-6.04524 26.27512,-13.00158 4.01364,-3.99994 7.14963,-8.3972 9.40531,-13.16157 -14.15569,-0.39911 -28.23645,-4.00972 -41.05247,-10.83095 z" />
+ <path fill="url(#s)" d="m 196.96038,146.11854 c 0.10259,-12.84514 -4.43017,-23.98541 -13.50635,-33.1346 L 147.57292,77.225374 c -0.24349,-0.240885 -0.46469,-0.487992 -0.68678,-0.744877 -1.48416,-1.739529 -2.18788,-3.583056 -2.21018,-5.807022 -0.0259,-2.470184 0.84911,-4.508375 2.7605,-6.407902 1.91406,-1.909304 3.8531,-2.737735 6.36564,-2.684403 2.53662,0.024 4.62728,0.943097 6.57257,2.881734 l 60.59178,60.384846 12.11408,-12.06914 c 1.12203,-0.90755 1.95777,-1.76887 2.87823,-2.93418 5.91879,-7.51544 5.26947,-18.272609 -1.51003,-25.02895 L 187.20456,37.727314 c -9.19393,-9.157192 -20.36703,-13.776677 -33.16789,-13.7269 -12.94266,-0.05067 -24.14163,4.548375 -33.28739,13.662901 -9.02892,8.996307 -13.64015,19.93925 -13.7008,32.487501 l -0.004,0.14222 c -0.002,0.167998 -0.005,0.336884 -0.005,0.506659 -0.091,12.232701 4.10729,22.95787 12.48154,31.881285 0.40226,0.43022 0.80274,0.85777 1.22283,1.27821 l 35.75088,35.62612 c 1.88909,1.88174 2.71769,3.79638 2.69361,6.20968 l 0.003,0.20977 c 0.0527,2.43197 -0.76081,4.34571 -2.6276,6.20791 -1.44759,1.43909 -3.06286,2.27818 -4.9564,2.60262 12.81601,6.82123 26.89677,10.43184 41.05246,10.83362 2.80598,-5.92525 4.2509,-12.41848 4.29906,-19.43526 z" />
+ <path fill="#ffffff" d="m 215.68093,93.181574 c 2.84701,-2.838179 7.46359,-2.836401 10.30883,0 2.84433,2.834623 2.84612,7.435446 -0.002,10.270956 -2.84345,2.83818 -7.46271,2.83818 -10.30704,0 -2.84524,-2.83551 -2.84791,-7.435444 0,-10.270956 z" />
+ </g>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/old.svg b/testing/web-platform/tests/tools/third_party/websockets/logo/old.svg
new file mode 100644
index 0000000000..a073139e33
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/logo/old.svg
@@ -0,0 +1,14 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="360" height="120" viewBox="0 0 21 7">
+ <linearGradient id="w" x1="0" y1="0" x2="1" y2="1">
+ <stop offset="0%" stop-color="#5a9fd4" />
+ <stop offset="100%" stop-color="#306998" />
+ </linearGradient>
+ <linearGradient id="s" x1="0" y1="0" x2="1" y2="1">
+ <stop offset="0%" stop-color="#ffe873" />
+ <stop offset="100%" stop-color="#ffd43b" />
+ </linearGradient>
+ <polyline fill="none" stroke="url(#w)" stroke-linecap="round" stroke-linejoin="round"
+ points="1,1 1,5 5,5 5,1 5,5 9,5 9,1"/>
+ <polyline fill="none" stroke="url(#s)" stroke-linecap="round" stroke-linejoin="round"
+ points="19,1 11,1 11,3 19,3 19,5 11,5"/>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/vertical.svg b/testing/web-platform/tests/tools/third_party/websockets/logo/vertical.svg
new file mode 100644
index 0000000000..b07fb22387
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/logo/vertical.svg
@@ -0,0 +1,31 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="480" height="320" viewBox="0 0 480 320">
+ <linearGradient id="w" x1="0.2333" y1="0" x2="0.5889" y2="0.5333">
+ <stop offset="0%" stop-color="#ffe873" />
+ <stop offset="100%" stop-color="#ffd43b" />
+ </linearGradient>
+ <linearGradient id="s" x1="0.2333" y1="0" x2="0.5889" y2="0.5333">
+ <stop offset="0%" stop-color="#5a9fd4" />
+ <stop offset="100%" stop-color="#306998" />
+ </linearGradient>
+ <g>
+ <path fill="url(#w)" d="m 263.40708,146.81618 c -0.43704,0.0747 -0.88656,0.12978 -1.35572,0.14933 -2.45813,0.0764 -4.25357,-0.58665 -5.82335,-2.15107 l -8.89246,-8.85942 -11.23464,-11.19805 -36.04076,-35.919454 c -3.43568,-3.42217 -7.33248,-5.347474 -11.58962,-5.723468 -2.22981,-0.198219 -4.47388,0.03111 -6.64036,0.675545 -3.24213,0.944875 -6.13552,2.664848 -8.59366,5.116366 -3.83437,3.819499 -5.86349,8.414979 -5.87598,13.287801 -0.0607,4.95281 1.95153,9.60074 5.8082,13.44424 l 55.62289,55.43648 c 1.82219,1.84175 2.65971,3.79549 2.63384,6.14568 l 0.004,0.208 c 0.0527,2.43196 -0.75991,4.34571 -2.6267,6.20612 -1.78028,1.77598 -3.8094,2.65241 -6.30945,2.75552 -2.45814,0.0764 -4.25446,-0.58844 -5.82514,-2.15286 L 160.50255,128.2618 c -5.21417,-5.19459 -11.7029,-6.98745 -18.22998,-5.04881 -3.2457,0.9431 -6.13553,2.66307 -8.59545,5.11459 -3.83437,3.82127 -5.86527,8.41676 -5.87597,13.28957 -0.0562,4.95281 1.95152,9.60252 5.80641,13.4478 l 58.10689,57.90577 c 8.31984,8.29143 19.34042,11.9376 32.74331,10.83806 12.57967,-1.02043 23.02317,-5.5848 31.03441,-13.57313 7.51265,-7.4861 11.96423,-16.35175 13.28695,-26.42537 10.47206,-1.68264 19.29494,-6.04524 26.27512,-13.00158 4.01364,-3.99994 7.14963,-8.3972 9.40531,-13.16157 -14.15569,-0.39911 -28.23645,-4.00972 -41.05247,-10.83095 z" />
+ <path fill="url(#s)" d="m 308.76038,138.11854 c 0.10259,-12.84514 -4.43017,-23.98541 -13.50635,-33.1346 L 259.37292,69.225372 c -0.24349,-0.240885 -0.46469,-0.487992 -0.68678,-0.744877 -1.48416,-1.739529 -2.18788,-3.583056 -2.21018,-5.807022 -0.0259,-2.470184 0.84911,-4.508375 2.7605,-6.407902 1.91406,-1.909304 3.8531,-2.737735 6.36564,-2.684403 2.53662,0.024 4.62728,0.943097 6.57257,2.881734 l 60.59178,60.384848 12.11408,-12.06914 c 1.12203,-0.90755 1.95777,-1.76887 2.87823,-2.93418 5.91879,-7.515442 5.26947,-18.272611 -1.51003,-25.028952 L 299.00456,29.727312 c -9.19393,-9.157192 -20.36703,-13.776677 -33.16789,-13.7269 -12.94266,-0.05067 -24.14163,4.548375 -33.28739,13.662901 -9.02892,8.996307 -13.64015,19.93925 -13.7008,32.487501 l -0.004,0.14222 c -0.002,0.167998 -0.005,0.336884 -0.005,0.506659 -0.091,12.232701 4.10729,22.95787 12.48154,31.881285 0.40226,0.43022 0.80274,0.85777 1.22283,1.27821 l 35.75088,35.626122 c 1.88909,1.88174 2.71769,3.79638 2.69361,6.20968 l 0.003,0.20977 c 0.0527,2.43197 -0.76081,4.34571 -2.6276,6.20791 -1.44759,1.43909 -3.06286,2.27818 -4.9564,2.60262 12.81601,6.82123 26.89677,10.43184 41.05246,10.83362 2.80598,-5.92525 4.2509,-12.41848 4.29906,-19.43526 z" />
+ <path fill="#ffffff" d="m 327.48093,85.181572 c 2.84701,-2.838179 7.46359,-2.836401 10.30883,0 2.84433,2.834623 2.84612,7.435446 -0.002,10.270956 -2.84345,2.83818 -7.46271,2.83818 -10.30704,0 -2.84524,-2.83551 -2.84791,-7.435444 0,-10.270956 z" />
+ </g>
+ <g>
+ <g fill="#ffd43b">
+ <path d="m 25.719398,284.91839 c 0,2.59075 0.912299,4.79875 2.736898,6.62269 1.824599,1.82657 4.033255,2.73821 6.625313,2.73821 2.591402,0 4.800058,-0.91164 6.624002,-2.73821 1.825254,-1.82394 2.738209,-4.03194 2.738209,-6.62269 v -21.77984 c 0,-1.32126 0.475811,-2.45901 1.42809,-3.40998 0.952278,-0.95359 2.089375,-1.43006 3.411947,-1.43006 h 0.0793 c 1.348132,0 2.471467,0.47647 3.371969,1.43006 0.952278,0.95097 1.428745,2.08938 1.428745,3.40998 v 21.77984 c 0,2.59075 0.912299,4.79875 2.738209,6.62269 1.823944,1.82657 4.031289,2.73821 6.624002,2.73821 2.618274,0 4.839382,-0.91164 6.663981,-2.73821 1.825254,-1.82394 2.738209,-4.03194 2.738209,-6.62269 v -21.77984 c 0,-1.32126 0.475156,-2.45901 1.42809,-3.40998 0.897881,-0.95359 2.022526,-1.43006 3.371969,-1.43006 h 0.07865 c 1.323228,0 2.460325,0.47647 3.411948,1.43006 0.926062,0.95097 1.388766,2.08938 1.388766,3.40998 v 21.77984 c 0,5.26211 -1.865233,9.75807 -5.593077,13.48657 -3.729156,3.7285 -8.22577,5.59373 -13.487876,5.59373 -6.294998,0 -11.028207,-2.08807 -14.202904,-6.26747 -3.199602,4.1794 -7.94723,6.26747 -14.240916,6.26747 -5.262763,0 -9.759377,-1.86523 -13.487876,-5.59373 C 17.866544,294.67646 16,290.18115 16,284.91839 v -21.77984 c 0,-1.32126 0.476467,-2.45901 1.428745,-3.40998 0.951623,-0.95359 2.075612,-1.43006 3.371969,-1.43006 h 0.11928 c 1.295702,0 2.419036,0.47647 3.372625,1.43006 0.950967,0.95097 1.427434,2.08938 1.427434,3.40998 v 21.77984 z" />
+ <path d="m 132.94801,271.6291 c 0.31786,0.66063 0.47712,1.33371 0.47712,2.02252 0,0.55577 -0.10551,1.11089 -0.3172,1.66665 -0.45026,1.24262 -1.29636,2.14181 -2.53898,2.69692 -3.70293,1.66665 -8.56853,3.8622 -14.59875,6.58599 -7.48453,3.38442 -11.87497,5.38139 -13.17067,5.9909 2.00942,2.53832 5.14414,3.80715 9.40219,3.80715 2.82931,0 5.39515,-0.83234 7.69556,-2.499 2.24798,-1.63977 3.82222,-3.75537 4.72141,-6.34808 0.76746,-2.16868 2.30107,-3.25269 4.60148,-3.25269 1.63912,0 2.94859,0.68881 3.92708,2.06185 0.6082,0.84742 0.9123,1.7335 0.9123,2.65891 0,0.55577 -0.10552,1.12399 -0.31655,1.70532 -1.56048,4.52348 -4.29869,8.17334 -8.21135,10.95087 -3.96706,2.88108 -8.41059,4.32293 -13.32993,4.32293 -6.29434,0 -11.67639,-2.23356 -16.145474,-6.70395 -4.469743,-4.46975 -6.704615,-9.85114 -6.704615,-16.14679 0,-6.29434 2.234872,-11.67507 6.704615,-16.14678 4.468434,-4.46843 9.851134,-6.70396 16.145474,-6.70396 4.54773,0 8.70027,1.24392 12.45629,3.7285 3.72785,2.43607 6.49162,5.63437 8.29,9.60274 z m -20.74695,-3.5332 c -3.64985,0 -6.7577,1.28391 -9.32289,3.84909 -2.53897,2.5665 -3.808452,5.67435 -3.808452,9.32289 v 0.27789 l 22.175692,-9.95731 c -1.95633,-2.32597 -4.97177,-3.49256 -9.04435,-3.49256 z" />
+ <path d="m 146.11999,242.03442 c 1.2957,0 2.41904,0.46336 3.37197,1.38876 0.95228,0.95097 1.42874,2.08938 1.42874,3.4113 v 15.4311 c 2.98792,-2.64318 7.36525,-3.96707 13.13004,-3.96707 6.29434,0 11.67638,2.23488 16.14613,6.70396 4.46908,4.47106 6.70461,9.85245 6.70461,16.14679 0,6.29499 -2.23553,11.67638 -6.70461,16.14678 -4.46909,4.4704 -9.85113,6.70396 -16.14613,6.70396 -6.295,0 -11.66262,-2.22111 -16.10549,-6.66529 -4.4704,-4.41469 -6.71838,-9.77052 -6.7446,-16.06617 v -34.43341 c 0,-1.32257 0.47647,-2.46032 1.42875,-3.41129 0.95162,-0.92541 2.07561,-1.38877 3.37197,-1.38877 h 0.11862 z m 17.93009,26.06148 c -3.64919,0 -6.75704,1.28391 -9.32288,3.84909 -2.53767,2.5665 -3.80781,5.67435 -3.80781,9.32289 0,3.62364 1.27014,6.71772 3.80781,9.28291 2.56584,2.56519 5.67303,3.84778 9.32288,3.84778 3.62364,0 6.71773,-1.28259 9.28357,-3.84778 2.56387,-2.56519 3.84712,-5.65927 3.84712,-9.28291 0,-3.64788 -1.28325,-6.75639 -3.84712,-9.32289 -2.56584,-2.56518 -5.65927,-3.84909 -9.28357,-3.84909 z" />
+ </g>
+ <g fill="#306998">
+ <path d="m 205.94246,268.01922 c -1.16397,0 -2.14247,0.39586 -2.93548,1.18888 -0.79368,0.82054 -1.19019,1.79838 -1.19019,2.93548 0,1.58735 0.76681,2.77753 2.30172,3.56989 0.52825,0.29165 2.7369,0.95228 6.62466,1.98386 3.14717,0.89985 5.48691,2.07627 7.02051,3.53057 2.19621,2.09003 3.29267,5.06549 3.29267,8.92704 0,3.80714 -1.34879,7.0736 -4.04571,9.79739 -2.72444,2.69823 -5.9909,4.04636 -9.7987,4.04636 h -10.35381 c -1.29701,0 -2.41969,-0.47516 -3.37262,-1.42875 -0.95228,-0.89853 -1.42875,-2.02252 -1.42875,-3.37065 v -0.0806 c 0,-1.32126 0.47647,-2.45901 1.42875,-3.41129 0.95227,-0.95228 2.07561,-1.42874 3.37262,-1.42874 h 10.75032 c 1.16331,0 2.14246,-0.39586 2.93548,-1.18888 0.79368,-0.79367 1.19019,-1.77151 1.19019,-2.93548 0,-1.45561 -0.7537,-2.55339 -2.26044,-3.29201 -0.3965,-0.18678 -2.61892,-0.84742 -6.66529,-1.98386 -3.14782,-0.9254 -5.48887,-2.14377 -7.02247,-3.65051 -2.19555,-2.1418 -3.29202,-5.17035 -3.29202,-9.08432 0,-3.80846 1.34945,-7.06049 4.04702,-9.75807 2.72314,-2.72379 5.99024,-4.087 9.79805,-4.087 h 7.2997 c 1.32192,0 2.45967,0.47647 3.41195,1.43006 0.95162,0.95097 1.42809,2.08938 1.42809,3.40998 v 0.0793 c 0,1.34945 -0.47647,2.47409 -1.42809,3.37263 -0.95228,0.95097 -2.09003,1.42874 -3.41195,1.42874 z" />
+ <path d="m 249.06434,258.29851 c 6.29434,0 11.67573,2.23488 16.14612,6.70396 4.46909,4.47106 6.70396,9.85245 6.70396,16.14679 0,6.29499 -2.23487,11.67638 -6.70396,16.14678 -4.46974,4.46974 -9.85178,6.70396 -16.14612,6.70396 -6.29435,0 -11.67639,-2.23356 -16.14548,-6.70396 -4.46974,-4.46974 -6.70461,-9.85113 -6.70461,-16.14678 0,-6.29434 2.23487,-11.67508 6.70461,-16.14679 4.46909,-4.46908 9.85113,-6.70396 16.14548,-6.70396 z m 0,9.79739 c -3.64986,0 -6.7577,1.28391 -9.32289,3.84909 -2.53963,2.5665 -3.80911,5.67435 -3.80911,9.32289 0,3.62364 1.26948,6.71772 3.80911,9.28291 2.56519,2.56519 5.67238,3.84778 9.32289,3.84778 3.62298,0 6.71706,-1.28259 9.28291,-3.84778 2.56518,-2.56519 3.84778,-5.65927 3.84778,-9.28291 0,-3.64788 -1.2826,-6.75639 -3.84778,-9.32289 -2.56585,-2.56518 -5.65928,-3.84909 -9.28291,-3.84909 z" />
+ <path d="m 307.22146,259.37007 c 2.24864,0.71438 3.37263,2.24798 3.37263,4.60148 v 0.19989 c 0,1.6116 -0.64884,2.89419 -1.94454,3.84778 -0.89919,0.63376 -1.82525,0.95097 -2.77622,0.95097 -0.50334,0 -1.01913,-0.0793 -1.54737,-0.23791 -1.29636,-0.42272 -2.63204,-0.63638 -4.00638,-0.63638 -3.64986,0 -6.75836,1.28391 -9.32289,3.84909 -2.53963,2.5665 -3.80846,5.67435 -3.80846,9.32289 0,3.62364 1.26883,6.71772 3.80846,9.28291 2.56453,2.56519 5.67238,3.84778 9.32289,3.84778 1.375,0 2.71068,-0.21103 4.00638,-0.63507 0.50203,-0.1586 1.00471,-0.2379 1.50739,-0.2379 0.97718,0 1.91767,0.31851 2.81686,0.95358 1.2957,0.95097 1.94453,2.24798 1.94453,3.88776 0,2.32728 -1.12464,3.86089 -3.37262,4.60148 -2.22111,0.6875 -4.52152,1.03027 -6.90189,1.03027 -6.29434,0 -11.67638,-2.23356 -16.14678,-6.70396 -4.46843,-4.46974 -6.70396,-9.85113 -6.70396,-16.14678 0,-6.29435 2.23487,-11.67508 6.70396,-16.14679 4.46974,-4.46843 9.85178,-6.70396 16.14678,-6.70396 2.37906,0.001 4.68012,0.35981 6.90123,1.07287 z" />
+ <path d="m 322.25671,242.03442 c 1.29504,0 2.41903,0.46336 3.37262,1.38876 0.95163,0.95097 1.42809,2.08938 1.42809,3.4113 v 27.49154 h 1.50739 c 3.38508,0 6.33301,-1.12399 8.84708,-3.37263 2.45901,-2.24798 3.86023,-5.0242 4.20431,-8.33063 0.15861,-1.24261 0.68816,-2.26174 1.58735,-3.0541 0.89854,-0.84611 1.96944,-1.27015 3.21271,-1.27015 h 0.11863 c 1.40252,0 2.5796,0.53021 3.53122,1.58735 0.84676,0.92541 1.26949,1.99697 1.26949,3.21271 0,0.15861 -0.0138,0.33163 -0.0393,0.51579 -0.63507,6.63842 -3.17405,11.61019 -7.61692,14.91531 2.32663,1.43006 4.46909,3.84909 6.42739,7.26039 2.03563,3.51746 3.05476,7.31412 3.05476,11.38473 v 2.02515 c 0,1.34813 -0.47712,2.47147 -1.42809,3.37066 -0.95359,0.95359 -2.07692,1.42874 -3.37263,1.42874 h -0.11928 c -1.29635,0 -2.41969,-0.47515 -3.37196,-1.42874 -0.95228,-0.89854 -1.42809,-2.02253 -1.42809,-3.37066 v -2.02515 c -0.0275,-3.59414 -1.31012,-6.67708 -3.84844,-9.24358 -2.56584,-2.53832 -5.66058,-3.80715 -9.28291,-3.80715 h -3.25269 v 15.07523 c 0,1.34813 -0.47646,2.47146 -1.42809,3.37065 -0.95293,0.95359 -2.07758,1.42875 -3.37262,1.42875 h -0.12059 c -1.2957,0 -2.41838,-0.47516 -3.37132,-1.42875 -0.95162,-0.89853 -1.42809,-2.02252 -1.42809,-3.37065 v -52.36547 c 0,-1.32257 0.47647,-2.46032 1.42809,-3.41129 0.95228,-0.92541 2.07562,-1.38877 3.37132,-1.38877 h 0.12059 z" />
+ <path d="m 402.31164,271.6291 c 0.31721,0.66063 0.47581,1.33371 0.47581,2.02252 0,0.55577 -0.10617,1.11089 -0.31655,1.66665 -0.45025,1.24262 -1.29635,2.14181 -2.53897,2.69692 -3.70294,1.66665 -8.56919,3.8622 -14.59876,6.58599 -7.48452,3.38442 -11.87496,5.38139 -13.17067,5.9909 2.00877,2.53832 5.14349,3.80715 9.40219,3.80715 2.82866,0 5.3945,-0.83234 7.69622,-2.499 2.24732,-1.63977 3.82091,-3.75537 4.7201,-6.34808 0.76681,-2.16868 2.30172,-3.25269 4.60148,-3.25269 1.63978,0 2.94924,0.68881 3.92839,2.06185 0.60689,0.84742 0.91165,1.7335 0.91165,2.65891 0,0.55577 -0.10552,1.12399 -0.31721,1.70532 -1.56048,4.52348 -4.29738,8.17334 -8.21135,10.95087 -3.96706,2.88108 -8.40994,4.32293 -13.32928,4.32293 -6.29434,0 -11.67638,-2.23356 -16.14547,-6.70395 -4.46974,-4.46975 -6.70461,-9.85114 -6.70461,-16.14679 0,-6.29434 2.23487,-11.67507 6.70461,-16.14678 4.46843,-4.46843 9.85113,-6.70396 16.14547,-6.70396 4.54774,0 8.70093,1.24392 12.4563,3.7285 3.7285,2.43607 6.49161,5.63437 8.29065,9.60274 z m -20.7476,-3.5332 c -3.6492,0 -6.7577,1.28391 -9.32289,3.84909 -2.53897,2.5665 -3.80846,5.67435 -3.80846,9.32289 v 0.27789 l 22.1757,-9.95731 c -1.95699,-2.32597 -4.97177,-3.49256 -9.04435,-3.49256 z" />
+ <path d="m 415.48166,242.03442 c 1.2957,0 2.41969,0.46336 3.37262,1.38876 0.95162,0.95097 1.42809,2.08938 1.42809,3.4113 v 11.46403 h 5.95092 c 1.2957,0 2.41903,0.47647 3.37262,1.43006 0.95163,0.95097 1.42678,2.08938 1.42678,3.40998 v 0.0793 c 0,1.34945 -0.47515,2.47409 -1.42678,3.37263 -0.95293,0.95097 -2.07692,1.42874 -3.37262,1.42874 h -5.95092 v 23.52252 c 0,0.76811 0.26347,1.41695 0.79367,1.94453 0.5289,0.53021 1.19019,0.79368 1.98321,0.79368 h 3.17404 c 1.2957,0 2.41903,0.47646 3.37262,1.42874 0.95163,0.95228 1.42678,2.09003 1.42678,3.41129 v 0.0806 c 0,1.34813 -0.47515,2.47146 -1.42678,3.37065 C 428.65298,303.52484 427.52899,304 426.23329,304 h -3.17404 c -3.43817,0 -6.38675,-1.21574 -8.84642,-3.6492 -2.43411,-2.45901 -3.6492,-5.39515 -3.6492,-8.80775 v -44.70726 c 0,-1.32258 0.47581,-2.46033 1.42809,-3.4113 0.95228,-0.9254 2.07627,-1.38876 3.37197,-1.38876 h 0.11797 z" />
+ <path d="m 448.88545,268.01922 c -1.16397,0 -2.14246,0.39586 -2.93548,1.18888 -0.79368,0.82054 -1.19019,1.79838 -1.19019,2.93548 0,1.58735 0.76681,2.77753 2.30042,3.56989 0.5302,0.29165 2.7382,0.95228 6.62596,1.98386 3.14652,0.89985 5.48691,2.07627 7.02117,3.53057 2.19489,2.09003 3.29267,5.06549 3.29267,8.92704 0,3.80714 -1.34945,7.0736 -4.04637,9.79739 -2.72379,2.69823 -5.99089,4.04636 -9.79869,4.04636 h -10.35382 c -1.29635,0 -2.41969,-0.47516 -3.37262,-1.42875 -0.95228,-0.89853 -1.42744,-2.02252 -1.42744,-3.37065 v -0.0806 c 0,-1.32126 0.47516,-2.45901 1.42744,-3.41129 0.95228,-0.95228 2.07627,-1.42874 3.37262,-1.42874 h 10.75032 c 1.16332,0 2.14312,-0.39586 2.93549,-1.18888 0.79367,-0.79367 1.19018,-1.77151 1.19018,-2.93548 0,-1.45561 -0.7537,-2.55339 -2.26043,-3.29201 -0.39782,-0.18678 -2.61893,-0.84742 -6.66529,-1.98386 -3.14783,-0.9254 -5.48887,-2.14377 -7.02248,-3.65051 -2.19555,-2.1418 -3.29201,-5.17035 -3.29201,-9.08432 0,-3.80846 1.34944,-7.06049 4.04701,-9.75807 2.72314,-2.72379 5.99025,-4.087 9.7987,-4.087 h 7.29906 c 1.32322,0 2.45967,0.47647 3.41129,1.43006 0.95228,0.95097 1.42809,2.08938 1.42809,3.40998 v 0.0793 c 0,1.34945 -0.47581,2.47409 -1.42809,3.37263 -0.95162,0.95097 -2.08872,1.42874 -3.41129,1.42874 z" />
+ </g>
+ </g>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/pyproject.toml b/testing/web-platform/tests/tools/third_party/websockets/pyproject.toml
new file mode 100644
index 0000000000..f24616dd7e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/pyproject.toml
@@ -0,0 +1,87 @@
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "websockets"
+description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
+requires-python = ">=3.8"
+license = { text = "BSD-3-Clause" }
+authors = [
+ { name = "Aymeric Augustin", email = "aymeric.augustin@m4x.org" },
+]
+keywords = ["WebSocket"]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Web Environment",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: BSD License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+]
+dynamic = ["version", "readme"]
+
+[project.urls]
+Homepage = "https://github.com/python-websockets/websockets"
+Changelog = "https://websockets.readthedocs.io/en/stable/project/changelog.html"
+Documentation = "https://websockets.readthedocs.io/"
+Funding = "https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme"
+Tracker = "https://github.com/python-websockets/websockets/issues"
+
+# On a macOS runner, build Intel, Universal, and Apple Silicon wheels.
+[tool.cibuildwheel.macos]
+archs = ["x86_64", "universal2", "arm64"]
+
+# On an Linux Intel runner with QEMU installed, build Intel and ARM wheels.
+[tool.cibuildwheel.linux]
+archs = ["auto", "aarch64"]
+
+[tool.coverage.run]
+branch = true
+omit = [
+ # */websockets matches src/websockets and .tox/**/site-packages/websockets
+ "*/websockets/__main__.py",
+ "*/websockets/legacy/async_timeout.py",
+ "*/websockets/legacy/compatibility.py",
+ "tests/maxi_cov.py",
+]
+
+[tool.coverage.paths]
+source = [
+ "src/websockets",
+ ".tox/*/lib/python*/site-packages/websockets",
+]
+
+[tool.coverage.report]
+exclude_lines = [
+ "except ImportError:",
+ "if self.debug:",
+ "if sys.platform != \"win32\":",
+ "if typing.TYPE_CHECKING:",
+ "pragma: no cover",
+ "raise AssertionError",
+ "raise NotImplementedError",
+ "self.fail\\(\".*\"\\)",
+ "@unittest.skip",
+]
+
+[tool.ruff]
+select = [
+ "E", # pycodestyle
+ "F", # Pyflakes
+ "W", # pycodestyle
+ "I", # isort
+]
+ignore = [
+ "F403",
+ "F405",
+]
+
+[tool.ruff.isort]
+combine-as-imports = true
+lines-after-imports = 2
diff --git a/testing/web-platform/tests/tools/third_party/websockets/setup.cfg b/testing/web-platform/tests/tools/third_party/websockets/setup.cfg
deleted file mode 100644
index dc424fe195..0000000000
--- a/testing/web-platform/tests/tools/third_party/websockets/setup.cfg
+++ /dev/null
@@ -1,42 +0,0 @@
-[bdist_wheel]
-python-tag = py37.py38.py39.py310
-
-[metadata]
-license_file = LICENSE
-project_urls =
- Changelog = https://websockets.readthedocs.io/en/stable/project/changelog.html
- Documentation = https://websockets.readthedocs.io/
- Funding = https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme
- Tracker = https://github.com/aaugustin/websockets/issues
-
-[flake8]
-ignore = E203,E731,F403,F405,W503
-max-line-length = 88
-
-[isort]
-profile = black
-combine_as_imports = True
-lines_after_imports = 2
-
-[coverage:run]
-branch = True
-omit =
- */__main__.py
-source =
- websockets
- tests
-
-[coverage:paths]
-source =
- src/websockets
- .tox/*/lib/python*/site-packages/websockets
-
-[coverage:report]
-exclude_lines =
- if self.debug:
- pragma: no cover
-
-[egg_info]
-tag_build =
-tag_date = 0
-
diff --git a/testing/web-platform/tests/tools/third_party/websockets/setup.py b/testing/web-platform/tests/tools/third_party/websockets/setup.py
index b2d07737d2..ae0aaa65de 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/setup.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/setup.py
@@ -1,3 +1,4 @@
+import os
import pathlib
import re
@@ -6,58 +7,32 @@ import setuptools
root_dir = pathlib.Path(__file__).parent
-description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
+exec((root_dir / "src" / "websockets" / "version.py").read_text(encoding="utf-8"))
-long_description = (root_dir / 'README.rst').read_text(encoding='utf-8')
-
-# PyPI disables the "raw" directive.
+# PyPI disables the "raw" directive. Remove this section of the README.
long_description = re.sub(
r"^\.\. raw:: html.*?^(?=\w)",
"",
- long_description,
+ (root_dir / "README.rst").read_text(encoding="utf-8"),
flags=re.DOTALL | re.MULTILINE,
)
-exec((root_dir / 'src' / 'websockets' / 'version.py').read_text(encoding='utf-8'))
-
-packages = ['websockets', 'websockets/legacy', 'websockets/extensions']
-
-ext_modules = [
- setuptools.Extension(
- 'websockets.speedups',
- sources=['src/websockets/speedups.c'],
- optional=not (root_dir / '.cibuildwheel').exists(),
- )
-]
-
+# Set BUILD_EXTENSION to yes or no to force building or not building the
+# speedups extension. If unset, the extension is built only if possible.
+if os.environ.get("BUILD_EXTENSION") == "no":
+ ext_modules = []
+else:
+ ext_modules = [
+ setuptools.Extension(
+ "websockets.speedups",
+ sources=["src/websockets/speedups.c"],
+ optional=os.environ.get("BUILD_EXTENSION") != "yes",
+ )
+ ]
+
+# Static values are declared in pyproject.toml.
setuptools.setup(
- name='websockets',
version=version,
- description=description,
long_description=long_description,
- url='https://github.com/aaugustin/websockets',
- author='Aymeric Augustin',
- author_email='aymeric.augustin@m4x.org',
- license='BSD',
- classifiers=[
- 'Development Status :: 5 - Production/Stable',
- 'Environment :: Web Environment',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: BSD License',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.7',
- 'Programming Language :: Python :: 3.8',
- 'Programming Language :: Python :: 3.9',
- 'Programming Language :: Python :: 3.10',
- ],
- package_dir = {'': 'src'},
- package_data = {'websockets': ['py.typed']},
- packages=packages,
ext_modules=ext_modules,
- include_package_data=True,
- zip_safe=False,
- python_requires='>=3.7',
- test_loader='unittest:TestLoader',
)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/PKG-INFO b/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/PKG-INFO
deleted file mode 100644
index 3b042a3f9f..0000000000
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/PKG-INFO
+++ /dev/null
@@ -1,174 +0,0 @@
-Metadata-Version: 2.1
-Name: websockets
-Version: 10.3
-Summary: An implementation of the WebSocket Protocol (RFC 6455 & 7692)
-Home-page: https://github.com/aaugustin/websockets
-Author: Aymeric Augustin
-Author-email: aymeric.augustin@m4x.org
-License: BSD
-Project-URL: Changelog, https://websockets.readthedocs.io/en/stable/project/changelog.html
-Project-URL: Documentation, https://websockets.readthedocs.io/
-Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme
-Project-URL: Tracker, https://github.com/aaugustin/websockets/issues
-Platform: UNKNOWN
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Environment :: Web Environment
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: BSD License
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Requires-Python: >=3.7
-License-File: LICENSE
-
-.. image:: logo/horizontal.svg
- :width: 480px
- :alt: websockets
-
-|licence| |version| |pyversions| |wheel| |tests| |docs|
-
-.. |licence| image:: https://img.shields.io/pypi/l/websockets.svg
- :target: https://pypi.python.org/pypi/websockets
-
-.. |version| image:: https://img.shields.io/pypi/v/websockets.svg
- :target: https://pypi.python.org/pypi/websockets
-
-.. |pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg
- :target: https://pypi.python.org/pypi/websockets
-
-.. |wheel| image:: https://img.shields.io/pypi/wheel/websockets.svg
- :target: https://pypi.python.org/pypi/websockets
-
-.. |tests| image:: https://img.shields.io/github/checks-status/aaugustin/websockets/main
- :target: https://github.com/aaugustin/websockets/actions/workflows/tests.yml
-
-.. |docs| image:: https://img.shields.io/readthedocs/websockets.svg
- :target: https://websockets.readthedocs.io/
-
-What is ``websockets``?
------------------------
-
-websockets is a library for building WebSocket_ servers and clients in Python
-with a focus on correctness, simplicity, robustness, and performance.
-
-.. _WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
-
-Built on top of ``asyncio``, Python's standard asynchronous I/O framework, it
-provides an elegant coroutine-based API.
-
-`Documentation is available on Read the Docs. <https://websockets.readthedocs.io/>`_
-
-Here's how a client sends and receives messages:
-
-.. copy-pasted because GitHub doesn't support the include directive
-
-.. code:: python
-
- #!/usr/bin/env python
-
- import asyncio
- from websockets import connect
-
- async def hello(uri):
- async with connect(uri) as websocket:
- await websocket.send("Hello world!")
- await websocket.recv()
-
- asyncio.run(hello("ws://localhost:8765"))
-
-And here's an echo server:
-
-.. code:: python
-
- #!/usr/bin/env python
-
- import asyncio
- from websockets import serve
-
- async def echo(websocket):
- async for message in websocket:
- await websocket.send(message)
-
- async def main():
- async with serve(echo, "localhost", 8765):
- await asyncio.Future() # run forever
-
- asyncio.run(main())
-
-Does that look good?
-
-`Get started with the tutorial! <https://websockets.readthedocs.io/en/stable/intro/index.html>`_
-
-Why should I use ``websockets``?
---------------------------------
-
-The development of ``websockets`` is shaped by four principles:
-
-1. **Correctness**: ``websockets`` is heavily tested for compliance
- with :rfc:`6455`. Continuous integration fails under 100% branch
- coverage.
-
-2. **Simplicity**: all you need to understand is ``msg = await ws.recv()`` and
- ``await ws.send(msg)``. ``websockets`` takes care of managing connections
- so you can focus on your application.
-
-3. **Robustness**: ``websockets`` is built for production. For example, it was
- the only library to `handle backpressure correctly`_ before the issue
- became widely known in the Python community.
-
-4. **Performance**: memory usage is optimized and configurable. A C extension
- accelerates expensive operations. It's pre-compiled for Linux, macOS and
- Windows and packaged in the wheel format for each system and Python version.
-
-Documentation is a first class concern in the project. Head over to `Read the
-Docs`_ and see for yourself.
-
-.. _Read the Docs: https://websockets.readthedocs.io/
-.. _handle backpressure correctly: https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/#websocket-servers
-
-Why shouldn't I use ``websockets``?
------------------------------------
-
-* If you prefer callbacks over coroutines: ``websockets`` was created to
- provide the best coroutine-based API to manage WebSocket connections in
- Python. Pick another library for a callback-based API.
-
-* If you're looking for a mixed HTTP / WebSocket library: ``websockets`` aims
- at being an excellent implementation of :rfc:`6455`: The WebSocket Protocol
- and :rfc:`7692`: Compression Extensions for WebSocket. Its support for HTTP
- is minimal — just enough for a HTTP health check.
-
- If you want to do both in the same server, look at HTTP frameworks that
- build on top of ``websockets`` to support WebSocket connections, like
- Sanic_.
-
-.. _Sanic: https://sanicframework.org/en/
-
-What else?
-----------
-
-Bug reports, patches and suggestions are welcome!
-
-To report a security vulnerability, please use the `Tidelift security
-contact`_. Tidelift will coordinate the fix and disclosure.
-
-.. _Tidelift security contact: https://tidelift.com/security
-
-For anything else, please open an issue_ or send a `pull request`_.
-
-.. _issue: https://github.com/aaugustin/websockets/issues/new
-.. _pull request: https://github.com/aaugustin/websockets/compare/
-
-Participants must uphold the `Contributor Covenant code of conduct`_.
-
-.. _Contributor Covenant code of conduct: https://github.com/aaugustin/websockets/blob/main/CODE_OF_CONDUCT.md
-
-``websockets`` is released under the `BSD license`_.
-
-.. _BSD license: https://github.com/aaugustin/websockets/blob/main/LICENSE
-
-
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/SOURCES.txt b/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/SOURCES.txt
deleted file mode 100644
index 2a51106fee..0000000000
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,42 +0,0 @@
-LICENSE
-MANIFEST.in
-README.rst
-setup.cfg
-setup.py
-src/websockets/__init__.py
-src/websockets/__main__.py
-src/websockets/auth.py
-src/websockets/client.py
-src/websockets/connection.py
-src/websockets/datastructures.py
-src/websockets/exceptions.py
-src/websockets/frames.py
-src/websockets/headers.py
-src/websockets/http.py
-src/websockets/http11.py
-src/websockets/imports.py
-src/websockets/py.typed
-src/websockets/server.py
-src/websockets/speedups.c
-src/websockets/streams.py
-src/websockets/typing.py
-src/websockets/uri.py
-src/websockets/utils.py
-src/websockets/version.py
-src/websockets.egg-info/PKG-INFO
-src/websockets.egg-info/SOURCES.txt
-src/websockets.egg-info/dependency_links.txt
-src/websockets.egg-info/not-zip-safe
-src/websockets.egg-info/top_level.txt
-src/websockets/extensions/__init__.py
-src/websockets/extensions/base.py
-src/websockets/extensions/permessage_deflate.py
-src/websockets/legacy/__init__.py
-src/websockets/legacy/auth.py
-src/websockets/legacy/client.py
-src/websockets/legacy/compatibility.py
-src/websockets/legacy/framing.py
-src/websockets/legacy/handshake.py
-src/websockets/legacy/http.py
-src/websockets/legacy/protocol.py
-src/websockets/legacy/server.py \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/not-zip-safe b/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/not-zip-safe
deleted file mode 100644
index 8b13789179..0000000000
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/not-zip-safe
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/top_level.txt b/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/top_level.txt
deleted file mode 100644
index 5474af7431..0000000000
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/top_level.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-websockets
-websockets/extensions
-websockets/legacy
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py
index ec34841247..fdb028f4c4 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py
@@ -1,23 +1,24 @@
from __future__ import annotations
+import typing
+
from .imports import lazy_import
-from .version import version as __version__ # noqa
+from .version import version as __version__ # noqa: F401
-__all__ = [ # noqa
+__all__ = [
+ # .client
+ "ClientProtocol",
+ # .datastructures
+ "Headers",
+ "HeadersLike",
+ "MultipleValuesError",
+ # .exceptions
"AbortHandshake",
- "basic_auth_protocol_factory",
- "BasicAuthWebSocketServerProtocol",
- "broadcast",
- "ClientConnection",
- "connect",
"ConnectionClosed",
"ConnectionClosedError",
"ConnectionClosedOK",
- "Data",
"DuplicateParameter",
- "ExtensionName",
- "ExtensionParameter",
"InvalidHandshake",
"InvalidHeader",
"InvalidHeaderFormat",
@@ -31,84 +32,159 @@ __all__ = [ # noqa
"InvalidStatusCode",
"InvalidUpgrade",
"InvalidURI",
- "LoggerLike",
"NegotiationError",
- "Origin",
- "parse_uri",
"PayloadTooBig",
"ProtocolError",
"RedirectHandshake",
"SecurityError",
- "serve",
- "ServerConnection",
- "Subprotocol",
- "unix_connect",
- "unix_serve",
- "WebSocketClientProtocol",
- "WebSocketCommonProtocol",
"WebSocketException",
"WebSocketProtocolError",
+ # .legacy.auth
+ "BasicAuthWebSocketServerProtocol",
+ "basic_auth_protocol_factory",
+ # .legacy.client
+ "WebSocketClientProtocol",
+ "connect",
+ "unix_connect",
+ # .legacy.protocol
+ "WebSocketCommonProtocol",
+ "broadcast",
+ # .legacy.server
"WebSocketServer",
"WebSocketServerProtocol",
- "WebSocketURI",
+ "serve",
+ "unix_serve",
+ # .server
+ "ServerProtocol",
+ # .typing
+ "Data",
+ "ExtensionName",
+ "ExtensionParameter",
+ "LoggerLike",
+ "StatusLike",
+ "Origin",
+ "Subprotocol",
]
-lazy_import(
- globals(),
- aliases={
- "auth": ".legacy",
- "basic_auth_protocol_factory": ".legacy.auth",
- "BasicAuthWebSocketServerProtocol": ".legacy.auth",
- "broadcast": ".legacy.protocol",
- "ClientConnection": ".client",
- "connect": ".legacy.client",
- "unix_connect": ".legacy.client",
- "WebSocketClientProtocol": ".legacy.client",
- "Headers": ".datastructures",
- "MultipleValuesError": ".datastructures",
- "WebSocketException": ".exceptions",
- "ConnectionClosed": ".exceptions",
- "ConnectionClosedError": ".exceptions",
- "ConnectionClosedOK": ".exceptions",
- "InvalidHandshake": ".exceptions",
- "SecurityError": ".exceptions",
- "InvalidMessage": ".exceptions",
- "InvalidHeader": ".exceptions",
- "InvalidHeaderFormat": ".exceptions",
- "InvalidHeaderValue": ".exceptions",
- "InvalidOrigin": ".exceptions",
- "InvalidUpgrade": ".exceptions",
- "InvalidStatus": ".exceptions",
- "InvalidStatusCode": ".exceptions",
- "NegotiationError": ".exceptions",
- "DuplicateParameter": ".exceptions",
- "InvalidParameterName": ".exceptions",
- "InvalidParameterValue": ".exceptions",
- "AbortHandshake": ".exceptions",
- "RedirectHandshake": ".exceptions",
- "InvalidState": ".exceptions",
- "InvalidURI": ".exceptions",
- "PayloadTooBig": ".exceptions",
- "ProtocolError": ".exceptions",
- "WebSocketProtocolError": ".exceptions",
- "protocol": ".legacy",
- "WebSocketCommonProtocol": ".legacy.protocol",
- "ServerConnection": ".server",
- "serve": ".legacy.server",
- "unix_serve": ".legacy.server",
- "WebSocketServerProtocol": ".legacy.server",
- "WebSocketServer": ".legacy.server",
- "Data": ".typing",
- "LoggerLike": ".typing",
- "Origin": ".typing",
- "ExtensionHeader": ".typing",
- "ExtensionParameter": ".typing",
- "Subprotocol": ".typing",
- },
- deprecated_aliases={
- "framing": ".legacy",
- "handshake": ".legacy",
- "parse_uri": ".uri",
- "WebSocketURI": ".uri",
- },
-)
+# When type checking, import non-deprecated aliases eagerly. Else, import on demand.
+if typing.TYPE_CHECKING:
+ from .client import ClientProtocol
+ from .datastructures import Headers, HeadersLike, MultipleValuesError
+ from .exceptions import (
+ AbortHandshake,
+ ConnectionClosed,
+ ConnectionClosedError,
+ ConnectionClosedOK,
+ DuplicateParameter,
+ InvalidHandshake,
+ InvalidHeader,
+ InvalidHeaderFormat,
+ InvalidHeaderValue,
+ InvalidMessage,
+ InvalidOrigin,
+ InvalidParameterName,
+ InvalidParameterValue,
+ InvalidState,
+ InvalidStatus,
+ InvalidStatusCode,
+ InvalidUpgrade,
+ InvalidURI,
+ NegotiationError,
+ PayloadTooBig,
+ ProtocolError,
+ RedirectHandshake,
+ SecurityError,
+ WebSocketException,
+ WebSocketProtocolError,
+ )
+ from .legacy.auth import (
+ BasicAuthWebSocketServerProtocol,
+ basic_auth_protocol_factory,
+ )
+ from .legacy.client import WebSocketClientProtocol, connect, unix_connect
+ from .legacy.protocol import WebSocketCommonProtocol, broadcast
+ from .legacy.server import (
+ WebSocketServer,
+ WebSocketServerProtocol,
+ serve,
+ unix_serve,
+ )
+ from .server import ServerProtocol
+ from .typing import (
+ Data,
+ ExtensionName,
+ ExtensionParameter,
+ LoggerLike,
+ Origin,
+ StatusLike,
+ Subprotocol,
+ )
+else:
+ lazy_import(
+ globals(),
+ aliases={
+ # .client
+ "ClientProtocol": ".client",
+ # .datastructures
+ "Headers": ".datastructures",
+ "HeadersLike": ".datastructures",
+ "MultipleValuesError": ".datastructures",
+ # .exceptions
+ "AbortHandshake": ".exceptions",
+ "ConnectionClosed": ".exceptions",
+ "ConnectionClosedError": ".exceptions",
+ "ConnectionClosedOK": ".exceptions",
+ "DuplicateParameter": ".exceptions",
+ "InvalidHandshake": ".exceptions",
+ "InvalidHeader": ".exceptions",
+ "InvalidHeaderFormat": ".exceptions",
+ "InvalidHeaderValue": ".exceptions",
+ "InvalidMessage": ".exceptions",
+ "InvalidOrigin": ".exceptions",
+ "InvalidParameterName": ".exceptions",
+ "InvalidParameterValue": ".exceptions",
+ "InvalidState": ".exceptions",
+ "InvalidStatus": ".exceptions",
+ "InvalidStatusCode": ".exceptions",
+ "InvalidUpgrade": ".exceptions",
+ "InvalidURI": ".exceptions",
+ "NegotiationError": ".exceptions",
+ "PayloadTooBig": ".exceptions",
+ "ProtocolError": ".exceptions",
+ "RedirectHandshake": ".exceptions",
+ "SecurityError": ".exceptions",
+ "WebSocketException": ".exceptions",
+ "WebSocketProtocolError": ".exceptions",
+ # .legacy.auth
+ "BasicAuthWebSocketServerProtocol": ".legacy.auth",
+ "basic_auth_protocol_factory": ".legacy.auth",
+ # .legacy.client
+ "WebSocketClientProtocol": ".legacy.client",
+ "connect": ".legacy.client",
+ "unix_connect": ".legacy.client",
+ # .legacy.protocol
+ "WebSocketCommonProtocol": ".legacy.protocol",
+ "broadcast": ".legacy.protocol",
+ # .legacy.server
+ "WebSocketServer": ".legacy.server",
+ "WebSocketServerProtocol": ".legacy.server",
+ "serve": ".legacy.server",
+ "unix_serve": ".legacy.server",
+ # .server
+ "ServerProtocol": ".server",
+ # .typing
+ "Data": ".typing",
+ "ExtensionName": ".typing",
+ "ExtensionParameter": ".typing",
+ "LoggerLike": ".typing",
+ "Origin": ".typing",
+ "StatusLike": "typing",
+ "Subprotocol": ".typing",
+ },
+ deprecated_aliases={
+ "framing": ".legacy",
+ "handshake": ".legacy",
+ "parse_uri": ".uri",
+ "WebSocketURI": ".uri",
+ },
+ )
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py
index c562d21b54..f2ea5cf4e8 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py
@@ -1,16 +1,18 @@
from __future__ import annotations
import argparse
-import asyncio
import os
import signal
import sys
import threading
-from typing import Any, Set
-from .exceptions import ConnectionClosed
-from .frames import Close
-from .legacy.client import connect
+
+try:
+ import readline # noqa: F401
+except ImportError: # Windows has no `readline` normally
+ pass
+
+from .sync.client import ClientConnection, connect
from .version import version as websockets_version
@@ -46,21 +48,6 @@ if sys.platform == "win32":
raise RuntimeError("unable to set console mode")
-def exit_from_event_loop_thread(
- loop: asyncio.AbstractEventLoop,
- stop: asyncio.Future[None],
-) -> None:
- loop.stop()
- if not stop.done():
- # When exiting the thread that runs the event loop, raise
- # KeyboardInterrupt in the main thread to exit the program.
- if sys.platform == "win32":
- ctrl_c = signal.CTRL_C_EVENT
- else:
- ctrl_c = signal.SIGINT
- os.kill(os.getpid(), ctrl_c)
-
-
def print_during_input(string: str) -> None:
sys.stdout.write(
# Save cursor position
@@ -93,63 +80,20 @@ def print_over_input(string: str) -> None:
sys.stdout.flush()
-async def run_client(
- uri: str,
- loop: asyncio.AbstractEventLoop,
- inputs: asyncio.Queue[str],
- stop: asyncio.Future[None],
-) -> None:
- try:
- websocket = await connect(uri)
- except Exception as exc:
- print_over_input(f"Failed to connect to {uri}: {exc}.")
- exit_from_event_loop_thread(loop, stop)
- return
- else:
- print_during_input(f"Connected to {uri}.")
-
- try:
- while True:
- incoming: asyncio.Future[Any] = asyncio.create_task(websocket.recv())
- outgoing: asyncio.Future[Any] = asyncio.create_task(inputs.get())
- done: Set[asyncio.Future[Any]]
- pending: Set[asyncio.Future[Any]]
- done, pending = await asyncio.wait(
- [incoming, outgoing, stop], return_when=asyncio.FIRST_COMPLETED
- )
-
- # Cancel pending tasks to avoid leaking them.
- if incoming in pending:
- incoming.cancel()
- if outgoing in pending:
- outgoing.cancel()
-
- if incoming in done:
- try:
- message = incoming.result()
- except ConnectionClosed:
- break
- else:
- if isinstance(message, str):
- print_during_input("< " + message)
- else:
- print_during_input("< (binary) " + message.hex())
-
- if outgoing in done:
- message = outgoing.result()
- await websocket.send(message)
-
- if stop in done:
- break
-
- finally:
- await websocket.close()
- assert websocket.close_code is not None and websocket.close_reason is not None
- close_status = Close(websocket.close_code, websocket.close_reason)
-
- print_over_input(f"Connection closed: {close_status}.")
-
- exit_from_event_loop_thread(loop, stop)
+def print_incoming_messages(websocket: ClientConnection, stop: threading.Event) -> None:
+ for message in websocket:
+ if isinstance(message, str):
+ print_during_input("< " + message)
+ else:
+ print_during_input("< (binary) " + message.hex())
+ if not stop.is_set():
+ # When the server closes the connection, raise KeyboardInterrupt
+ # in the main thread to exit the program.
+ if sys.platform == "win32":
+ ctrl_c = signal.CTRL_C_EVENT
+ else:
+ ctrl_c = signal.SIGINT
+ os.kill(os.getpid(), ctrl_c)
def main() -> None:
@@ -184,29 +128,17 @@ def main() -> None:
sys.stderr.flush()
try:
- import readline # noqa
- except ImportError: # Windows has no `readline` normally
- pass
-
- # Create an event loop that will run in a background thread.
- loop = asyncio.new_event_loop()
-
- # Due to zealous removal of the loop parameter in the Queue constructor,
- # we need a factory coroutine to run in the freshly created event loop.
- async def queue_factory() -> asyncio.Queue[str]:
- return asyncio.Queue()
-
- # Create a queue of user inputs. There's no need to limit its size.
- inputs: asyncio.Queue[str] = loop.run_until_complete(queue_factory())
-
- # Create a stop condition when receiving SIGINT or SIGTERM.
- stop: asyncio.Future[None] = loop.create_future()
+ websocket = connect(args.uri)
+ except Exception as exc:
+ print(f"Failed to connect to {args.uri}: {exc}.")
+ sys.exit(1)
+ else:
+ print(f"Connected to {args.uri}.")
- # Schedule the task that will manage the connection.
- loop.create_task(run_client(args.uri, loop, inputs, stop))
+ stop = threading.Event()
- # Start the event loop in a background thread.
- thread = threading.Thread(target=loop.run_forever)
+ # Start the thread that reads messages from the connection.
+ thread = threading.Thread(target=print_incoming_messages, args=(websocket, stop))
thread.start()
# Read from stdin in the main thread in order to receive signals.
@@ -214,17 +146,14 @@ def main() -> None:
while True:
# Since there's no size limit, put_nowait is identical to put.
message = input("> ")
- loop.call_soon_threadsafe(inputs.put_nowait, message)
+ websocket.send(message)
except (KeyboardInterrupt, EOFError): # ^C, ^D
- loop.call_soon_threadsafe(stop.set_result, None)
+ stop.set()
+ websocket.close()
+ print_over_input("Connection closed.")
- # Wait for the event loop to terminate.
thread.join()
- # For reasons unclear, even though the loop is closed in the thread,
- # it still thinks it's running here.
- loop.close()
-
if __name__ == "__main__":
main()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py
index afcb38cffe..b792e02f5c 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py
@@ -1,4 +1,6 @@
from __future__ import annotations
# See #940 for why lazy_import isn't used here for backwards compatibility.
-from .legacy.auth import * # noqa
+# See #1400 for why listing compatibility imports in __all__ helps PyCharm.
+from .legacy.auth import *
+from .legacy.auth import __all__ # noqa: F401
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py
index df8e53429a..b2f622042d 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py
@@ -1,8 +1,8 @@
from __future__ import annotations
-from typing import Generator, List, Optional, Sequence
+import warnings
+from typing import Any, Generator, List, Optional, Sequence
-from .connection import CLIENT, CONNECTING, OPEN, Connection, State
from .datastructures import Headers, MultipleValuesError
from .exceptions import (
InvalidHandshake,
@@ -23,8 +23,8 @@ from .headers import (
parse_subprotocol,
parse_upgrade,
)
-from .http import USER_AGENT
from .http11 import Request, Response
+from .protocol import CLIENT, CONNECTING, OPEN, Protocol, State
from .typing import (
ConnectionOption,
ExtensionHeader,
@@ -38,13 +38,15 @@ from .utils import accept_key, generate_key
# See #940 for why lazy_import isn't used here for backwards compatibility.
-from .legacy.client import * # isort:skip # noqa
+# See #1400 for why listing compatibility imports in __all__ helps PyCharm.
+from .legacy.client import * # isort:skip # noqa: I001
+from .legacy.client import __all__ as legacy__all__
-__all__ = ["ClientConnection"]
+__all__ = ["ClientProtocol"] + legacy__all__
-class ClientConnection(Connection):
+class ClientProtocol(Protocol):
"""
Sans-I/O implementation of a WebSocket client connection.
@@ -60,16 +62,17 @@ class ClientConnection(Connection):
preference.
state: initial state of the WebSocket connection.
max_size: maximum size of incoming messages in bytes;
- :obj:`None` to disable the limit.
+ :obj:`None` disables the limit.
logger: logger for this connection;
defaults to ``logging.getLogger("websockets.client")``;
- see the :doc:`logging guide <../topics/logging>` for details.
+ see the :doc:`logging guide <../../topics/logging>` for details.
"""
def __init__(
self,
wsuri: WebSocketURI,
+ *,
origin: Optional[Origin] = None,
extensions: Optional[Sequence[ClientExtensionFactory]] = None,
subprotocols: Optional[Sequence[Subprotocol]] = None,
@@ -89,7 +92,7 @@ class ClientConnection(Connection):
self.available_subprotocols = subprotocols
self.key = generate_key()
- def connect(self) -> Request: # noqa: F811
+ def connect(self) -> Request:
"""
Create a handshake request to open a connection.
@@ -131,8 +134,6 @@ class ClientConnection(Connection):
protocol_header = build_subprotocol(self.available_subprotocols)
headers["Sec-WebSocket-Protocol"] = protocol_header
- headers["User-Agent"] = USER_AGENT
-
return Request(self.wsuri.resource_name, headers)
def process_response(self, response: Response) -> None:
@@ -223,7 +224,6 @@ class ClientConnection(Connection):
extensions = headers.get_all("Sec-WebSocket-Extensions")
if extensions:
-
if self.available_extensions is None:
raise InvalidHandshake("no extensions supported")
@@ -232,9 +232,7 @@ class ClientConnection(Connection):
)
for name, response_params in parsed_extensions:
-
for extension_factory in self.available_extensions:
-
# Skip non-matching extensions based on their name.
if extension_factory.name != name:
continue
@@ -281,7 +279,6 @@ class ClientConnection(Connection):
subprotocols = headers.get_all("Sec-WebSocket-Protocol")
if subprotocols:
-
if self.available_subprotocols is None:
raise InvalidHandshake("no subprotocols supported")
@@ -317,11 +314,17 @@ class ClientConnection(Connection):
def parse(self) -> Generator[None, None, None]:
if self.state is CONNECTING:
- response = yield from Response.parse(
- self.reader.read_line,
- self.reader.read_exact,
- self.reader.read_to_eof,
- )
+ try:
+ response = yield from Response.parse(
+ self.reader.read_line,
+ self.reader.read_exact,
+ self.reader.read_to_eof,
+ )
+ except Exception as exc:
+ self.handshake_exc = exc
+ self.parser = self.discard()
+ next(self.parser) # start coroutine
+ yield
if self.debug:
code, phrase = response.status_code, response.reason_phrase
@@ -335,13 +338,23 @@ class ClientConnection(Connection):
self.process_response(response)
except InvalidHandshake as exc:
response._exception = exc
+ self.events.append(response)
self.handshake_exc = exc
self.parser = self.discard()
next(self.parser) # start coroutine
- else:
- assert self.state is CONNECTING
- self.state = OPEN
- finally:
- self.events.append(response)
+ yield
+
+ assert self.state is CONNECTING
+ self.state = OPEN
+ self.events.append(response)
yield from super().parse()
+
+
+class ClientConnection(ClientProtocol):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ warnings.warn(
+ "ClientConnection was renamed to ClientProtocol",
+ DeprecationWarning,
+ )
+ super().__init__(*args, **kwargs)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/connection.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/connection.py
index db8b536993..88bcda1aaf 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/connection.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/connection.py
@@ -1,702 +1,13 @@
from __future__ import annotations
-import enum
-import logging
-import uuid
-from typing import Generator, List, Optional, Type, Union
+import warnings
-from .exceptions import (
- ConnectionClosed,
- ConnectionClosedError,
- ConnectionClosedOK,
- InvalidState,
- PayloadTooBig,
- ProtocolError,
-)
-from .extensions import Extension
-from .frames import (
- OK_CLOSE_CODES,
- OP_BINARY,
- OP_CLOSE,
- OP_CONT,
- OP_PING,
- OP_PONG,
- OP_TEXT,
- Close,
- Frame,
-)
-from .http11 import Request, Response
-from .streams import StreamReader
-from .typing import LoggerLike, Origin, Subprotocol
-
-
-__all__ = [
- "Connection",
- "Side",
- "State",
- "SEND_EOF",
-]
-
-Event = Union[Request, Response, Frame]
-"""Events that :meth:`~Connection.events_received` may return."""
-
-
-class Side(enum.IntEnum):
- """A WebSocket connection is either a server or a client."""
-
- SERVER, CLIENT = range(2)
-
-
-SERVER = Side.SERVER
-CLIENT = Side.CLIENT
-
-
-class State(enum.IntEnum):
- """A WebSocket connection is in one of these four states."""
-
- CONNECTING, OPEN, CLOSING, CLOSED = range(4)
-
-
-CONNECTING = State.CONNECTING
-OPEN = State.OPEN
-CLOSING = State.CLOSING
-CLOSED = State.CLOSED
-
-
-SEND_EOF = b""
-"""Sentinel signaling that the TCP connection must be half-closed."""
-
-
-class Connection:
- """
- Sans-I/O implementation of a WebSocket connection.
-
- Args:
- side: :attr:`~Side.CLIENT` or :attr:`~Side.SERVER`.
- state: initial state of the WebSocket connection.
- max_size: maximum size of incoming messages in bytes;
- :obj:`None` to disable the limit.
- logger: logger for this connection; depending on ``side``,
- defaults to ``logging.getLogger("websockets.client")``
- or ``logging.getLogger("websockets.server")``;
- see the :doc:`logging guide <../topics/logging>` for details.
-
- """
-
- def __init__(
- self,
- side: Side,
- state: State = OPEN,
- max_size: Optional[int] = 2**20,
- logger: Optional[LoggerLike] = None,
- ) -> None:
- # Unique identifier. For logs.
- self.id: uuid.UUID = uuid.uuid4()
- """Unique identifier of the connection. Useful in logs."""
-
- # Logger or LoggerAdapter for this connection.
- if logger is None:
- logger = logging.getLogger(f"websockets.{side.name.lower()}")
- self.logger: LoggerLike = logger
- """Logger for this connection."""
-
- # Track if DEBUG is enabled. Shortcut logging calls if it isn't.
- self.debug = logger.isEnabledFor(logging.DEBUG)
-
- # Connection side. CLIENT or SERVER.
- self.side = side
-
- # Connection state. Initially OPEN because subclasses handle CONNECTING.
- self.state = state
-
- # Maximum size of incoming messages in bytes.
- self.max_size = max_size
-
- # Current size of incoming message in bytes. Only set while reading a
- # fragmented message i.e. a data frames with the FIN bit not set.
- self.cur_size: Optional[int] = None
-
- # True while sending a fragmented message i.e. a data frames with the
- # FIN bit not set.
- self.expect_continuation_frame = False
-
- # WebSocket protocol parameters.
- self.origin: Optional[Origin] = None
- self.extensions: List[Extension] = []
- self.subprotocol: Optional[Subprotocol] = None
-
- # Close code and reason, set when a close frame is sent or received.
- self.close_rcvd: Optional[Close] = None
- self.close_sent: Optional[Close] = None
- self.close_rcvd_then_sent: Optional[bool] = None
-
- # Track if an exception happened during the handshake.
- self.handshake_exc: Optional[Exception] = None
- """
- Exception to raise if the opening handshake failed.
-
- :obj:`None` if the opening handshake succeeded.
-
- """
-
- # Track if send_eof() was called.
- self.eof_sent = False
-
- # Parser state.
- self.reader = StreamReader()
- self.events: List[Event] = []
- self.writes: List[bytes] = []
- self.parser = self.parse()
- next(self.parser) # start coroutine
- self.parser_exc: Optional[Exception] = None
-
- @property
- def state(self) -> State:
- """
- WebSocket connection state.
-
- Defined in 4.1, 4.2, 7.1.3, and 7.1.4 of :rfc:`6455`.
-
- """
- return self._state
-
- @state.setter
- def state(self, state: State) -> None:
- if self.debug:
- self.logger.debug("= connection is %s", state.name)
- self._state = state
-
- @property
- def close_code(self) -> Optional[int]:
- """
- `WebSocket close code`_.
-
- .. _WebSocket close code:
- https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5
-
- :obj:`None` if the connection isn't closed yet.
-
- """
- if self.state is not CLOSED:
- return None
- elif self.close_rcvd is None:
- return 1006
- else:
- return self.close_rcvd.code
-
- @property
- def close_reason(self) -> Optional[str]:
- """
- `WebSocket close reason`_.
-
- .. _WebSocket close reason:
- https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6
-
- :obj:`None` if the connection isn't closed yet.
-
- """
- if self.state is not CLOSED:
- return None
- elif self.close_rcvd is None:
- return ""
- else:
- return self.close_rcvd.reason
-
- @property
- def close_exc(self) -> ConnectionClosed:
- """
- Exception to raise when trying to interact with a closed connection.
-
- Don't raise this exception while the connection :attr:`state`
- is :attr:`~websockets.connection.State.CLOSING`; wait until
- it's :attr:`~websockets.connection.State.CLOSED`.
-
- Indeed, the exception includes the close code and reason, which are
- known only once the connection is closed.
-
- Raises:
- AssertionError: if the connection isn't closed yet.
-
- """
- assert self.state is CLOSED, "connection isn't closed yet"
- exc_type: Type[ConnectionClosed]
- if (
- self.close_rcvd is not None
- and self.close_sent is not None
- and self.close_rcvd.code in OK_CLOSE_CODES
- and self.close_sent.code in OK_CLOSE_CODES
- ):
- exc_type = ConnectionClosedOK
- else:
- exc_type = ConnectionClosedError
- exc: ConnectionClosed = exc_type(
- self.close_rcvd,
- self.close_sent,
- self.close_rcvd_then_sent,
- )
- # Chain to the exception raised in the parser, if any.
- exc.__cause__ = self.parser_exc
- return exc
-
- # Public methods for receiving data.
-
- def receive_data(self, data: bytes) -> None:
- """
- Receive data from the network.
-
- After calling this method:
-
- - You must call :meth:`data_to_send` and send this data to the network.
- - You should call :meth:`events_received` and process resulting events.
-
- Raises:
- EOFError: if :meth:`receive_eof` was called earlier.
-
- """
- self.reader.feed_data(data)
- next(self.parser)
-
- def receive_eof(self) -> None:
- """
- Receive the end of the data stream from the network.
-
- After calling this method:
-
- - You must call :meth:`data_to_send` and send this data to the network.
- - You aren't expected to call :meth:`events_received`; it won't return
- any new events.
-
- Raises:
- EOFError: if :meth:`receive_eof` was called earlier.
-
- """
- self.reader.feed_eof()
- next(self.parser)
-
- # Public methods for sending events.
-
- def send_continuation(self, data: bytes, fin: bool) -> None:
- """
- Send a `Continuation frame`_.
-
- .. _Continuation frame:
- https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
-
- Parameters:
- data: payload containing the same kind of data
- as the initial frame.
- fin: FIN bit; set it to :obj:`True` if this is the last frame
- of a fragmented message and to :obj:`False` otherwise.
-
- Raises:
- ProtocolError: if a fragmented message isn't in progress.
-
- """
- if not self.expect_continuation_frame:
- raise ProtocolError("unexpected continuation frame")
- self.expect_continuation_frame = not fin
- self.send_frame(Frame(OP_CONT, data, fin))
-
- def send_text(self, data: bytes, fin: bool = True) -> None:
- """
- Send a `Text frame`_.
-
- .. _Text frame:
- https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
-
- Parameters:
- data: payload containing text encoded with UTF-8.
- fin: FIN bit; set it to :obj:`False` if this is the first frame of
- a fragmented message.
-
- Raises:
- ProtocolError: if a fragmented message is in progress.
-
- """
- if self.expect_continuation_frame:
- raise ProtocolError("expected a continuation frame")
- self.expect_continuation_frame = not fin
- self.send_frame(Frame(OP_TEXT, data, fin))
-
- def send_binary(self, data: bytes, fin: bool = True) -> None:
- """
- Send a `Binary frame`_.
+# lazy_import doesn't support this use case.
+from .protocol import SEND_EOF, Protocol as Connection, Side, State # noqa: F401
- .. _Binary frame:
- https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
- Parameters:
- data: payload containing arbitrary binary data.
- fin: FIN bit; set it to :obj:`False` if this is the first frame of
- a fragmented message.
-
- Raises:
- ProtocolError: if a fragmented message is in progress.
-
- """
- if self.expect_continuation_frame:
- raise ProtocolError("expected a continuation frame")
- self.expect_continuation_frame = not fin
- self.send_frame(Frame(OP_BINARY, data, fin))
-
- def send_close(self, code: Optional[int] = None, reason: str = "") -> None:
- """
- Send a `Close frame`_.
-
- .. _Close frame:
- https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1
-
- Parameters:
- code: close code.
- reason: close reason.
-
- Raises:
- ProtocolError: if a fragmented message is being sent, if the code
- isn't valid, or if a reason is provided without a code
-
- """
- if self.expect_continuation_frame:
- raise ProtocolError("expected a continuation frame")
- if code is None:
- if reason != "":
- raise ProtocolError("cannot send a reason without a code")
- close = Close(1005, "")
- data = b""
- else:
- close = Close(code, reason)
- data = close.serialize()
- # send_frame() guarantees that self.state is OPEN at this point.
- # 7.1.3. The WebSocket Closing Handshake is Started
- self.send_frame(Frame(OP_CLOSE, data))
- self.close_sent = close
- self.state = CLOSING
-
- def send_ping(self, data: bytes) -> None:
- """
- Send a `Ping frame`_.
-
- .. _Ping frame:
- https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2
-
- Parameters:
- data: payload containing arbitrary binary data.
-
- """
- self.send_frame(Frame(OP_PING, data))
-
- def send_pong(self, data: bytes) -> None:
- """
- Send a `Pong frame`_.
-
- .. _Pong frame:
- https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3
-
- Parameters:
- data: payload containing arbitrary binary data.
-
- """
- self.send_frame(Frame(OP_PONG, data))
-
- def fail(self, code: int, reason: str = "") -> None:
- """
- `Fail the WebSocket connection`_.
-
- .. _Fail the WebSocket connection:
- https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.7
-
- Parameters:
- code: close code
- reason: close reason
-
- Raises:
- ProtocolError: if the code isn't valid.
- """
- # 7.1.7. Fail the WebSocket Connection
-
- # Send a close frame when the state is OPEN (a close frame was already
- # sent if it's CLOSING), except when failing the connection because
- # of an error reading from or writing to the network.
- if self.state is OPEN:
- if code != 1006:
- close = Close(code, reason)
- data = close.serialize()
- self.send_frame(Frame(OP_CLOSE, data))
- self.close_sent = close
- self.state = CLOSING
-
- # When failing the connection, a server closes the TCP connection
- # without waiting for the client to complete the handshake, while a
- # client waits for the server to close the TCP connection, possibly
- # after sending a close frame that the client will ignore.
- if self.side is SERVER and not self.eof_sent:
- self.send_eof()
-
- # 7.1.7. Fail the WebSocket Connection "An endpoint MUST NOT continue
- # to attempt to process data(including a responding Close frame) from
- # the remote endpoint after being instructed to _Fail the WebSocket
- # Connection_."
- self.parser = self.discard()
- next(self.parser) # start coroutine
-
- # Public method for getting incoming events after receiving data.
-
- def events_received(self) -> List[Event]:
- """
- Fetch events generated from data received from the network.
-
- Call this method immediately after any of the ``receive_*()`` methods.
-
- Process resulting events, likely by passing them to the application.
-
- Returns:
- List[Event]: Events read from the connection.
- """
- events, self.events = self.events, []
- return events
-
- # Public method for getting outgoing data after receiving data or sending events.
-
- def data_to_send(self) -> List[bytes]:
- """
- Obtain data to send to the network.
-
- Call this method immediately after any of the ``receive_*()``,
- ``send_*()``, or :meth:`fail` methods.
-
- Write resulting data to the connection.
-
- The empty bytestring :data:`~websockets.connection.SEND_EOF` signals
- the end of the data stream. When you receive it, half-close the TCP
- connection.
-
- Returns:
- List[bytes]: Data to write to the connection.
-
- """
- writes, self.writes = self.writes, []
- return writes
-
- def close_expected(self) -> bool:
- """
- Tell if the TCP connection is expected to close soon.
-
- Call this method immediately after any of the ``receive_*()`` or
- :meth:`fail` methods.
-
- If it returns :obj:`True`, schedule closing the TCP connection after a
- short timeout if the other side hasn't already closed it.
-
- Returns:
- bool: Whether the TCP connection is expected to close soon.
-
- """
- # We expect a TCP close if and only if we sent a close frame:
- # * Normal closure: once we send a close frame, we expect a TCP close:
- # server waits for client to complete the TCP closing handshake;
- # client waits for server to initiate the TCP closing handshake.
- # * Abnormal closure: we always send a close frame and the same logic
- # applies, except on EOFError where we don't send a close frame
- # because we already received the TCP close, so we don't expect it.
- # We already got a TCP Close if and only if the state is CLOSED.
- return self.state is CLOSING or self.handshake_exc is not None
-
- # Private methods for receiving data.
-
- def parse(self) -> Generator[None, None, None]:
- """
- Parse incoming data into frames.
-
- :meth:`receive_data` and :meth:`receive_eof` run this generator
- coroutine until it needs more data or reaches EOF.
-
- """
- try:
- while True:
- if (yield from self.reader.at_eof()):
- if self.debug:
- self.logger.debug("< EOF")
- # If the WebSocket connection is closed cleanly, with a
- # closing handhshake, recv_frame() substitutes parse()
- # with discard(). This branch is reached only when the
- # connection isn't closed cleanly.
- raise EOFError("unexpected end of stream")
-
- if self.max_size is None:
- max_size = None
- elif self.cur_size is None:
- max_size = self.max_size
- else:
- max_size = self.max_size - self.cur_size
-
- # During a normal closure, execution ends here on the next
- # iteration of the loop after receiving a close frame. At
- # this point, recv_frame() replaced parse() by discard().
- frame = yield from Frame.parse(
- self.reader.read_exact,
- mask=self.side is SERVER,
- max_size=max_size,
- extensions=self.extensions,
- )
-
- if self.debug:
- self.logger.debug("< %s", frame)
-
- self.recv_frame(frame)
-
- except ProtocolError as exc:
- self.fail(1002, str(exc))
- self.parser_exc = exc
-
- except EOFError as exc:
- self.fail(1006, str(exc))
- self.parser_exc = exc
-
- except UnicodeDecodeError as exc:
- self.fail(1007, f"{exc.reason} at position {exc.start}")
- self.parser_exc = exc
-
- except PayloadTooBig as exc:
- self.fail(1009, str(exc))
- self.parser_exc = exc
-
- except Exception as exc:
- self.logger.error("parser failed", exc_info=True)
- # Don't include exception details, which may be security-sensitive.
- self.fail(1011)
- self.parser_exc = exc
-
- # During an abnormal closure, execution ends here after catching an
- # exception. At this point, fail() replaced parse() by discard().
- yield
- raise AssertionError("parse() shouldn't step after error") # pragma: no cover
-
- def discard(self) -> Generator[None, None, None]:
- """
- Discard incoming data.
-
- This coroutine replaces :meth:`parse`:
-
- - after receiving a close frame, during a normal closure (1.4);
- - after sending a close frame, during an abnormal closure (7.1.7).
-
- """
- # The server close the TCP connection in the same circumstances where
- # discard() replaces parse(). The client closes the connection later,
- # after the server closes the connection or a timeout elapses.
- # (The latter case cannot be handled in this Sans-I/O layer.)
- assert (self.side is SERVER) == (self.eof_sent)
- while not (yield from self.reader.at_eof()):
- self.reader.discard()
- if self.debug:
- self.logger.debug("< EOF")
- # A server closes the TCP connection immediately, while a client
- # waits for the server to close the TCP connection.
- if self.side is CLIENT:
- self.send_eof()
- self.state = CLOSED
- # If discard() completes normally, execution ends here.
- yield
- # Once the reader reaches EOF, its feed_data/eof() methods raise an
- # error, so our receive_data/eof() methods don't step the generator.
- raise AssertionError("discard() shouldn't step after EOF") # pragma: no cover
-
- def recv_frame(self, frame: Frame) -> None:
- """
- Process an incoming frame.
-
- """
- if frame.opcode is OP_TEXT or frame.opcode is OP_BINARY:
- if self.cur_size is not None:
- raise ProtocolError("expected a continuation frame")
- if frame.fin:
- self.cur_size = None
- else:
- self.cur_size = len(frame.data)
-
- elif frame.opcode is OP_CONT:
- if self.cur_size is None:
- raise ProtocolError("unexpected continuation frame")
- if frame.fin:
- self.cur_size = None
- else:
- self.cur_size += len(frame.data)
-
- elif frame.opcode is OP_PING:
- # 5.5.2. Ping: "Upon receipt of a Ping frame, an endpoint MUST
- # send a Pong frame in response"
- pong_frame = Frame(OP_PONG, frame.data)
- self.send_frame(pong_frame)
-
- elif frame.opcode is OP_PONG:
- # 5.5.3 Pong: "A response to an unsolicited Pong frame is not
- # expected."
- pass
-
- elif frame.opcode is OP_CLOSE:
- # 7.1.5. The WebSocket Connection Close Code
- # 7.1.6. The WebSocket Connection Close Reason
- self.close_rcvd = Close.parse(frame.data)
- if self.state is CLOSING:
- assert self.close_sent is not None
- self.close_rcvd_then_sent = False
-
- if self.cur_size is not None:
- raise ProtocolError("incomplete fragmented message")
-
- # 5.5.1 Close: "If an endpoint receives a Close frame and did
- # not previously send a Close frame, the endpoint MUST send a
- # Close frame in response. (When sending a Close frame in
- # response, the endpoint typically echos the status code it
- # received.)"
-
- if self.state is OPEN:
- # Echo the original data instead of re-serializing it with
- # Close.serialize() because that fails when the close frame
- # is empty and Close.parse() synthetizes a 1005 close code.
- # The rest is identical to send_close().
- self.send_frame(Frame(OP_CLOSE, frame.data))
- self.close_sent = self.close_rcvd
- self.close_rcvd_then_sent = True
- self.state = CLOSING
-
- # 7.1.2. Start the WebSocket Closing Handshake: "Once an
- # endpoint has both sent and received a Close control frame,
- # that endpoint SHOULD _Close the WebSocket Connection_"
-
- # A server closes the TCP connection immediately, while a client
- # waits for the server to close the TCP connection.
- if self.side is SERVER:
- self.send_eof()
-
- # 1.4. Closing Handshake: "after receiving a control frame
- # indicating the connection should be closed, a peer discards
- # any further data received."
- self.parser = self.discard()
- next(self.parser) # start coroutine
-
- else: # pragma: no cover
- # This can't happen because Frame.parse() validates opcodes.
- raise AssertionError(f"unexpected opcode: {frame.opcode:02x}")
-
- self.events.append(frame)
-
- # Private methods for sending events.
-
- def send_frame(self, frame: Frame) -> None:
- if self.state is not OPEN:
- raise InvalidState(
- f"cannot write to a WebSocket in the {self.state.name} state"
- )
-
- if self.debug:
- self.logger.debug("> %s", frame)
- self.writes.append(
- frame.serialize(mask=self.side is CLIENT, extensions=self.extensions)
- )
-
- def send_eof(self) -> None:
- assert not self.eof_sent
- self.eof_sent = True
- if self.debug:
- self.logger.debug("> EOF")
- self.writes.append(SEND_EOF)
+warnings.warn(
+ "websockets.connection was renamed to websockets.protocol "
+ "and Connection was renamed to Protocol",
+ DeprecationWarning,
+)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/datastructures.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/datastructures.py
index 36a2cbaf99..a0a648463a 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/datastructures.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/datastructures.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-import sys
from typing import (
Any,
Dict,
@@ -9,17 +8,12 @@ from typing import (
List,
Mapping,
MutableMapping,
+ Protocol,
Tuple,
Union,
)
-if sys.version_info[:2] >= (3, 8):
- from typing import Protocol
-else: # pragma: no cover
- Protocol = object # mypy will report errors on Python 3.7.
-
-
__all__ = ["Headers", "HeadersLike", "MultipleValuesError"]
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py
index 0c4fc51851..f7169e3b17 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py
@@ -34,6 +34,7 @@ import http
from typing import Optional
from . import datastructures, frames, http11
+from .typing import StatusLike
__all__ = [
@@ -120,19 +121,23 @@ class ConnectionClosed(WebSocketException):
@property
def code(self) -> int:
- return 1006 if self.rcvd is None else self.rcvd.code
+ if self.rcvd is None:
+ return frames.CloseCode.ABNORMAL_CLOSURE
+ return self.rcvd.code
@property
def reason(self) -> str:
- return "" if self.rcvd is None else self.rcvd.reason
+ if self.rcvd is None:
+ return ""
+ return self.rcvd.reason
class ConnectionClosedError(ConnectionClosed):
"""
Like :exc:`ConnectionClosed`, when the connection terminated with an error.
- A close code other than 1000 (OK) or 1001 (going away) was received or
- sent, or the closing handshake didn't complete properly.
+ A close frame with a code other than 1000 (OK) or 1001 (going away) was
+ received or sent, or the closing handshake didn't complete properly.
"""
@@ -141,7 +146,8 @@ class ConnectionClosedOK(ConnectionClosed):
"""
Like :exc:`ConnectionClosed`, when the connection terminated properly.
- A close code 1000 (OK) or 1001 (going away) was received and sent.
+ A close code with code 1000 (OK) or 1001 (going away) or without a code was
+ received and sent.
"""
@@ -171,7 +177,7 @@ class InvalidMessage(InvalidHandshake):
class InvalidHeader(InvalidHandshake):
"""
- Raised when a HTTP header doesn't have a valid format or value.
+ Raised when an HTTP header doesn't have a valid format or value.
"""
@@ -190,7 +196,7 @@ class InvalidHeader(InvalidHandshake):
class InvalidHeaderFormat(InvalidHeader):
"""
- Raised when a HTTP header cannot be parsed.
+ Raised when an HTTP header cannot be parsed.
The format of the header doesn't match the grammar for that header.
@@ -202,7 +208,7 @@ class InvalidHeaderFormat(InvalidHeader):
class InvalidHeaderValue(InvalidHeader):
"""
- Raised when a HTTP header has a wrong value.
+ Raised when an HTTP header has a wrong value.
The format of the header is correct but a value isn't acceptable.
@@ -310,7 +316,7 @@ class InvalidParameterValue(NegotiationError):
class AbortHandshake(InvalidHandshake):
"""
- Raised to abort the handshake on purpose and return a HTTP response.
+ Raised to abort the handshake on purpose and return an HTTP response.
This exception is an implementation detail.
@@ -325,11 +331,12 @@ class AbortHandshake(InvalidHandshake):
def __init__(
self,
- status: http.HTTPStatus,
+ status: StatusLike,
headers: datastructures.HeadersLike,
body: bytes = b"",
) -> None:
- self.status = status
+ # If a user passes an int instead of a HTTPStatus, fix it automatically.
+ self.status = http.HTTPStatus(status)
self.headers = datastructures.Headers(headers)
self.body = body
@@ -369,7 +376,7 @@ class InvalidState(WebSocketException, AssertionError):
class InvalidURI(WebSocketException):
"""
- Raised when connecting to an URI that isn't a valid WebSocket URI.
+ Raised when connecting to a URI that isn't a valid WebSocket URI.
"""
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py
index 0609676185..6c481a46cc 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py
@@ -38,6 +38,7 @@ class Extension:
PayloadTooBig: if decoding the payload exceeds ``max_size``.
"""
+ raise NotImplementedError
def encode(self, frame: frames.Frame) -> frames.Frame:
"""
@@ -50,6 +51,7 @@ class Extension:
Frame: Encoded frame.
"""
+ raise NotImplementedError
class ClientExtensionFactory:
@@ -69,6 +71,7 @@ class ClientExtensionFactory:
List[ExtensionParameter]: Parameters to send to the server.
"""
+ raise NotImplementedError
def process_response_params(
self,
@@ -91,6 +94,7 @@ class ClientExtensionFactory:
NegotiationError: if parameters aren't acceptable.
"""
+ raise NotImplementedError
class ServerExtensionFactory:
@@ -126,3 +130,4 @@ class ServerExtensionFactory:
the client aren't acceptable.
"""
+ raise NotImplementedError
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py
index e0de5e8f85..b391837c66 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py
@@ -211,7 +211,6 @@ def _extract_parameters(
client_max_window_bits: Optional[Union[int, bool]] = None
for name, value in params:
-
if name == "server_no_context_takeover":
if server_no_context_takeover:
raise exceptions.DuplicateParameter(name)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/frames.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/frames.py
index 043b688b52..6b1befb2e0 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/frames.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/frames.py
@@ -13,7 +13,7 @@ from .typing import Data
try:
from .speedups import apply_mask
-except ImportError: # pragma: no cover
+except ImportError:
from .utils import apply_mask
@@ -52,45 +52,70 @@ DATA_OPCODES = OP_CONT, OP_TEXT, OP_BINARY
CTRL_OPCODES = OP_CLOSE, OP_PING, OP_PONG
-# See https://www.iana.org/assignments/websocket/websocket.xhtml
-CLOSE_CODES = {
- 1000: "OK",
- 1001: "going away",
- 1002: "protocol error",
- 1003: "unsupported type",
+class CloseCode(enum.IntEnum):
+ """Close code values for WebSocket close frames."""
+
+ NORMAL_CLOSURE = 1000
+ GOING_AWAY = 1001
+ PROTOCOL_ERROR = 1002
+ UNSUPPORTED_DATA = 1003
# 1004 is reserved
- 1005: "no status code [internal]",
- 1006: "connection closed abnormally [internal]",
- 1007: "invalid data",
- 1008: "policy violation",
- 1009: "message too big",
- 1010: "extension required",
- 1011: "unexpected error",
- 1012: "service restart",
- 1013: "try again later",
- 1014: "bad gateway",
- 1015: "TLS failure [internal]",
+ NO_STATUS_RCVD = 1005
+ ABNORMAL_CLOSURE = 1006
+ INVALID_DATA = 1007
+ POLICY_VIOLATION = 1008
+ MESSAGE_TOO_BIG = 1009
+ MANDATORY_EXTENSION = 1010
+ INTERNAL_ERROR = 1011
+ SERVICE_RESTART = 1012
+ TRY_AGAIN_LATER = 1013
+ BAD_GATEWAY = 1014
+ TLS_HANDSHAKE = 1015
+
+
+# See https://www.iana.org/assignments/websocket/websocket.xhtml
+CLOSE_CODE_EXPLANATIONS: dict[int, str] = {
+ CloseCode.NORMAL_CLOSURE: "OK",
+ CloseCode.GOING_AWAY: "going away",
+ CloseCode.PROTOCOL_ERROR: "protocol error",
+ CloseCode.UNSUPPORTED_DATA: "unsupported data",
+ CloseCode.NO_STATUS_RCVD: "no status received [internal]",
+ CloseCode.ABNORMAL_CLOSURE: "abnormal closure [internal]",
+ CloseCode.INVALID_DATA: "invalid frame payload data",
+ CloseCode.POLICY_VIOLATION: "policy violation",
+ CloseCode.MESSAGE_TOO_BIG: "message too big",
+ CloseCode.MANDATORY_EXTENSION: "mandatory extension",
+ CloseCode.INTERNAL_ERROR: "internal error",
+ CloseCode.SERVICE_RESTART: "service restart",
+ CloseCode.TRY_AGAIN_LATER: "try again later",
+ CloseCode.BAD_GATEWAY: "bad gateway",
+ CloseCode.TLS_HANDSHAKE: "TLS handshake failure [internal]",
}
# Close code that are allowed in a close frame.
# Using a set optimizes `code in EXTERNAL_CLOSE_CODES`.
EXTERNAL_CLOSE_CODES = {
- 1000,
- 1001,
- 1002,
- 1003,
- 1007,
- 1008,
- 1009,
- 1010,
- 1011,
- 1012,
- 1013,
- 1014,
+ CloseCode.NORMAL_CLOSURE,
+ CloseCode.GOING_AWAY,
+ CloseCode.PROTOCOL_ERROR,
+ CloseCode.UNSUPPORTED_DATA,
+ CloseCode.INVALID_DATA,
+ CloseCode.POLICY_VIOLATION,
+ CloseCode.MESSAGE_TOO_BIG,
+ CloseCode.MANDATORY_EXTENSION,
+ CloseCode.INTERNAL_ERROR,
+ CloseCode.SERVICE_RESTART,
+ CloseCode.TRY_AGAIN_LATER,
+ CloseCode.BAD_GATEWAY,
}
-OK_CLOSE_CODES = {1000, 1001}
+
+OK_CLOSE_CODES = {
+ CloseCode.NORMAL_CLOSURE,
+ CloseCode.GOING_AWAY,
+ CloseCode.NO_STATUS_RCVD,
+}
BytesLike = bytes, bytearray, memoryview
@@ -123,7 +148,7 @@ class Frame:
def __str__(self) -> str:
"""
- Return a human-readable represention of a frame.
+ Return a human-readable representation of a frame.
"""
coding = None
@@ -191,6 +216,8 @@ class Frame:
extensions: list of extensions, applied in reverse order.
Raises:
+ EOFError: if the connection is closed without a full WebSocket frame.
+ UnicodeDecodeError: if the frame contains invalid UTF-8.
PayloadTooBig: if the frame's payload size exceeds ``max_size``.
ProtocolError: if the frame contains incorrect values.
@@ -383,7 +410,7 @@ class Close:
def __str__(self) -> str:
"""
- Return a human-readable represention of a close code and reason.
+ Return a human-readable representation of a close code and reason.
"""
if 3000 <= self.code < 4000:
@@ -391,7 +418,7 @@ class Close:
elif 4000 <= self.code < 5000:
explanation = "private use"
else:
- explanation = CLOSE_CODES.get(self.code, "unknown")
+ explanation = CLOSE_CODE_EXPLANATIONS.get(self.code, "unknown")
result = f"{self.code} ({explanation})"
if self.reason:
@@ -419,7 +446,7 @@ class Close:
close.check()
return close
elif len(data) == 0:
- return cls(1005, "")
+ return cls(CloseCode.NO_STATUS_RCVD, "")
else:
raise exceptions.ProtocolError("close frame too short")
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py
index b14fa94bdc..9f86f6a1ff 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import sys
+import typing
from .imports import lazy_import
from .version import version as websockets_version
@@ -9,18 +10,22 @@ from .version import version as websockets_version
# For backwards compatibility:
-lazy_import(
- globals(),
- # Headers and MultipleValuesError used to be defined in this module.
- aliases={
- "Headers": ".datastructures",
- "MultipleValuesError": ".datastructures",
- },
- deprecated_aliases={
- "read_request": ".legacy.http",
- "read_response": ".legacy.http",
- },
-)
+# When type checking, import non-deprecated aliases eagerly. Else, import on demand.
+if typing.TYPE_CHECKING:
+ from .datastructures import Headers, MultipleValuesError # noqa: F401
+else:
+ lazy_import(
+ globals(),
+ # Headers and MultipleValuesError used to be defined in this module.
+ aliases={
+ "Headers": ".datastructures",
+ "MultipleValuesError": ".datastructures",
+ },
+ deprecated_aliases={
+ "read_request": ".legacy.http",
+ "read_response": ".legacy.http",
+ },
+ )
__all__ = ["USER_AGENT"]
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http11.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http11.py
index 84048fa47b..ec4e3b8b7d 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http11.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http11.py
@@ -8,14 +8,12 @@ from typing import Callable, Generator, Optional
from . import datastructures, exceptions
-# Maximum total size of headers is around 256 * 4 KiB = 1 MiB
-MAX_HEADERS = 256
+# Maximum total size of headers is around 128 * 8 KiB = 1 MiB.
+MAX_HEADERS = 128
-# We can use the same limit for the request line and header lines:
-# "GET <4096 bytes> HTTP/1.1\r\n" = 4111 bytes
-# "Set-Cookie: <4097 bytes>\r\n" = 4111 bytes
-# (RFC requires 4096 bytes; for some reason Firefox supports 4097 bytes.)
-MAX_LINE = 4111
+# Limit request line and header lines. 8KiB is the most common default
+# configuration of popular HTTP servers.
+MAX_LINE = 8192
# Support for HTTP response bodies is intended to read an error message
# returned by a server. It isn't designed to perform large file transfers.
@@ -70,7 +68,7 @@ class Request:
def exception(self) -> Optional[Exception]: # pragma: no cover
warnings.warn(
"Request.exception is deprecated; "
- "use ServerConnection.handshake_exc instead",
+ "use ServerProtocol.handshake_exc instead",
DeprecationWarning,
)
return self._exception
@@ -174,7 +172,7 @@ class Response:
def exception(self) -> Optional[Exception]: # pragma: no cover
warnings.warn(
"Response.exception is deprecated; "
- "use ClientConnection.handshake_exc instead",
+ "use ClientProtocol.handshake_exc instead",
DeprecationWarning,
)
return self._exception
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/async_timeout.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/async_timeout.py
new file mode 100644
index 0000000000..8264094f5b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/async_timeout.py
@@ -0,0 +1,265 @@
+# From https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py
+# Licensed under the Apache License (Apache-2.0)
+
+import asyncio
+import enum
+import sys
+import warnings
+from types import TracebackType
+from typing import Optional, Type
+
+
+# From https://github.com/python/typing_extensions/blob/main/src/typing_extensions.py
+# Licensed under the Python Software Foundation License (PSF-2.0)
+
+if sys.version_info >= (3, 11):
+ from typing import final
+else:
+ # @final exists in 3.8+, but we backport it for all versions
+ # before 3.11 to keep support for the __final__ attribute.
+ # See https://bugs.python.org/issue46342
+ def final(f):
+ """This decorator can be used to indicate to type checkers that
+ the decorated method cannot be overridden, and decorated class
+ cannot be subclassed. For example:
+
+ class Base:
+ @final
+ def done(self) -> None:
+ ...
+ class Sub(Base):
+ def done(self) -> None: # Error reported by type checker
+ ...
+ @final
+ class Leaf:
+ ...
+ class Other(Leaf): # Error reported by type checker
+ ...
+
+ There is no runtime checking of these properties. The decorator
+ sets the ``__final__`` attribute to ``True`` on the decorated object
+ to allow runtime introspection.
+ """
+ try:
+ f.__final__ = True
+ except (AttributeError, TypeError):
+ # Skip the attribute silently if it is not writable.
+ # AttributeError happens if the object has __slots__ or a
+ # read-only property, TypeError if it's a builtin class.
+ pass
+ return f
+
+
+# End https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py
+
+__version__ = "4.0.2"
+
+
+__all__ = ("timeout", "timeout_at", "Timeout")
+
+
+def timeout(delay: Optional[float]) -> "Timeout":
+ """timeout context manager.
+
+ Useful in cases when you want to apply timeout logic around block
+ of code or in cases when asyncio.wait_for is not suitable. For example:
+
+ >>> async with timeout(0.001):
+ ... async with aiohttp.get('https://github.com') as r:
+ ... await r.text()
+
+
+ delay - value in seconds or None to disable timeout logic
+ """
+ loop = asyncio.get_running_loop()
+ if delay is not None:
+ deadline = loop.time() + delay # type: Optional[float]
+ else:
+ deadline = None
+ return Timeout(deadline, loop)
+
+
+def timeout_at(deadline: Optional[float]) -> "Timeout":
+ """Schedule the timeout at absolute time.
+
+ deadline argument points on the time in the same clock system
+ as loop.time().
+
+ Please note: it is not POSIX time but a time with
+ undefined starting base, e.g. the time of the system power on.
+
+ >>> async with timeout_at(loop.time() + 10):
+ ... async with aiohttp.get('https://github.com') as r:
+ ... await r.text()
+
+
+ """
+ loop = asyncio.get_running_loop()
+ return Timeout(deadline, loop)
+
+
+class _State(enum.Enum):
+ INIT = "INIT"
+ ENTER = "ENTER"
+ TIMEOUT = "TIMEOUT"
+ EXIT = "EXIT"
+
+
+@final
+class Timeout:
+ # Internal class, please don't instantiate it directly
+ # Use timeout() and timeout_at() public factories instead.
+ #
+ # Implementation note: `async with timeout()` is preferred
+ # over `with timeout()`.
+ # While technically the Timeout class implementation
+ # doesn't need to be async at all,
+ # the `async with` statement explicitly points that
+ # the context manager should be used from async function context.
+ #
+ # This design allows to avoid many silly misusages.
+ #
+ # TimeoutError is raised immediately when scheduled
+ # if the deadline is passed.
+ # The purpose is to time out as soon as possible
+ # without waiting for the next await expression.
+
+ __slots__ = ("_deadline", "_loop", "_state", "_timeout_handler")
+
+ def __init__(
+ self, deadline: Optional[float], loop: asyncio.AbstractEventLoop
+ ) -> None:
+ self._loop = loop
+ self._state = _State.INIT
+
+ self._timeout_handler = None # type: Optional[asyncio.Handle]
+ if deadline is None:
+ self._deadline = None # type: Optional[float]
+ else:
+ self.update(deadline)
+
+ def __enter__(self) -> "Timeout":
+ warnings.warn(
+ "with timeout() is deprecated, use async with timeout() instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ self._do_enter()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> Optional[bool]:
+ self._do_exit(exc_type)
+ return None
+
+ async def __aenter__(self) -> "Timeout":
+ self._do_enter()
+ return self
+
+ async def __aexit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> Optional[bool]:
+ self._do_exit(exc_type)
+ return None
+
+ @property
+ def expired(self) -> bool:
+ """Is timeout expired during execution?"""
+ return self._state == _State.TIMEOUT
+
+ @property
+ def deadline(self) -> Optional[float]:
+ return self._deadline
+
+ def reject(self) -> None:
+ """Reject scheduled timeout if any."""
+ # cancel is maybe better name but
+ # task.cancel() raises CancelledError in asyncio world.
+ if self._state not in (_State.INIT, _State.ENTER):
+ raise RuntimeError(f"invalid state {self._state.value}")
+ self._reject()
+
+ def _reject(self) -> None:
+ if self._timeout_handler is not None:
+ self._timeout_handler.cancel()
+ self._timeout_handler = None
+
+ def shift(self, delay: float) -> None:
+ """Advance timeout on delay seconds.
+
+ The delay can be negative.
+
+ Raise RuntimeError if shift is called when deadline is not scheduled
+ """
+ deadline = self._deadline
+ if deadline is None:
+ raise RuntimeError("cannot shift timeout if deadline is not scheduled")
+ self.update(deadline + delay)
+
+ def update(self, deadline: float) -> None:
+ """Set deadline to absolute value.
+
+ deadline argument points on the time in the same clock system
+ as loop.time().
+
+ If new deadline is in the past the timeout is raised immediately.
+
+ Please note: it is not POSIX time but a time with
+ undefined starting base, e.g. the time of the system power on.
+ """
+ if self._state == _State.EXIT:
+ raise RuntimeError("cannot reschedule after exit from context manager")
+ if self._state == _State.TIMEOUT:
+ raise RuntimeError("cannot reschedule expired timeout")
+ if self._timeout_handler is not None:
+ self._timeout_handler.cancel()
+ self._deadline = deadline
+ if self._state != _State.INIT:
+ self._reschedule()
+
+ def _reschedule(self) -> None:
+ assert self._state == _State.ENTER
+ deadline = self._deadline
+ if deadline is None:
+ return
+
+ now = self._loop.time()
+ if self._timeout_handler is not None:
+ self._timeout_handler.cancel()
+
+ task = asyncio.current_task()
+ if deadline <= now:
+ self._timeout_handler = self._loop.call_soon(self._on_timeout, task)
+ else:
+ self._timeout_handler = self._loop.call_at(deadline, self._on_timeout, task)
+
+ def _do_enter(self) -> None:
+ if self._state != _State.INIT:
+ raise RuntimeError(f"invalid state {self._state.value}")
+ self._state = _State.ENTER
+ self._reschedule()
+
+ def _do_exit(self, exc_type: Optional[Type[BaseException]]) -> None:
+ if exc_type is asyncio.CancelledError and self._state == _State.TIMEOUT:
+ self._timeout_handler = None
+ raise asyncio.TimeoutError
+ # timeout has not expired
+ self._state = _State.EXIT
+ self._reject()
+ return None
+
+ def _on_timeout(self, task: "asyncio.Task[None]") -> None:
+ task.cancel()
+ self._state = _State.TIMEOUT
+ # drop the reference early
+ self._timeout_handler = None
+
+
+# End https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/auth.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/auth.py
index 8825c14ecf..d3425836e1 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/auth.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/auth.py
@@ -67,7 +67,7 @@ class BasicAuthWebSocketServerProtocol(WebSocketServerProtocol):
Returns:
bool: :obj:`True` if the handshake should continue;
- :obj:`False` if it should fail with a HTTP 401 error.
+ :obj:`False` if it should fail with an HTTP 401 error.
"""
if self._check_credentials is not None:
@@ -81,7 +81,7 @@ class BasicAuthWebSocketServerProtocol(WebSocketServerProtocol):
request_headers: Headers,
) -> Optional[HTTPResponse]:
"""
- Check HTTP Basic Auth and return a HTTP 401 response if needed.
+ Check HTTP Basic Auth and return an HTTP 401 response if needed.
"""
try:
@@ -118,8 +118,8 @@ def basic_auth_protocol_factory(
realm: Optional[str] = None,
credentials: Optional[Union[Credentials, Iterable[Credentials]]] = None,
check_credentials: Optional[Callable[[str, str], Awaitable[bool]]] = None,
- create_protocol: Optional[Callable[[Any], BasicAuthWebSocketServerProtocol]] = None,
-) -> Callable[[Any], BasicAuthWebSocketServerProtocol]:
+ create_protocol: Optional[Callable[..., BasicAuthWebSocketServerProtocol]] = None,
+) -> Callable[..., BasicAuthWebSocketServerProtocol]:
"""
Protocol factory that enforces HTTP Basic Auth.
@@ -135,20 +135,20 @@ def basic_auth_protocol_factory(
)
Args:
- realm: indicates the scope of protection. It should contain only ASCII
- characters because the encoding of non-ASCII characters is
- undefined. Refer to section 2.2 of :rfc:`7235` for details.
- credentials: defines hard coded authorized credentials. It can be a
+ realm: Scope of protection. It should contain only ASCII characters
+ because the encoding of non-ASCII characters is undefined.
+ Refer to section 2.2 of :rfc:`7235` for details.
+ credentials: Hard coded authorized credentials. It can be a
``(username, password)`` pair or a list of such pairs.
- check_credentials: defines a coroutine that verifies credentials.
- This coroutine receives ``username`` and ``password`` arguments
+ check_credentials: Coroutine that verifies credentials.
+ It receives ``username`` and ``password`` arguments
and returns a :class:`bool`. One of ``credentials`` or
``check_credentials`` must be provided but not both.
- create_protocol: factory that creates the protocol. By default, this
+ create_protocol: Factory that creates the protocol. By default, this
is :class:`BasicAuthWebSocketServerProtocol`. It can be replaced
by a subclass.
Raises:
- TypeError: if the ``credentials`` or ``check_credentials`` argument is
+ TypeError: If the ``credentials`` or ``check_credentials`` argument is
wrong.
"""
@@ -175,11 +175,7 @@ def basic_auth_protocol_factory(
return hmac.compare_digest(expected_password, password)
if create_protocol is None:
- # Not sure why mypy cannot figure this out.
- create_protocol = cast(
- Callable[[Any], BasicAuthWebSocketServerProtocol],
- BasicAuthWebSocketServerProtocol,
- )
+ create_protocol = BasicAuthWebSocketServerProtocol
return functools.partial(
create_protocol,
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/client.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/client.py
index fadc3efe87..48622523ee 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/client.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/client.py
@@ -44,6 +44,7 @@ from ..headers import (
from ..http import USER_AGENT
from ..typing import ExtensionHeader, LoggerLike, Origin, Subprotocol
from ..uri import WebSocketURI, parse_uri
+from .compatibility import asyncio_timeout
from .handshake import build_request, check_response
from .http import read_response
from .protocol import WebSocketCommonProtocol
@@ -65,12 +66,13 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
await process(message)
The iterator exits normally when the connection is closed with close code
- 1000 (OK) or 1001 (going away). It raises
+ 1000 (OK) or 1001 (going away) or without a close code. It raises
a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection
is closed with any other code.
See :func:`connect` for the documentation of ``logger``, ``origin``,
- ``extensions``, ``subprotocols``, and ``extra_headers``.
+ ``extensions``, ``subprotocols``, ``extra_headers``, and
+ ``user_agent_header``.
See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the
documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``,
@@ -89,6 +91,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
extensions: Optional[Sequence[ClientExtensionFactory]] = None,
subprotocols: Optional[Sequence[Subprotocol]] = None,
extra_headers: Optional[HeadersLike] = None,
+ user_agent_header: Optional[str] = USER_AGENT,
**kwargs: Any,
) -> None:
if logger is None:
@@ -98,6 +101,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
self.available_extensions = extensions
self.available_subprotocols = subprotocols
self.extra_headers = extra_headers
+ self.user_agent_header = user_agent_header
def write_http_request(self, path: str, headers: Headers) -> None:
"""
@@ -127,16 +131,12 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
after this coroutine returns.
Raises:
- InvalidMessage: if the HTTP message is malformed or isn't an
+ InvalidMessage: If the HTTP message is malformed or isn't an
HTTP/1.1 GET response.
"""
try:
status_code, reason, headers = await read_response(self.reader)
- # Remove this branch when dropping support for Python < 3.8
- # because CancelledError no longer inherits Exception.
- except asyncio.CancelledError: # pragma: no cover
- raise
except Exception as exc:
raise InvalidMessage("did not receive a valid HTTP response") from exc
@@ -185,7 +185,6 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
header_values = headers.get_all("Sec-WebSocket-Extensions")
if header_values:
-
if available_extensions is None:
raise InvalidHandshake("no extensions supported")
@@ -194,9 +193,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
)
for name, response_params in parsed_header_values:
-
for extension_factory in available_extensions:
-
# Skip non-matching extensions based on their name.
if extension_factory.name != name:
continue
@@ -242,7 +239,6 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
header_values = headers.get_all("Sec-WebSocket-Protocol")
if header_values:
-
if available_subprotocols is None:
raise InvalidHandshake("no subprotocols supported")
@@ -274,15 +270,15 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
Args:
wsuri: URI of the WebSocket server.
- origin: value of the ``Origin`` header.
- available_extensions: list of supported extensions, in order in
- which they should be tried.
- available_subprotocols: list of supported subprotocols, in order
- of decreasing preference.
- extra_headers: arbitrary HTTP headers to add to the request.
+ origin: Value of the ``Origin`` header.
+ extensions: List of supported extensions, in order in which they
+ should be negotiated and run.
+ subprotocols: List of supported subprotocols, in order of decreasing
+ preference.
+ extra_headers: Arbitrary HTTP headers to add to the handshake request.
Raises:
- InvalidHandshake: if the handshake fails.
+ InvalidHandshake: If the handshake fails.
"""
request_headers = Headers()
@@ -315,7 +311,8 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
if self.extra_headers is not None:
request_headers.update(self.extra_headers)
- request_headers.setdefault("User-Agent", USER_AGENT)
+ if self.user_agent_header is not None:
+ request_headers.setdefault("User-Agent", self.user_agent_header)
self.write_http_request(wsuri.resource_name, request_headers)
@@ -376,25 +373,26 @@ class Connect:
Args:
uri: URI of the WebSocket server.
- create_protocol: factory for the :class:`asyncio.Protocol` managing
- the connection; defaults to :class:`WebSocketClientProtocol`; may
- be set to a wrapper or a subclass to customize connection handling.
- logger: logger for this connection;
- defaults to ``logging.getLogger("websockets.client")``;
- see the :doc:`logging guide <../topics/logging>` for details.
- compression: shortcut that enables the "permessage-deflate" extension
- by default; may be set to :obj:`None` to disable compression;
- see the :doc:`compression guide <../topics/compression>` for details.
- origin: value of the ``Origin`` header. This is useful when connecting
- to a server that validates the ``Origin`` header to defend against
- Cross-Site WebSocket Hijacking attacks.
- extensions: list of supported extensions, in order in which they
- should be tried.
- subprotocols: list of supported subprotocols, in order of decreasing
+ create_protocol: Factory for the :class:`asyncio.Protocol` managing
+ the connection. It defaults to :class:`WebSocketClientProtocol`.
+ Set it to a wrapper or a subclass to customize connection handling.
+ logger: Logger for this client.
+ It defaults to ``logging.getLogger("websockets.client")``.
+ See the :doc:`logging guide <../../topics/logging>` for details.
+ compression: The "permessage-deflate" extension is enabled by default.
+ Set ``compression`` to :obj:`None` to disable it. See the
+ :doc:`compression guide <../../topics/compression>` for details.
+ origin: Value of the ``Origin`` header, for servers that require it.
+ extensions: List of supported extensions, in order in which they
+ should be negotiated and run.
+ subprotocols: List of supported subprotocols, in order of decreasing
preference.
- extra_headers: arbitrary HTTP headers to add to the request.
- open_timeout: timeout for opening the connection in seconds;
- :obj:`None` to disable the timeout
+ extra_headers: Arbitrary HTTP headers to add to the handshake request.
+ user_agent_header: Value of the ``User-Agent`` request header.
+ It defaults to ``"Python/x.y.z websockets/X.Y"``.
+ Setting it to :obj:`None` removes the header.
+ open_timeout: Timeout for opening the connection in seconds.
+ :obj:`None` disables the timeout.
See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the
documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``,
@@ -415,13 +413,11 @@ class Connect:
the TCP connection. The host name from ``uri`` is still used in the TLS
handshake for secure connections and in the ``Host`` header.
- Returns:
- WebSocketClientProtocol: WebSocket connection.
-
Raises:
- InvalidURI: if ``uri`` isn't a valid WebSocket URI.
- InvalidHandshake: if the opening handshake fails.
- ~asyncio.TimeoutError: if the opening handshake times out.
+ InvalidURI: If ``uri`` isn't a valid WebSocket URI.
+ OSError: If the TCP connection fails.
+ InvalidHandshake: If the opening handshake fails.
+ ~asyncio.TimeoutError: If the opening handshake times out.
"""
@@ -431,13 +427,14 @@ class Connect:
self,
uri: str,
*,
- create_protocol: Optional[Callable[[Any], WebSocketClientProtocol]] = None,
+ create_protocol: Optional[Callable[..., WebSocketClientProtocol]] = None,
logger: Optional[LoggerLike] = None,
compression: Optional[str] = "deflate",
origin: Optional[Origin] = None,
extensions: Optional[Sequence[ClientExtensionFactory]] = None,
subprotocols: Optional[Sequence[Subprotocol]] = None,
extra_headers: Optional[HeadersLike] = None,
+ user_agent_header: Optional[str] = USER_AGENT,
open_timeout: Optional[float] = 10,
ping_interval: Optional[float] = 20,
ping_timeout: Optional[float] = 20,
@@ -503,6 +500,7 @@ class Connect:
extensions=extensions,
subprotocols=subprotocols,
extra_headers=extra_headers,
+ user_agent_header=user_agent_header,
ping_interval=ping_interval,
ping_timeout=ping_timeout,
close_timeout=close_timeout,
@@ -530,6 +528,8 @@ class Connect:
else:
# If sock is given, host and port shouldn't be specified.
host, port = None, None
+ if kwargs.get("ssl"):
+ kwargs.setdefault("server_hostname", wsuri.host)
# If host and port are given, override values from the URI.
host = kwargs.pop("host", host)
port = kwargs.pop("port", port)
@@ -597,10 +597,6 @@ class Connect:
try:
async with self as protocol:
yield protocol
- # Remove this branch when dropping support for Python < 3.8
- # because CancelledError no longer inherits Exception.
- except asyncio.CancelledError: # pragma: no cover
- raise
except Exception:
# Add a random initial delay between 0 and 5 seconds.
# See 7.2.3. Recovering from Abnormal Closure in RFC 6544.
@@ -647,13 +643,13 @@ class Connect:
return self.__await_impl_timeout__().__await__()
async def __await_impl_timeout__(self) -> WebSocketClientProtocol:
- return await asyncio.wait_for(self.__await_impl__(), self.open_timeout)
+ async with asyncio_timeout(self.open_timeout):
+ return await self.__await_impl__()
async def __await_impl__(self) -> WebSocketClientProtocol:
for redirects in range(self.MAX_REDIRECTS_ALLOWED):
- transport, protocol = await self._create_connection()
- protocol = cast(WebSocketClientProtocol, protocol)
-
+ _transport, _protocol = await self._create_connection()
+ protocol = cast(WebSocketClientProtocol, _protocol)
try:
await protocol.handshake(
self._wsuri,
@@ -701,7 +697,7 @@ def unix_connect(
It's mainly useful for debugging servers listening on Unix sockets.
Args:
- path: file system path to the Unix socket.
+ path: File system path to the Unix socket.
uri: URI of the WebSocket server; the host is used in the TLS
handshake for secure connections and in the ``Host`` header.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py
index df81de9dbc..6bd01e70de 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py
@@ -1,13 +1,12 @@
from __future__ import annotations
-import asyncio
import sys
-from typing import Any, Dict
-def loop_if_py_lt_38(loop: asyncio.AbstractEventLoop) -> Dict[str, Any]:
- """
- Helper for the removal of the loop argument in Python 3.10.
+__all__ = ["asyncio_timeout"]
- """
- return {"loop": loop} if sys.version_info[:2] < (3, 8) else {}
+
+if sys.version_info[:2] >= (3, 11):
+ from asyncio import timeout as asyncio_timeout # noqa: F401
+else:
+ from .async_timeout import timeout as asyncio_timeout # noqa: F401
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/framing.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/framing.py
index c4de7eb28b..b77b869e3f 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/framing.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/framing.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-import dataclasses
import struct
from typing import Any, Awaitable, Callable, NamedTuple, Optional, Sequence, Tuple
@@ -10,12 +9,11 @@ from ..exceptions import PayloadTooBig, ProtocolError
try:
from ..speedups import apply_mask
-except ImportError: # pragma: no cover
+except ImportError:
from ..utils import apply_mask
class Frame(NamedTuple):
-
fin: bool
opcode: frames.Opcode
data: bytes
@@ -53,16 +51,16 @@ class Frame(NamedTuple):
Read a WebSocket frame.
Args:
- reader: coroutine that reads exactly the requested number of
+ reader: Coroutine that reads exactly the requested number of
bytes, unless the end of file is reached.
- mask: whether the frame should be masked i.e. whether the read
+ mask: Whether the frame should be masked i.e. whether the read
happens on the server side.
- max_size: maximum payload size in bytes.
- extensions: list of extensions, applied in reverse order.
+ max_size: Maximum payload size in bytes.
+ extensions: List of extensions, applied in reverse order.
Raises:
- PayloadTooBig: if the frame exceeds ``max_size``.
- ProtocolError: if the frame contains incorrect values.
+ PayloadTooBig: If the frame exceeds ``max_size``.
+ ProtocolError: If the frame contains incorrect values.
"""
@@ -130,14 +128,14 @@ class Frame(NamedTuple):
Write a WebSocket frame.
Args:
- frame: frame to write.
- write: function that writes bytes.
- mask: whether the frame should be masked i.e. whether the write
+ frame: Frame to write.
+ write: Function that writes bytes.
+ mask: Whether the frame should be masked i.e. whether the write
happens on the client side.
- extensions: list of extensions, applied in order.
+ extensions: List of extensions, applied in order.
Raises:
- ProtocolError: if the frame contains incorrect values.
+ ProtocolError: If the frame contains incorrect values.
"""
# The frame is written in a single call to write in order to prevent
@@ -147,8 +145,11 @@ class Frame(NamedTuple):
# Backwards compatibility with previously documented public APIs
-
-from ..frames import Close, prepare_ctrl as encode_data, prepare_data # noqa
+from ..frames import ( # noqa: E402, F401, I001
+ Close,
+ prepare_ctrl as encode_data,
+ prepare_data,
+)
def parse_close(data: bytes) -> Tuple[int, str]:
@@ -156,14 +157,15 @@ def parse_close(data: bytes) -> Tuple[int, str]:
Parse the payload from a close frame.
Returns:
- Tuple[int, str]: close code and reason.
+ Close code and reason.
Raises:
- ProtocolError: if data is ill-formed.
- UnicodeDecodeError: if the reason isn't valid UTF-8.
+ ProtocolError: If data is ill-formed.
+ UnicodeDecodeError: If the reason isn't valid UTF-8.
"""
- return dataclasses.astuple(Close.parse(data)) # type: ignore
+ close = Close.parse(data)
+ return close.code, close.reason
def serialize_close(code: int, reason: str) -> bytes:
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py
index 569937bb9a..ad8faf0404 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py
@@ -21,7 +21,7 @@ def build_request(headers: Headers) -> str:
Update request headers passed in argument.
Args:
- headers: handshake request headers.
+ headers: Handshake request headers.
Returns:
str: ``key`` that must be passed to :func:`check_response`.
@@ -45,14 +45,14 @@ def check_request(headers: Headers) -> str:
the responsibility of the caller.
Args:
- headers: handshake request headers.
+ headers: Handshake request headers.
Returns:
str: ``key`` that must be passed to :func:`build_response`.
Raises:
- InvalidHandshake: if the handshake request is invalid;
- then the server must return 400 Bad Request error.
+ InvalidHandshake: If the handshake request is invalid.
+ Then, the server must return a 400 Bad Request error.
"""
connection: List[ConnectionOption] = sum(
@@ -110,8 +110,8 @@ def build_response(headers: Headers, key: str) -> None:
Update response headers passed in argument.
Args:
- headers: handshake response headers.
- key: returned by :func:`check_request`.
+ headers: Handshake response headers.
+ key: Returned by :func:`check_request`.
"""
headers["Upgrade"] = "websocket"
@@ -128,11 +128,11 @@ def check_response(headers: Headers, key: str) -> None:
the caller.
Args:
- headers: handshake response headers.
- key: returned by :func:`build_request`.
+ headers: Handshake response headers.
+ key: Returned by :func:`build_request`.
Raises:
- InvalidHandshake: if the handshake response is invalid.
+ InvalidHandshake: If the handshake response is invalid.
"""
connection: List[ConnectionOption] = sum(
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/http.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/http.py
index cc2ef1f067..2ac7f7092d 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/http.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/http.py
@@ -10,8 +10,8 @@ from ..exceptions import SecurityError
__all__ = ["read_request", "read_response"]
-MAX_HEADERS = 256
-MAX_LINE = 4110
+MAX_HEADERS = 128
+MAX_LINE = 8192
def d(value: bytes) -> str:
@@ -56,12 +56,12 @@ async def read_request(stream: asyncio.StreamReader) -> Tuple[str, Headers]:
body, it may be read from ``stream`` after this coroutine returns.
Args:
- stream: input to read the request from
+ stream: Input to read the request from.
Raises:
- EOFError: if the connection is closed without a full HTTP request
- SecurityError: if the request exceeds a security limit
- ValueError: if the request isn't well formatted
+ EOFError: If the connection is closed without a full HTTP request.
+ SecurityError: If the request exceeds a security limit.
+ ValueError: If the request isn't well formatted.
"""
# https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.1
@@ -103,12 +103,12 @@ async def read_response(stream: asyncio.StreamReader) -> Tuple[int, str, Headers
body, it may be read from ``stream`` after this coroutine returns.
Args:
- stream: input to read the response from
+ stream: Input to read the response from.
Raises:
- EOFError: if the connection is closed without a full HTTP response
- SecurityError: if the response exceeds a security limit
- ValueError: if the response isn't well formatted
+ EOFError: If the connection is closed without a full HTTP response.
+ SecurityError: If the response exceeds a security limit.
+ ValueError: If the response isn't well formatted.
"""
# https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.2
@@ -192,7 +192,7 @@ async def read_line(stream: asyncio.StreamReader) -> bytes:
"""
# Security: this is bounded by the StreamReader's limit (default = 32 KiB).
line = await stream.readline()
- # Security: this guarantees header values are small (hard-coded = 4 KiB)
+ # Security: this guarantees header values are small (hard-coded = 8 KiB)
if len(line) > MAX_LINE:
raise SecurityError("line too long")
# Not mandatory but safe - https://www.rfc-editor.org/rfc/rfc7230.html#section-3.5
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py
index 3f734fe760..19cee0e652 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py
@@ -7,6 +7,8 @@ import logging
import random
import ssl
import struct
+import sys
+import time
import uuid
import warnings
from typing import (
@@ -14,17 +16,18 @@ from typing import (
AsyncIterable,
AsyncIterator,
Awaitable,
+ Callable,
Deque,
Dict,
Iterable,
List,
Mapping,
Optional,
+ Tuple,
Union,
cast,
)
-from ..connection import State
from ..datastructures import Headers
from ..exceptions import (
ConnectionClosed,
@@ -44,12 +47,14 @@ from ..frames import (
OP_PONG,
OP_TEXT,
Close,
+ CloseCode,
Opcode,
prepare_ctrl,
prepare_data,
)
+from ..protocol import State
from ..typing import Data, LoggerLike, Subprotocol
-from .compatibility import loop_if_py_lt_38
+from .compatibility import asyncio_timeout
from .framing import Frame
@@ -76,38 +81,38 @@ class WebSocketCommonProtocol(asyncio.Protocol):
simplicity.
Once the connection is open, a Ping_ frame is sent every ``ping_interval``
- seconds. This serves as a keepalive. It helps keeping the connection
- open, especially in the presence of proxies with short timeouts on
- inactive connections. Set ``ping_interval`` to :obj:`None` to disable
- this behavior.
+ seconds. This serves as a keepalive. It helps keeping the connection open,
+ especially in the presence of proxies with short timeouts on inactive
+ connections. Set ``ping_interval`` to :obj:`None` to disable this behavior.
.. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2
If the corresponding Pong_ frame isn't received within ``ping_timeout``
- seconds, the connection is considered unusable and is closed with code
- 1011. This ensures that the remote endpoint remains responsive. Set
+ seconds, the connection is considered unusable and is closed with code 1011.
+ This ensures that the remote endpoint remains responsive. Set
``ping_timeout`` to :obj:`None` to disable this behavior.
.. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3
+ See the discussion of :doc:`timeouts <../../topics/timeouts>` for details.
+
The ``close_timeout`` parameter defines a maximum wait time for completing
the closing handshake and terminating the TCP connection. For legacy
reasons, :meth:`close` completes in at most ``5 * close_timeout`` seconds
for clients and ``4 * close_timeout`` for servers.
- See the discussion of :doc:`timeouts <../topics/timeouts>` for details.
-
- ``close_timeout`` needs to be a parameter of the protocol because
- websockets usually calls :meth:`close` implicitly upon exit:
+ ``close_timeout`` is a parameter of the protocol because websockets usually
+ calls :meth:`close` implicitly upon exit:
- * on the client side, when :func:`~websockets.client.connect` is used as a
+ * on the client side, when using :func:`~websockets.client.connect` as a
context manager;
- * on the server side, when the connection handler terminates;
+ * on the server side, when the connection handler terminates.
- To apply a timeout to any other API, wrap it in :func:`~asyncio.wait_for`.
+ To apply a timeout to any other API, wrap it in :func:`~asyncio.timeout` or
+ :func:`~asyncio.wait_for`.
The ``max_size`` parameter enforces the maximum size for incoming messages
- in bytes. The default value is 1 MiB. If a larger message is received,
+ in bytes. The default value is 1 MiB. If a larger message is received,
:meth:`recv` will raise :exc:`~websockets.exceptions.ConnectionClosedError`
and the connection will be closed with code 1009.
@@ -124,38 +129,38 @@ class WebSocketCommonProtocol(asyncio.Protocol):
Since Python can use up to 4 bytes of memory to represent a single
character, each connection may use up to ``4 * max_size * max_queue``
- bytes of memory to store incoming messages. By default, this is 128 MiB.
+ bytes of memory to store incoming messages. By default, this is 128 MiB.
You may want to lower the limits, depending on your application's
requirements.
The ``read_limit`` argument sets the high-water limit of the buffer for
incoming bytes. The low-water limit is half the high-water limit. The
- default value is 64 KiB, half of asyncio's default (based on the current
+ default value is 64 KiB, half of asyncio's default (based on the current
implementation of :class:`~asyncio.StreamReader`).
The ``write_limit`` argument sets the high-water limit of the buffer for
outgoing bytes. The low-water limit is a quarter of the high-water limit.
- The default value is 64 KiB, equal to asyncio's default (based on the
+ The default value is 64 KiB, equal to asyncio's default (based on the
current implementation of ``FlowControlMixin``).
- See the discussion of :doc:`memory usage <../topics/memory>` for details.
+ See the discussion of :doc:`memory usage <../../topics/memory>` for details.
Args:
- logger: logger for this connection;
- defaults to ``logging.getLogger("websockets.protocol")``;
- see the :doc:`logging guide <../topics/logging>` for details.
- ping_interval: delay between keepalive pings in seconds;
- :obj:`None` to disable keepalive pings.
- ping_timeout: timeout for keepalive pings in seconds;
- :obj:`None` to disable timeouts.
- close_timeout: timeout for closing the connection in seconds;
- for legacy reasons, the actual timeout is 4 or 5 times larger.
- max_size: maximum size of incoming messages in bytes;
- :obj:`None` to disable the limit.
- max_queue: maximum number of incoming messages in receive buffer;
- :obj:`None` to disable the limit.
- read_limit: high-water mark of read buffer in bytes.
- write_limit: high-water mark of write buffer in bytes.
+ logger: Logger for this server.
+ It defaults to ``logging.getLogger("websockets.protocol")``.
+ See the :doc:`logging guide <../../topics/logging>` for details.
+ ping_interval: Delay between keepalive pings in seconds.
+ :obj:`None` disables keepalive pings.
+ ping_timeout: Timeout for keepalive pings in seconds.
+ :obj:`None` disables timeouts.
+ close_timeout: Timeout for closing the connection in seconds.
+ For legacy reasons, the actual timeout is 4 or 5 times larger.
+ max_size: Maximum size of incoming messages in bytes.
+ :obj:`None` disables the limit.
+ max_queue: Maximum number of incoming messages in receive buffer.
+ :obj:`None` disables the limit.
+ read_limit: High-water mark of read buffer in bytes.
+ write_limit: High-water mark of write buffer in bytes.
"""
@@ -217,8 +222,6 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# Logger or LoggerAdapter for this connection.
if logger is None:
logger = logging.getLogger("websockets.protocol")
- # https://github.com/python/typeshed/issues/5561
- logger = cast(logging.Logger, logger)
self.logger: LoggerLike = logging.LoggerAdapter(logger, {"websocket": self})
"""Logger for this connection."""
@@ -242,7 +245,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
self._paused = False
self._drain_waiter: Optional[asyncio.Future[None]] = None
- self._drain_lock = asyncio.Lock(**loop_if_py_lt_38(loop))
+ self._drain_lock = asyncio.Lock()
# This class implements the data transfer and closing handshake, which
# are shared between the client-side and the server-side.
@@ -285,7 +288,19 @@ class WebSocketCommonProtocol(asyncio.Protocol):
self._fragmented_message_waiter: Optional[asyncio.Future[None]] = None
# Mapping of ping IDs to pong waiters, in chronological order.
- self.pings: Dict[bytes, asyncio.Future[None]] = {}
+ self.pings: Dict[bytes, Tuple[asyncio.Future[float], float]] = {}
+
+ self.latency: float = 0
+ """
+ Latency of the connection, in seconds.
+
+ This value is updated after sending a ping frame and receiving a
+ matching pong frame. Before the first ping, :attr:`latency` is ``0``.
+
+ By default, websockets enables a :ref:`keepalive <keepalive>` mechanism
+ that sends ping frames automatically at regular intervals. You can also
+ send ping frames and measure latency with :meth:`ping`.
+ """
# Task running the data transfer.
self.transfer_data_task: asyncio.Task[None]
@@ -325,7 +340,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# write(...); yield from drain()
# in a loop would never call connection_lost(), so it
# would not see an error when the socket is closed.
- await asyncio.sleep(0, **loop_if_py_lt_38(self.loop))
+ await asyncio.sleep(0)
await self._drain_helper()
def connection_open(self) -> None:
@@ -445,7 +460,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
if self.state is not State.CLOSED:
return None
elif self.close_rcvd is None:
- return 1006
+ return CloseCode.ABNORMAL_CLOSURE
else:
return self.close_rcvd.code
@@ -471,10 +486,11 @@ class WebSocketCommonProtocol(asyncio.Protocol):
"""
Iterate on incoming messages.
- The iterator exits normally when the connection is closed with the
- close code 1000 (OK) or 1001(going away). It raises
- a :exc:`~websockets.exceptions.ConnectionClosedError` exception when
- the connection is closed with any other code.
+ The iterator exits normally when the connection is closed with the close
+ code 1000 (OK) or 1001 (going away) or without a close code.
+
+ It raises a :exc:`~websockets.exceptions.ConnectionClosedError`
+ exception when the connection is closed with any other code.
"""
try:
@@ -488,8 +504,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
Receive the next message.
When the connection is closed, :meth:`recv` raises
- :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it
- raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal
+ :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises
+ :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal
connection closure and
:exc:`~websockets.exceptions.ConnectionClosedError` after a protocol
error or a network failure. This is how you detect the end of the
@@ -498,8 +514,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
Canceling :meth:`recv` is safe. There's no risk of losing the next
message. The next invocation of :meth:`recv` will return it.
- This makes it possible to enforce a timeout by wrapping :meth:`recv`
- in :func:`~asyncio.wait_for`.
+ This makes it possible to enforce a timeout by wrapping :meth:`recv` in
+ :func:`~asyncio.timeout` or :func:`~asyncio.wait_for`.
Returns:
Data: A string (:class:`str`) for a Text_ frame. A bytestring
@@ -509,8 +525,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
.. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
Raises:
- ConnectionClosed: when the connection is closed.
- RuntimeError: if two coroutines call :meth:`recv` concurrently.
+ ConnectionClosed: When the connection is closed.
+ RuntimeError: If two coroutines call :meth:`recv` concurrently.
"""
if self._pop_message_waiter is not None:
@@ -536,7 +552,6 @@ class WebSocketCommonProtocol(asyncio.Protocol):
await asyncio.wait(
[pop_message_waiter, self.transfer_data_task],
return_when=asyncio.FIRST_COMPLETED,
- **loop_if_py_lt_38(self.loop),
)
finally:
self._pop_message_waiter = None
@@ -613,8 +628,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
to send.
Raises:
- ConnectionClosed: when the connection is closed.
- TypeError: if ``message`` doesn't have a supported type.
+ ConnectionClosed: When the connection is closed.
+ TypeError: If ``message`` doesn't have a supported type.
"""
await self.ensure_open()
@@ -639,16 +654,15 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# Fragmented message -- regular iterator.
elif isinstance(message, Iterable):
-
# Work around https://github.com/python/mypy/issues/6227
message = cast(Iterable[Data], message)
iter_message = iter(message)
try:
- message_chunk = next(iter_message)
+ fragment = next(iter_message)
except StopIteration:
return
- opcode, data = prepare_data(message_chunk)
+ opcode, data = prepare_data(fragment)
self._fragmented_message_waiter = asyncio.Future()
try:
@@ -656,8 +670,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
await self.write_frame(False, opcode, data)
# Other fragments.
- for message_chunk in iter_message:
- confirm_opcode, data = prepare_data(message_chunk)
+ for fragment in iter_message:
+ confirm_opcode, data = prepare_data(fragment)
if confirm_opcode != opcode:
raise TypeError("data contains inconsistent types")
await self.write_frame(False, OP_CONT, data)
@@ -668,7 +682,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
except (Exception, asyncio.CancelledError):
# We're half-way through a fragmented message and we can't
# complete it. This makes the connection unusable.
- self.fail_connection(1011)
+ self.fail_connection(CloseCode.INTERNAL_ERROR)
raise
finally:
@@ -678,18 +692,22 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# Fragmented message -- asynchronous iterator
elif isinstance(message, AsyncIterable):
- # aiter_message = aiter(message) without aiter
- # https://github.com/python/mypy/issues/5738
- aiter_message = type(message).__aiter__(message) # type: ignore
+ # Implement aiter_message = aiter(message) without aiter
+ # Work around https://github.com/python/mypy/issues/5738
+ aiter_message = cast(
+ Callable[[AsyncIterable[Data]], AsyncIterator[Data]],
+ type(message).__aiter__,
+ )(message)
try:
- # message_chunk = anext(aiter_message) without anext
- # https://github.com/python/mypy/issues/5738
- message_chunk = await type(aiter_message).__anext__( # type: ignore
- aiter_message
- )
+ # Implement fragment = anext(aiter_message) without anext
+ # Work around https://github.com/python/mypy/issues/5738
+ fragment = await cast(
+ Callable[[AsyncIterator[Data]], Awaitable[Data]],
+ type(aiter_message).__anext__,
+ )(aiter_message)
except StopAsyncIteration:
return
- opcode, data = prepare_data(message_chunk)
+ opcode, data = prepare_data(fragment)
self._fragmented_message_waiter = asyncio.Future()
try:
@@ -697,11 +715,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
await self.write_frame(False, opcode, data)
# Other fragments.
- # https://github.com/python/mypy/issues/5738
- # coverage reports this code as not covered, but it is
- # exercised by tests - changing it breaks the tests!
- async for message_chunk in aiter_message: # type: ignore # pragma: no cover # noqa
- confirm_opcode, data = prepare_data(message_chunk)
+ async for fragment in aiter_message:
+ confirm_opcode, data = prepare_data(fragment)
if confirm_opcode != opcode:
raise TypeError("data contains inconsistent types")
await self.write_frame(False, OP_CONT, data)
@@ -712,7 +727,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
except (Exception, asyncio.CancelledError):
# We're half-way through a fragmented message and we can't
# complete it. This makes the connection unusable.
- self.fail_connection(1011)
+ self.fail_connection(CloseCode.INTERNAL_ERROR)
raise
finally:
@@ -722,7 +737,11 @@ class WebSocketCommonProtocol(asyncio.Protocol):
else:
raise TypeError("data must be str, bytes-like, or iterable")
- async def close(self, code: int = 1000, reason: str = "") -> None:
+ async def close(
+ self,
+ code: int = CloseCode.NORMAL_CLOSURE,
+ reason: str = "",
+ ) -> None:
"""
Perform the closing handshake.
@@ -747,19 +766,16 @@ class WebSocketCommonProtocol(asyncio.Protocol):
"""
try:
- await asyncio.wait_for(
- self.write_close_frame(Close(code, reason)),
- self.close_timeout,
- **loop_if_py_lt_38(self.loop),
- )
+ async with asyncio_timeout(self.close_timeout):
+ await self.write_close_frame(Close(code, reason))
except asyncio.TimeoutError:
# If the close frame cannot be sent because the send buffers
# are full, the closing handshake won't complete anyway.
# Fail the connection to shut down faster.
self.fail_connection()
- # If no close frame is received within the timeout, wait_for() cancels
- # the data transfer task and raises TimeoutError.
+ # If no close frame is received within the timeout, asyncio_timeout()
+ # cancels the data transfer task and raises TimeoutError.
# If close() is called multiple times concurrently and one of these
# calls hits the timeout, the data transfer task will be canceled.
@@ -768,11 +784,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
try:
# If close() is canceled during the wait, self.transfer_data_task
# is canceled before the timeout elapses.
- await asyncio.wait_for(
- self.transfer_data_task,
- self.close_timeout,
- **loop_if_py_lt_38(self.loop),
- )
+ async with asyncio_timeout(self.close_timeout):
+ await self.transfer_data_task
except (asyncio.TimeoutError, asyncio.CancelledError):
pass
@@ -798,8 +811,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
.. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2
- A ping may serve as a keepalive or as a check that the remote endpoint
- received all messages up to this point
+ A ping may serve as a keepalive, as a check that the remote endpoint
+ received all messages up to this point, or to measure :attr:`latency`.
Canceling :meth:`ping` is discouraged. If :meth:`ping` doesn't return
immediately, it means the write buffer is full. If you don't want to
@@ -814,18 +827,20 @@ class WebSocketCommonProtocol(asyncio.Protocol):
containing four random bytes.
Returns:
- ~asyncio.Future: A future that will be completed when the
- corresponding pong is received. You can ignore it if you
- don't intend to wait.
+ ~asyncio.Future[float]: A future that will be completed when the
+ corresponding pong is received. You can ignore it if you don't
+ intend to wait. The result of the future is the latency of the
+ connection in seconds.
::
pong_waiter = await ws.ping()
- await pong_waiter # only if you want to wait for the pong
+ # only if you want to wait for the corresponding pong
+ latency = await pong_waiter
Raises:
- ConnectionClosed: when the connection is closed.
- RuntimeError: if another ping was sent with the same data and
+ ConnectionClosed: When the connection is closed.
+ RuntimeError: If another ping was sent with the same data and
the corresponding pong wasn't received yet.
"""
@@ -842,11 +857,14 @@ class WebSocketCommonProtocol(asyncio.Protocol):
while data is None or data in self.pings:
data = struct.pack("!I", random.getrandbits(32))
- self.pings[data] = self.loop.create_future()
+ pong_waiter = self.loop.create_future()
+ # Resolution of time.monotonic() may be too low on Windows.
+ ping_timestamp = time.perf_counter()
+ self.pings[data] = (pong_waiter, ping_timestamp)
await self.write_frame(True, OP_PING, data)
- return asyncio.shield(self.pings[data])
+ return asyncio.shield(pong_waiter)
async def pong(self, data: Data = b"") -> None:
"""
@@ -861,11 +879,11 @@ class WebSocketCommonProtocol(asyncio.Protocol):
wait, you should close the connection.
Args:
- data (Data): payload of the pong; a string will be encoded to
+ data (Data): Payload of the pong. A string will be encoded to
UTF-8.
Raises:
- ConnectionClosed: when the connection is closed.
+ ConnectionClosed: When the connection is closed.
"""
await self.ensure_open()
@@ -973,7 +991,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
except ProtocolError as exc:
self.transfer_data_exc = exc
- self.fail_connection(1002)
+ self.fail_connection(CloseCode.PROTOCOL_ERROR)
except (ConnectionError, TimeoutError, EOFError, ssl.SSLError) as exc:
# Reading data with self.reader.readexactly may raise:
@@ -984,15 +1002,15 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# bytes are available than requested;
# - ssl.SSLError if the other side infringes the TLS protocol.
self.transfer_data_exc = exc
- self.fail_connection(1006)
+ self.fail_connection(CloseCode.ABNORMAL_CLOSURE)
except UnicodeDecodeError as exc:
self.transfer_data_exc = exc
- self.fail_connection(1007)
+ self.fail_connection(CloseCode.INVALID_DATA)
except PayloadTooBig as exc:
self.transfer_data_exc = exc
- self.fail_connection(1009)
+ self.fail_connection(CloseCode.MESSAGE_TOO_BIG)
except Exception as exc:
# This shouldn't happen often because exceptions expected under
@@ -1001,7 +1019,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
self.logger.error("data transfer failed", exc_info=True)
self.transfer_data_exc = exc
- self.fail_connection(1011)
+ self.fail_connection(CloseCode.INTERNAL_ERROR)
async def read_message(self) -> Optional[Data]:
"""
@@ -1030,7 +1048,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
return frame.data.decode("utf-8") if text else frame.data
# 5.4. Fragmentation
- chunks: List[Data] = []
+ fragments: List[Data] = []
max_size = self.max_size
if text:
decoder_factory = codecs.getincrementaldecoder("utf-8")
@@ -1038,14 +1056,14 @@ class WebSocketCommonProtocol(asyncio.Protocol):
if max_size is None:
def append(frame: Frame) -> None:
- nonlocal chunks
- chunks.append(decoder.decode(frame.data, frame.fin))
+ nonlocal fragments
+ fragments.append(decoder.decode(frame.data, frame.fin))
else:
def append(frame: Frame) -> None:
- nonlocal chunks, max_size
- chunks.append(decoder.decode(frame.data, frame.fin))
+ nonlocal fragments, max_size
+ fragments.append(decoder.decode(frame.data, frame.fin))
assert isinstance(max_size, int)
max_size -= len(frame.data)
@@ -1053,14 +1071,14 @@ class WebSocketCommonProtocol(asyncio.Protocol):
if max_size is None:
def append(frame: Frame) -> None:
- nonlocal chunks
- chunks.append(frame.data)
+ nonlocal fragments
+ fragments.append(frame.data)
else:
def append(frame: Frame) -> None:
- nonlocal chunks, max_size
- chunks.append(frame.data)
+ nonlocal fragments, max_size
+ fragments.append(frame.data)
assert isinstance(max_size, int)
max_size -= len(frame.data)
@@ -1074,7 +1092,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
raise ProtocolError("unexpected opcode")
append(frame)
- return ("" if text else b"").join(chunks)
+ return ("" if text else b"").join(fragments)
async def read_data_frame(self, max_size: Optional[int]) -> Optional[Frame]:
"""
@@ -1099,7 +1117,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
try:
# Echo the original data instead of re-serializing it with
# Close.serialize() because that fails when the close frame
- # is empty and Close.parse() synthetizes a 1005 close code.
+ # is empty and Close.parse() synthesizes a 1005 close code.
await self.write_close_frame(self.close_rcvd, frame.data)
except ConnectionClosed:
# Connection closed before we could echo the close frame.
@@ -1117,18 +1135,20 @@ class WebSocketCommonProtocol(asyncio.Protocol):
elif frame.opcode == OP_PONG:
if frame.data in self.pings:
+ pong_timestamp = time.perf_counter()
# Sending a pong for only the most recent ping is legal.
# Acknowledge all previous pings too in that case.
ping_id = None
ping_ids = []
- for ping_id, ping in self.pings.items():
+ for ping_id, (pong_waiter, ping_timestamp) in self.pings.items():
ping_ids.append(ping_id)
- if not ping.done():
- ping.set_result(None)
+ if not pong_waiter.done():
+ pong_waiter.set_result(pong_timestamp - ping_timestamp)
if ping_id == frame.data:
+ self.latency = pong_timestamp - ping_timestamp
break
- else: # pragma: no cover
- assert False, "ping_id is in self.pings"
+ else:
+ raise AssertionError("solicited pong not found in pings")
# Remove acknowledged pings from self.pings.
for ping_id in ping_ids:
del self.pings[ping_id]
@@ -1231,10 +1251,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
try:
while True:
- await asyncio.sleep(
- self.ping_interval,
- **loop_if_py_lt_38(self.loop),
- )
+ await asyncio.sleep(self.ping_interval)
# ping() raises CancelledError if the connection is closed,
# when close_connection() cancels self.keepalive_ping_task.
@@ -1247,23 +1264,18 @@ class WebSocketCommonProtocol(asyncio.Protocol):
if self.ping_timeout is not None:
try:
- await asyncio.wait_for(
- pong_waiter,
- self.ping_timeout,
- **loop_if_py_lt_38(self.loop),
- )
+ async with asyncio_timeout(self.ping_timeout):
+ await pong_waiter
self.logger.debug("% received keepalive pong")
except asyncio.TimeoutError:
if self.debug:
self.logger.debug("! timed out waiting for keepalive pong")
- self.fail_connection(1011, "keepalive ping timeout")
+ self.fail_connection(
+ CloseCode.INTERNAL_ERROR,
+ "keepalive ping timeout",
+ )
break
- # Remove this branch when dropping support for Python < 3.8
- # because CancelledError no longer inherits Exception.
- except asyncio.CancelledError:
- raise
-
except ConnectionClosed:
pass
@@ -1297,9 +1309,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# A client should wait for a TCP close from the server.
if self.is_client and hasattr(self, "transfer_data_task"):
if await self.wait_for_connection_lost():
- # Coverage marks this line as a partially executed branch.
- # I supect a bug in coverage. Ignore it for now.
- return # pragma: no cover
+ return
if self.debug:
self.logger.debug("! timed out waiting for TCP close")
@@ -1317,9 +1327,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
pass
if await self.wait_for_connection_lost():
- # Coverage marks this line as a partially executed branch.
- # I supect a bug in coverage. Ignore it for now.
- return # pragma: no cover
+ return
if self.debug:
self.logger.debug("! timed out waiting for TCP close")
@@ -1352,12 +1360,11 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# Abort the TCP connection. Buffers are discarded.
if self.debug:
self.logger.debug("x aborting TCP connection")
- self.transport.abort()
+ # Due to a bug in coverage, this is erroneously reported as not covered.
+ self.transport.abort() # pragma: no cover
# connection_lost() is called quickly after aborting.
- # Coverage marks this line as a partially executed branch.
- # I supect a bug in coverage. Ignore it for now.
- await self.wait_for_connection_lost() # pragma: no cover
+ await self.wait_for_connection_lost()
async def wait_for_connection_lost(self) -> bool:
"""
@@ -1369,11 +1376,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
"""
if not self.connection_lost_waiter.done():
try:
- await asyncio.wait_for(
- asyncio.shield(self.connection_lost_waiter),
- self.close_timeout,
- **loop_if_py_lt_38(self.loop),
- )
+ async with asyncio_timeout(self.close_timeout):
+ await asyncio.shield(self.connection_lost_waiter)
except asyncio.TimeoutError:
pass
# Re-check self.connection_lost_waiter.done() synchronously because
@@ -1381,7 +1385,11 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# and the moment this coroutine resumes running.
return self.connection_lost_waiter.done()
- def fail_connection(self, code: int = 1006, reason: str = "") -> None:
+ def fail_connection(
+ self,
+ code: int = CloseCode.ABNORMAL_CLOSURE,
+ reason: str = "",
+ ) -> None:
"""
7.1.7. Fail the WebSocket Connection
@@ -1412,7 +1420,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# sent if it's CLOSING), except when failing the connection because of
# an error reading from or writing to the network.
# Don't send a close frame if the connection is broken.
- if code != 1006 and self.state is State.OPEN:
+ if code != CloseCode.ABNORMAL_CLOSURE and self.state is State.OPEN:
close = Close(code, reason)
# Write the close frame without draining the write buffer.
@@ -1449,13 +1457,13 @@ class WebSocketCommonProtocol(asyncio.Protocol):
assert self.state is State.CLOSED
exc = self.connection_closed_exc()
- for ping in self.pings.values():
- ping.set_exception(exc)
+ for pong_waiter, _ping_timestamp in self.pings.values():
+ pong_waiter.set_exception(exc)
# If the exception is never retrieved, it will be logged when ping
# is garbage-collected. This is confusing for users.
# Given that ping is done (with an exception), canceling it does
# nothing, but it prevents logging the exception.
- ping.cancel()
+ pong_waiter.cancel()
# asyncio.Protocol methods
@@ -1496,7 +1504,6 @@ class WebSocketCommonProtocol(asyncio.Protocol):
self.connection_lost_waiter.set_result(None)
if True: # pragma: no cover
-
# Copied from asyncio.StreamReaderProtocol
if self.reader is not None:
if exc is None:
@@ -1552,13 +1559,17 @@ class WebSocketCommonProtocol(asyncio.Protocol):
self.reader.feed_eof()
-def broadcast(websockets: Iterable[WebSocketCommonProtocol], message: Data) -> None:
+def broadcast(
+ websockets: Iterable[WebSocketCommonProtocol],
+ message: Data,
+ raise_exceptions: bool = False,
+) -> None:
"""
Broadcast a message to several WebSocket connections.
- A string (:class:`str`) is sent as a Text_ frame. A bytestring or
- bytes-like object (:class:`bytes`, :class:`bytearray`, or
- :class:`memoryview`) is sent as a Binary_ frame.
+ A string (:class:`str`) is sent as a Text_ frame. A bytestring or bytes-like
+ object (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) is sent
+ as a Binary_ frame.
.. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
.. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
@@ -1566,33 +1577,42 @@ def broadcast(websockets: Iterable[WebSocketCommonProtocol], message: Data) -> N
:func:`broadcast` pushes the message synchronously to all connections even
if their write buffers are overflowing. There's no backpressure.
- :func:`broadcast` skips silently connections that aren't open in order to
- avoid errors on connections where the closing handshake is in progress.
-
- If you broadcast messages faster than a connection can handle them,
- messages will pile up in its write buffer until the connection times out.
- Keep low values for ``ping_interval`` and ``ping_timeout`` to prevent
- excessive memory usage by slow connections when you use :func:`broadcast`.
+ If you broadcast messages faster than a connection can handle them, messages
+ will pile up in its write buffer until the connection times out. Keep
+ ``ping_interval`` and ``ping_timeout`` low to prevent excessive memory usage
+ from slow connections.
Unlike :meth:`~websockets.server.WebSocketServerProtocol.send`,
:func:`broadcast` doesn't support sending fragmented messages. Indeed,
- fragmentation is useful for sending large messages without buffering
- them in memory, while :func:`broadcast` buffers one copy per connection
- as fast as possible.
+ fragmentation is useful for sending large messages without buffering them in
+ memory, while :func:`broadcast` buffers one copy per connection as fast as
+ possible.
+
+ :func:`broadcast` skips connections that aren't open in order to avoid
+ errors on connections where the closing handshake is in progress.
+
+ :func:`broadcast` ignores failures to write the message on some connections.
+ It continues writing to other connections. On Python 3.11 and above, you
+ may set ``raise_exceptions`` to :obj:`True` to record failures and raise all
+ exceptions in a :pep:`654` :exc:`ExceptionGroup`.
Args:
- websockets (Iterable[WebSocketCommonProtocol]): WebSocket connections
- to which the message will be sent.
- message (Data): message to send.
+ websockets: WebSocket connections to which the message will be sent.
+ message: Message to send.
+ raise_exceptions: Whether to raise an exception in case of failures.
Raises:
- RuntimeError: if a connection is busy sending a fragmented message.
- TypeError: if ``message`` doesn't have a supported type.
+ TypeError: If ``message`` doesn't have a supported type.
"""
if not isinstance(message, (str, bytes, bytearray, memoryview)):
raise TypeError("data must be str or bytes-like")
+ if raise_exceptions:
+ if sys.version_info[:2] < (3, 11): # pragma: no cover
+ raise ValueError("raise_exceptions requires at least Python 3.11")
+ exceptions = []
+
opcode, data = prepare_data(message)
for websocket in websockets:
@@ -1600,6 +1620,26 @@ def broadcast(websockets: Iterable[WebSocketCommonProtocol], message: Data) -> N
continue
if websocket._fragmented_message_waiter is not None:
- raise RuntimeError("busy sending a fragmented message")
+ if raise_exceptions:
+ exception = RuntimeError("sending a fragmented message")
+ exceptions.append(exception)
+ else:
+ websocket.logger.warning(
+ "skipped broadcast: sending a fragmented message",
+ )
+
+ try:
+ websocket.write_frame_sync(True, opcode, data)
+ except Exception as write_exception:
+ if raise_exceptions:
+ exception = RuntimeError("failed to write message")
+ exception.__cause__ = write_exception
+ exceptions.append(exception)
+ else:
+ websocket.logger.warning(
+ "skipped broadcast: failed to write message",
+ exc_info=True,
+ )
- websocket.write_frame_sync(True, opcode, data)
+ if raise_exceptions:
+ raise ExceptionGroup("skipped broadcast", exceptions)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/server.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/server.py
index 3e51db1b71..7c24dd74af 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/server.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/server.py
@@ -25,7 +25,6 @@ from typing import (
cast,
)
-from ..connection import State
from ..datastructures import Headers, HeadersLike, MultipleValuesError
from ..exceptions import (
AbortHandshake,
@@ -45,8 +44,9 @@ from ..headers import (
validate_subprotocols,
)
from ..http import USER_AGENT
-from ..typing import ExtensionHeader, LoggerLike, Origin, Subprotocol
-from .compatibility import loop_if_py_lt_38
+from ..protocol import State
+from ..typing import ExtensionHeader, LoggerLike, Origin, StatusLike, Subprotocol
+from .compatibility import asyncio_timeout
from .handshake import build_response, check_request
from .http import read_request
from .protocol import WebSocketCommonProtocol
@@ -57,7 +57,7 @@ __all__ = ["serve", "unix_serve", "WebSocketServerProtocol", "WebSocketServer"]
HeadersLikeOrCallable = Union[HeadersLike, Callable[[str, Headers], HeadersLike]]
-HTTPResponse = Tuple[http.HTTPStatus, HeadersLike, bytes]
+HTTPResponse = Tuple[StatusLike, HeadersLike, bytes]
class WebSocketServerProtocol(WebSocketCommonProtocol):
@@ -73,7 +73,7 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
await process(message)
The iterator exits normally when the connection is closed with close code
- 1000 (OK) or 1001 (going away). It raises
+ 1000 (OK) or 1001 (going away) or without a close code. It raises
a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection
is closed with any other code.
@@ -84,7 +84,7 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
ws_server: WebSocket server that created this connection.
See :func:`serve` for the documentation of ``ws_handler``, ``logger``, ``origins``,
- ``extensions``, ``subprotocols``, and ``extra_headers``.
+ ``extensions``, ``subprotocols``, ``extra_headers``, and ``server_header``.
See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the
documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``,
@@ -108,12 +108,14 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
extensions: Optional[Sequence[ServerExtensionFactory]] = None,
subprotocols: Optional[Sequence[Subprotocol]] = None,
extra_headers: Optional[HeadersLikeOrCallable] = None,
+ server_header: Optional[str] = USER_AGENT,
process_request: Optional[
Callable[[str, Headers], Awaitable[Optional[HTTPResponse]]]
] = None,
select_subprotocol: Optional[
Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol]
] = None,
+ open_timeout: Optional[float] = 10,
**kwargs: Any,
) -> None:
if logger is None:
@@ -132,8 +134,10 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
self.available_extensions = extensions
self.available_subprotocols = subprotocols
self.extra_headers = extra_headers
+ self.server_header = server_header
self._process_request = process_request
self._select_subprotocol = select_subprotocol
+ self.open_timeout = open_timeout
def connection_made(self, transport: asyncio.BaseTransport) -> None:
"""
@@ -153,22 +157,20 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
Handle the lifecycle of a WebSocket connection.
Since this method doesn't have a caller able to handle exceptions, it
- attemps to log relevant ones and guarantees that the TCP connection is
+ attempts to log relevant ones and guarantees that the TCP connection is
closed before exiting.
"""
try:
-
try:
- await self.handshake(
- origins=self.origins,
- available_extensions=self.available_extensions,
- available_subprotocols=self.available_subprotocols,
- extra_headers=self.extra_headers,
- )
- # Remove this branch when dropping support for Python < 3.8
- # because CancelledError no longer inherits Exception.
- except asyncio.CancelledError: # pragma: no cover
+ async with asyncio_timeout(self.open_timeout):
+ await self.handshake(
+ origins=self.origins,
+ available_extensions=self.available_extensions,
+ available_subprotocols=self.available_subprotocols,
+ extra_headers=self.extra_headers,
+ )
+ except asyncio.TimeoutError: # pragma: no cover
raise
except ConnectionError:
raise
@@ -216,14 +218,16 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
)
headers.setdefault("Date", email.utils.formatdate(usegmt=True))
- headers.setdefault("Server", USER_AGENT)
+ if self.server_header is not None:
+ headers.setdefault("Server", self.server_header)
+
headers.setdefault("Content-Length", str(len(body)))
headers.setdefault("Content-Type", "text/plain")
headers.setdefault("Connection", "close")
self.write_http_response(status, headers, body)
self.logger.info(
- "connection failed (%d %s)", status.value, status.phrase
+ "connection rejected (%d %s)", status.value, status.phrase
)
await self.close_transport()
return
@@ -325,9 +329,9 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
You may override this method in a :class:`WebSocketServerProtocol`
subclass, for example:
- * to return a HTTP 200 OK response on a given path; then a load
+ * to return an HTTP 200 OK response on a given path; then a load
balancer can use this path for a health check;
- * to authenticate the request and return a HTTP 401 Unauthorized or a
+ * to authenticate the request and return an HTTP 401 Unauthorized or an
HTTP 403 Forbidden when authentication fails.
You may also override this method with the ``process_request``
@@ -345,7 +349,7 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
request_headers: request headers.
Returns:
- Optional[Tuple[http.HTTPStatus, HeadersLike, bytes]]: :obj:`None`
+ Optional[Tuple[StatusLike, HeadersLike, bytes]]: :obj:`None`
to continue the WebSocket handshake normally.
An HTTP response, represented by a 3-uple of the response status,
@@ -439,15 +443,12 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
header_values = headers.get_all("Sec-WebSocket-Extensions")
if header_values and available_extensions:
-
parsed_header_values: List[ExtensionHeader] = sum(
[parse_extension(header_value) for header_value in header_values], []
)
for name, request_params in parsed_header_values:
-
for ext_factory in available_extensions:
-
# Skip non-matching extensions based on their name.
if ext_factory.name != name:
continue
@@ -499,7 +500,6 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
header_values = headers.get_all("Sec-WebSocket-Protocol")
if header_values and available_subprotocols:
-
parsed_header_values: List[Subprotocol] = sum(
[parse_subprotocol(header_value) for header_value in header_values], []
)
@@ -516,31 +516,29 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
server_subprotocols: Sequence[Subprotocol],
) -> Optional[Subprotocol]:
"""
- Pick a subprotocol among those offered by the client.
+ Pick a subprotocol among those supported by the client and the server.
- If several subprotocols are supported by the client and the server,
- the default implementation selects the preferred subprotocol by
- giving equal value to the priorities of the client and the server.
- If no subprotocol is supported by the client and the server, it
- proceeds without a subprotocol.
+ If several subprotocols are available, select the preferred subprotocol
+ by giving equal weight to the preferences of the client and the server.
- This is unlikely to be the most useful implementation in practice.
- Many servers providing a subprotocol will require that the client
- uses that subprotocol. Such rules can be implemented in a subclass.
+ If no subprotocol is available, proceed without a subprotocol.
- You may also override this method with the ``select_subprotocol``
- argument of :func:`serve` and :class:`WebSocketServerProtocol`.
+ You may provide a ``select_subprotocol`` argument to :func:`serve` or
+ :class:`WebSocketServerProtocol` to override this logic. For example,
+ you could reject the handshake if the client doesn't support a
+ particular subprotocol, rather than accept the handshake without that
+ subprotocol.
Args:
client_subprotocols: list of subprotocols offered by the client.
server_subprotocols: list of subprotocols available on the server.
Returns:
- Optional[Subprotocol]: Selected subprotocol.
+ Optional[Subprotocol]: Selected subprotocol, if a common subprotocol
+ was found.
:obj:`None` to continue without a subprotocol.
-
"""
if self._select_subprotocol is not None:
return self._select_subprotocol(client_subprotocols, server_subprotocols)
@@ -548,10 +546,10 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
subprotocols = set(client_subprotocols) & set(server_subprotocols)
if not subprotocols:
return None
- priority = lambda p: (
- client_subprotocols.index(p) + server_subprotocols.index(p)
- )
- return sorted(subprotocols, key=priority)[0]
+ return sorted(
+ subprotocols,
+ key=lambda p: client_subprotocols.index(p) + server_subprotocols.index(p),
+ )[0]
async def handshake(
self,
@@ -594,7 +592,8 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
# The connection may drop while process_request is running.
if self.state is State.CLOSED:
- raise self.connection_closed_exc() # pragma: no cover
+ # This subclass of ConnectionError is silently ignored in handler().
+ raise BrokenPipeError("connection closed during opening handshake")
# Change the response to a 503 error if the server is shutting down.
if not self.ws_server.is_serving():
@@ -635,7 +634,8 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
response_headers.update(extra_headers)
response_headers.setdefault("Date", email.utils.formatdate(usegmt=True))
- response_headers.setdefault("Server", USER_AGENT)
+ if self.server_header is not None:
+ response_headers.setdefault("Server", self.server_header)
self.write_http_response(http.HTTPStatus.SWITCHING_PROTOCOLS, response_headers)
@@ -658,9 +658,9 @@ class WebSocketServer:
when shutting down.
Args:
- logger: logger for this server;
- defaults to ``logging.getLogger("websockets.server")``;
- see the :doc:`logging guide <../topics/logging>` for details.
+ logger: Logger for this server.
+ It defaults to ``logging.getLogger("websockets.server")``.
+ See the :doc:`logging guide <../../topics/logging>` for details.
"""
@@ -707,7 +707,7 @@ class WebSocketServer:
self.logger.info("server listening on %s", name)
# Initialized here because we need a reference to the event loop.
- # This should be moved back to __init__ in Python 3.10.
+ # This should be moved back to __init__ when dropping Python < 3.10.
self.closed_waiter = server.get_loop().create_future()
def register(self, protocol: WebSocketServerProtocol) -> None:
@@ -724,26 +724,30 @@ class WebSocketServer:
"""
self.websockets.remove(protocol)
- def close(self) -> None:
+ def close(self, close_connections: bool = True) -> None:
"""
Close the server.
- This method:
+ * Close the underlying :class:`~asyncio.Server`.
+ * When ``close_connections`` is :obj:`True`, which is the default,
+ close existing connections. Specifically:
- * closes the underlying :class:`~asyncio.Server`;
- * rejects new WebSocket connections with an HTTP 503 (service
- unavailable) error; this happens when the server accepted the TCP
- connection but didn't complete the WebSocket opening handshake prior
- to closing;
- * closes open WebSocket connections with close code 1001 (going away).
+ * Reject opening WebSocket connections with an HTTP 503 (service
+ unavailable) error. This happens when the server accepted the TCP
+ connection but didn't complete the opening handshake before closing.
+ * Close open WebSocket connections with close code 1001 (going away).
+
+ * Wait until all connection handlers terminate.
:meth:`close` is idempotent.
"""
if self.close_task is None:
- self.close_task = self.get_loop().create_task(self._close())
+ self.close_task = self.get_loop().create_task(
+ self._close(close_connections)
+ )
- async def _close(self) -> None:
+ async def _close(self, close_connections: bool) -> None:
"""
Implementation of :meth:`close`.
@@ -757,36 +761,30 @@ class WebSocketServer:
# Stop accepting new connections.
self.server.close()
- # Wait until self.server.close() completes.
- await self.server.wait_closed()
-
# Wait until all accepted connections reach connection_made() and call
# register(). See https://bugs.python.org/issue34852 for details.
- await asyncio.sleep(0, **loop_if_py_lt_38(self.get_loop()))
-
- # Close OPEN connections with status code 1001. Since the server was
- # closed, handshake() closes OPENING connections with a HTTP 503
- # error. Wait until all connections are closed.
-
- close_tasks = [
- asyncio.create_task(websocket.close(1001))
- for websocket in self.websockets
- if websocket.state is not State.CONNECTING
- ]
- # asyncio.wait doesn't accept an empty first argument.
- if close_tasks:
- await asyncio.wait(
- close_tasks,
- **loop_if_py_lt_38(self.get_loop()),
- )
-
- # Wait until all connection handlers are complete.
+ await asyncio.sleep(0)
+
+ if close_connections:
+ # Close OPEN connections with close code 1001. After server.close(),
+ # handshake() closes OPENING connections with an HTTP 503 error.
+ close_tasks = [
+ asyncio.create_task(websocket.close(1001))
+ for websocket in self.websockets
+ if websocket.state is not State.CONNECTING
+ ]
+ # asyncio.wait doesn't accept an empty first argument.
+ if close_tasks:
+ await asyncio.wait(close_tasks)
+
+ # Wait until all TCP connections are closed.
+ await self.server.wait_closed()
+ # Wait until all connection handlers terminate.
# asyncio.wait doesn't accept an empty first argument.
if self.websockets:
await asyncio.wait(
- [websocket.handler_task for websocket in self.websockets],
- **loop_if_py_lt_38(self.get_loop()),
+ [websocket.handler_task for websocket in self.websockets]
)
# Tell wait_closed() to return.
@@ -829,19 +827,37 @@ class WebSocketServer:
"""
return self.server.is_serving()
- async def start_serving(self) -> None:
+ async def start_serving(self) -> None: # pragma: no cover
"""
See :meth:`asyncio.Server.start_serving`.
+ Typical use::
+
+ server = await serve(..., start_serving=False)
+ # perform additional setup here...
+ # ... then start the server
+ await server.start_serving()
+
"""
- await self.server.start_serving() # pragma: no cover
+ await self.server.start_serving()
- async def serve_forever(self) -> None:
+ async def serve_forever(self) -> None: # pragma: no cover
"""
See :meth:`asyncio.Server.serve_forever`.
+ Typical use::
+
+ server = await serve(...)
+ # this coroutine doesn't return
+ # canceling it stops the server
+ await server.serve_forever()
+
+ This is an alternative to using :func:`serve` as an asynchronous context
+ manager. Shutdown is triggered by canceling :meth:`serve_forever`
+ instead of exiting a :func:`serve` context.
+
"""
- await self.server.serve_forever() # pragma: no cover
+ await self.server.serve_forever()
@property
def sockets(self) -> Iterable[socket.socket]:
@@ -851,17 +867,17 @@ class WebSocketServer:
"""
return self.server.sockets
- async def __aenter__(self) -> WebSocketServer:
- return self # pragma: no cover
+ async def __aenter__(self) -> WebSocketServer: # pragma: no cover
+ return self
async def __aexit__(
self,
exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
- ) -> None:
- self.close() # pragma: no cover
- await self.wait_closed() # pragma: no cover
+ ) -> None: # pragma: no cover
+ self.close()
+ await self.wait_closed()
class Serve:
@@ -879,53 +895,61 @@ class Serve:
server performs the closing handshake and closes the connection.
Awaiting :func:`serve` yields a :class:`WebSocketServer`. This object
- provides :meth:`~WebSocketServer.close` and
- :meth:`~WebSocketServer.wait_closed` methods for shutting down the server.
+ provides a :meth:`~WebSocketServer.close` method to shut down the server::
- :func:`serve` can be used as an asynchronous context manager::
+ stop = asyncio.Future() # set this future to exit the server
+
+ server = await serve(...)
+ await stop
+ await server.close()
+
+ :func:`serve` can be used as an asynchronous context manager. Then, the
+ server is shut down automatically when exiting the context::
stop = asyncio.Future() # set this future to exit the server
async with serve(...):
await stop
- The server is shut down automatically when exiting the context.
-
Args:
- ws_handler: connection handler. It receives the WebSocket connection,
+ ws_handler: Connection handler. It receives the WebSocket connection,
which is a :class:`WebSocketServerProtocol`, in argument.
- host: network interfaces the server is bound to;
- see :meth:`~asyncio.loop.create_server` for details.
- port: TCP port the server listens on;
- see :meth:`~asyncio.loop.create_server` for details.
- create_protocol: factory for the :class:`asyncio.Protocol` managing
- the connection; defaults to :class:`WebSocketServerProtocol`; may
- be set to a wrapper or a subclass to customize connection handling.
- logger: logger for this server;
- defaults to ``logging.getLogger("websockets.server")``;
- see the :doc:`logging guide <../topics/logging>` for details.
- compression: shortcut that enables the "permessage-deflate" extension
- by default; may be set to :obj:`None` to disable compression;
- see the :doc:`compression guide <../topics/compression>` for details.
- origins: acceptable values of the ``Origin`` header; include
- :obj:`None` in the list if the lack of an origin is acceptable.
- This is useful for defending against Cross-Site WebSocket
- Hijacking attacks.
- extensions: list of supported extensions, in order in which they
- should be tried.
- subprotocols: list of supported subprotocols, in order of decreasing
+ host: Network interfaces the server binds to.
+ See :meth:`~asyncio.loop.create_server` for details.
+ port: TCP port the server listens on.
+ See :meth:`~asyncio.loop.create_server` for details.
+ create_protocol: Factory for the :class:`asyncio.Protocol` managing
+ the connection. It defaults to :class:`WebSocketServerProtocol`.
+ Set it to a wrapper or a subclass to customize connection handling.
+ logger: Logger for this server.
+ It defaults to ``logging.getLogger("websockets.server")``.
+ See the :doc:`logging guide <../../topics/logging>` for details.
+ compression: The "permessage-deflate" extension is enabled by default.
+ Set ``compression`` to :obj:`None` to disable it. See the
+ :doc:`compression guide <../../topics/compression>` for details.
+ origins: Acceptable values of the ``Origin`` header, for defending
+ against Cross-Site WebSocket Hijacking attacks. Include :obj:`None`
+ in the list if the lack of an origin is acceptable.
+ extensions: List of supported extensions, in order in which they
+ should be negotiated and run.
+ subprotocols: List of supported subprotocols, in order of decreasing
preference.
extra_headers (Union[HeadersLike, Callable[[str, Headers], HeadersLike]]):
- arbitrary HTTP headers to add to the request; this can be
+ Arbitrary HTTP headers to add to the response. This can be
a :data:`~websockets.datastructures.HeadersLike` or a callable
taking the request path and headers in arguments and returning
a :data:`~websockets.datastructures.HeadersLike`.
+ server_header: Value of the ``Server`` response header.
+ It defaults to ``"Python/x.y.z websockets/X.Y"``.
+ Setting it to :obj:`None` removes the header.
process_request (Optional[Callable[[str, Headers], \
- Awaitable[Optional[Tuple[http.HTTPStatus, HeadersLike, bytes]]]]]):
- intercept HTTP request before the opening handshake;
- see :meth:`~WebSocketServerProtocol.process_request` for details.
- select_subprotocol: select a subprotocol supported by the client;
- see :meth:`~WebSocketServerProtocol.select_subprotocol` for details.
+ Awaitable[Optional[Tuple[StatusLike, HeadersLike, bytes]]]]]):
+ Intercept HTTP request before the opening handshake.
+ See :meth:`~WebSocketServerProtocol.process_request` for details.
+ select_subprotocol: Select a subprotocol supported by the client.
+ See :meth:`~WebSocketServerProtocol.select_subprotocol` for details.
+ open_timeout: Timeout for opening connections in seconds.
+ :obj:`None` disables the timeout.
See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the
documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``,
@@ -955,19 +979,21 @@ class Serve:
host: Optional[Union[str, Sequence[str]]] = None,
port: Optional[int] = None,
*,
- create_protocol: Optional[Callable[[Any], WebSocketServerProtocol]] = None,
+ create_protocol: Optional[Callable[..., WebSocketServerProtocol]] = None,
logger: Optional[LoggerLike] = None,
compression: Optional[str] = "deflate",
origins: Optional[Sequence[Optional[Origin]]] = None,
extensions: Optional[Sequence[ServerExtensionFactory]] = None,
subprotocols: Optional[Sequence[Subprotocol]] = None,
extra_headers: Optional[HeadersLikeOrCallable] = None,
+ server_header: Optional[str] = USER_AGENT,
process_request: Optional[
Callable[[str, Headers], Awaitable[Optional[HTTPResponse]]]
] = None,
select_subprotocol: Optional[
Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol]
] = None,
+ open_timeout: Optional[float] = 10,
ping_interval: Optional[float] = 20,
ping_timeout: Optional[float] = 20,
close_timeout: Optional[float] = None,
@@ -1030,6 +1056,7 @@ class Serve:
host=host,
port=port,
secure=secure,
+ open_timeout=open_timeout,
ping_interval=ping_interval,
ping_timeout=ping_timeout,
close_timeout=close_timeout,
@@ -1043,6 +1070,7 @@ class Serve:
extensions=extensions,
subprotocols=subprotocols,
extra_headers=extra_headers,
+ server_header=server_header,
process_request=process_request,
select_subprotocol=select_subprotocol,
logger=logger,
@@ -1106,17 +1134,18 @@ def unix_serve(
**kwargs: Any,
) -> Serve:
"""
- Similar to :func:`serve`, but for listening on Unix sockets.
+ Start a WebSocket server listening on a Unix socket.
- This function builds upon the event
- loop's :meth:`~asyncio.loop.create_unix_server` method.
+ This function is identical to :func:`serve`, except the ``host`` and
+ ``port`` arguments are replaced by ``path``. It is only available on Unix.
- It is only available on Unix.
+ Unrecognized keyword arguments are passed the event loop's
+ :meth:`~asyncio.loop.create_unix_server` method.
It's useful for deploying a server behind a reverse proxy such as nginx.
Args:
- path: file system path to the Unix socket.
+ path: File system path to the Unix socket.
"""
return serve(ws_handler, path=path, unix=True, **kwargs)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py
new file mode 100644
index 0000000000..765e6b9bb4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py
@@ -0,0 +1,708 @@
+from __future__ import annotations
+
+import enum
+import logging
+import uuid
+from typing import Generator, List, Optional, Type, Union
+
+from .exceptions import (
+ ConnectionClosed,
+ ConnectionClosedError,
+ ConnectionClosedOK,
+ InvalidState,
+ PayloadTooBig,
+ ProtocolError,
+)
+from .extensions import Extension
+from .frames import (
+ OK_CLOSE_CODES,
+ OP_BINARY,
+ OP_CLOSE,
+ OP_CONT,
+ OP_PING,
+ OP_PONG,
+ OP_TEXT,
+ Close,
+ CloseCode,
+ Frame,
+)
+from .http11 import Request, Response
+from .streams import StreamReader
+from .typing import LoggerLike, Origin, Subprotocol
+
+
+__all__ = [
+ "Protocol",
+ "Side",
+ "State",
+ "SEND_EOF",
+]
+
+Event = Union[Request, Response, Frame]
+"""Events that :meth:`~Protocol.events_received` may return."""
+
+
+class Side(enum.IntEnum):
+ """A WebSocket connection is either a server or a client."""
+
+ SERVER, CLIENT = range(2)
+
+
+SERVER = Side.SERVER
+CLIENT = Side.CLIENT
+
+
+class State(enum.IntEnum):
+ """A WebSocket connection is in one of these four states."""
+
+ CONNECTING, OPEN, CLOSING, CLOSED = range(4)
+
+
+CONNECTING = State.CONNECTING
+OPEN = State.OPEN
+CLOSING = State.CLOSING
+CLOSED = State.CLOSED
+
+
+SEND_EOF = b""
+"""Sentinel signaling that the TCP connection must be half-closed."""
+
+
+class Protocol:
+ """
+ Sans-I/O implementation of a WebSocket connection.
+
+ Args:
+ side: :attr:`~Side.CLIENT` or :attr:`~Side.SERVER`.
+ state: initial state of the WebSocket connection.
+ max_size: maximum size of incoming messages in bytes;
+ :obj:`None` disables the limit.
+ logger: logger for this connection; depending on ``side``,
+ defaults to ``logging.getLogger("websockets.client")``
+ or ``logging.getLogger("websockets.server")``;
+ see the :doc:`logging guide <../../topics/logging>` for details.
+
+ """
+
+ def __init__(
+ self,
+ side: Side,
+ *,
+ state: State = OPEN,
+ max_size: Optional[int] = 2**20,
+ logger: Optional[LoggerLike] = None,
+ ) -> None:
+ # Unique identifier. For logs.
+ self.id: uuid.UUID = uuid.uuid4()
+ """Unique identifier of the connection. Useful in logs."""
+
+ # Logger or LoggerAdapter for this connection.
+ if logger is None:
+ logger = logging.getLogger(f"websockets.{side.name.lower()}")
+ self.logger: LoggerLike = logger
+ """Logger for this connection."""
+
+ # Track if DEBUG is enabled. Shortcut logging calls if it isn't.
+ self.debug = logger.isEnabledFor(logging.DEBUG)
+
+ # Connection side. CLIENT or SERVER.
+ self.side = side
+
+ # Connection state. Initially OPEN because subclasses handle CONNECTING.
+ self.state = state
+
+ # Maximum size of incoming messages in bytes.
+ self.max_size = max_size
+
+ # Current size of incoming message in bytes. Only set while reading a
+ # fragmented message i.e. a data frames with the FIN bit not set.
+ self.cur_size: Optional[int] = None
+
+ # True while sending a fragmented message i.e. a data frames with the
+ # FIN bit not set.
+ self.expect_continuation_frame = False
+
+ # WebSocket protocol parameters.
+ self.origin: Optional[Origin] = None
+ self.extensions: List[Extension] = []
+ self.subprotocol: Optional[Subprotocol] = None
+
+ # Close code and reason, set when a close frame is sent or received.
+ self.close_rcvd: Optional[Close] = None
+ self.close_sent: Optional[Close] = None
+ self.close_rcvd_then_sent: Optional[bool] = None
+
+ # Track if an exception happened during the handshake.
+ self.handshake_exc: Optional[Exception] = None
+ """
+ Exception to raise if the opening handshake failed.
+
+ :obj:`None` if the opening handshake succeeded.
+
+ """
+
+ # Track if send_eof() was called.
+ self.eof_sent = False
+
+ # Parser state.
+ self.reader = StreamReader()
+ self.events: List[Event] = []
+ self.writes: List[bytes] = []
+ self.parser = self.parse()
+ next(self.parser) # start coroutine
+ self.parser_exc: Optional[Exception] = None
+
+ @property
+ def state(self) -> State:
+ """
+ WebSocket connection state.
+
+ Defined in 4.1, 4.2, 7.1.3, and 7.1.4 of :rfc:`6455`.
+
+ """
+ return self._state
+
+ @state.setter
+ def state(self, state: State) -> None:
+ if self.debug:
+ self.logger.debug("= connection is %s", state.name)
+ self._state = state
+
+ @property
+ def close_code(self) -> Optional[int]:
+ """
+ `WebSocket close code`_.
+
+ .. _WebSocket close code:
+ https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5
+
+ :obj:`None` if the connection isn't closed yet.
+
+ """
+ if self.state is not CLOSED:
+ return None
+ elif self.close_rcvd is None:
+ return CloseCode.ABNORMAL_CLOSURE
+ else:
+ return self.close_rcvd.code
+
+ @property
+ def close_reason(self) -> Optional[str]:
+ """
+ `WebSocket close reason`_.
+
+ .. _WebSocket close reason:
+ https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6
+
+ :obj:`None` if the connection isn't closed yet.
+
+ """
+ if self.state is not CLOSED:
+ return None
+ elif self.close_rcvd is None:
+ return ""
+ else:
+ return self.close_rcvd.reason
+
+ @property
+ def close_exc(self) -> ConnectionClosed:
+ """
+ Exception to raise when trying to interact with a closed connection.
+
+ Don't raise this exception while the connection :attr:`state`
+ is :attr:`~websockets.protocol.State.CLOSING`; wait until
+ it's :attr:`~websockets.protocol.State.CLOSED`.
+
+ Indeed, the exception includes the close code and reason, which are
+ known only once the connection is closed.
+
+ Raises:
+ AssertionError: if the connection isn't closed yet.
+
+ """
+ assert self.state is CLOSED, "connection isn't closed yet"
+ exc_type: Type[ConnectionClosed]
+ if (
+ self.close_rcvd is not None
+ and self.close_sent is not None
+ and self.close_rcvd.code in OK_CLOSE_CODES
+ and self.close_sent.code in OK_CLOSE_CODES
+ ):
+ exc_type = ConnectionClosedOK
+ else:
+ exc_type = ConnectionClosedError
+ exc: ConnectionClosed = exc_type(
+ self.close_rcvd,
+ self.close_sent,
+ self.close_rcvd_then_sent,
+ )
+ # Chain to the exception raised in the parser, if any.
+ exc.__cause__ = self.parser_exc
+ return exc
+
+ # Public methods for receiving data.
+
+ def receive_data(self, data: bytes) -> None:
+ """
+ Receive data from the network.
+
+ After calling this method:
+
+ - You must call :meth:`data_to_send` and send this data to the network.
+ - You should call :meth:`events_received` and process resulting events.
+
+ Raises:
+ EOFError: if :meth:`receive_eof` was called earlier.
+
+ """
+ self.reader.feed_data(data)
+ next(self.parser)
+
+ def receive_eof(self) -> None:
+ """
+ Receive the end of the data stream from the network.
+
+ After calling this method:
+
+ - You must call :meth:`data_to_send` and send this data to the network;
+ it will return ``[b""]``, signaling the end of the stream, or ``[]``.
+ - You aren't expected to call :meth:`events_received`; it won't return
+ any new events.
+
+ Raises:
+ EOFError: if :meth:`receive_eof` was called earlier.
+
+ """
+ self.reader.feed_eof()
+ next(self.parser)
+
+ # Public methods for sending events.
+
+ def send_continuation(self, data: bytes, fin: bool) -> None:
+ """
+ Send a `Continuation frame`_.
+
+ .. _Continuation frame:
+ https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
+
+ Parameters:
+ data: payload containing the same kind of data
+ as the initial frame.
+ fin: FIN bit; set it to :obj:`True` if this is the last frame
+ of a fragmented message and to :obj:`False` otherwise.
+
+ Raises:
+ ProtocolError: if a fragmented message isn't in progress.
+
+ """
+ if not self.expect_continuation_frame:
+ raise ProtocolError("unexpected continuation frame")
+ self.expect_continuation_frame = not fin
+ self.send_frame(Frame(OP_CONT, data, fin))
+
+ def send_text(self, data: bytes, fin: bool = True) -> None:
+ """
+ Send a `Text frame`_.
+
+ .. _Text frame:
+ https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
+
+ Parameters:
+ data: payload containing text encoded with UTF-8.
+ fin: FIN bit; set it to :obj:`False` if this is the first frame of
+ a fragmented message.
+
+ Raises:
+ ProtocolError: if a fragmented message is in progress.
+
+ """
+ if self.expect_continuation_frame:
+ raise ProtocolError("expected a continuation frame")
+ self.expect_continuation_frame = not fin
+ self.send_frame(Frame(OP_TEXT, data, fin))
+
+ def send_binary(self, data: bytes, fin: bool = True) -> None:
+ """
+ Send a `Binary frame`_.
+
+ .. _Binary frame:
+ https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
+
+ Parameters:
+ data: payload containing arbitrary binary data.
+ fin: FIN bit; set it to :obj:`False` if this is the first frame of
+ a fragmented message.
+
+ Raises:
+ ProtocolError: if a fragmented message is in progress.
+
+ """
+ if self.expect_continuation_frame:
+ raise ProtocolError("expected a continuation frame")
+ self.expect_continuation_frame = not fin
+ self.send_frame(Frame(OP_BINARY, data, fin))
+
+ def send_close(self, code: Optional[int] = None, reason: str = "") -> None:
+ """
+ Send a `Close frame`_.
+
+ .. _Close frame:
+ https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1
+
+ Parameters:
+ code: close code.
+ reason: close reason.
+
+ Raises:
+ ProtocolError: if a fragmented message is being sent, if the code
+ isn't valid, or if a reason is provided without a code
+
+ """
+ if self.expect_continuation_frame:
+ raise ProtocolError("expected a continuation frame")
+ if code is None:
+ if reason != "":
+ raise ProtocolError("cannot send a reason without a code")
+ close = Close(CloseCode.NO_STATUS_RCVD, "")
+ data = b""
+ else:
+ close = Close(code, reason)
+ data = close.serialize()
+ # send_frame() guarantees that self.state is OPEN at this point.
+ # 7.1.3. The WebSocket Closing Handshake is Started
+ self.send_frame(Frame(OP_CLOSE, data))
+ self.close_sent = close
+ self.state = CLOSING
+
+ def send_ping(self, data: bytes) -> None:
+ """
+ Send a `Ping frame`_.
+
+ .. _Ping frame:
+ https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2
+
+ Parameters:
+ data: payload containing arbitrary binary data.
+
+ """
+ self.send_frame(Frame(OP_PING, data))
+
+ def send_pong(self, data: bytes) -> None:
+ """
+ Send a `Pong frame`_.
+
+ .. _Pong frame:
+ https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3
+
+ Parameters:
+ data: payload containing arbitrary binary data.
+
+ """
+ self.send_frame(Frame(OP_PONG, data))
+
+ def fail(self, code: int, reason: str = "") -> None:
+ """
+ `Fail the WebSocket connection`_.
+
+ .. _Fail the WebSocket connection:
+ https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.7
+
+ Parameters:
+ code: close code
+ reason: close reason
+
+ Raises:
+ ProtocolError: if the code isn't valid.
+ """
+ # 7.1.7. Fail the WebSocket Connection
+
+ # Send a close frame when the state is OPEN (a close frame was already
+ # sent if it's CLOSING), except when failing the connection because
+ # of an error reading from or writing to the network.
+ if self.state is OPEN:
+ if code != CloseCode.ABNORMAL_CLOSURE:
+ close = Close(code, reason)
+ data = close.serialize()
+ self.send_frame(Frame(OP_CLOSE, data))
+ self.close_sent = close
+ self.state = CLOSING
+
+ # When failing the connection, a server closes the TCP connection
+ # without waiting for the client to complete the handshake, while a
+ # client waits for the server to close the TCP connection, possibly
+ # after sending a close frame that the client will ignore.
+ if self.side is SERVER and not self.eof_sent:
+ self.send_eof()
+
+ # 7.1.7. Fail the WebSocket Connection "An endpoint MUST NOT continue
+ # to attempt to process data(including a responding Close frame) from
+ # the remote endpoint after being instructed to _Fail the WebSocket
+ # Connection_."
+ self.parser = self.discard()
+ next(self.parser) # start coroutine
+
+ # Public method for getting incoming events after receiving data.
+
+ def events_received(self) -> List[Event]:
+ """
+ Fetch events generated from data received from the network.
+
+ Call this method immediately after any of the ``receive_*()`` methods.
+
+ Process resulting events, likely by passing them to the application.
+
+ Returns:
+ List[Event]: Events read from the connection.
+ """
+ events, self.events = self.events, []
+ return events
+
+ # Public method for getting outgoing data after receiving data or sending events.
+
+ def data_to_send(self) -> List[bytes]:
+ """
+ Obtain data to send to the network.
+
+ Call this method immediately after any of the ``receive_*()``,
+ ``send_*()``, or :meth:`fail` methods.
+
+ Write resulting data to the connection.
+
+ The empty bytestring :data:`~websockets.protocol.SEND_EOF` signals
+ the end of the data stream. When you receive it, half-close the TCP
+ connection.
+
+ Returns:
+ List[bytes]: Data to write to the connection.
+
+ """
+ writes, self.writes = self.writes, []
+ return writes
+
+ def close_expected(self) -> bool:
+ """
+ Tell if the TCP connection is expected to close soon.
+
+ Call this method immediately after any of the ``receive_*()``,
+ ``send_close()``, or :meth:`fail` methods.
+
+ If it returns :obj:`True`, schedule closing the TCP connection after a
+ short timeout if the other side hasn't already closed it.
+
+ Returns:
+ bool: Whether the TCP connection is expected to close soon.
+
+ """
+ # We expect a TCP close if and only if we sent a close frame:
+ # * Normal closure: once we send a close frame, we expect a TCP close:
+ # server waits for client to complete the TCP closing handshake;
+ # client waits for server to initiate the TCP closing handshake.
+ # * Abnormal closure: we always send a close frame and the same logic
+ # applies, except on EOFError where we don't send a close frame
+ # because we already received the TCP close, so we don't expect it.
+ # We already got a TCP Close if and only if the state is CLOSED.
+ return self.state is CLOSING or self.handshake_exc is not None
+
+ # Private methods for receiving data.
+
+ def parse(self) -> Generator[None, None, None]:
+ """
+ Parse incoming data into frames.
+
+ :meth:`receive_data` and :meth:`receive_eof` run this generator
+ coroutine until it needs more data or reaches EOF.
+
+ :meth:`parse` never raises an exception. Instead, it sets the
+ :attr:`parser_exc` and yields control.
+
+ """
+ try:
+ while True:
+ if (yield from self.reader.at_eof()):
+ if self.debug:
+ self.logger.debug("< EOF")
+ # If the WebSocket connection is closed cleanly, with a
+ # closing handhshake, recv_frame() substitutes parse()
+ # with discard(). This branch is reached only when the
+ # connection isn't closed cleanly.
+ raise EOFError("unexpected end of stream")
+
+ if self.max_size is None:
+ max_size = None
+ elif self.cur_size is None:
+ max_size = self.max_size
+ else:
+ max_size = self.max_size - self.cur_size
+
+ # During a normal closure, execution ends here on the next
+ # iteration of the loop after receiving a close frame. At
+ # this point, recv_frame() replaced parse() by discard().
+ frame = yield from Frame.parse(
+ self.reader.read_exact,
+ mask=self.side is SERVER,
+ max_size=max_size,
+ extensions=self.extensions,
+ )
+
+ if self.debug:
+ self.logger.debug("< %s", frame)
+
+ self.recv_frame(frame)
+
+ except ProtocolError as exc:
+ self.fail(CloseCode.PROTOCOL_ERROR, str(exc))
+ self.parser_exc = exc
+
+ except EOFError as exc:
+ self.fail(CloseCode.ABNORMAL_CLOSURE, str(exc))
+ self.parser_exc = exc
+
+ except UnicodeDecodeError as exc:
+ self.fail(CloseCode.INVALID_DATA, f"{exc.reason} at position {exc.start}")
+ self.parser_exc = exc
+
+ except PayloadTooBig as exc:
+ self.fail(CloseCode.MESSAGE_TOO_BIG, str(exc))
+ self.parser_exc = exc
+
+ except Exception as exc:
+ self.logger.error("parser failed", exc_info=True)
+ # Don't include exception details, which may be security-sensitive.
+ self.fail(CloseCode.INTERNAL_ERROR)
+ self.parser_exc = exc
+
+ # During an abnormal closure, execution ends here after catching an
+ # exception. At this point, fail() replaced parse() by discard().
+ yield
+ raise AssertionError("parse() shouldn't step after error")
+
+ def discard(self) -> Generator[None, None, None]:
+ """
+ Discard incoming data.
+
+ This coroutine replaces :meth:`parse`:
+
+ - after receiving a close frame, during a normal closure (1.4);
+ - after sending a close frame, during an abnormal closure (7.1.7).
+
+ """
+ # The server close the TCP connection in the same circumstances where
+ # discard() replaces parse(). The client closes the connection later,
+ # after the server closes the connection or a timeout elapses.
+ # (The latter case cannot be handled in this Sans-I/O layer.)
+ assert (self.side is SERVER) == (self.eof_sent)
+ while not (yield from self.reader.at_eof()):
+ self.reader.discard()
+ if self.debug:
+ self.logger.debug("< EOF")
+ # A server closes the TCP connection immediately, while a client
+ # waits for the server to close the TCP connection.
+ if self.side is CLIENT:
+ self.send_eof()
+ self.state = CLOSED
+ # If discard() completes normally, execution ends here.
+ yield
+ # Once the reader reaches EOF, its feed_data/eof() methods raise an
+ # error, so our receive_data/eof() methods don't step the generator.
+ raise AssertionError("discard() shouldn't step after EOF")
+
+ def recv_frame(self, frame: Frame) -> None:
+ """
+ Process an incoming frame.
+
+ """
+ if frame.opcode is OP_TEXT or frame.opcode is OP_BINARY:
+ if self.cur_size is not None:
+ raise ProtocolError("expected a continuation frame")
+ if frame.fin:
+ self.cur_size = None
+ else:
+ self.cur_size = len(frame.data)
+
+ elif frame.opcode is OP_CONT:
+ if self.cur_size is None:
+ raise ProtocolError("unexpected continuation frame")
+ if frame.fin:
+ self.cur_size = None
+ else:
+ self.cur_size += len(frame.data)
+
+ elif frame.opcode is OP_PING:
+ # 5.5.2. Ping: "Upon receipt of a Ping frame, an endpoint MUST
+ # send a Pong frame in response"
+ pong_frame = Frame(OP_PONG, frame.data)
+ self.send_frame(pong_frame)
+
+ elif frame.opcode is OP_PONG:
+ # 5.5.3 Pong: "A response to an unsolicited Pong frame is not
+ # expected."
+ pass
+
+ elif frame.opcode is OP_CLOSE:
+ # 7.1.5. The WebSocket Connection Close Code
+ # 7.1.6. The WebSocket Connection Close Reason
+ self.close_rcvd = Close.parse(frame.data)
+ if self.state is CLOSING:
+ assert self.close_sent is not None
+ self.close_rcvd_then_sent = False
+
+ if self.cur_size is not None:
+ raise ProtocolError("incomplete fragmented message")
+
+ # 5.5.1 Close: "If an endpoint receives a Close frame and did
+ # not previously send a Close frame, the endpoint MUST send a
+ # Close frame in response. (When sending a Close frame in
+ # response, the endpoint typically echos the status code it
+ # received.)"
+
+ if self.state is OPEN:
+ # Echo the original data instead of re-serializing it with
+ # Close.serialize() because that fails when the close frame
+ # is empty and Close.parse() synthesizes a 1005 close code.
+ # The rest is identical to send_close().
+ self.send_frame(Frame(OP_CLOSE, frame.data))
+ self.close_sent = self.close_rcvd
+ self.close_rcvd_then_sent = True
+ self.state = CLOSING
+
+ # 7.1.2. Start the WebSocket Closing Handshake: "Once an
+ # endpoint has both sent and received a Close control frame,
+ # that endpoint SHOULD _Close the WebSocket Connection_"
+
+ # A server closes the TCP connection immediately, while a client
+ # waits for the server to close the TCP connection.
+ if self.side is SERVER:
+ self.send_eof()
+
+ # 1.4. Closing Handshake: "after receiving a control frame
+ # indicating the connection should be closed, a peer discards
+ # any further data received."
+ self.parser = self.discard()
+ next(self.parser) # start coroutine
+
+ else:
+ # This can't happen because Frame.parse() validates opcodes.
+ raise AssertionError(f"unexpected opcode: {frame.opcode:02x}")
+
+ self.events.append(frame)
+
+ # Private methods for sending events.
+
+ def send_frame(self, frame: Frame) -> None:
+ if self.state is not OPEN:
+ raise InvalidState(
+ f"cannot write to a WebSocket in the {self.state.name} state"
+ )
+
+ if self.debug:
+ self.logger.debug("> %s", frame)
+ self.writes.append(
+ frame.serialize(mask=self.side is CLIENT, extensions=self.extensions)
+ )
+
+ def send_eof(self) -> None:
+ assert not self.eof_sent
+ self.eof_sent = True
+ if self.debug:
+ self.logger.debug("> EOF")
+ self.writes.append(SEND_EOF)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py
index 5dad50b6a1..191660553f 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py
@@ -4,9 +4,9 @@ import base64
import binascii
import email.utils
import http
-from typing import Generator, List, Optional, Sequence, Tuple, cast
+import warnings
+from typing import Any, Callable, Generator, List, Optional, Sequence, Tuple, cast
-from .connection import CONNECTING, OPEN, SERVER, Connection, State
from .datastructures import Headers, MultipleValuesError
from .exceptions import (
InvalidHandshake,
@@ -25,13 +25,14 @@ from .headers import (
parse_subprotocol,
parse_upgrade,
)
-from .http import USER_AGENT
from .http11 import Request, Response
+from .protocol import CONNECTING, OPEN, SERVER, Protocol, State
from .typing import (
ConnectionOption,
ExtensionHeader,
LoggerLike,
Origin,
+ StatusLike,
Subprotocol,
UpgradeProtocol,
)
@@ -39,13 +40,15 @@ from .utils import accept_key
# See #940 for why lazy_import isn't used here for backwards compatibility.
-from .legacy.server import * # isort:skip # noqa
+# See #1400 for why listing compatibility imports in __all__ helps PyCharm.
+from .legacy.server import * # isort:skip # noqa: I001
+from .legacy.server import __all__ as legacy__all__
-__all__ = ["ServerConnection"]
+__all__ = ["ServerProtocol"] + legacy__all__
-class ServerConnection(Connection):
+class ServerProtocol(Protocol):
"""
Sans-I/O implementation of a WebSocket server connection.
@@ -58,20 +61,31 @@ class ServerConnection(Connection):
should be tried.
subprotocols: list of supported subprotocols, in order of decreasing
preference.
+ select_subprotocol: Callback for selecting a subprotocol among
+ those supported by the client and the server. It has the same
+ signature as the :meth:`select_subprotocol` method, including a
+ :class:`ServerProtocol` instance as first argument.
state: initial state of the WebSocket connection.
max_size: maximum size of incoming messages in bytes;
- :obj:`None` to disable the limit.
+ :obj:`None` disables the limit.
logger: logger for this connection;
defaults to ``logging.getLogger("websockets.client")``;
- see the :doc:`logging guide <../topics/logging>` for details.
+ see the :doc:`logging guide <../../topics/logging>` for details.
"""
def __init__(
self,
+ *,
origins: Optional[Sequence[Optional[Origin]]] = None,
extensions: Optional[Sequence[ServerExtensionFactory]] = None,
subprotocols: Optional[Sequence[Subprotocol]] = None,
+ select_subprotocol: Optional[
+ Callable[
+ [ServerProtocol, Sequence[Subprotocol]],
+ Optional[Subprotocol],
+ ]
+ ] = None,
state: State = CONNECTING,
max_size: Optional[int] = 2**20,
logger: Optional[LoggerLike] = None,
@@ -85,6 +99,14 @@ class ServerConnection(Connection):
self.origins = origins
self.available_extensions = extensions
self.available_subprotocols = subprotocols
+ if select_subprotocol is not None:
+ # Bind select_subprotocol then shadow self.select_subprotocol.
+ # Use setattr to work around https://github.com/python/mypy/issues/2427.
+ setattr(
+ self,
+ "select_subprotocol",
+ select_subprotocol.__get__(self, self.__class__),
+ )
def accept(self, request: Request) -> Response:
"""
@@ -95,13 +117,13 @@ class ServerConnection(Connection):
You must send the handshake response with :meth:`send_response`.
- You can modify it before sending it, for example to add HTTP headers.
+ You may modify it before sending it, for example to add HTTP headers.
Args:
request: WebSocket handshake request event received from the client.
Returns:
- Response: WebSocket handshake response event to send to the client.
+ WebSocket handshake response event to send to the client.
"""
try:
@@ -145,6 +167,8 @@ class ServerConnection(Connection):
f"Failed to open a WebSocket connection: {exc}.\n",
)
except Exception as exc:
+ # Handle exceptions raised by user-provided select_subprotocol and
+ # unexpected errors.
request._exception = exc
self.handshake_exc = exc
self.logger.error("opening handshake failed", exc_info=True)
@@ -170,13 +194,12 @@ class ServerConnection(Connection):
if protocol_header is not None:
headers["Sec-WebSocket-Protocol"] = protocol_header
- headers["Server"] = USER_AGENT
-
self.logger.info("connection open")
return Response(101, "Switching Protocols", headers)
def process_request(
- self, request: Request
+ self,
+ request: Request,
) -> Tuple[str, Optional[str], Optional[str]]:
"""
Check a handshake request and negotiate extensions and subprotocol.
@@ -274,6 +297,7 @@ class ServerConnection(Connection):
Optional[Origin]: origin, if it is acceptable.
Raises:
+ InvalidHandshake: if the Origin header is invalid.
InvalidOrigin: if the origin isn't acceptable.
"""
@@ -298,8 +322,8 @@ class ServerConnection(Connection):
Accept or reject each extension proposed in the client request.
Negotiate parameters for accepted extensions.
- :rfc:`6455` leaves the rules up to the specification of each
- :extension.
+ Per :rfc:`6455`, negotiation rules are defined by the specification of
+ each extension.
To provide this level of flexibility, for each extension proposed by
the client, we check for a match with each extension available in the
@@ -324,7 +348,7 @@ class ServerConnection(Connection):
HTTP response header and list of accepted extensions.
Raises:
- InvalidHandshake: to abort the handshake with an HTTP 400 error.
+ InvalidHandshake: if the Sec-WebSocket-Extensions header is invalid.
"""
response_header_value: Optional[str] = None
@@ -335,15 +359,12 @@ class ServerConnection(Connection):
header_values = headers.get_all("Sec-WebSocket-Extensions")
if header_values and self.available_extensions:
-
parsed_header_values: List[ExtensionHeader] = sum(
[parse_extension(header_value) for header_value in header_values], []
)
for name, request_params in parsed_header_values:
-
for ext_factory in self.available_extensions:
-
# Skip non-matching extensions based on their name.
if ext_factory.name != name:
continue
@@ -384,64 +405,83 @@ class ServerConnection(Connection):
also the value of the ``Sec-WebSocket-Protocol`` response header.
Raises:
- InvalidHandshake: to abort the handshake with an HTTP 400 error.
+ InvalidHandshake: if the Sec-WebSocket-Subprotocol header is invalid.
"""
- subprotocol: Optional[Subprotocol] = None
-
- header_values = headers.get_all("Sec-WebSocket-Protocol")
-
- if header_values and self.available_subprotocols:
-
- parsed_header_values: List[Subprotocol] = sum(
- [parse_subprotocol(header_value) for header_value in header_values], []
- )
-
- subprotocol = self.select_subprotocol(
- parsed_header_values, self.available_subprotocols
- )
+ subprotocols: Sequence[Subprotocol] = sum(
+ [
+ parse_subprotocol(header_value)
+ for header_value in headers.get_all("Sec-WebSocket-Protocol")
+ ],
+ [],
+ )
- return subprotocol
+ return self.select_subprotocol(subprotocols)
def select_subprotocol(
self,
- client_subprotocols: Sequence[Subprotocol],
- server_subprotocols: Sequence[Subprotocol],
+ subprotocols: Sequence[Subprotocol],
) -> Optional[Subprotocol]:
"""
Pick a subprotocol among those offered by the client.
- If several subprotocols are supported by the client and the server,
- the default implementation selects the preferred subprotocols by
- giving equal value to the priorities of the client and the server.
+ If several subprotocols are supported by both the client and the server,
+ pick the first one in the list declared the server.
+
+ If the server doesn't support any subprotocols, continue without a
+ subprotocol, regardless of what the client offers.
+
+ If the server supports at least one subprotocol and the client doesn't
+ offer any, abort the handshake with an HTTP 400 error.
- If no common subprotocol is supported by the client and the server, it
- proceeds without a subprotocol.
+ You provide a ``select_subprotocol`` argument to :class:`ServerProtocol`
+ to override this logic. For example, you could accept the connection
+ even if client doesn't offer a subprotocol, rather than reject it.
- This is unlikely to be the most useful implementation in practice, as
- many servers providing a subprotocol will require that the client uses
- that subprotocol.
+ Here's how to negotiate the ``chat`` subprotocol if the client supports
+ it and continue without a subprotocol otherwise::
+
+ def select_subprotocol(protocol, subprotocols):
+ if "chat" in subprotocols:
+ return "chat"
Args:
- client_subprotocols: list of subprotocols offered by the client.
- server_subprotocols: list of subprotocols available on the server.
+ subprotocols: list of subprotocols offered by the client.
Returns:
- Optional[Subprotocol]: Subprotocol, if a common subprotocol was
- found.
+ Optional[Subprotocol]: Selected subprotocol, if a common subprotocol
+ was found.
+
+ :obj:`None` to continue without a subprotocol.
+
+ Raises:
+ NegotiationError: custom implementations may raise this exception
+ to abort the handshake with an HTTP 400 error.
"""
- subprotocols = set(client_subprotocols) & set(server_subprotocols)
- if not subprotocols:
+ # Server doesn't offer any subprotocols.
+ if not self.available_subprotocols: # None or empty list
return None
- priority = lambda p: (
- client_subprotocols.index(p) + server_subprotocols.index(p)
+
+ # Server offers at least one subprotocol but client doesn't offer any.
+ if not subprotocols:
+ raise NegotiationError("missing subprotocol")
+
+ # Server and client both offer subprotocols. Look for a shared one.
+ proposed_subprotocols = set(subprotocols)
+ for subprotocol in self.available_subprotocols:
+ if subprotocol in proposed_subprotocols:
+ return subprotocol
+
+ # No common subprotocol was found.
+ raise NegotiationError(
+ "invalid subprotocol; expected one of "
+ + ", ".join(self.available_subprotocols)
)
- return sorted(subprotocols, key=priority)[0]
def reject(
self,
- status: http.HTTPStatus,
+ status: StatusLike,
text: str,
) -> Response:
"""
@@ -462,6 +502,8 @@ class ServerConnection(Connection):
Response: WebSocket handshake response event to send to the client.
"""
+ # If a user passes an int instead of a HTTPStatus, fix it automatically.
+ status = http.HTTPStatus(status)
body = text.encode()
headers = Headers(
[
@@ -469,16 +511,15 @@ class ServerConnection(Connection):
("Connection", "close"),
("Content-Length", str(len(body))),
("Content-Type", "text/plain; charset=utf-8"),
- ("Server", USER_AGENT),
]
)
response = Response(status.value, status.phrase, headers, body)
# When reject() is called from accept(), handshake_exc is already set.
# If a user calls reject(), set handshake_exc to guarantee invariant:
- # "handshake_exc is None if and only if opening handshake succeded."
+ # "handshake_exc is None if and only if opening handshake succeeded."
if self.handshake_exc is None:
self.handshake_exc = InvalidStatus(response)
- self.logger.info("connection failed (%d %s)", status.value, status.phrase)
+ self.logger.info("connection rejected (%d %s)", status.value, status.phrase)
return response
def send_response(self, response: Response) -> None:
@@ -509,7 +550,16 @@ class ServerConnection(Connection):
def parse(self) -> Generator[None, None, None]:
if self.state is CONNECTING:
- request = yield from Request.parse(self.reader.read_line)
+ try:
+ request = yield from Request.parse(
+ self.reader.read_line,
+ )
+ except Exception as exc:
+ self.handshake_exc = exc
+ self.send_eof()
+ self.parser = self.discard()
+ next(self.parser) # start coroutine
+ yield
if self.debug:
self.logger.debug("< GET %s HTTP/1.1", request.path)
@@ -519,3 +569,12 @@ class ServerConnection(Connection):
self.events.append(request)
yield from super().parse()
+
+
+class ServerConnection(ServerProtocol):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ warnings.warn(
+ "ServerConnection was renamed to ServerProtocol",
+ DeprecationWarning,
+ )
+ super().__init__(*args, **kwargs)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi
new file mode 100644
index 0000000000..821438a064
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi
@@ -0,0 +1 @@
+def apply_mask(data: bytes, mask: bytes) -> bytes: ...
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/client.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/client.py
new file mode 100644
index 0000000000..087ff5f569
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/client.py
@@ -0,0 +1,328 @@
+from __future__ import annotations
+
+import socket
+import ssl
+import threading
+from typing import Any, Optional, Sequence, Type
+
+from ..client import ClientProtocol
+from ..datastructures import HeadersLike
+from ..extensions.base import ClientExtensionFactory
+from ..extensions.permessage_deflate import enable_client_permessage_deflate
+from ..headers import validate_subprotocols
+from ..http import USER_AGENT
+from ..http11 import Response
+from ..protocol import CONNECTING, OPEN, Event
+from ..typing import LoggerLike, Origin, Subprotocol
+from ..uri import parse_uri
+from .connection import Connection
+from .utils import Deadline
+
+
+__all__ = ["connect", "unix_connect", "ClientConnection"]
+
+
+class ClientConnection(Connection):
+ """
+ Threaded implementation of a WebSocket client connection.
+
+ :class:`ClientConnection` provides :meth:`recv` and :meth:`send` methods for
+ receiving and sending messages.
+
+ It supports iteration to receive messages::
+
+ for message in websocket:
+ process(message)
+
+ The iterator exits normally when the connection is closed with close code
+ 1000 (OK) or 1001 (going away) or without a close code. It raises a
+ :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is
+ closed with any other code.
+
+ Args:
+ socket: Socket connected to a WebSocket server.
+ protocol: Sans-I/O connection.
+ close_timeout: Timeout for closing the connection in seconds.
+
+ """
+
+ def __init__(
+ self,
+ socket: socket.socket,
+ protocol: ClientProtocol,
+ *,
+ close_timeout: Optional[float] = 10,
+ ) -> None:
+ self.protocol: ClientProtocol
+ self.response_rcvd = threading.Event()
+ super().__init__(
+ socket,
+ protocol,
+ close_timeout=close_timeout,
+ )
+
+ def handshake(
+ self,
+ additional_headers: Optional[HeadersLike] = None,
+ user_agent_header: Optional[str] = USER_AGENT,
+ timeout: Optional[float] = None,
+ ) -> None:
+ """
+ Perform the opening handshake.
+
+ """
+ with self.send_context(expected_state=CONNECTING):
+ self.request = self.protocol.connect()
+ if additional_headers is not None:
+ self.request.headers.update(additional_headers)
+ if user_agent_header is not None:
+ self.request.headers["User-Agent"] = user_agent_header
+ self.protocol.send_request(self.request)
+
+ if not self.response_rcvd.wait(timeout):
+ self.close_socket()
+ self.recv_events_thread.join()
+ raise TimeoutError("timed out during handshake")
+
+ if self.response is None:
+ self.close_socket()
+ self.recv_events_thread.join()
+ raise ConnectionError("connection closed during handshake")
+
+ if self.protocol.state is not OPEN:
+ self.recv_events_thread.join(self.close_timeout)
+ self.close_socket()
+ self.recv_events_thread.join()
+
+ if self.protocol.handshake_exc is not None:
+ raise self.protocol.handshake_exc
+
+ def process_event(self, event: Event) -> None:
+ """
+ Process one incoming event.
+
+ """
+ # First event - handshake response.
+ if self.response is None:
+ assert isinstance(event, Response)
+ self.response = event
+ self.response_rcvd.set()
+ # Later events - frames.
+ else:
+ super().process_event(event)
+
+ def recv_events(self) -> None:
+ """
+ Read incoming data from the socket and process events.
+
+ """
+ try:
+ super().recv_events()
+ finally:
+ # If the connection is closed during the handshake, unblock it.
+ self.response_rcvd.set()
+
+
+def connect(
+ uri: str,
+ *,
+ # TCP/TLS — unix and path are only for unix_connect()
+ sock: Optional[socket.socket] = None,
+ ssl_context: Optional[ssl.SSLContext] = None,
+ server_hostname: Optional[str] = None,
+ unix: bool = False,
+ path: Optional[str] = None,
+ # WebSocket
+ origin: Optional[Origin] = None,
+ extensions: Optional[Sequence[ClientExtensionFactory]] = None,
+ subprotocols: Optional[Sequence[Subprotocol]] = None,
+ additional_headers: Optional[HeadersLike] = None,
+ user_agent_header: Optional[str] = USER_AGENT,
+ compression: Optional[str] = "deflate",
+ # Timeouts
+ open_timeout: Optional[float] = 10,
+ close_timeout: Optional[float] = 10,
+ # Limits
+ max_size: Optional[int] = 2**20,
+ # Logging
+ logger: Optional[LoggerLike] = None,
+ # Escape hatch for advanced customization
+ create_connection: Optional[Type[ClientConnection]] = None,
+) -> ClientConnection:
+ """
+ Connect to the WebSocket server at ``uri``.
+
+ This function returns a :class:`ClientConnection` instance, which you can
+ use to send and receive messages.
+
+ :func:`connect` may be used as a context manager::
+
+ async with websockets.sync.client.connect(...) as websocket:
+ ...
+
+ The connection is closed automatically when exiting the context.
+
+ Args:
+ uri: URI of the WebSocket server.
+ sock: Preexisting TCP socket. ``sock`` overrides the host and port
+ from ``uri``. You may call :func:`socket.create_connection` to
+ create a suitable TCP socket.
+ ssl_context: Configuration for enabling TLS on the connection.
+ server_hostname: Host name for the TLS handshake. ``server_hostname``
+ overrides the host name from ``uri``.
+ origin: Value of the ``Origin`` header, for servers that require it.
+ extensions: List of supported extensions, in order in which they
+ should be negotiated and run.
+ subprotocols: List of supported subprotocols, in order of decreasing
+ preference.
+ additional_headers (HeadersLike | None): Arbitrary HTTP headers to add
+ to the handshake request.
+ user_agent_header: Value of the ``User-Agent`` request header.
+ It defaults to ``"Python/x.y.z websockets/X.Y"``.
+ Setting it to :obj:`None` removes the header.
+ compression: The "permessage-deflate" extension is enabled by default.
+ Set ``compression`` to :obj:`None` to disable it. See the
+ :doc:`compression guide <../../topics/compression>` for details.
+ open_timeout: Timeout for opening the connection in seconds.
+ :obj:`None` disables the timeout.
+ close_timeout: Timeout for closing the connection in seconds.
+ :obj:`None` disables the timeout.
+ max_size: Maximum size of incoming messages in bytes.
+ :obj:`None` disables the limit.
+ logger: Logger for this client.
+ It defaults to ``logging.getLogger("websockets.client")``.
+ See the :doc:`logging guide <../../topics/logging>` for details.
+ create_connection: Factory for the :class:`ClientConnection` managing
+ the connection. Set it to a wrapper or a subclass to customize
+ connection handling.
+
+ Raises:
+ InvalidURI: If ``uri`` isn't a valid WebSocket URI.
+ OSError: If the TCP connection fails.
+ InvalidHandshake: If the opening handshake fails.
+ TimeoutError: If the opening handshake times out.
+
+ """
+
+ # Process parameters
+
+ wsuri = parse_uri(uri)
+ if not wsuri.secure and ssl_context is not None:
+ raise TypeError("ssl_context argument is incompatible with a ws:// URI")
+
+ if unix:
+ if path is None and sock is None:
+ raise TypeError("missing path argument")
+ elif path is not None and sock is not None:
+ raise TypeError("path and sock arguments are incompatible")
+ else:
+ assert path is None # private argument, only set by unix_connect()
+
+ if subprotocols is not None:
+ validate_subprotocols(subprotocols)
+
+ if compression == "deflate":
+ extensions = enable_client_permessage_deflate(extensions)
+ elif compression is not None:
+ raise ValueError(f"unsupported compression: {compression}")
+
+ # Calculate timeouts on the TCP, TLS, and WebSocket handshakes.
+ # The TCP and TLS timeouts must be set on the socket, then removed
+ # to avoid conflicting with the WebSocket timeout in handshake().
+ deadline = Deadline(open_timeout)
+
+ if create_connection is None:
+ create_connection = ClientConnection
+
+ try:
+ # Connect socket
+
+ if sock is None:
+ if unix:
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.settimeout(deadline.timeout())
+ assert path is not None # validated above -- this is for mpypy
+ sock.connect(path)
+ else:
+ sock = socket.create_connection(
+ (wsuri.host, wsuri.port),
+ deadline.timeout(),
+ )
+ sock.settimeout(None)
+
+ # Disable Nagle algorithm
+
+ if not unix:
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
+
+ # Initialize TLS wrapper and perform TLS handshake
+
+ if wsuri.secure:
+ if ssl_context is None:
+ ssl_context = ssl.create_default_context()
+ if server_hostname is None:
+ server_hostname = wsuri.host
+ sock.settimeout(deadline.timeout())
+ sock = ssl_context.wrap_socket(sock, server_hostname=server_hostname)
+ sock.settimeout(None)
+
+ # Initialize WebSocket connection
+
+ protocol = ClientProtocol(
+ wsuri,
+ origin=origin,
+ extensions=extensions,
+ subprotocols=subprotocols,
+ state=CONNECTING,
+ max_size=max_size,
+ logger=logger,
+ )
+
+ # Initialize WebSocket protocol
+
+ connection = create_connection(
+ sock,
+ protocol,
+ close_timeout=close_timeout,
+ )
+ # On failure, handshake() closes the socket and raises an exception.
+ connection.handshake(
+ additional_headers,
+ user_agent_header,
+ deadline.timeout(),
+ )
+
+ except Exception:
+ if sock is not None:
+ sock.close()
+ raise
+
+ return connection
+
+
+def unix_connect(
+ path: Optional[str] = None,
+ uri: Optional[str] = None,
+ **kwargs: Any,
+) -> ClientConnection:
+ """
+ Connect to a WebSocket server listening on a Unix socket.
+
+ This function is identical to :func:`connect`, except for the additional
+ ``path`` argument. It's only available on Unix.
+
+ It's mainly useful for debugging servers listening on Unix sockets.
+
+ Args:
+ path: File system path to the Unix socket.
+ uri: URI of the WebSocket server. ``uri`` defaults to
+ ``ws://localhost/`` or, when a ``ssl_context`` is provided, to
+ ``wss://localhost/``.
+
+ """
+ if uri is None:
+ if kwargs.get("ssl_context") is None:
+ uri = "ws://localhost/"
+ else:
+ uri = "wss://localhost/"
+ return connect(uri=uri, unix=True, path=path, **kwargs)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/connection.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/connection.py
new file mode 100644
index 0000000000..4a8879e370
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/connection.py
@@ -0,0 +1,773 @@
+from __future__ import annotations
+
+import contextlib
+import logging
+import random
+import socket
+import struct
+import threading
+import uuid
+from types import TracebackType
+from typing import Any, Dict, Iterable, Iterator, Mapping, Optional, Type, Union
+
+from ..exceptions import ConnectionClosed, ConnectionClosedOK, ProtocolError
+from ..frames import DATA_OPCODES, BytesLike, CloseCode, Frame, Opcode, prepare_ctrl
+from ..http11 import Request, Response
+from ..protocol import CLOSED, OPEN, Event, Protocol, State
+from ..typing import Data, LoggerLike, Subprotocol
+from .messages import Assembler
+from .utils import Deadline
+
+
+__all__ = ["Connection"]
+
+logger = logging.getLogger(__name__)
+
+
+class Connection:
+ """
+ Threaded implementation of a WebSocket connection.
+
+ :class:`Connection` provides APIs shared between WebSocket servers and
+ clients.
+
+ You shouldn't use it directly. Instead, use
+ :class:`~websockets.sync.client.ClientConnection` or
+ :class:`~websockets.sync.server.ServerConnection`.
+
+ """
+
+ recv_bufsize = 65536
+
+ def __init__(
+ self,
+ socket: socket.socket,
+ protocol: Protocol,
+ *,
+ close_timeout: Optional[float] = 10,
+ ) -> None:
+ self.socket = socket
+ self.protocol = protocol
+ self.close_timeout = close_timeout
+
+ # Inject reference to this instance in the protocol's logger.
+ self.protocol.logger = logging.LoggerAdapter(
+ self.protocol.logger,
+ {"websocket": self},
+ )
+
+ # Copy attributes from the protocol for convenience.
+ self.id: uuid.UUID = self.protocol.id
+ """Unique identifier of the connection. Useful in logs."""
+ self.logger: LoggerLike = self.protocol.logger
+ """Logger for this connection."""
+ self.debug = self.protocol.debug
+
+ # HTTP handshake request and response.
+ self.request: Optional[Request] = None
+ """Opening handshake request."""
+ self.response: Optional[Response] = None
+ """Opening handshake response."""
+
+ # Mutex serializing interactions with the protocol.
+ self.protocol_mutex = threading.Lock()
+
+ # Assembler turning frames into messages and serializing reads.
+ self.recv_messages = Assembler()
+
+ # Whether we are busy sending a fragmented message.
+ self.send_in_progress = False
+
+ # Deadline for the closing handshake.
+ self.close_deadline: Optional[Deadline] = None
+
+ # Mapping of ping IDs to pong waiters, in chronological order.
+ self.pings: Dict[bytes, threading.Event] = {}
+
+ # Receiving events from the socket.
+ self.recv_events_thread = threading.Thread(target=self.recv_events)
+ self.recv_events_thread.start()
+
+ # Exception raised in recv_events, to be chained to ConnectionClosed
+ # in the user thread in order to show why the TCP connection dropped.
+ self.recv_events_exc: Optional[BaseException] = None
+
+ # Public attributes
+
+ @property
+ def local_address(self) -> Any:
+ """
+ Local address of the connection.
+
+ For IPv4 connections, this is a ``(host, port)`` tuple.
+
+ The format of the address depends on the address family.
+ See :meth:`~socket.socket.getsockname`.
+
+ """
+ return self.socket.getsockname()
+
+ @property
+ def remote_address(self) -> Any:
+ """
+ Remote address of the connection.
+
+ For IPv4 connections, this is a ``(host, port)`` tuple.
+
+ The format of the address depends on the address family.
+ See :meth:`~socket.socket.getpeername`.
+
+ """
+ return self.socket.getpeername()
+
+ @property
+ def subprotocol(self) -> Optional[Subprotocol]:
+ """
+ Subprotocol negotiated during the opening handshake.
+
+ :obj:`None` if no subprotocol was negotiated.
+
+ """
+ return self.protocol.subprotocol
+
+ # Public methods
+
+ def __enter__(self) -> Connection:
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_value: Optional[BaseException],
+ traceback: Optional[TracebackType],
+ ) -> None:
+ if exc_type is None:
+ self.close()
+ else:
+ self.close(CloseCode.INTERNAL_ERROR)
+
+ def __iter__(self) -> Iterator[Data]:
+ """
+ Iterate on incoming messages.
+
+ The iterator calls :meth:`recv` and yields messages in an infinite loop.
+
+ It exits when the connection is closed normally. It raises a
+ :exc:`~websockets.exceptions.ConnectionClosedError` exception after a
+ protocol error or a network failure.
+
+ """
+ try:
+ while True:
+ yield self.recv()
+ except ConnectionClosedOK:
+ return
+
+ def recv(self, timeout: Optional[float] = None) -> Data:
+ """
+ Receive the next message.
+
+ When the connection is closed, :meth:`recv` raises
+ :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises
+ :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal closure
+ and :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol
+ error or a network failure. This is how you detect the end of the
+ message stream.
+
+ If ``timeout`` is :obj:`None`, block until a message is received. If
+ ``timeout`` is set and no message is received within ``timeout``
+ seconds, raise :exc:`TimeoutError`. Set ``timeout`` to ``0`` to check if
+ a message was already received.
+
+ If the message is fragmented, wait until all fragments are received,
+ reassemble them, and return the whole message.
+
+ Returns:
+ A string (:class:`str`) for a Text_ frame or a bytestring
+ (:class:`bytes`) for a Binary_ frame.
+
+ .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
+ .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
+
+ Raises:
+ ConnectionClosed: When the connection is closed.
+ RuntimeError: If two threads call :meth:`recv` or
+ :meth:`recv_streaming` concurrently.
+
+ """
+ try:
+ return self.recv_messages.get(timeout)
+ except EOFError:
+ raise self.protocol.close_exc from self.recv_events_exc
+ except RuntimeError:
+ raise RuntimeError(
+ "cannot call recv while another thread "
+ "is already running recv or recv_streaming"
+ ) from None
+
+ def recv_streaming(self) -> Iterator[Data]:
+ """
+ Receive the next message frame by frame.
+
+ If the message is fragmented, yield each fragment as it is received.
+ The iterator must be fully consumed, or else the connection will become
+ unusable.
+
+ :meth:`recv_streaming` raises the same exceptions as :meth:`recv`.
+
+ Returns:
+ An iterator of strings (:class:`str`) for a Text_ frame or
+ bytestrings (:class:`bytes`) for a Binary_ frame.
+
+ .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
+ .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
+
+ Raises:
+ ConnectionClosed: When the connection is closed.
+ RuntimeError: If two threads call :meth:`recv` or
+ :meth:`recv_streaming` concurrently.
+
+ """
+ try:
+ yield from self.recv_messages.get_iter()
+ except EOFError:
+ raise self.protocol.close_exc from self.recv_events_exc
+ except RuntimeError:
+ raise RuntimeError(
+ "cannot call recv_streaming while another thread "
+ "is already running recv or recv_streaming"
+ ) from None
+
+ def send(self, message: Union[Data, Iterable[Data]]) -> None:
+ """
+ Send a message.
+
+ A string (:class:`str`) is sent as a Text_ frame. A bytestring or
+ bytes-like object (:class:`bytes`, :class:`bytearray`, or
+ :class:`memoryview`) is sent as a Binary_ frame.
+
+ .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
+ .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
+
+ :meth:`send` also accepts an iterable of strings, bytestrings, or
+ bytes-like objects to enable fragmentation_. Each item is treated as a
+ message fragment and sent in its own frame. All items must be of the
+ same type, or else :meth:`send` will raise a :exc:`TypeError` and the
+ connection will be closed.
+
+ .. _fragmentation: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.4
+
+ :meth:`send` rejects dict-like objects because this is often an error.
+ (If you really want to send the keys of a dict-like object as fragments,
+ call its :meth:`~dict.keys` method and pass the result to :meth:`send`.)
+
+ When the connection is closed, :meth:`send` raises
+ :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it
+ raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal
+ connection closure and
+ :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol
+ error or a network failure.
+
+ Args:
+ message: Message to send.
+
+ Raises:
+ ConnectionClosed: When the connection is closed.
+ RuntimeError: If a connection is busy sending a fragmented message.
+ TypeError: If ``message`` doesn't have a supported type.
+
+ """
+ # Unfragmented message -- this case must be handled first because
+ # strings and bytes-like objects are iterable.
+
+ if isinstance(message, str):
+ with self.send_context():
+ if self.send_in_progress:
+ raise RuntimeError(
+ "cannot call send while another thread "
+ "is already running send"
+ )
+ self.protocol.send_text(message.encode("utf-8"))
+
+ elif isinstance(message, BytesLike):
+ with self.send_context():
+ if self.send_in_progress:
+ raise RuntimeError(
+ "cannot call send while another thread "
+ "is already running send"
+ )
+ self.protocol.send_binary(message)
+
+ # Catch a common mistake -- passing a dict to send().
+
+ elif isinstance(message, Mapping):
+ raise TypeError("data is a dict-like object")
+
+ # Fragmented message -- regular iterator.
+
+ elif isinstance(message, Iterable):
+ chunks = iter(message)
+ try:
+ chunk = next(chunks)
+ except StopIteration:
+ return
+
+ try:
+ # First fragment.
+ if isinstance(chunk, str):
+ text = True
+ with self.send_context():
+ if self.send_in_progress:
+ raise RuntimeError(
+ "cannot call send while another thread "
+ "is already running send"
+ )
+ self.send_in_progress = True
+ self.protocol.send_text(
+ chunk.encode("utf-8"),
+ fin=False,
+ )
+ elif isinstance(chunk, BytesLike):
+ text = False
+ with self.send_context():
+ if self.send_in_progress:
+ raise RuntimeError(
+ "cannot call send while another thread "
+ "is already running send"
+ )
+ self.send_in_progress = True
+ self.protocol.send_binary(
+ chunk,
+ fin=False,
+ )
+ else:
+ raise TypeError("data iterable must contain bytes or str")
+
+ # Other fragments
+ for chunk in chunks:
+ if isinstance(chunk, str) and text:
+ with self.send_context():
+ assert self.send_in_progress
+ self.protocol.send_continuation(
+ chunk.encode("utf-8"),
+ fin=False,
+ )
+ elif isinstance(chunk, BytesLike) and not text:
+ with self.send_context():
+ assert self.send_in_progress
+ self.protocol.send_continuation(
+ chunk,
+ fin=False,
+ )
+ else:
+ raise TypeError("data iterable must contain uniform types")
+
+ # Final fragment.
+ with self.send_context():
+ self.protocol.send_continuation(b"", fin=True)
+ self.send_in_progress = False
+
+ except RuntimeError:
+ # We didn't start sending a fragmented message.
+ raise
+
+ except Exception:
+ # We're half-way through a fragmented message and we can't
+ # complete it. This makes the connection unusable.
+ with self.send_context():
+ self.protocol.fail(
+ CloseCode.INTERNAL_ERROR,
+ "error in fragmented message",
+ )
+ raise
+
+ else:
+ raise TypeError("data must be bytes, str, or iterable")
+
+ def close(self, code: int = CloseCode.NORMAL_CLOSURE, reason: str = "") -> None:
+ """
+ Perform the closing handshake.
+
+ :meth:`close` waits for the other end to complete the handshake, for the
+ TCP connection to terminate, and for all incoming messages to be read
+ with :meth:`recv`.
+
+ :meth:`close` is idempotent: it doesn't do anything once the
+ connection is closed.
+
+ Args:
+ code: WebSocket close code.
+ reason: WebSocket close reason.
+
+ """
+ try:
+ # The context manager takes care of waiting for the TCP connection
+ # to terminate after calling a method that sends a close frame.
+ with self.send_context():
+ if self.send_in_progress:
+ self.protocol.fail(
+ CloseCode.INTERNAL_ERROR,
+ "close during fragmented message",
+ )
+ else:
+ self.protocol.send_close(code, reason)
+ except ConnectionClosed:
+ # Ignore ConnectionClosed exceptions raised from send_context().
+ # They mean that the connection is closed, which was the goal.
+ pass
+
+ def ping(self, data: Optional[Data] = None) -> threading.Event:
+ """
+ Send a Ping_.
+
+ .. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2
+
+ A ping may serve as a keepalive or as a check that the remote endpoint
+ received all messages up to this point
+
+ Args:
+ data: Payload of the ping. A :class:`str` will be encoded to UTF-8.
+ If ``data`` is :obj:`None`, the payload is four random bytes.
+
+ Returns:
+ An event that will be set when the corresponding pong is received.
+ You can ignore it if you don't intend to wait.
+
+ ::
+
+ pong_event = ws.ping()
+ pong_event.wait() # only if you want to wait for the pong
+
+ Raises:
+ ConnectionClosed: When the connection is closed.
+ RuntimeError: If another ping was sent with the same data and
+ the corresponding pong wasn't received yet.
+
+ """
+ if data is not None:
+ data = prepare_ctrl(data)
+
+ with self.send_context():
+ # Protect against duplicates if a payload is explicitly set.
+ if data in self.pings:
+ raise RuntimeError("already waiting for a pong with the same data")
+
+ # Generate a unique random payload otherwise.
+ while data is None or data in self.pings:
+ data = struct.pack("!I", random.getrandbits(32))
+
+ pong_waiter = threading.Event()
+ self.pings[data] = pong_waiter
+ self.protocol.send_ping(data)
+ return pong_waiter
+
+ def pong(self, data: Data = b"") -> None:
+ """
+ Send a Pong_.
+
+ .. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3
+
+ An unsolicited pong may serve as a unidirectional heartbeat.
+
+ Args:
+ data: Payload of the pong. A :class:`str` will be encoded to UTF-8.
+
+ Raises:
+ ConnectionClosed: When the connection is closed.
+
+ """
+ data = prepare_ctrl(data)
+
+ with self.send_context():
+ self.protocol.send_pong(data)
+
+ # Private methods
+
+ def process_event(self, event: Event) -> None:
+ """
+ Process one incoming event.
+
+ This method is overridden in subclasses to handle the handshake.
+
+ """
+ assert isinstance(event, Frame)
+ if event.opcode in DATA_OPCODES:
+ self.recv_messages.put(event)
+
+ if event.opcode is Opcode.PONG:
+ self.acknowledge_pings(bytes(event.data))
+
+ def acknowledge_pings(self, data: bytes) -> None:
+ """
+ Acknowledge pings when receiving a pong.
+
+ """
+ with self.protocol_mutex:
+ # Ignore unsolicited pong.
+ if data not in self.pings:
+ return
+ # Sending a pong for only the most recent ping is legal.
+ # Acknowledge all previous pings too in that case.
+ ping_id = None
+ ping_ids = []
+ for ping_id, ping in self.pings.items():
+ ping_ids.append(ping_id)
+ ping.set()
+ if ping_id == data:
+ break
+ else:
+ raise AssertionError("solicited pong not found in pings")
+ # Remove acknowledged pings from self.pings.
+ for ping_id in ping_ids:
+ del self.pings[ping_id]
+
+ def recv_events(self) -> None:
+ """
+ Read incoming data from the socket and process events.
+
+ Run this method in a thread as long as the connection is alive.
+
+ ``recv_events()`` exits immediately when the ``self.socket`` is closed.
+
+ """
+ try:
+ while True:
+ try:
+ if self.close_deadline is not None:
+ self.socket.settimeout(self.close_deadline.timeout())
+ data = self.socket.recv(self.recv_bufsize)
+ except Exception as exc:
+ if self.debug:
+ self.logger.debug("error while receiving data", exc_info=True)
+ # When the closing handshake is initiated by our side,
+ # recv() may block until send_context() closes the socket.
+ # In that case, send_context() already set recv_events_exc.
+ # Calling set_recv_events_exc() avoids overwriting it.
+ with self.protocol_mutex:
+ self.set_recv_events_exc(exc)
+ break
+
+ if data == b"":
+ break
+
+ # Acquire the connection lock.
+ with self.protocol_mutex:
+ # Feed incoming data to the connection.
+ self.protocol.receive_data(data)
+
+ # This isn't expected to raise an exception.
+ events = self.protocol.events_received()
+
+ # Write outgoing data to the socket.
+ try:
+ self.send_data()
+ except Exception as exc:
+ if self.debug:
+ self.logger.debug("error while sending data", exc_info=True)
+ # Similarly to the above, avoid overriding an exception
+ # set by send_context(), in case of a race condition
+ # i.e. send_context() closes the socket after recv()
+ # returns above but before send_data() calls send().
+ self.set_recv_events_exc(exc)
+ break
+
+ if self.protocol.close_expected():
+ # If the connection is expected to close soon, set the
+ # close deadline based on the close timeout.
+ if self.close_deadline is None:
+ self.close_deadline = Deadline(self.close_timeout)
+
+ # Unlock conn_mutex before processing events. Else, the
+ # application can't send messages in response to events.
+
+ # If self.send_data raised an exception, then events are lost.
+ # Given that automatic responses write small amounts of data,
+ # this should be uncommon, so we don't handle the edge case.
+
+ try:
+ for event in events:
+ # This may raise EOFError if the closing handshake
+ # times out while a message is waiting to be read.
+ self.process_event(event)
+ except EOFError:
+ break
+
+ # Breaking out of the while True: ... loop means that we believe
+ # that the socket doesn't work anymore.
+ with self.protocol_mutex:
+ # Feed the end of the data stream to the connection.
+ self.protocol.receive_eof()
+
+ # This isn't expected to generate events.
+ assert not self.protocol.events_received()
+
+ # There is no error handling because send_data() can only write
+ # the end of the data stream here and it handles errors itself.
+ self.send_data()
+
+ except Exception as exc:
+ # This branch should never run. It's a safety net in case of bugs.
+ self.logger.error("unexpected internal error", exc_info=True)
+ with self.protocol_mutex:
+ self.set_recv_events_exc(exc)
+ # We don't know where we crashed. Force protocol state to CLOSED.
+ self.protocol.state = CLOSED
+ finally:
+ # This isn't expected to raise an exception.
+ self.close_socket()
+
+ @contextlib.contextmanager
+ def send_context(
+ self,
+ *,
+ expected_state: State = OPEN, # CONNECTING during the opening handshake
+ ) -> Iterator[None]:
+ """
+ Create a context for writing to the connection from user code.
+
+ On entry, :meth:`send_context` acquires the connection lock and checks
+ that the connection is open; on exit, it writes outgoing data to the
+ socket::
+
+ with self.send_context():
+ self.protocol.send_text(message.encode("utf-8"))
+
+ When the connection isn't open on entry, when the connection is expected
+ to close on exit, or when an unexpected error happens, terminating the
+ connection, :meth:`send_context` waits until the connection is closed
+ then raises :exc:`~websockets.exceptions.ConnectionClosed`.
+
+ """
+ # Should we wait until the connection is closed?
+ wait_for_close = False
+ # Should we close the socket and raise ConnectionClosed?
+ raise_close_exc = False
+ # What exception should we chain ConnectionClosed to?
+ original_exc: Optional[BaseException] = None
+
+ # Acquire the protocol lock.
+ with self.protocol_mutex:
+ if self.protocol.state is expected_state:
+ # Let the caller interact with the protocol.
+ try:
+ yield
+ except (ProtocolError, RuntimeError):
+ # The protocol state wasn't changed. Exit immediately.
+ raise
+ except Exception as exc:
+ self.logger.error("unexpected internal error", exc_info=True)
+ # This branch should never run. It's a safety net in case of
+ # bugs. Since we don't know what happened, we will close the
+ # connection and raise the exception to the caller.
+ wait_for_close = False
+ raise_close_exc = True
+ original_exc = exc
+ else:
+ # Check if the connection is expected to close soon.
+ if self.protocol.close_expected():
+ wait_for_close = True
+ # If the connection is expected to close soon, set the
+ # close deadline based on the close timeout.
+
+ # Since we tested earlier that protocol.state was OPEN
+ # (or CONNECTING) and we didn't release protocol_mutex,
+ # it is certain that self.close_deadline is still None.
+ assert self.close_deadline is None
+ self.close_deadline = Deadline(self.close_timeout)
+ # Write outgoing data to the socket.
+ try:
+ self.send_data()
+ except Exception as exc:
+ if self.debug:
+ self.logger.debug("error while sending data", exc_info=True)
+ # While the only expected exception here is OSError,
+ # other exceptions would be treated identically.
+ wait_for_close = False
+ raise_close_exc = True
+ original_exc = exc
+
+ else: # self.protocol.state is not expected_state
+ # Minor layering violation: we assume that the connection
+ # will be closing soon if it isn't in the expected state.
+ wait_for_close = True
+ raise_close_exc = True
+
+ # To avoid a deadlock, release the connection lock by exiting the
+ # context manager before waiting for recv_events() to terminate.
+
+ # If the connection is expected to close soon and the close timeout
+ # elapses, close the socket to terminate the connection.
+ if wait_for_close:
+ if self.close_deadline is None:
+ timeout = self.close_timeout
+ else:
+ # Thread.join() returns immediately if timeout is negative.
+ timeout = self.close_deadline.timeout(raise_if_elapsed=False)
+ self.recv_events_thread.join(timeout)
+
+ if self.recv_events_thread.is_alive():
+ # There's no risk to overwrite another error because
+ # original_exc is never set when wait_for_close is True.
+ assert original_exc is None
+ original_exc = TimeoutError("timed out while closing connection")
+ # Set recv_events_exc before closing the socket in order to get
+ # proper exception reporting.
+ raise_close_exc = True
+ with self.protocol_mutex:
+ self.set_recv_events_exc(original_exc)
+
+ # If an error occurred, close the socket to terminate the connection and
+ # raise an exception.
+ if raise_close_exc:
+ self.close_socket()
+ self.recv_events_thread.join()
+ raise self.protocol.close_exc from original_exc
+
+ def send_data(self) -> None:
+ """
+ Send outgoing data.
+
+ This method requires holding protocol_mutex.
+
+ Raises:
+ OSError: When a socket operations fails.
+
+ """
+ assert self.protocol_mutex.locked()
+ for data in self.protocol.data_to_send():
+ if data:
+ if self.close_deadline is not None:
+ self.socket.settimeout(self.close_deadline.timeout())
+ self.socket.sendall(data)
+ else:
+ try:
+ self.socket.shutdown(socket.SHUT_WR)
+ except OSError: # socket already closed
+ pass
+
+ def set_recv_events_exc(self, exc: Optional[BaseException]) -> None:
+ """
+ Set recv_events_exc, if not set yet.
+
+ This method requires holding protocol_mutex.
+
+ """
+ assert self.protocol_mutex.locked()
+ if self.recv_events_exc is None:
+ self.recv_events_exc = exc
+
+ def close_socket(self) -> None:
+ """
+ Shutdown and close socket. Close message assembler.
+
+ Calling close_socket() guarantees that recv_events() terminates. Indeed,
+ recv_events() may block only on socket.recv() or on recv_messages.put().
+
+ """
+ # shutdown() is required to interrupt recv() on Linux.
+ try:
+ self.socket.shutdown(socket.SHUT_RDWR)
+ except OSError:
+ pass # socket is already closed
+ self.socket.close()
+ self.recv_messages.close()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/messages.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/messages.py
new file mode 100644
index 0000000000..67a22313ca
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/messages.py
@@ -0,0 +1,281 @@
+from __future__ import annotations
+
+import codecs
+import queue
+import threading
+from typing import Iterator, List, Optional, cast
+
+from ..frames import Frame, Opcode
+from ..typing import Data
+
+
+__all__ = ["Assembler"]
+
+UTF8Decoder = codecs.getincrementaldecoder("utf-8")
+
+
+class Assembler:
+ """
+ Assemble messages from frames.
+
+ """
+
+ def __init__(self) -> None:
+ # Serialize reads and writes -- except for reads via synchronization
+ # primitives provided by the threading and queue modules.
+ self.mutex = threading.Lock()
+
+ # We create a latch with two events to ensure proper interleaving of
+ # writing and reading messages.
+ # put() sets this event to tell get() that a message can be fetched.
+ self.message_complete = threading.Event()
+ # get() sets this event to let put() that the message was fetched.
+ self.message_fetched = threading.Event()
+
+ # This flag prevents concurrent calls to get() by user code.
+ self.get_in_progress = False
+ # This flag prevents concurrent calls to put() by library code.
+ self.put_in_progress = False
+
+ # Decoder for text frames, None for binary frames.
+ self.decoder: Optional[codecs.IncrementalDecoder] = None
+
+ # Buffer of frames belonging to the same message.
+ self.chunks: List[Data] = []
+
+ # When switching from "buffering" to "streaming", we use a thread-safe
+ # queue for transferring frames from the writing thread (library code)
+ # to the reading thread (user code). We're buffering when chunks_queue
+ # is None and streaming when it's a SimpleQueue. None is a sentinel
+ # value marking the end of the stream, superseding message_complete.
+
+ # Stream data from frames belonging to the same message.
+ # Remove quotes around type when dropping Python < 3.9.
+ self.chunks_queue: Optional["queue.SimpleQueue[Optional[Data]]"] = None
+
+ # This flag marks the end of the stream.
+ self.closed = False
+
+ def get(self, timeout: Optional[float] = None) -> Data:
+ """
+ Read the next message.
+
+ :meth:`get` returns a single :class:`str` or :class:`bytes`.
+
+ If the message is fragmented, :meth:`get` waits until the last frame is
+ received, then it reassembles the message and returns it. To receive
+ messages frame by frame, use :meth:`get_iter` instead.
+
+ Args:
+ timeout: If a timeout is provided and elapses before a complete
+ message is received, :meth:`get` raises :exc:`TimeoutError`.
+
+ Raises:
+ EOFError: If the stream of frames has ended.
+ RuntimeError: If two threads run :meth:`get` or :meth:``get_iter`
+ concurrently.
+
+ """
+ with self.mutex:
+ if self.closed:
+ raise EOFError("stream of frames ended")
+
+ if self.get_in_progress:
+ raise RuntimeError("get or get_iter is already running")
+
+ self.get_in_progress = True
+
+ # If the message_complete event isn't set yet, release the lock to
+ # allow put() to run and eventually set it.
+ # Locking with get_in_progress ensures only one thread can get here.
+ completed = self.message_complete.wait(timeout)
+
+ with self.mutex:
+ self.get_in_progress = False
+
+ # Waiting for a complete message timed out.
+ if not completed:
+ raise TimeoutError(f"timed out in {timeout:.1f}s")
+
+ # get() was unblocked by close() rather than put().
+ if self.closed:
+ raise EOFError("stream of frames ended")
+
+ assert self.message_complete.is_set()
+ self.message_complete.clear()
+
+ joiner: Data = b"" if self.decoder is None else ""
+ # mypy cannot figure out that chunks have the proper type.
+ message: Data = joiner.join(self.chunks) # type: ignore
+
+ assert not self.message_fetched.is_set()
+ self.message_fetched.set()
+
+ self.chunks = []
+ assert self.chunks_queue is None
+
+ return message
+
+ def get_iter(self) -> Iterator[Data]:
+ """
+ Stream the next message.
+
+ Iterating the return value of :meth:`get_iter` yields a :class:`str` or
+ :class:`bytes` for each frame in the message.
+
+ The iterator must be fully consumed before calling :meth:`get_iter` or
+ :meth:`get` again. Else, :exc:`RuntimeError` is raised.
+
+ This method only makes sense for fragmented messages. If messages aren't
+ fragmented, use :meth:`get` instead.
+
+ Raises:
+ EOFError: If the stream of frames has ended.
+ RuntimeError: If two threads run :meth:`get` or :meth:``get_iter`
+ concurrently.
+
+ """
+ with self.mutex:
+ if self.closed:
+ raise EOFError("stream of frames ended")
+
+ if self.get_in_progress:
+ raise RuntimeError("get or get_iter is already running")
+
+ chunks = self.chunks
+ self.chunks = []
+ self.chunks_queue = cast(
+ # Remove quotes around type when dropping Python < 3.9.
+ "queue.SimpleQueue[Optional[Data]]",
+ queue.SimpleQueue(),
+ )
+
+ # Sending None in chunk_queue supersedes setting message_complete
+ # when switching to "streaming". If message is already complete
+ # when the switch happens, put() didn't send None, so we have to.
+ if self.message_complete.is_set():
+ self.chunks_queue.put(None)
+
+ self.get_in_progress = True
+
+ # Locking with get_in_progress ensures only one thread can get here.
+ yield from chunks
+ while True:
+ chunk = self.chunks_queue.get()
+ if chunk is None:
+ break
+ yield chunk
+
+ with self.mutex:
+ self.get_in_progress = False
+
+ assert self.message_complete.is_set()
+ self.message_complete.clear()
+
+ # get_iter() was unblocked by close() rather than put().
+ if self.closed:
+ raise EOFError("stream of frames ended")
+
+ assert not self.message_fetched.is_set()
+ self.message_fetched.set()
+
+ assert self.chunks == []
+ self.chunks_queue = None
+
+ def put(self, frame: Frame) -> None:
+ """
+ Add ``frame`` to the next message.
+
+ When ``frame`` is the final frame in a message, :meth:`put` waits until
+ the message is fetched, either by calling :meth:`get` or by fully
+ consuming the return value of :meth:`get_iter`.
+
+ :meth:`put` assumes that the stream of frames respects the protocol. If
+ it doesn't, the behavior is undefined.
+
+ Raises:
+ EOFError: If the stream of frames has ended.
+ RuntimeError: If two threads run :meth:`put` concurrently.
+
+ """
+ with self.mutex:
+ if self.closed:
+ raise EOFError("stream of frames ended")
+
+ if self.put_in_progress:
+ raise RuntimeError("put is already running")
+
+ if frame.opcode is Opcode.TEXT:
+ self.decoder = UTF8Decoder(errors="strict")
+ elif frame.opcode is Opcode.BINARY:
+ self.decoder = None
+ elif frame.opcode is Opcode.CONT:
+ pass
+ else:
+ # Ignore control frames.
+ return
+
+ data: Data
+ if self.decoder is not None:
+ data = self.decoder.decode(frame.data, frame.fin)
+ else:
+ data = frame.data
+
+ if self.chunks_queue is None:
+ self.chunks.append(data)
+ else:
+ self.chunks_queue.put(data)
+
+ if not frame.fin:
+ return
+
+ # Message is complete. Wait until it's fetched to return.
+
+ assert not self.message_complete.is_set()
+ self.message_complete.set()
+
+ if self.chunks_queue is not None:
+ self.chunks_queue.put(None)
+
+ assert not self.message_fetched.is_set()
+
+ self.put_in_progress = True
+
+ # Release the lock to allow get() to run and eventually set the event.
+ self.message_fetched.wait()
+
+ with self.mutex:
+ self.put_in_progress = False
+
+ assert self.message_fetched.is_set()
+ self.message_fetched.clear()
+
+ # put() was unblocked by close() rather than get() or get_iter().
+ if self.closed:
+ raise EOFError("stream of frames ended")
+
+ self.decoder = None
+
+ def close(self) -> None:
+ """
+ End the stream of frames.
+
+ Callling :meth:`close` concurrently with :meth:`get`, :meth:`get_iter`,
+ or :meth:`put` is safe. They will raise :exc:`EOFError`.
+
+ """
+ with self.mutex:
+ if self.closed:
+ return
+
+ self.closed = True
+
+ # Unblock get or get_iter.
+ if self.get_in_progress:
+ self.message_complete.set()
+ if self.chunks_queue is not None:
+ self.chunks_queue.put(None)
+
+ # Unblock put().
+ if self.put_in_progress:
+ self.message_fetched.set()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/server.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/server.py
new file mode 100644
index 0000000000..14767968c9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/server.py
@@ -0,0 +1,530 @@
+from __future__ import annotations
+
+import http
+import logging
+import os
+import selectors
+import socket
+import ssl
+import sys
+import threading
+from types import TracebackType
+from typing import Any, Callable, Optional, Sequence, Type
+
+from websockets.frames import CloseCode
+
+from ..extensions.base import ServerExtensionFactory
+from ..extensions.permessage_deflate import enable_server_permessage_deflate
+from ..headers import validate_subprotocols
+from ..http import USER_AGENT
+from ..http11 import Request, Response
+from ..protocol import CONNECTING, OPEN, Event
+from ..server import ServerProtocol
+from ..typing import LoggerLike, Origin, Subprotocol
+from .connection import Connection
+from .utils import Deadline
+
+
+__all__ = ["serve", "unix_serve", "ServerConnection", "WebSocketServer"]
+
+
+class ServerConnection(Connection):
+ """
+ Threaded implementation of a WebSocket server connection.
+
+ :class:`ServerConnection` provides :meth:`recv` and :meth:`send` methods for
+ receiving and sending messages.
+
+ It supports iteration to receive messages::
+
+ for message in websocket:
+ process(message)
+
+ The iterator exits normally when the connection is closed with close code
+ 1000 (OK) or 1001 (going away) or without a close code. It raises a
+ :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is
+ closed with any other code.
+
+ Args:
+ socket: Socket connected to a WebSocket client.
+ protocol: Sans-I/O connection.
+ close_timeout: Timeout for closing the connection in seconds.
+
+ """
+
+ def __init__(
+ self,
+ socket: socket.socket,
+ protocol: ServerProtocol,
+ *,
+ close_timeout: Optional[float] = 10,
+ ) -> None:
+ self.protocol: ServerProtocol
+ self.request_rcvd = threading.Event()
+ super().__init__(
+ socket,
+ protocol,
+ close_timeout=close_timeout,
+ )
+
+ def handshake(
+ self,
+ process_request: Optional[
+ Callable[
+ [ServerConnection, Request],
+ Optional[Response],
+ ]
+ ] = None,
+ process_response: Optional[
+ Callable[
+ [ServerConnection, Request, Response],
+ Optional[Response],
+ ]
+ ] = None,
+ server_header: Optional[str] = USER_AGENT,
+ timeout: Optional[float] = None,
+ ) -> None:
+ """
+ Perform the opening handshake.
+
+ """
+ if not self.request_rcvd.wait(timeout):
+ self.close_socket()
+ self.recv_events_thread.join()
+ raise TimeoutError("timed out during handshake")
+
+ if self.request is None:
+ self.close_socket()
+ self.recv_events_thread.join()
+ raise ConnectionError("connection closed during handshake")
+
+ with self.send_context(expected_state=CONNECTING):
+ self.response = None
+
+ if process_request is not None:
+ try:
+ self.response = process_request(self, self.request)
+ except Exception as exc:
+ self.protocol.handshake_exc = exc
+ self.logger.error("opening handshake failed", exc_info=True)
+ self.response = self.protocol.reject(
+ http.HTTPStatus.INTERNAL_SERVER_ERROR,
+ (
+ "Failed to open a WebSocket connection.\n"
+ "See server log for more information.\n"
+ ),
+ )
+
+ if self.response is None:
+ self.response = self.protocol.accept(self.request)
+
+ if server_header is not None:
+ self.response.headers["Server"] = server_header
+
+ if process_response is not None:
+ try:
+ response = process_response(self, self.request, self.response)
+ except Exception as exc:
+ self.protocol.handshake_exc = exc
+ self.logger.error("opening handshake failed", exc_info=True)
+ self.response = self.protocol.reject(
+ http.HTTPStatus.INTERNAL_SERVER_ERROR,
+ (
+ "Failed to open a WebSocket connection.\n"
+ "See server log for more information.\n"
+ ),
+ )
+ else:
+ if response is not None:
+ self.response = response
+
+ self.protocol.send_response(self.response)
+
+ if self.protocol.state is not OPEN:
+ self.recv_events_thread.join(self.close_timeout)
+ self.close_socket()
+ self.recv_events_thread.join()
+
+ if self.protocol.handshake_exc is not None:
+ raise self.protocol.handshake_exc
+
+ def process_event(self, event: Event) -> None:
+ """
+ Process one incoming event.
+
+ """
+ # First event - handshake request.
+ if self.request is None:
+ assert isinstance(event, Request)
+ self.request = event
+ self.request_rcvd.set()
+ # Later events - frames.
+ else:
+ super().process_event(event)
+
+ def recv_events(self) -> None:
+ """
+ Read incoming data from the socket and process events.
+
+ """
+ try:
+ super().recv_events()
+ finally:
+ # If the connection is closed during the handshake, unblock it.
+ self.request_rcvd.set()
+
+
+class WebSocketServer:
+ """
+ WebSocket server returned by :func:`serve`.
+
+ This class mirrors the API of :class:`~socketserver.BaseServer`, notably the
+ :meth:`~socketserver.BaseServer.serve_forever` and
+ :meth:`~socketserver.BaseServer.shutdown` methods, as well as the context
+ manager protocol.
+
+ Args:
+ socket: Server socket listening for new connections.
+ handler: Handler for one connection. Receives the socket and address
+ returned by :meth:`~socket.socket.accept`.
+ logger: Logger for this server.
+
+ """
+
+ def __init__(
+ self,
+ socket: socket.socket,
+ handler: Callable[[socket.socket, Any], None],
+ logger: Optional[LoggerLike] = None,
+ ):
+ self.socket = socket
+ self.handler = handler
+ if logger is None:
+ logger = logging.getLogger("websockets.server")
+ self.logger = logger
+ if sys.platform != "win32":
+ self.shutdown_watcher, self.shutdown_notifier = os.pipe()
+
+ def serve_forever(self) -> None:
+ """
+ See :meth:`socketserver.BaseServer.serve_forever`.
+
+ This method doesn't return. Calling :meth:`shutdown` from another thread
+ stops the server.
+
+ Typical use::
+
+ with serve(...) as server:
+ server.serve_forever()
+
+ """
+ poller = selectors.DefaultSelector()
+ poller.register(self.socket, selectors.EVENT_READ)
+ if sys.platform != "win32":
+ poller.register(self.shutdown_watcher, selectors.EVENT_READ)
+
+ while True:
+ poller.select()
+ try:
+ # If the socket is closed, this will raise an exception and exit
+ # the loop. So we don't need to check the return value of select().
+ sock, addr = self.socket.accept()
+ except OSError:
+ break
+ thread = threading.Thread(target=self.handler, args=(sock, addr))
+ thread.start()
+
+ def shutdown(self) -> None:
+ """
+ See :meth:`socketserver.BaseServer.shutdown`.
+
+ """
+ self.socket.close()
+ if sys.platform != "win32":
+ os.write(self.shutdown_notifier, b"x")
+
+ def fileno(self) -> int:
+ """
+ See :meth:`socketserver.BaseServer.fileno`.
+
+ """
+ return self.socket.fileno()
+
+ def __enter__(self) -> WebSocketServer:
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_value: Optional[BaseException],
+ traceback: Optional[TracebackType],
+ ) -> None:
+ self.shutdown()
+
+
+def serve(
+ handler: Callable[[ServerConnection], None],
+ host: Optional[str] = None,
+ port: Optional[int] = None,
+ *,
+ # TCP/TLS — unix and path are only for unix_serve()
+ sock: Optional[socket.socket] = None,
+ ssl_context: Optional[ssl.SSLContext] = None,
+ unix: bool = False,
+ path: Optional[str] = None,
+ # WebSocket
+ origins: Optional[Sequence[Optional[Origin]]] = None,
+ extensions: Optional[Sequence[ServerExtensionFactory]] = None,
+ subprotocols: Optional[Sequence[Subprotocol]] = None,
+ select_subprotocol: Optional[
+ Callable[
+ [ServerConnection, Sequence[Subprotocol]],
+ Optional[Subprotocol],
+ ]
+ ] = None,
+ process_request: Optional[
+ Callable[
+ [ServerConnection, Request],
+ Optional[Response],
+ ]
+ ] = None,
+ process_response: Optional[
+ Callable[
+ [ServerConnection, Request, Response],
+ Optional[Response],
+ ]
+ ] = None,
+ server_header: Optional[str] = USER_AGENT,
+ compression: Optional[str] = "deflate",
+ # Timeouts
+ open_timeout: Optional[float] = 10,
+ close_timeout: Optional[float] = 10,
+ # Limits
+ max_size: Optional[int] = 2**20,
+ # Logging
+ logger: Optional[LoggerLike] = None,
+ # Escape hatch for advanced customization
+ create_connection: Optional[Type[ServerConnection]] = None,
+) -> WebSocketServer:
+ """
+ Create a WebSocket server listening on ``host`` and ``port``.
+
+ Whenever a client connects, the server creates a :class:`ServerConnection`,
+ performs the opening handshake, and delegates to the ``handler``.
+
+ The handler receives a :class:`ServerConnection` instance, which you can use
+ to send and receive messages.
+
+ Once the handler completes, either normally or with an exception, the server
+ performs the closing handshake and closes the connection.
+
+ :class:`WebSocketServer` mirrors the API of
+ :class:`~socketserver.BaseServer`. Treat it as a context manager to ensure
+ that it will be closed and call the :meth:`~WebSocketServer.serve_forever`
+ method to serve requests::
+
+ def handler(websocket):
+ ...
+
+ with websockets.sync.server.serve(handler, ...) as server:
+ server.serve_forever()
+
+ Args:
+ handler: Connection handler. It receives the WebSocket connection,
+ which is a :class:`ServerConnection`, in argument.
+ host: Network interfaces the server binds to.
+ See :func:`~socket.create_server` for details.
+ port: TCP port the server listens on.
+ See :func:`~socket.create_server` for details.
+ sock: Preexisting TCP socket. ``sock`` replaces ``host`` and ``port``.
+ You may call :func:`socket.create_server` to create a suitable TCP
+ socket.
+ ssl_context: Configuration for enabling TLS on the connection.
+ origins: Acceptable values of the ``Origin`` header, for defending
+ against Cross-Site WebSocket Hijacking attacks. Include :obj:`None`
+ in the list if the lack of an origin is acceptable.
+ extensions: List of supported extensions, in order in which they
+ should be negotiated and run.
+ subprotocols: List of supported subprotocols, in order of decreasing
+ preference.
+ select_subprotocol: Callback for selecting a subprotocol among
+ those supported by the client and the server. It receives a
+ :class:`ServerConnection` (not a
+ :class:`~websockets.server.ServerProtocol`!) instance and a list of
+ subprotocols offered by the client. Other than the first argument,
+ it has the same behavior as the
+ :meth:`ServerProtocol.select_subprotocol
+ <websockets.server.ServerProtocol.select_subprotocol>` method.
+ process_request: Intercept the request during the opening handshake.
+ Return an HTTP response to force the response or :obj:`None` to
+ continue normally. When you force an HTTP 101 Continue response,
+ the handshake is successful. Else, the connection is aborted.
+ process_response: Intercept the response during the opening handshake.
+ Return an HTTP response to force the response or :obj:`None` to
+ continue normally. When you force an HTTP 101 Continue response,
+ the handshake is successful. Else, the connection is aborted.
+ server_header: Value of the ``Server`` response header.
+ It defaults to ``"Python/x.y.z websockets/X.Y"``. Setting it to
+ :obj:`None` removes the header.
+ compression: The "permessage-deflate" extension is enabled by default.
+ Set ``compression`` to :obj:`None` to disable it. See the
+ :doc:`compression guide <../../topics/compression>` for details.
+ open_timeout: Timeout for opening connections in seconds.
+ :obj:`None` disables the timeout.
+ close_timeout: Timeout for closing connections in seconds.
+ :obj:`None` disables the timeout.
+ max_size: Maximum size of incoming messages in bytes.
+ :obj:`None` disables the limit.
+ logger: Logger for this server.
+ It defaults to ``logging.getLogger("websockets.server")``. See the
+ :doc:`logging guide <../../topics/logging>` for details.
+ create_connection: Factory for the :class:`ServerConnection` managing
+ the connection. Set it to a wrapper or a subclass to customize
+ connection handling.
+ """
+
+ # Process parameters
+
+ if subprotocols is not None:
+ validate_subprotocols(subprotocols)
+
+ if compression == "deflate":
+ extensions = enable_server_permessage_deflate(extensions)
+ elif compression is not None:
+ raise ValueError(f"unsupported compression: {compression}")
+
+ if create_connection is None:
+ create_connection = ServerConnection
+
+ # Bind socket and listen
+
+ if sock is None:
+ if unix:
+ if path is None:
+ raise TypeError("missing path argument")
+ sock = socket.create_server(path, family=socket.AF_UNIX)
+ else:
+ sock = socket.create_server((host, port))
+ else:
+ if path is not None:
+ raise TypeError("path and sock arguments are incompatible")
+
+ # Initialize TLS wrapper
+
+ if ssl_context is not None:
+ sock = ssl_context.wrap_socket(
+ sock,
+ server_side=True,
+ # Delay TLS handshake until after we set a timeout on the socket.
+ do_handshake_on_connect=False,
+ )
+
+ # Define request handler
+
+ def conn_handler(sock: socket.socket, addr: Any) -> None:
+ # Calculate timeouts on the TLS and WebSocket handshakes.
+ # The TLS timeout must be set on the socket, then removed
+ # to avoid conflicting with the WebSocket timeout in handshake().
+ deadline = Deadline(open_timeout)
+
+ try:
+ # Disable Nagle algorithm
+
+ if not unix:
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
+
+ # Perform TLS handshake
+
+ if ssl_context is not None:
+ sock.settimeout(deadline.timeout())
+ assert isinstance(sock, ssl.SSLSocket) # mypy cannot figure this out
+ sock.do_handshake()
+ sock.settimeout(None)
+
+ # Create a closure so that select_subprotocol has access to self.
+
+ protocol_select_subprotocol: Optional[
+ Callable[
+ [ServerProtocol, Sequence[Subprotocol]],
+ Optional[Subprotocol],
+ ]
+ ] = None
+
+ if select_subprotocol is not None:
+
+ def protocol_select_subprotocol(
+ protocol: ServerProtocol,
+ subprotocols: Sequence[Subprotocol],
+ ) -> Optional[Subprotocol]:
+ # mypy doesn't know that select_subprotocol is immutable.
+ assert select_subprotocol is not None
+ # Ensure this function is only used in the intended context.
+ assert protocol is connection.protocol
+ return select_subprotocol(connection, subprotocols)
+
+ # Initialize WebSocket connection
+
+ protocol = ServerProtocol(
+ origins=origins,
+ extensions=extensions,
+ subprotocols=subprotocols,
+ select_subprotocol=protocol_select_subprotocol,
+ state=CONNECTING,
+ max_size=max_size,
+ logger=logger,
+ )
+
+ # Initialize WebSocket protocol
+
+ assert create_connection is not None # help mypy
+ connection = create_connection(
+ sock,
+ protocol,
+ close_timeout=close_timeout,
+ )
+ # On failure, handshake() closes the socket, raises an exception, and
+ # logs it.
+ connection.handshake(
+ process_request,
+ process_response,
+ server_header,
+ deadline.timeout(),
+ )
+
+ except Exception:
+ sock.close()
+ return
+
+ try:
+ handler(connection)
+ except Exception:
+ protocol.logger.error("connection handler failed", exc_info=True)
+ connection.close(CloseCode.INTERNAL_ERROR)
+ else:
+ connection.close()
+
+ # Initialize server
+
+ return WebSocketServer(sock, conn_handler, logger)
+
+
+def unix_serve(
+ handler: Callable[[ServerConnection], Any],
+ path: Optional[str] = None,
+ **kwargs: Any,
+) -> WebSocketServer:
+ """
+ Create a WebSocket server listening on a Unix socket.
+
+ This function is identical to :func:`serve`, except the ``host`` and
+ ``port`` arguments are replaced by ``path``. It's only available on Unix.
+
+ It's useful for deploying a server behind a reverse proxy such as nginx.
+
+ Args:
+ handler: Connection handler. It receives the WebSocket connection,
+ which is a :class:`ServerConnection`, in argument.
+ path: File system path to the Unix socket.
+
+ """
+ return serve(handler, path=path, unix=True, **kwargs)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/utils.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/utils.py
new file mode 100644
index 0000000000..471f32e19d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/utils.py
@@ -0,0 +1,46 @@
+from __future__ import annotations
+
+import time
+from typing import Optional
+
+
+__all__ = ["Deadline"]
+
+
+class Deadline:
+ """
+ Manage timeouts across multiple steps.
+
+ Args:
+ timeout: Time available in seconds or :obj:`None` if there is no limit.
+
+ """
+
+ def __init__(self, timeout: Optional[float]) -> None:
+ self.deadline: Optional[float]
+ if timeout is None:
+ self.deadline = None
+ else:
+ self.deadline = time.monotonic() + timeout
+
+ def timeout(self, *, raise_if_elapsed: bool = True) -> Optional[float]:
+ """
+ Calculate a timeout from a deadline.
+
+ Args:
+ raise_if_elapsed (bool): Whether to raise :exc:`TimeoutError`
+ if the deadline lapsed.
+
+ Raises:
+ TimeoutError: If the deadline lapsed.
+
+ Returns:
+ Time left in seconds or :obj:`None` if there is no limit.
+
+ """
+ if self.deadline is None:
+ return None
+ timeout = self.deadline - time.monotonic()
+ if raise_if_elapsed and timeout <= 0:
+ raise TimeoutError("timed out")
+ return timeout
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py
index e672ba0069..cc3e3ec0d9 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import http
import logging
from typing import List, NewType, Optional, Tuple, Union
@@ -7,6 +8,7 @@ from typing import List, NewType, Optional, Tuple, Union
__all__ = [
"Data",
"LoggerLike",
+ "StatusLike",
"Origin",
"Subprotocol",
"ExtensionName",
@@ -30,6 +32,11 @@ LoggerLike = Union[logging.Logger, logging.LoggerAdapter]
"""Types accepted where a :class:`~logging.Logger` is expected."""
+StatusLike = Union[http.HTTPStatus, int]
+"""
+Types accepted where an :class:`~http.HTTPStatus` is expected."""
+
+
Origin = NewType("Origin", str)
"""Value of a ``Origin`` header."""
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py
index fff0c38064..385090f66a 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py
@@ -33,8 +33,8 @@ class WebSocketURI:
port: int
path: str
query: str
- username: Optional[str]
- password: Optional[str]
+ username: Optional[str] = None
+ password: Optional[str] = None
@property
def resource_name(self) -> str:
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py
index c30bfd68f3..d1c99458e2 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+import importlib.metadata
+
__all__ = ["tag", "version", "commit"]
@@ -18,7 +20,7 @@ __all__ = ["tag", "version", "commit"]
released = True
-tag = version = commit = "10.3"
+tag = version = commit = "12.0"
if not released: # pragma: no cover
@@ -44,7 +46,11 @@ if not released: # pragma: no cover
text=True,
).stdout.strip()
# subprocess.run raises FileNotFoundError if git isn't on $PATH.
- except (FileNotFoundError, subprocess.CalledProcessError):
+ except (
+ FileNotFoundError,
+ subprocess.CalledProcessError,
+ subprocess.TimeoutExpired,
+ ):
pass
else:
description_re = r"[0-9.]+-([0-9]+)-(g[0-9a-f]{7,}(?:-dirty)?)"
@@ -56,8 +62,6 @@ if not released: # pragma: no cover
# Read version from package metadata if it is installed.
try:
- import importlib.metadata # move up when dropping Python 3.7
-
return importlib.metadata.version("websockets")
except ImportError:
pass
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/tests/__init__.py
new file mode 100644
index 0000000000..dd78609f5b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/__init__.py
@@ -0,0 +1,5 @@
+import logging
+
+
+# Avoid displaying stack traces at the ERROR logging level.
+logging.basicConfig(level=logging.CRITICAL)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_base.py b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_base.py
new file mode 100644
index 0000000000..b18ffb6fb8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_base.py
@@ -0,0 +1,4 @@
+from websockets.extensions.base import *
+
+
+# Abstract classes don't provide any behavior to test.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_permessage_deflate.py b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_permessage_deflate.py
new file mode 100644
index 0000000000..0e698566fb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_permessage_deflate.py
@@ -0,0 +1,977 @@
+import dataclasses
+import unittest
+
+from websockets.exceptions import (
+ DuplicateParameter,
+ InvalidParameterName,
+ InvalidParameterValue,
+ NegotiationError,
+ PayloadTooBig,
+ ProtocolError,
+)
+from websockets.extensions.permessage_deflate import *
+from websockets.frames import (
+ OP_BINARY,
+ OP_CLOSE,
+ OP_CONT,
+ OP_PING,
+ OP_PONG,
+ OP_TEXT,
+ Close,
+ CloseCode,
+ Frame,
+)
+
+from .utils import ClientNoOpExtensionFactory, ServerNoOpExtensionFactory
+
+
+class PerMessageDeflateTestsMixin:
+ def assertExtensionEqual(self, extension1, extension2):
+ self.assertEqual(
+ extension1.remote_no_context_takeover,
+ extension2.remote_no_context_takeover,
+ )
+ self.assertEqual(
+ extension1.local_no_context_takeover,
+ extension2.local_no_context_takeover,
+ )
+ self.assertEqual(
+ extension1.remote_max_window_bits,
+ extension2.remote_max_window_bits,
+ )
+ self.assertEqual(
+ extension1.local_max_window_bits,
+ extension2.local_max_window_bits,
+ )
+
+
+class PerMessageDeflateTests(unittest.TestCase, PerMessageDeflateTestsMixin):
+ def setUp(self):
+ # Set up an instance of the permessage-deflate extension with the most
+ # common settings. Since the extension is symmetrical, this instance
+ # may be used for testing both encoding and decoding.
+ self.extension = PerMessageDeflate(False, False, 15, 15)
+
+ def test_name(self):
+ assert self.extension.name == "permessage-deflate"
+
+ def test_repr(self):
+ self.assertExtensionEqual(eval(repr(self.extension)), self.extension)
+
+ # Control frames aren't encoded or decoded.
+
+ def test_no_encode_decode_ping_frame(self):
+ frame = Frame(OP_PING, b"")
+
+ self.assertEqual(self.extension.encode(frame), frame)
+
+ self.assertEqual(self.extension.decode(frame), frame)
+
+ def test_no_encode_decode_pong_frame(self):
+ frame = Frame(OP_PONG, b"")
+
+ self.assertEqual(self.extension.encode(frame), frame)
+
+ self.assertEqual(self.extension.decode(frame), frame)
+
+ def test_no_encode_decode_close_frame(self):
+ frame = Frame(OP_CLOSE, Close(CloseCode.NORMAL_CLOSURE, "").serialize())
+
+ self.assertEqual(self.extension.encode(frame), frame)
+
+ self.assertEqual(self.extension.decode(frame), frame)
+
+ # Data frames are encoded and decoded.
+
+ def test_encode_decode_text_frame(self):
+ frame = Frame(OP_TEXT, "café".encode("utf-8"))
+
+ enc_frame = self.extension.encode(frame)
+
+ self.assertEqual(
+ enc_frame,
+ dataclasses.replace(frame, rsv1=True, data=b"JNL;\xbc\x12\x00"),
+ )
+
+ dec_frame = self.extension.decode(enc_frame)
+
+ self.assertEqual(dec_frame, frame)
+
+ def test_encode_decode_binary_frame(self):
+ frame = Frame(OP_BINARY, b"tea")
+
+ enc_frame = self.extension.encode(frame)
+
+ self.assertEqual(
+ enc_frame,
+ dataclasses.replace(frame, rsv1=True, data=b"*IM\x04\x00"),
+ )
+
+ dec_frame = self.extension.decode(enc_frame)
+
+ self.assertEqual(dec_frame, frame)
+
+ def test_encode_decode_fragmented_text_frame(self):
+ frame1 = Frame(OP_TEXT, "café".encode("utf-8"), fin=False)
+ frame2 = Frame(OP_CONT, " & ".encode("utf-8"), fin=False)
+ frame3 = Frame(OP_CONT, "croissants".encode("utf-8"))
+
+ enc_frame1 = self.extension.encode(frame1)
+ enc_frame2 = self.extension.encode(frame2)
+ enc_frame3 = self.extension.encode(frame3)
+
+ self.assertEqual(
+ enc_frame1,
+ dataclasses.replace(
+ frame1, rsv1=True, data=b"JNL;\xbc\x12\x00\x00\x00\xff\xff"
+ ),
+ )
+ self.assertEqual(
+ enc_frame2,
+ dataclasses.replace(frame2, data=b"RPS\x00\x00\x00\x00\xff\xff"),
+ )
+ self.assertEqual(
+ enc_frame3,
+ dataclasses.replace(frame3, data=b"J.\xca\xcf,.N\xcc+)\x06\x00"),
+ )
+
+ dec_frame1 = self.extension.decode(enc_frame1)
+ dec_frame2 = self.extension.decode(enc_frame2)
+ dec_frame3 = self.extension.decode(enc_frame3)
+
+ self.assertEqual(dec_frame1, frame1)
+ self.assertEqual(dec_frame2, frame2)
+ self.assertEqual(dec_frame3, frame3)
+
+ def test_encode_decode_fragmented_binary_frame(self):
+ frame1 = Frame(OP_TEXT, b"tea ", fin=False)
+ frame2 = Frame(OP_CONT, b"time")
+
+ enc_frame1 = self.extension.encode(frame1)
+ enc_frame2 = self.extension.encode(frame2)
+
+ self.assertEqual(
+ enc_frame1,
+ dataclasses.replace(
+ frame1, rsv1=True, data=b"*IMT\x00\x00\x00\x00\xff\xff"
+ ),
+ )
+ self.assertEqual(
+ enc_frame2,
+ dataclasses.replace(frame2, data=b"*\xc9\xccM\x05\x00"),
+ )
+
+ dec_frame1 = self.extension.decode(enc_frame1)
+ dec_frame2 = self.extension.decode(enc_frame2)
+
+ self.assertEqual(dec_frame1, frame1)
+ self.assertEqual(dec_frame2, frame2)
+
+ def test_no_decode_text_frame(self):
+ frame = Frame(OP_TEXT, "café".encode("utf-8"))
+
+ # Try decoding a frame that wasn't encoded.
+ self.assertEqual(self.extension.decode(frame), frame)
+
+ def test_no_decode_binary_frame(self):
+ frame = Frame(OP_TEXT, b"tea")
+
+ # Try decoding a frame that wasn't encoded.
+ self.assertEqual(self.extension.decode(frame), frame)
+
+ def test_no_decode_fragmented_text_frame(self):
+ frame1 = Frame(OP_TEXT, "café".encode("utf-8"), fin=False)
+ frame2 = Frame(OP_CONT, " & ".encode("utf-8"), fin=False)
+ frame3 = Frame(OP_CONT, "croissants".encode("utf-8"))
+
+ dec_frame1 = self.extension.decode(frame1)
+ dec_frame2 = self.extension.decode(frame2)
+ dec_frame3 = self.extension.decode(frame3)
+
+ self.assertEqual(dec_frame1, frame1)
+ self.assertEqual(dec_frame2, frame2)
+ self.assertEqual(dec_frame3, frame3)
+
+ def test_no_decode_fragmented_binary_frame(self):
+ frame1 = Frame(OP_TEXT, b"tea ", fin=False)
+ frame2 = Frame(OP_CONT, b"time")
+
+ dec_frame1 = self.extension.decode(frame1)
+ dec_frame2 = self.extension.decode(frame2)
+
+ self.assertEqual(dec_frame1, frame1)
+ self.assertEqual(dec_frame2, frame2)
+
+ def test_context_takeover(self):
+ frame = Frame(OP_TEXT, "café".encode("utf-8"))
+
+ enc_frame1 = self.extension.encode(frame)
+ enc_frame2 = self.extension.encode(frame)
+
+ self.assertEqual(enc_frame1.data, b"JNL;\xbc\x12\x00")
+ self.assertEqual(enc_frame2.data, b"J\x06\x11\x00\x00")
+
+ def test_remote_no_context_takeover(self):
+ # No context takeover when decoding messages.
+ self.extension = PerMessageDeflate(True, False, 15, 15)
+
+ frame = Frame(OP_TEXT, "café".encode("utf-8"))
+
+ enc_frame1 = self.extension.encode(frame)
+ enc_frame2 = self.extension.encode(frame)
+
+ self.assertEqual(enc_frame1.data, b"JNL;\xbc\x12\x00")
+ self.assertEqual(enc_frame2.data, b"J\x06\x11\x00\x00")
+
+ dec_frame1 = self.extension.decode(enc_frame1)
+ self.assertEqual(dec_frame1, frame)
+
+ with self.assertRaises(ProtocolError):
+ self.extension.decode(enc_frame2)
+
+ def test_local_no_context_takeover(self):
+ # No context takeover when encoding and decoding messages.
+ self.extension = PerMessageDeflate(True, True, 15, 15)
+
+ frame = Frame(OP_TEXT, "café".encode("utf-8"))
+
+ enc_frame1 = self.extension.encode(frame)
+ enc_frame2 = self.extension.encode(frame)
+
+ self.assertEqual(enc_frame1.data, b"JNL;\xbc\x12\x00")
+ self.assertEqual(enc_frame2.data, b"JNL;\xbc\x12\x00")
+
+ dec_frame1 = self.extension.decode(enc_frame1)
+ dec_frame2 = self.extension.decode(enc_frame2)
+
+ self.assertEqual(dec_frame1, frame)
+ self.assertEqual(dec_frame2, frame)
+
+ # Compression settings can be customized.
+
+ def test_compress_settings(self):
+ # Configure an extension so that no compression actually occurs.
+ extension = PerMessageDeflate(False, False, 15, 15, {"level": 0})
+
+ frame = Frame(OP_TEXT, "café".encode("utf-8"))
+
+ enc_frame = extension.encode(frame)
+
+ self.assertEqual(
+ enc_frame,
+ dataclasses.replace(
+ frame,
+ rsv1=True,
+ data=b"\x00\x05\x00\xfa\xffcaf\xc3\xa9\x00", # not compressed
+ ),
+ )
+
+ # Frames aren't decoded beyond max_size.
+
+ def test_decompress_max_size(self):
+ frame = Frame(OP_TEXT, ("a" * 20).encode("utf-8"))
+
+ enc_frame = self.extension.encode(frame)
+
+ self.assertEqual(enc_frame.data, b"JL\xc4\x04\x00\x00")
+
+ with self.assertRaises(PayloadTooBig):
+ self.extension.decode(enc_frame, max_size=10)
+
+
+class ClientPerMessageDeflateFactoryTests(
+ unittest.TestCase, PerMessageDeflateTestsMixin
+):
+ def test_name(self):
+ assert ClientPerMessageDeflateFactory.name == "permessage-deflate"
+
+ def test_init(self):
+ for config in [
+ (False, False, 8, None), # server_max_window_bits ≥ 8
+ (False, True, 15, None), # server_max_window_bits ≤ 15
+ (True, False, None, 8), # client_max_window_bits ≥ 8
+ (True, True, None, 15), # client_max_window_bits ≤ 15
+ (False, False, None, True), # client_max_window_bits
+ (False, False, None, None, {"memLevel": 4}),
+ ]:
+ with self.subTest(config=config):
+ # This does not raise an exception.
+ ClientPerMessageDeflateFactory(*config)
+
+ def test_init_error(self):
+ for config in [
+ (False, False, 7, 8), # server_max_window_bits < 8
+ (False, True, 8, 7), # client_max_window_bits < 8
+ (True, False, 16, 15), # server_max_window_bits > 15
+ (True, True, 15, 16), # client_max_window_bits > 15
+ (False, False, True, None), # server_max_window_bits
+ (False, False, None, None, {"wbits": 11}),
+ ]:
+ with self.subTest(config=config):
+ with self.assertRaises(ValueError):
+ ClientPerMessageDeflateFactory(*config)
+
+ def test_get_request_params(self):
+ for config, result in [
+ # Test without any parameter
+ (
+ (False, False, None, None),
+ [],
+ ),
+ # Test server_no_context_takeover
+ (
+ (True, False, None, None),
+ [("server_no_context_takeover", None)],
+ ),
+ # Test client_no_context_takeover
+ (
+ (False, True, None, None),
+ [("client_no_context_takeover", None)],
+ ),
+ # Test server_max_window_bits
+ (
+ (False, False, 10, None),
+ [("server_max_window_bits", "10")],
+ ),
+ # Test client_max_window_bits
+ (
+ (False, False, None, 10),
+ [("client_max_window_bits", "10")],
+ ),
+ (
+ (False, False, None, True),
+ [("client_max_window_bits", None)],
+ ),
+ # Test all parameters together
+ (
+ (True, True, 12, 12),
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "12"),
+ ("client_max_window_bits", "12"),
+ ],
+ ),
+ ]:
+ with self.subTest(config=config):
+ factory = ClientPerMessageDeflateFactory(*config)
+ self.assertEqual(factory.get_request_params(), result)
+
+ def test_process_response_params(self):
+ for config, response_params, result in [
+ # Test without any parameter
+ (
+ (False, False, None, None),
+ [],
+ (False, False, 15, 15),
+ ),
+ (
+ (False, False, None, None),
+ [("unknown", None)],
+ InvalidParameterName,
+ ),
+ # Test server_no_context_takeover
+ (
+ (False, False, None, None),
+ [("server_no_context_takeover", None)],
+ (True, False, 15, 15),
+ ),
+ (
+ (True, False, None, None),
+ [],
+ NegotiationError,
+ ),
+ (
+ (True, False, None, None),
+ [("server_no_context_takeover", None)],
+ (True, False, 15, 15),
+ ),
+ (
+ (True, False, None, None),
+ [("server_no_context_takeover", None)] * 2,
+ DuplicateParameter,
+ ),
+ (
+ (True, False, None, None),
+ [("server_no_context_takeover", "42")],
+ InvalidParameterValue,
+ ),
+ # Test client_no_context_takeover
+ (
+ (False, False, None, None),
+ [("client_no_context_takeover", None)],
+ (False, True, 15, 15),
+ ),
+ (
+ (False, True, None, None),
+ [],
+ (False, True, 15, 15),
+ ),
+ (
+ (False, True, None, None),
+ [("client_no_context_takeover", None)],
+ (False, True, 15, 15),
+ ),
+ (
+ (False, True, None, None),
+ [("client_no_context_takeover", None)] * 2,
+ DuplicateParameter,
+ ),
+ (
+ (False, True, None, None),
+ [("client_no_context_takeover", "42")],
+ InvalidParameterValue,
+ ),
+ # Test server_max_window_bits
+ (
+ (False, False, None, None),
+ [("server_max_window_bits", "7")],
+ NegotiationError,
+ ),
+ (
+ (False, False, None, None),
+ [("server_max_window_bits", "10")],
+ (False, False, 10, 15),
+ ),
+ (
+ (False, False, None, None),
+ [("server_max_window_bits", "16")],
+ NegotiationError,
+ ),
+ (
+ (False, False, 12, None),
+ [],
+ NegotiationError,
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "10")],
+ (False, False, 10, 15),
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "12")],
+ (False, False, 12, 15),
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "13")],
+ NegotiationError,
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "12")] * 2,
+ DuplicateParameter,
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "42")],
+ InvalidParameterValue,
+ ),
+ # Test client_max_window_bits
+ (
+ (False, False, None, None),
+ [("client_max_window_bits", "10")],
+ NegotiationError,
+ ),
+ (
+ (False, False, None, True),
+ [],
+ (False, False, 15, 15),
+ ),
+ (
+ (False, False, None, True),
+ [("client_max_window_bits", "7")],
+ NegotiationError,
+ ),
+ (
+ (False, False, None, True),
+ [("client_max_window_bits", "10")],
+ (False, False, 15, 10),
+ ),
+ (
+ (False, False, None, True),
+ [("client_max_window_bits", "16")],
+ NegotiationError,
+ ),
+ (
+ (False, False, None, 12),
+ [],
+ (False, False, 15, 12),
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "10")],
+ (False, False, 15, 10),
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "12")],
+ (False, False, 15, 12),
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "13")],
+ NegotiationError,
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "12")] * 2,
+ DuplicateParameter,
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "42")],
+ InvalidParameterValue,
+ ),
+ # Test all parameters together
+ (
+ (True, True, 12, 12),
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "10"),
+ ("client_max_window_bits", "10"),
+ ],
+ (True, True, 10, 10),
+ ),
+ (
+ (False, False, None, True),
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "10"),
+ ("client_max_window_bits", "10"),
+ ],
+ (True, True, 10, 10),
+ ),
+ (
+ (True, True, 12, 12),
+ [
+ ("server_no_context_takeover", None),
+ ("server_max_window_bits", "12"),
+ ],
+ (True, True, 12, 12),
+ ),
+ ]:
+ with self.subTest(config=config, response_params=response_params):
+ factory = ClientPerMessageDeflateFactory(*config)
+ if isinstance(result, type) and issubclass(result, Exception):
+ with self.assertRaises(result):
+ factory.process_response_params(response_params, [])
+ else:
+ extension = factory.process_response_params(response_params, [])
+ expected = PerMessageDeflate(*result)
+ self.assertExtensionEqual(extension, expected)
+
+ def test_process_response_params_deduplication(self):
+ factory = ClientPerMessageDeflateFactory(False, False, None, None)
+ with self.assertRaises(NegotiationError):
+ factory.process_response_params(
+ [], [PerMessageDeflate(False, False, 15, 15)]
+ )
+
+ def test_enable_client_permessage_deflate(self):
+ for extensions, (
+ expected_len,
+ expected_position,
+ expected_compress_settings,
+ ) in [
+ (
+ None,
+ (1, 0, {"memLevel": 5}),
+ ),
+ (
+ [],
+ (1, 0, {"memLevel": 5}),
+ ),
+ (
+ [ClientNoOpExtensionFactory()],
+ (2, 1, {"memLevel": 5}),
+ ),
+ (
+ [ClientPerMessageDeflateFactory(compress_settings={"memLevel": 7})],
+ (1, 0, {"memLevel": 7}),
+ ),
+ (
+ [
+ ClientPerMessageDeflateFactory(compress_settings={"memLevel": 7}),
+ ClientNoOpExtensionFactory(),
+ ],
+ (2, 0, {"memLevel": 7}),
+ ),
+ (
+ [
+ ClientNoOpExtensionFactory(),
+ ClientPerMessageDeflateFactory(compress_settings={"memLevel": 7}),
+ ],
+ (2, 1, {"memLevel": 7}),
+ ),
+ ]:
+ with self.subTest(extensions=extensions):
+ extensions = enable_client_permessage_deflate(extensions)
+ self.assertEqual(len(extensions), expected_len)
+ extension = extensions[expected_position]
+ self.assertIsInstance(extension, ClientPerMessageDeflateFactory)
+ self.assertEqual(
+ extension.compress_settings,
+ expected_compress_settings,
+ )
+
+
+class ServerPerMessageDeflateFactoryTests(
+ unittest.TestCase, PerMessageDeflateTestsMixin
+):
+ def test_name(self):
+ assert ServerPerMessageDeflateFactory.name == "permessage-deflate"
+
+ def test_init(self):
+ for config in [
+ (False, False, 8, None), # server_max_window_bits ≥ 8
+ (False, True, 15, None), # server_max_window_bits ≤ 15
+ (True, False, None, 8), # client_max_window_bits ≥ 8
+ (True, True, None, 15), # client_max_window_bits ≤ 15
+ (False, False, None, None, {"memLevel": 4}),
+ (False, False, None, 12, {}, True), # require_client_max_window_bits
+ ]:
+ with self.subTest(config=config):
+ # This does not raise an exception.
+ ServerPerMessageDeflateFactory(*config)
+
+ def test_init_error(self):
+ for config in [
+ (False, False, 7, 8), # server_max_window_bits < 8
+ (False, True, 8, 7), # client_max_window_bits < 8
+ (True, False, 16, 15), # server_max_window_bits > 15
+ (True, True, 15, 16), # client_max_window_bits > 15
+ (False, False, None, True), # client_max_window_bits
+ (False, False, True, None), # server_max_window_bits
+ (False, False, None, None, {"wbits": 11}),
+ (False, False, None, None, {}, True), # require_client_max_window_bits
+ ]:
+ with self.subTest(config=config):
+ with self.assertRaises(ValueError):
+ ServerPerMessageDeflateFactory(*config)
+
+ def test_process_request_params(self):
+ # Parameters in result appear swapped vs. config because the order is
+ # (remote, local) vs. (server, client).
+ for config, request_params, response_params, result in [
+ # Test without any parameter
+ (
+ (False, False, None, None),
+ [],
+ [],
+ (False, False, 15, 15),
+ ),
+ (
+ (False, False, None, None),
+ [("unknown", None)],
+ None,
+ InvalidParameterName,
+ ),
+ # Test server_no_context_takeover
+ (
+ (False, False, None, None),
+ [("server_no_context_takeover", None)],
+ [("server_no_context_takeover", None)],
+ (False, True, 15, 15),
+ ),
+ (
+ (True, False, None, None),
+ [],
+ [("server_no_context_takeover", None)],
+ (False, True, 15, 15),
+ ),
+ (
+ (True, False, None, None),
+ [("server_no_context_takeover", None)],
+ [("server_no_context_takeover", None)],
+ (False, True, 15, 15),
+ ),
+ (
+ (True, False, None, None),
+ [("server_no_context_takeover", None)] * 2,
+ None,
+ DuplicateParameter,
+ ),
+ (
+ (True, False, None, None),
+ [("server_no_context_takeover", "42")],
+ None,
+ InvalidParameterValue,
+ ),
+ # Test client_no_context_takeover
+ (
+ (False, False, None, None),
+ [("client_no_context_takeover", None)],
+ [("client_no_context_takeover", None)], # doesn't matter
+ (True, False, 15, 15),
+ ),
+ (
+ (False, True, None, None),
+ [],
+ [("client_no_context_takeover", None)],
+ (True, False, 15, 15),
+ ),
+ (
+ (False, True, None, None),
+ [("client_no_context_takeover", None)],
+ [("client_no_context_takeover", None)], # doesn't matter
+ (True, False, 15, 15),
+ ),
+ (
+ (False, True, None, None),
+ [("client_no_context_takeover", None)] * 2,
+ None,
+ DuplicateParameter,
+ ),
+ (
+ (False, True, None, None),
+ [("client_no_context_takeover", "42")],
+ None,
+ InvalidParameterValue,
+ ),
+ # Test server_max_window_bits
+ (
+ (False, False, None, None),
+ [("server_max_window_bits", "7")],
+ None,
+ NegotiationError,
+ ),
+ (
+ (False, False, None, None),
+ [("server_max_window_bits", "10")],
+ [("server_max_window_bits", "10")],
+ (False, False, 15, 10),
+ ),
+ (
+ (False, False, None, None),
+ [("server_max_window_bits", "16")],
+ None,
+ NegotiationError,
+ ),
+ (
+ (False, False, 12, None),
+ [],
+ [("server_max_window_bits", "12")],
+ (False, False, 15, 12),
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "10")],
+ [("server_max_window_bits", "10")],
+ (False, False, 15, 10),
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "12")],
+ [("server_max_window_bits", "12")],
+ (False, False, 15, 12),
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "13")],
+ [("server_max_window_bits", "12")],
+ (False, False, 15, 12),
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "12")] * 2,
+ None,
+ DuplicateParameter,
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "42")],
+ None,
+ InvalidParameterValue,
+ ),
+ # Test client_max_window_bits
+ (
+ (False, False, None, None),
+ [("client_max_window_bits", None)],
+ [],
+ (False, False, 15, 15),
+ ),
+ (
+ (False, False, None, None),
+ [("client_max_window_bits", "7")],
+ None,
+ InvalidParameterValue,
+ ),
+ (
+ (False, False, None, None),
+ [("client_max_window_bits", "10")],
+ [("client_max_window_bits", "10")], # doesn't matter
+ (False, False, 10, 15),
+ ),
+ (
+ (False, False, None, None),
+ [("client_max_window_bits", "16")],
+ None,
+ InvalidParameterValue,
+ ),
+ (
+ (False, False, None, 12),
+ [],
+ [],
+ (False, False, 15, 15),
+ ),
+ (
+ (False, False, None, 12, {}, True),
+ [],
+ None,
+ NegotiationError,
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", None)],
+ [("client_max_window_bits", "12")],
+ (False, False, 12, 15),
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "10")],
+ [("client_max_window_bits", "10")],
+ (False, False, 10, 15),
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "12")],
+ [("client_max_window_bits", "12")], # doesn't matter
+ (False, False, 12, 15),
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "13")],
+ [("client_max_window_bits", "12")], # doesn't matter
+ (False, False, 12, 15),
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "12")] * 2,
+ None,
+ DuplicateParameter,
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "42")],
+ None,
+ InvalidParameterValue,
+ ),
+ # Test all parameters together
+ (
+ (True, True, 12, 12),
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "10"),
+ ("client_max_window_bits", "10"),
+ ],
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "10"),
+ ("client_max_window_bits", "10"),
+ ],
+ (True, True, 10, 10),
+ ),
+ (
+ (False, False, None, None),
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "10"),
+ ("client_max_window_bits", "10"),
+ ],
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "10"),
+ ("client_max_window_bits", "10"),
+ ],
+ (True, True, 10, 10),
+ ),
+ (
+ (True, True, 12, 12),
+ [("client_max_window_bits", None)],
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "12"),
+ ("client_max_window_bits", "12"),
+ ],
+ (True, True, 12, 12),
+ ),
+ ]:
+ with self.subTest(
+ config=config,
+ request_params=request_params,
+ response_params=response_params,
+ ):
+ factory = ServerPerMessageDeflateFactory(*config)
+ if isinstance(result, type) and issubclass(result, Exception):
+ with self.assertRaises(result):
+ factory.process_request_params(request_params, [])
+ else:
+ params, extension = factory.process_request_params(
+ request_params, []
+ )
+ self.assertEqual(params, response_params)
+ expected = PerMessageDeflate(*result)
+ self.assertExtensionEqual(extension, expected)
+
+ def test_process_response_params_deduplication(self):
+ factory = ServerPerMessageDeflateFactory(False, False, None, None)
+ with self.assertRaises(NegotiationError):
+ factory.process_request_params(
+ [], [PerMessageDeflate(False, False, 15, 15)]
+ )
+
+ def test_enable_server_permessage_deflate(self):
+ for extensions, (
+ expected_len,
+ expected_position,
+ expected_compress_settings,
+ ) in [
+ (
+ None,
+ (1, 0, {"memLevel": 5}),
+ ),
+ (
+ [],
+ (1, 0, {"memLevel": 5}),
+ ),
+ (
+ [ServerNoOpExtensionFactory()],
+ (2, 1, {"memLevel": 5}),
+ ),
+ (
+ [ServerPerMessageDeflateFactory(compress_settings={"memLevel": 7})],
+ (1, 0, {"memLevel": 7}),
+ ),
+ (
+ [
+ ServerPerMessageDeflateFactory(compress_settings={"memLevel": 7}),
+ ServerNoOpExtensionFactory(),
+ ],
+ (2, 0, {"memLevel": 7}),
+ ),
+ (
+ [
+ ServerNoOpExtensionFactory(),
+ ServerPerMessageDeflateFactory(compress_settings={"memLevel": 7}),
+ ],
+ (2, 1, {"memLevel": 7}),
+ ),
+ ]:
+ with self.subTest(extensions=extensions):
+ extensions = enable_server_permessage_deflate(extensions)
+ self.assertEqual(len(extensions), expected_len)
+ extension = extensions[expected_position]
+ self.assertIsInstance(extension, ServerPerMessageDeflateFactory)
+ self.assertEqual(
+ extension.compress_settings,
+ expected_compress_settings,
+ )
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/utils.py b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/utils.py
new file mode 100644
index 0000000000..24fb74b4e6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/utils.py
@@ -0,0 +1,113 @@
+import dataclasses
+
+from websockets.exceptions import NegotiationError
+
+
+class OpExtension:
+ name = "x-op"
+
+ def __init__(self, op=None):
+ self.op = op
+
+ def decode(self, frame, *, max_size=None):
+ return frame # pragma: no cover
+
+ def encode(self, frame):
+ return frame # pragma: no cover
+
+ def __eq__(self, other):
+ return isinstance(other, OpExtension) and self.op == other.op
+
+
+class ClientOpExtensionFactory:
+ name = "x-op"
+
+ def __init__(self, op=None):
+ self.op = op
+
+ def get_request_params(self):
+ return [("op", self.op)]
+
+ def process_response_params(self, params, accepted_extensions):
+ if params != [("op", self.op)]:
+ raise NegotiationError()
+ return OpExtension(self.op)
+
+
+class ServerOpExtensionFactory:
+ name = "x-op"
+
+ def __init__(self, op=None):
+ self.op = op
+
+ def process_request_params(self, params, accepted_extensions):
+ if params != [("op", self.op)]:
+ raise NegotiationError()
+ return [("op", self.op)], OpExtension(self.op)
+
+
+class NoOpExtension:
+ name = "x-no-op"
+
+ def __repr__(self):
+ return "NoOpExtension()"
+
+ def decode(self, frame, *, max_size=None):
+ return frame
+
+ def encode(self, frame):
+ return frame
+
+
+class ClientNoOpExtensionFactory:
+ name = "x-no-op"
+
+ def get_request_params(self):
+ return []
+
+ def process_response_params(self, params, accepted_extensions):
+ if params:
+ raise NegotiationError()
+ return NoOpExtension()
+
+
+class ServerNoOpExtensionFactory:
+ name = "x-no-op"
+
+ def __init__(self, params=None):
+ self.params = params or []
+
+ def process_request_params(self, params, accepted_extensions):
+ return self.params, NoOpExtension()
+
+
+class Rsv2Extension:
+ name = "x-rsv2"
+
+ def decode(self, frame, *, max_size=None):
+ assert frame.rsv2
+ return dataclasses.replace(frame, rsv2=False)
+
+ def encode(self, frame):
+ assert not frame.rsv2
+ return dataclasses.replace(frame, rsv2=True)
+
+ def __eq__(self, other):
+ return isinstance(other, Rsv2Extension)
+
+
+class ClientRsv2ExtensionFactory:
+ name = "x-rsv2"
+
+ def get_request_params(self):
+ return []
+
+ def process_response_params(self, params, accepted_extensions):
+ return Rsv2Extension()
+
+
+class ServerRsv2ExtensionFactory:
+ name = "x-rsv2"
+
+ def process_request_params(self, params, accepted_extensions):
+ return [], Rsv2Extension()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_auth.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_auth.py
new file mode 100644
index 0000000000..3754bcf3a5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_auth.py
@@ -0,0 +1,184 @@
+import hmac
+import unittest
+import urllib.error
+
+from websockets.exceptions import InvalidStatusCode
+from websockets.headers import build_authorization_basic
+from websockets.legacy.auth import *
+from websockets.legacy.auth import is_credentials
+
+from .test_client_server import ClientServerTestsMixin, with_client, with_server
+from .utils import AsyncioTestCase
+
+
+class AuthTests(unittest.TestCase):
+ def test_is_credentials(self):
+ self.assertTrue(is_credentials(("username", "password")))
+
+ def test_is_not_credentials(self):
+ self.assertFalse(is_credentials(None))
+ self.assertFalse(is_credentials("username"))
+
+
+class CustomWebSocketServerProtocol(BasicAuthWebSocketServerProtocol):
+ async def process_request(self, path, request_headers):
+ type(self).used = True
+ return await super().process_request(path, request_headers)
+
+
+class CheckWebSocketServerProtocol(BasicAuthWebSocketServerProtocol):
+ async def check_credentials(self, username, password):
+ return hmac.compare_digest(password, "letmein")
+
+
+class AuthClientServerTests(ClientServerTestsMixin, AsyncioTestCase):
+ create_protocol = basic_auth_protocol_factory(
+ realm="auth-tests", credentials=("hello", "iloveyou")
+ )
+
+ @with_server(create_protocol=create_protocol)
+ @with_client(user_info=("hello", "iloveyou"))
+ def test_basic_auth(self):
+ req_headers = self.client.request_headers
+ resp_headers = self.client.response_headers
+ self.assertEqual(req_headers["Authorization"], "Basic aGVsbG86aWxvdmV5b3U=")
+ self.assertNotIn("WWW-Authenticate", resp_headers)
+
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.loop.run_until_complete(self.client.recv())
+
+ def test_basic_auth_server_no_credentials(self):
+ with self.assertRaises(TypeError) as raised:
+ basic_auth_protocol_factory(realm="auth-tests", credentials=None)
+ self.assertEqual(
+ str(raised.exception), "provide either credentials or check_credentials"
+ )
+
+ def test_basic_auth_server_bad_credentials(self):
+ with self.assertRaises(TypeError) as raised:
+ basic_auth_protocol_factory(realm="auth-tests", credentials=42)
+ self.assertEqual(str(raised.exception), "invalid credentials argument: 42")
+
+ create_protocol_multiple_credentials = basic_auth_protocol_factory(
+ realm="auth-tests",
+ credentials=[("hello", "iloveyou"), ("goodbye", "stillloveu")],
+ )
+
+ @with_server(create_protocol=create_protocol_multiple_credentials)
+ @with_client(user_info=("hello", "iloveyou"))
+ def test_basic_auth_server_multiple_credentials(self):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.loop.run_until_complete(self.client.recv())
+
+ def test_basic_auth_bad_multiple_credentials(self):
+ with self.assertRaises(TypeError) as raised:
+ basic_auth_protocol_factory(
+ realm="auth-tests", credentials=[("hello", "iloveyou"), 42]
+ )
+ self.assertEqual(
+ str(raised.exception),
+ "invalid credentials argument: [('hello', 'iloveyou'), 42]",
+ )
+
+ async def check_credentials(username, password):
+ return hmac.compare_digest(password, "iloveyou")
+
+ create_protocol_check_credentials = basic_auth_protocol_factory(
+ realm="auth-tests",
+ check_credentials=check_credentials,
+ )
+
+ @with_server(create_protocol=create_protocol_check_credentials)
+ @with_client(user_info=("hello", "iloveyou"))
+ def test_basic_auth_check_credentials(self):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.loop.run_until_complete(self.client.recv())
+
+ create_protocol_custom_protocol = basic_auth_protocol_factory(
+ realm="auth-tests",
+ credentials=[("hello", "iloveyou")],
+ create_protocol=CustomWebSocketServerProtocol,
+ )
+
+ @with_server(create_protocol=create_protocol_custom_protocol)
+ @with_client(user_info=("hello", "iloveyou"))
+ def test_basic_auth_custom_protocol(self):
+ self.assertTrue(CustomWebSocketServerProtocol.used)
+ del CustomWebSocketServerProtocol.used
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.loop.run_until_complete(self.client.recv())
+
+ @with_server(create_protocol=CheckWebSocketServerProtocol)
+ @with_client(user_info=("hello", "letmein"))
+ def test_basic_auth_custom_protocol_subclass(self):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.loop.run_until_complete(self.client.recv())
+
+ # CustomWebSocketServerProtocol doesn't override check_credentials
+ @with_server(create_protocol=CustomWebSocketServerProtocol)
+ def test_basic_auth_defaults_to_deny_all(self):
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client(user_info=("hello", "iloveyou"))
+ self.assertEqual(raised.exception.status_code, 401)
+
+ @with_server(create_protocol=create_protocol)
+ def test_basic_auth_missing_credentials(self):
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client()
+ self.assertEqual(raised.exception.status_code, 401)
+
+ @with_server(create_protocol=create_protocol)
+ def test_basic_auth_missing_credentials_details(self):
+ with self.assertRaises(urllib.error.HTTPError) as raised:
+ self.loop.run_until_complete(self.make_http_request())
+ self.assertEqual(raised.exception.code, 401)
+ self.assertEqual(
+ raised.exception.headers["WWW-Authenticate"],
+ 'Basic realm="auth-tests", charset="UTF-8"',
+ )
+ self.assertEqual(raised.exception.read().decode(), "Missing credentials\n")
+
+ @with_server(create_protocol=create_protocol)
+ def test_basic_auth_unsupported_credentials(self):
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client(extra_headers={"Authorization": "Digest ..."})
+ self.assertEqual(raised.exception.status_code, 401)
+
+ @with_server(create_protocol=create_protocol)
+ def test_basic_auth_unsupported_credentials_details(self):
+ with self.assertRaises(urllib.error.HTTPError) as raised:
+ self.loop.run_until_complete(
+ self.make_http_request(headers={"Authorization": "Digest ..."})
+ )
+ self.assertEqual(raised.exception.code, 401)
+ self.assertEqual(
+ raised.exception.headers["WWW-Authenticate"],
+ 'Basic realm="auth-tests", charset="UTF-8"',
+ )
+ self.assertEqual(raised.exception.read().decode(), "Unsupported credentials\n")
+
+ @with_server(create_protocol=create_protocol)
+ def test_basic_auth_invalid_username(self):
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client(user_info=("goodbye", "iloveyou"))
+ self.assertEqual(raised.exception.status_code, 401)
+
+ @with_server(create_protocol=create_protocol)
+ def test_basic_auth_invalid_password(self):
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client(user_info=("hello", "ihateyou"))
+ self.assertEqual(raised.exception.status_code, 401)
+
+ @with_server(create_protocol=create_protocol)
+ def test_basic_auth_invalid_credentials_details(self):
+ with self.assertRaises(urllib.error.HTTPError) as raised:
+ authorization = build_authorization_basic("hello", "ihateyou")
+ self.loop.run_until_complete(
+ self.make_http_request(headers={"Authorization": authorization})
+ )
+ self.assertEqual(raised.exception.code, 401)
+ self.assertEqual(
+ raised.exception.headers["WWW-Authenticate"],
+ 'Basic realm="auth-tests", charset="UTF-8"',
+ )
+ self.assertEqual(raised.exception.read().decode(), "Invalid credentials\n")
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_client_server.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_client_server.py
new file mode 100644
index 0000000000..c49d91b707
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_client_server.py
@@ -0,0 +1,1636 @@
+import asyncio
+import contextlib
+import functools
+import http
+import logging
+import platform
+import random
+import socket
+import ssl
+import sys
+import unittest
+import unittest.mock
+import urllib.error
+import urllib.request
+import warnings
+
+from websockets.datastructures import Headers
+from websockets.exceptions import (
+ ConnectionClosed,
+ InvalidHandshake,
+ InvalidHeader,
+ InvalidStatusCode,
+ NegotiationError,
+)
+from websockets.extensions.permessage_deflate import (
+ ClientPerMessageDeflateFactory,
+ PerMessageDeflate,
+ ServerPerMessageDeflateFactory,
+)
+from websockets.frames import CloseCode
+from websockets.http import USER_AGENT
+from websockets.legacy.client import *
+from websockets.legacy.compatibility import asyncio_timeout
+from websockets.legacy.handshake import build_response
+from websockets.legacy.http import read_response
+from websockets.legacy.server import *
+from websockets.protocol import State
+from websockets.uri import parse_uri
+
+from ..extensions.utils import (
+ ClientNoOpExtensionFactory,
+ NoOpExtension,
+ ServerNoOpExtensionFactory,
+)
+from ..utils import CERTIFICATE, MS, temp_unix_socket_path
+from .utils import AsyncioTestCase
+
+
+async def default_handler(ws):
+ if ws.path == "/deprecated_attributes":
+ await ws.recv() # delay that allows catching warnings
+ await ws.send(repr((ws.host, ws.port, ws.secure)))
+ elif ws.path == "/close_timeout":
+ await ws.send(repr(ws.close_timeout))
+ elif ws.path == "/path":
+ await ws.send(str(ws.path))
+ elif ws.path == "/headers":
+ await ws.send(repr(ws.request_headers))
+ await ws.send(repr(ws.response_headers))
+ elif ws.path == "/extensions":
+ await ws.send(repr(ws.extensions))
+ elif ws.path == "/subprotocol":
+ await ws.send(repr(ws.subprotocol))
+ elif ws.path == "/slow_stop":
+ await ws.wait_closed()
+ await asyncio.sleep(2 * MS)
+ else:
+ await ws.send((await ws.recv()))
+
+
+async def redirect_request(path, headers, test, status):
+ if path == "/absolute_redirect":
+ location = get_server_uri(test.server, test.secure, "/")
+ elif path == "/relative_redirect":
+ location = "/"
+ elif path == "/infinite":
+ location = get_server_uri(test.server, test.secure, "/infinite")
+ elif path == "/force_insecure":
+ location = get_server_uri(test.server, False, "/")
+ elif path == "/missing_location":
+ return status, {}, b""
+ else:
+ return None
+ return status, {"Location": location}, b""
+
+
+@contextlib.contextmanager
+def temp_test_server(test, **kwargs):
+ test.start_server(**kwargs)
+ try:
+ yield
+ finally:
+ test.stop_server()
+
+
+def temp_test_redirecting_server(test, status=http.HTTPStatus.FOUND, **kwargs):
+ process_request = functools.partial(redirect_request, test=test, status=status)
+ return temp_test_server(test, process_request=process_request, **kwargs)
+
+
+@contextlib.contextmanager
+def temp_test_client(test, *args, **kwargs):
+ test.start_client(*args, **kwargs)
+ try:
+ yield
+ finally:
+ test.stop_client()
+
+
+def with_manager(manager, *args, **kwargs):
+ """
+ Return a decorator that wraps a function with a context manager.
+
+ """
+
+ def decorate(func):
+ @functools.wraps(func)
+ def _decorate(self, *_args, **_kwargs):
+ with manager(self, *args, **kwargs):
+ return func(self, *_args, **_kwargs)
+
+ return _decorate
+
+ return decorate
+
+
+def with_server(**kwargs):
+ """
+ Return a decorator for TestCase methods that starts and stops a server.
+
+ """
+ return with_manager(temp_test_server, **kwargs)
+
+
+def with_client(*args, **kwargs):
+ """
+ Return a decorator for TestCase methods that starts and stops a client.
+
+ """
+ return with_manager(temp_test_client, *args, **kwargs)
+
+
+def get_server_address(server):
+ """
+ Return an address on which the given server listens.
+
+ """
+ # Pick a random socket in order to test both IPv4 and IPv6 on systems
+ # where both are available. Randomizing tests is usually a bad idea. If
+ # needed, either use the first socket, or test separately IPv4 and IPv6.
+ server_socket = random.choice(server.sockets)
+
+ if server_socket.family == socket.AF_INET6: # pragma: no cover
+ return server_socket.getsockname()[:2] # (no IPv6 on CI)
+ elif server_socket.family == socket.AF_INET:
+ return server_socket.getsockname()
+ else: # pragma: no cover
+ raise ValueError("expected an IPv6, IPv4, or Unix socket")
+
+
+def get_server_uri(server, secure=False, resource_name="/", user_info=None):
+ """
+ Return a WebSocket URI for connecting to the given server.
+
+ """
+ proto = "wss" if secure else "ws"
+ user_info = ":".join(user_info) + "@" if user_info else ""
+ host, port = get_server_address(server)
+ if ":" in host: # IPv6 address
+ host = f"[{host}]"
+ return f"{proto}://{user_info}{host}:{port}{resource_name}"
+
+
+class UnauthorizedServerProtocol(WebSocketServerProtocol):
+ async def process_request(self, path, request_headers):
+ # Test returning headers as a Headers instance (1/3)
+ return http.HTTPStatus.UNAUTHORIZED, Headers([("X-Access", "denied")]), b""
+
+
+class ForbiddenServerProtocol(WebSocketServerProtocol):
+ async def process_request(self, path, request_headers):
+ # Test returning headers as a dict (2/3)
+ return http.HTTPStatus.FORBIDDEN, {"X-Access": "denied"}, b""
+
+
+class HealthCheckServerProtocol(WebSocketServerProtocol):
+ async def process_request(self, path, request_headers):
+ # Test returning headers as a list of pairs (3/3)
+ if path == "/__health__/":
+ return http.HTTPStatus.OK, [("X-Access", "OK")], b"status = green\n"
+
+
+class ProcessRequestReturningIntProtocol(WebSocketServerProtocol):
+ async def process_request(self, path, request_headers):
+ assert path == "/__health__/"
+ return 200, [], b"OK\n"
+
+
+class SlowOpeningHandshakeProtocol(WebSocketServerProtocol):
+ async def process_request(self, path, request_headers):
+ await asyncio.sleep(10 * MS)
+
+
+class FooClientProtocol(WebSocketClientProtocol):
+ pass
+
+
+class BarClientProtocol(WebSocketClientProtocol):
+ pass
+
+
+class ClientServerTestsMixin:
+ secure = False
+
+ def setUp(self):
+ super().setUp()
+ self.server = None
+
+ def start_server(self, deprecation_warnings=None, **kwargs):
+ handler = kwargs.pop("handler", default_handler)
+ # Disable compression by default in tests.
+ kwargs.setdefault("compression", None)
+ # Disable pings by default in tests.
+ kwargs.setdefault("ping_interval", None)
+
+ # This logic is encapsulated in a coroutine to prevent it from executing
+ # before the event loop is running which causes asyncio.get_event_loop()
+ # to raise a DeprecationWarning on Python ≥ 3.10.
+ async def start_server():
+ return await serve(handler, "localhost", 0, **kwargs)
+
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ warnings.simplefilter("always")
+ self.server = self.loop.run_until_complete(start_server())
+
+ expected_warnings = [] if deprecation_warnings is None else deprecation_warnings
+ self.assertDeprecationWarnings(recorded_warnings, expected_warnings)
+
+ def start_client(
+ self, resource_name="/", user_info=None, deprecation_warnings=None, **kwargs
+ ):
+ # Disable compression by default in tests.
+ kwargs.setdefault("compression", None)
+ # Disable pings by default in tests.
+ kwargs.setdefault("ping_interval", None)
+
+ secure = kwargs.get("ssl") is not None
+ try:
+ server_uri = kwargs.pop("uri")
+ except KeyError:
+ server_uri = get_server_uri(self.server, secure, resource_name, user_info)
+
+ # This logic is encapsulated in a coroutine to prevent it from executing
+ # before the event loop is running which causes asyncio.get_event_loop()
+ # to raise a DeprecationWarning on Python ≥ 3.10.
+ async def start_client():
+ return await connect(server_uri, **kwargs)
+
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ warnings.simplefilter("always")
+ self.client = self.loop.run_until_complete(start_client())
+
+ expected_warnings = [] if deprecation_warnings is None else deprecation_warnings
+ self.assertDeprecationWarnings(recorded_warnings, expected_warnings)
+
+ def stop_client(self):
+ self.loop.run_until_complete(
+ asyncio.wait_for(self.client.close_connection_task, timeout=1)
+ )
+
+ def stop_server(self):
+ self.server.close()
+ self.loop.run_until_complete(
+ asyncio.wait_for(self.server.wait_closed(), timeout=1)
+ )
+
+ @contextlib.contextmanager
+ def temp_server(self, **kwargs):
+ with temp_test_server(self, **kwargs):
+ yield
+
+ @contextlib.contextmanager
+ def temp_client(self, *args, **kwargs):
+ with temp_test_client(self, *args, **kwargs):
+ yield
+
+ def make_http_request(self, path="/", headers=None):
+ if headers is None:
+ headers = {}
+
+ # Set url to 'https?://<host>:<port><path>'.
+ url = get_server_uri(
+ self.server, resource_name=path, secure=self.secure
+ ).replace("ws", "http")
+
+ request = urllib.request.Request(url=url, headers=headers)
+
+ if self.secure:
+ open_health_check = functools.partial(
+ urllib.request.urlopen, request, context=self.client_context
+ )
+ else:
+ open_health_check = functools.partial(urllib.request.urlopen, request)
+
+ return self.loop.run_in_executor(None, open_health_check)
+
+
+class SecureClientServerTestsMixin(ClientServerTestsMixin):
+ secure = True
+
+ @property
+ def server_context(self):
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+ ssl_context.load_cert_chain(CERTIFICATE)
+ return ssl_context
+
+ @property
+ def client_context(self):
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ ssl_context.load_verify_locations(CERTIFICATE)
+ return ssl_context
+
+ def start_server(self, **kwargs):
+ kwargs.setdefault("ssl", self.server_context)
+ super().start_server(**kwargs)
+
+ def start_client(self, path="/", **kwargs):
+ kwargs.setdefault("ssl", self.client_context)
+ super().start_client(path, **kwargs)
+
+
+class CommonClientServerTests:
+ """
+ Mixin that defines most tests but doesn't inherit unittest.TestCase.
+
+ Tests are run by the ClientServerTests and SecureClientServerTests subclasses.
+
+ """
+
+ @with_server()
+ @with_client()
+ def test_basic(self):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ def test_redirect(self):
+ redirect_statuses = [
+ http.HTTPStatus.MOVED_PERMANENTLY,
+ http.HTTPStatus.FOUND,
+ http.HTTPStatus.SEE_OTHER,
+ http.HTTPStatus.TEMPORARY_REDIRECT,
+ http.HTTPStatus.PERMANENT_REDIRECT,
+ ]
+ for status in redirect_statuses:
+ with temp_test_redirecting_server(self, status):
+ with self.temp_client("/absolute_redirect"):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ def test_redirect_relative_location(self):
+ with temp_test_redirecting_server(self):
+ with self.temp_client("/relative_redirect"):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ def test_infinite_redirect(self):
+ with temp_test_redirecting_server(self):
+ with self.assertRaises(InvalidHandshake):
+ with self.temp_client("/infinite"):
+ self.fail("did not raise")
+
+ def test_redirect_missing_location(self):
+ with temp_test_redirecting_server(self):
+ with self.assertRaises(InvalidHeader):
+ with self.temp_client("/missing_location"):
+ self.fail("did not raise")
+
+ def test_loop_backwards_compatibility(self):
+ with self.temp_server(
+ loop=self.loop,
+ deprecation_warnings=["remove loop argument"],
+ ):
+ with self.temp_client(
+ loop=self.loop,
+ deprecation_warnings=["remove loop argument"],
+ ):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ @with_server()
+ def test_explicit_host_port(self):
+ uri = get_server_uri(self.server, self.secure)
+ wsuri = parse_uri(uri)
+
+ # Change host and port to invalid values.
+ scheme = "wss" if wsuri.secure else "ws"
+ port = 65535 - wsuri.port
+ changed_uri = f"{scheme}://example.com:{port}/"
+
+ with self.temp_client(uri=changed_uri, host=wsuri.host, port=wsuri.port):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ @with_server()
+ def test_explicit_socket(self):
+ class TrackedSocket(socket.socket):
+ def __init__(self, *args, **kwargs):
+ self.used_for_read = False
+ self.used_for_write = False
+ super().__init__(*args, **kwargs)
+
+ def recv(self, *args, **kwargs):
+ self.used_for_read = True
+ return super().recv(*args, **kwargs)
+
+ def recv_into(self, *args, **kwargs):
+ self.used_for_read = True
+ return super().recv_into(*args, **kwargs)
+
+ def send(self, *args, **kwargs):
+ self.used_for_write = True
+ return super().send(*args, **kwargs)
+
+ server_socket = [
+ sock for sock in self.server.sockets if sock.family == socket.AF_INET
+ ][0]
+ client_socket = TrackedSocket(socket.AF_INET, socket.SOCK_STREAM)
+ client_socket.connect(server_socket.getsockname())
+
+ try:
+ self.assertFalse(client_socket.used_for_read)
+ self.assertFalse(client_socket.used_for_write)
+
+ with self.temp_client(sock=client_socket):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ self.assertTrue(client_socket.used_for_read)
+ self.assertTrue(client_socket.used_for_write)
+
+ finally:
+ client_socket.close()
+
+ @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets")
+ def test_unix_socket(self):
+ with temp_unix_socket_path() as path:
+ # Like self.start_server() but with unix_serve().
+ async def start_server():
+ return await unix_serve(default_handler, path)
+
+ self.server = self.loop.run_until_complete(start_server())
+
+ try:
+ # Like self.start_client() but with unix_connect()
+ async def start_client():
+ return await unix_connect(path)
+
+ self.client = self.loop.run_until_complete(start_client())
+
+ try:
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ finally:
+ self.stop_client()
+
+ finally:
+ self.stop_server()
+
+ def test_ws_handler_argument_backwards_compatibility(self):
+ async def handler_with_path(ws, path):
+ await ws.send(path)
+
+ with self.temp_server(
+ handler=handler_with_path,
+ # Enable deprecation warning and announce deprecation in 11.0.
+ # deprecation_warnings=["remove second argument of ws_handler"],
+ ):
+ with self.temp_client("/path"):
+ self.assertEqual(
+ self.loop.run_until_complete(self.client.recv()),
+ "/path",
+ )
+
+ def test_ws_handler_argument_backwards_compatibility_partial(self):
+ async def handler_with_path(ws, path, extra):
+ await ws.send(path)
+
+ bound_handler_with_path = functools.partial(handler_with_path, extra=None)
+
+ with self.temp_server(
+ handler=bound_handler_with_path,
+ # Enable deprecation warning and announce deprecation in 11.0.
+ # deprecation_warnings=["remove second argument of ws_handler"],
+ ):
+ with self.temp_client("/path"):
+ self.assertEqual(
+ self.loop.run_until_complete(self.client.recv()),
+ "/path",
+ )
+
+ async def process_request_OK(path, request_headers):
+ return http.HTTPStatus.OK, [], b"OK\n"
+
+ @with_server(process_request=process_request_OK)
+ def test_process_request_argument(self):
+ response = self.loop.run_until_complete(self.make_http_request("/"))
+
+ with contextlib.closing(response):
+ self.assertEqual(response.code, 200)
+
+ def legacy_process_request_OK(path, request_headers):
+ return http.HTTPStatus.OK, [], b"OK\n"
+
+ @with_server(process_request=legacy_process_request_OK)
+ def test_process_request_argument_backwards_compatibility(self):
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ warnings.simplefilter("always")
+ response = self.loop.run_until_complete(self.make_http_request("/"))
+
+ with contextlib.closing(response):
+ self.assertEqual(response.code, 200)
+
+ self.assertDeprecationWarnings(
+ recorded_warnings, ["declare process_request as a coroutine"]
+ )
+
+ class ProcessRequestOKServerProtocol(WebSocketServerProtocol):
+ async def process_request(self, path, request_headers):
+ return http.HTTPStatus.OK, [], b"OK\n"
+
+ @with_server(create_protocol=ProcessRequestOKServerProtocol)
+ def test_process_request_override(self):
+ response = self.loop.run_until_complete(self.make_http_request("/"))
+
+ with contextlib.closing(response):
+ self.assertEqual(response.code, 200)
+
+ class LegacyProcessRequestOKServerProtocol(WebSocketServerProtocol):
+ def process_request(self, path, request_headers):
+ return http.HTTPStatus.OK, [], b"OK\n"
+
+ @with_server(create_protocol=LegacyProcessRequestOKServerProtocol)
+ def test_process_request_override_backwards_compatibility(self):
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ warnings.simplefilter("always")
+ response = self.loop.run_until_complete(self.make_http_request("/"))
+
+ with contextlib.closing(response):
+ self.assertEqual(response.code, 200)
+
+ self.assertDeprecationWarnings(
+ recorded_warnings, ["declare process_request as a coroutine"]
+ )
+
+ def select_subprotocol_chat(client_subprotocols, server_subprotocols):
+ return "chat"
+
+ @with_server(
+ subprotocols=["superchat", "chat"], select_subprotocol=select_subprotocol_chat
+ )
+ @with_client("/subprotocol", subprotocols=["superchat", "chat"])
+ def test_select_subprotocol_argument(self):
+ server_subprotocol = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_subprotocol, repr("chat"))
+ self.assertEqual(self.client.subprotocol, "chat")
+
+ class SelectSubprotocolChatServerProtocol(WebSocketServerProtocol):
+ def select_subprotocol(self, client_subprotocols, server_subprotocols):
+ return "chat"
+
+ @with_server(
+ subprotocols=["superchat", "chat"],
+ create_protocol=SelectSubprotocolChatServerProtocol,
+ )
+ @with_client("/subprotocol", subprotocols=["superchat", "chat"])
+ def test_select_subprotocol_override(self):
+ server_subprotocol = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_subprotocol, repr("chat"))
+ self.assertEqual(self.client.subprotocol, "chat")
+
+ @with_server()
+ @with_client("/deprecated_attributes")
+ def test_protocol_deprecated_attributes(self):
+ # The test could be connecting with IPv6 or IPv4.
+ expected_client_attrs = [
+ server_socket.getsockname()[:2] + (self.secure,)
+ for server_socket in self.server.sockets
+ ]
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ warnings.simplefilter("always")
+ client_attrs = (self.client.host, self.client.port, self.client.secure)
+ self.assertDeprecationWarnings(
+ recorded_warnings,
+ [
+ "use remote_address[0] instead of host",
+ "use remote_address[1] instead of port",
+ "don't use secure",
+ ],
+ )
+ self.assertIn(client_attrs, expected_client_attrs)
+
+ expected_server_attrs = ("localhost", 0, self.secure)
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ warnings.simplefilter("always")
+ self.loop.run_until_complete(self.client.send(""))
+ server_attrs = self.loop.run_until_complete(self.client.recv())
+ self.assertDeprecationWarnings(
+ recorded_warnings,
+ [
+ "use local_address[0] instead of host",
+ "use local_address[1] instead of port",
+ "don't use secure",
+ ],
+ )
+ self.assertEqual(server_attrs, repr(expected_server_attrs))
+
+ @with_server()
+ @with_client("/path")
+ def test_protocol_path(self):
+ client_path = self.client.path
+ self.assertEqual(client_path, "/path")
+ server_path = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_path, "/path")
+
+ @with_server()
+ @with_client("/headers")
+ def test_protocol_headers(self):
+ client_req = self.client.request_headers
+ client_resp = self.client.response_headers
+ self.assertEqual(client_req["User-Agent"], USER_AGENT)
+ self.assertEqual(client_resp["Server"], USER_AGENT)
+ server_req = self.loop.run_until_complete(self.client.recv())
+ server_resp = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_req, repr(client_req))
+ self.assertEqual(server_resp, repr(client_resp))
+
+ @with_server()
+ @with_client("/headers", extra_headers={"X-Spam": "Eggs"})
+ def test_protocol_custom_request_headers(self):
+ req_headers = self.loop.run_until_complete(self.client.recv())
+ self.loop.run_until_complete(self.client.recv())
+ self.assertIn("('X-Spam', 'Eggs')", req_headers)
+
+ @with_server()
+ @with_client("/headers", extra_headers={"User-Agent": "websockets"})
+ def test_protocol_custom_user_agent_header_legacy(self):
+ req_headers = self.loop.run_until_complete(self.client.recv())
+ self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(req_headers.count("User-Agent"), 1)
+ self.assertIn("('User-Agent', 'websockets')", req_headers)
+
+ @with_server()
+ @with_client("/headers", user_agent_header=None)
+ def test_protocol_no_user_agent_header(self):
+ req_headers = self.loop.run_until_complete(self.client.recv())
+ self.loop.run_until_complete(self.client.recv())
+ self.assertNotIn("User-Agent", req_headers)
+
+ @with_server()
+ @with_client("/headers", user_agent_header="websockets")
+ def test_protocol_custom_user_agent_header(self):
+ req_headers = self.loop.run_until_complete(self.client.recv())
+ self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(req_headers.count("User-Agent"), 1)
+ self.assertIn("('User-Agent', 'websockets')", req_headers)
+
+ @with_server(extra_headers=lambda p, r: {"X-Spam": "Eggs"})
+ @with_client("/headers")
+ def test_protocol_custom_response_headers_callable(self):
+ self.loop.run_until_complete(self.client.recv())
+ resp_headers = self.loop.run_until_complete(self.client.recv())
+ self.assertIn("('X-Spam', 'Eggs')", resp_headers)
+
+ @with_server(extra_headers=lambda p, r: None)
+ @with_client("/headers")
+ def test_protocol_custom_response_headers_callable_none(self):
+ self.loop.run_until_complete(self.client.recv()) # doesn't crash
+ self.loop.run_until_complete(self.client.recv()) # nothing to check
+
+ @with_server(extra_headers={"X-Spam": "Eggs"})
+ @with_client("/headers")
+ def test_protocol_custom_response_headers(self):
+ self.loop.run_until_complete(self.client.recv())
+ resp_headers = self.loop.run_until_complete(self.client.recv())
+ self.assertIn("('X-Spam', 'Eggs')", resp_headers)
+
+ @with_server(extra_headers={"Server": "websockets"})
+ @with_client("/headers")
+ def test_protocol_custom_server_header_legacy(self):
+ self.loop.run_until_complete(self.client.recv())
+ resp_headers = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(resp_headers.count("Server"), 1)
+ self.assertIn("('Server', 'websockets')", resp_headers)
+
+ @with_server(server_header=None)
+ @with_client("/headers")
+ def test_protocol_no_server_header(self):
+ self.loop.run_until_complete(self.client.recv())
+ resp_headers = self.loop.run_until_complete(self.client.recv())
+ self.assertNotIn("Server", resp_headers)
+
+ @with_server(server_header="websockets")
+ @with_client("/headers")
+ def test_protocol_custom_server_header(self):
+ self.loop.run_until_complete(self.client.recv())
+ resp_headers = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(resp_headers.count("Server"), 1)
+ self.assertIn("('Server', 'websockets')", resp_headers)
+
+ @with_server(create_protocol=HealthCheckServerProtocol)
+ def test_http_request_http_endpoint(self):
+ # Making an HTTP request to an HTTP endpoint succeeds.
+ response = self.loop.run_until_complete(self.make_http_request("/__health__/"))
+
+ with contextlib.closing(response):
+ self.assertEqual(response.code, 200)
+ self.assertEqual(response.read(), b"status = green\n")
+
+ @with_server(create_protocol=HealthCheckServerProtocol)
+ def test_http_request_ws_endpoint(self):
+ # Making an HTTP request to a WS endpoint fails.
+ with self.assertRaises(urllib.error.HTTPError) as raised:
+ self.loop.run_until_complete(self.make_http_request())
+
+ self.assertEqual(raised.exception.code, 426)
+ self.assertEqual(raised.exception.headers["Upgrade"], "websocket")
+
+ @with_server(create_protocol=HealthCheckServerProtocol)
+ def test_ws_connection_http_endpoint(self):
+ # Making a WS connection to an HTTP endpoint fails.
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client("/__health__/")
+
+ self.assertEqual(raised.exception.status_code, 200)
+
+ @with_server(create_protocol=HealthCheckServerProtocol)
+ def test_ws_connection_ws_endpoint(self):
+ # Making a WS connection to a WS endpoint succeeds.
+ self.start_client()
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.loop.run_until_complete(self.client.recv())
+ self.stop_client()
+
+ @with_server(create_protocol=HealthCheckServerProtocol, server_header=None)
+ def test_http_request_no_server_header(self):
+ response = self.loop.run_until_complete(self.make_http_request("/__health__/"))
+
+ with contextlib.closing(response):
+ self.assertNotIn("Server", response.headers)
+
+ @with_server(create_protocol=HealthCheckServerProtocol, server_header="websockets")
+ def test_http_request_custom_server_header(self):
+ response = self.loop.run_until_complete(self.make_http_request("/__health__/"))
+
+ with contextlib.closing(response):
+ self.assertEqual(response.headers["Server"], "websockets")
+
+ @with_server(create_protocol=ProcessRequestReturningIntProtocol)
+ def test_process_request_returns_int_status(self):
+ response = self.loop.run_until_complete(self.make_http_request("/__health__/"))
+
+ with contextlib.closing(response):
+ self.assertEqual(response.code, 200)
+ self.assertEqual(response.read(), b"OK\n")
+
+ def assert_client_raises_code(self, status_code):
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client()
+ self.assertEqual(raised.exception.status_code, status_code)
+
+ @with_server(create_protocol=UnauthorizedServerProtocol)
+ def test_server_create_protocol(self):
+ self.assert_client_raises_code(401)
+
+ def create_unauthorized_server_protocol(*args, **kwargs):
+ return UnauthorizedServerProtocol(*args, **kwargs)
+
+ @with_server(create_protocol=create_unauthorized_server_protocol)
+ def test_server_create_protocol_function(self):
+ self.assert_client_raises_code(401)
+
+ @with_server(
+ klass=UnauthorizedServerProtocol,
+ deprecation_warnings=["rename klass to create_protocol"],
+ )
+ def test_server_klass_backwards_compatibility(self):
+ self.assert_client_raises_code(401)
+
+ @with_server(
+ create_protocol=ForbiddenServerProtocol,
+ klass=UnauthorizedServerProtocol,
+ deprecation_warnings=["rename klass to create_protocol"],
+ )
+ def test_server_create_protocol_over_klass(self):
+ self.assert_client_raises_code(403)
+
+ @with_server()
+ @with_client("/path", create_protocol=FooClientProtocol)
+ def test_client_create_protocol(self):
+ self.assertIsInstance(self.client, FooClientProtocol)
+
+ @with_server()
+ @with_client(
+ "/path",
+ create_protocol=(lambda *args, **kwargs: FooClientProtocol(*args, **kwargs)),
+ )
+ def test_client_create_protocol_function(self):
+ self.assertIsInstance(self.client, FooClientProtocol)
+
+ @with_server()
+ @with_client(
+ "/path",
+ klass=FooClientProtocol,
+ deprecation_warnings=["rename klass to create_protocol"],
+ )
+ def test_client_klass(self):
+ self.assertIsInstance(self.client, FooClientProtocol)
+
+ @with_server()
+ @with_client(
+ "/path",
+ create_protocol=BarClientProtocol,
+ klass=FooClientProtocol,
+ deprecation_warnings=["rename klass to create_protocol"],
+ )
+ def test_client_create_protocol_over_klass(self):
+ self.assertIsInstance(self.client, BarClientProtocol)
+
+ @with_server(close_timeout=7)
+ @with_client("/close_timeout")
+ def test_server_close_timeout(self):
+ close_timeout = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(eval(close_timeout), 7)
+
+ @with_server(timeout=6, deprecation_warnings=["rename timeout to close_timeout"])
+ @with_client("/close_timeout")
+ def test_server_timeout_backwards_compatibility(self):
+ close_timeout = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(eval(close_timeout), 6)
+
+ @with_server(
+ close_timeout=7,
+ timeout=6,
+ deprecation_warnings=["rename timeout to close_timeout"],
+ )
+ @with_client("/close_timeout")
+ def test_server_close_timeout_over_timeout(self):
+ close_timeout = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(eval(close_timeout), 7)
+
+ @with_server()
+ @with_client("/close_timeout", close_timeout=7)
+ def test_client_close_timeout(self):
+ self.assertEqual(self.client.close_timeout, 7)
+
+ @with_server()
+ @with_client(
+ "/close_timeout",
+ timeout=6,
+ deprecation_warnings=["rename timeout to close_timeout"],
+ )
+ def test_client_timeout_backwards_compatibility(self):
+ self.assertEqual(self.client.close_timeout, 6)
+
+ @with_server()
+ @with_client(
+ "/close_timeout",
+ close_timeout=7,
+ timeout=6,
+ deprecation_warnings=["rename timeout to close_timeout"],
+ )
+ def test_client_close_timeout_over_timeout(self):
+ self.assertEqual(self.client.close_timeout, 7)
+
+ @with_server()
+ @with_client("/extensions")
+ def test_no_extension(self):
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_extensions, repr([]))
+ self.assertEqual(repr(self.client.extensions), repr([]))
+
+ @with_server(extensions=[ServerNoOpExtensionFactory()])
+ @with_client("/extensions", extensions=[ClientNoOpExtensionFactory()])
+ def test_extension(self):
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_extensions, repr([NoOpExtension()]))
+ self.assertEqual(repr(self.client.extensions), repr([NoOpExtension()]))
+
+ @with_server()
+ @with_client("/extensions", extensions=[ClientNoOpExtensionFactory()])
+ def test_extension_not_accepted(self):
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_extensions, repr([]))
+ self.assertEqual(repr(self.client.extensions), repr([]))
+
+ @with_server(extensions=[ServerNoOpExtensionFactory()])
+ @with_client("/extensions")
+ def test_extension_not_requested(self):
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_extensions, repr([]))
+ self.assertEqual(repr(self.client.extensions), repr([]))
+
+ @with_server(extensions=[ServerNoOpExtensionFactory([("foo", None)])])
+ def test_extension_client_rejection(self):
+ with self.assertRaises(NegotiationError):
+ self.start_client("/extensions", extensions=[ClientNoOpExtensionFactory()])
+
+ @with_server(
+ extensions=[
+ # No match because the client doesn't send client_max_window_bits.
+ ServerPerMessageDeflateFactory(
+ client_max_window_bits=10,
+ require_client_max_window_bits=True,
+ ),
+ ServerPerMessageDeflateFactory(),
+ ]
+ )
+ @with_client(
+ "/extensions",
+ extensions=[
+ ClientPerMessageDeflateFactory(client_max_window_bits=None),
+ ],
+ )
+ def test_extension_no_match_then_match(self):
+ # The order requested by the client has priority.
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(
+ server_extensions, repr([PerMessageDeflate(False, False, 15, 15)])
+ )
+ self.assertEqual(
+ repr(self.client.extensions),
+ repr([PerMessageDeflate(False, False, 15, 15)]),
+ )
+
+ @with_server(extensions=[ServerPerMessageDeflateFactory()])
+ @with_client("/extensions", extensions=[ClientNoOpExtensionFactory()])
+ def test_extension_mismatch(self):
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_extensions, repr([]))
+ self.assertEqual(repr(self.client.extensions), repr([]))
+
+ @with_server(
+ extensions=[ServerNoOpExtensionFactory(), ServerPerMessageDeflateFactory()]
+ )
+ @with_client(
+ "/extensions",
+ extensions=[ClientPerMessageDeflateFactory(), ClientNoOpExtensionFactory()],
+ )
+ def test_extension_order(self):
+ # The order requested by the client has priority.
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(
+ server_extensions,
+ repr([PerMessageDeflate(False, False, 15, 15), NoOpExtension()]),
+ )
+ self.assertEqual(
+ repr(self.client.extensions),
+ repr([PerMessageDeflate(False, False, 15, 15), NoOpExtension()]),
+ )
+
+ @with_server(extensions=[ServerNoOpExtensionFactory()])
+ @unittest.mock.patch.object(WebSocketServerProtocol, "process_extensions")
+ def test_extensions_error(self, _process_extensions):
+ _process_extensions.return_value = "x-no-op", [NoOpExtension()]
+
+ with self.assertRaises(NegotiationError):
+ self.start_client(
+ "/extensions", extensions=[ClientPerMessageDeflateFactory()]
+ )
+
+ @with_server(extensions=[ServerNoOpExtensionFactory()])
+ @unittest.mock.patch.object(WebSocketServerProtocol, "process_extensions")
+ def test_extensions_error_no_extensions(self, _process_extensions):
+ _process_extensions.return_value = "x-no-op", [NoOpExtension()]
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client("/extensions")
+
+ @with_server(compression="deflate")
+ @with_client("/extensions", compression="deflate")
+ def test_compression_deflate(self):
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(
+ server_extensions, repr([PerMessageDeflate(False, False, 12, 12)])
+ )
+ self.assertEqual(
+ repr(self.client.extensions),
+ repr([PerMessageDeflate(False, False, 12, 12)]),
+ )
+
+ def test_compression_unsupported_server(self):
+ with self.assertRaises(ValueError):
+ self.start_server(compression="xz")
+
+ @with_server()
+ def test_compression_unsupported_client(self):
+ with self.assertRaises(ValueError):
+ self.start_client(compression="xz")
+
+ @with_server()
+ @with_client("/subprotocol")
+ def test_no_subprotocol(self):
+ server_subprotocol = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_subprotocol, repr(None))
+ self.assertEqual(self.client.subprotocol, None)
+
+ @with_server(subprotocols=["superchat", "chat"])
+ @with_client("/subprotocol", subprotocols=["otherchat", "chat"])
+ def test_subprotocol(self):
+ server_subprotocol = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_subprotocol, repr("chat"))
+ self.assertEqual(self.client.subprotocol, "chat")
+
+ def test_invalid_subprotocol_server(self):
+ with self.assertRaises(TypeError):
+ self.start_server(subprotocols="sip")
+
+ @with_server()
+ def test_invalid_subprotocol_client(self):
+ with self.assertRaises(TypeError):
+ self.start_client(subprotocols="sip")
+
+ @with_server(subprotocols=["superchat"])
+ @with_client("/subprotocol", subprotocols=["otherchat"])
+ def test_subprotocol_not_accepted(self):
+ server_subprotocol = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_subprotocol, repr(None))
+ self.assertEqual(self.client.subprotocol, None)
+
+ @with_server()
+ @with_client("/subprotocol", subprotocols=["otherchat", "chat"])
+ def test_subprotocol_not_offered(self):
+ server_subprotocol = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_subprotocol, repr(None))
+ self.assertEqual(self.client.subprotocol, None)
+
+ @with_server(subprotocols=["superchat", "chat"])
+ @with_client("/subprotocol")
+ def test_subprotocol_not_requested(self):
+ server_subprotocol = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_subprotocol, repr(None))
+ self.assertEqual(self.client.subprotocol, None)
+
+ @with_server(subprotocols=["superchat"])
+ @unittest.mock.patch.object(WebSocketServerProtocol, "process_subprotocol")
+ def test_subprotocol_error(self, _process_subprotocol):
+ _process_subprotocol.return_value = "superchat"
+
+ with self.assertRaises(NegotiationError):
+ self.start_client("/subprotocol", subprotocols=["otherchat"])
+ self.run_loop_once()
+
+ @with_server(subprotocols=["superchat"])
+ @unittest.mock.patch.object(WebSocketServerProtocol, "process_subprotocol")
+ def test_subprotocol_error_no_subprotocols(self, _process_subprotocol):
+ _process_subprotocol.return_value = "superchat"
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client("/subprotocol")
+ self.run_loop_once()
+
+ @with_server(subprotocols=["superchat", "chat"])
+ @unittest.mock.patch.object(WebSocketServerProtocol, "process_subprotocol")
+ def test_subprotocol_error_two_subprotocols(self, _process_subprotocol):
+ _process_subprotocol.return_value = "superchat, chat"
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client("/subprotocol", subprotocols=["superchat", "chat"])
+ self.run_loop_once()
+
+ @with_server()
+ @unittest.mock.patch("websockets.legacy.server.read_request")
+ def test_server_receives_malformed_request(self, _read_request):
+ _read_request.side_effect = ValueError("read_request failed")
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client()
+
+ @with_server()
+ @unittest.mock.patch("websockets.legacy.client.read_response")
+ def test_client_receives_malformed_response(self, _read_response):
+ _read_response.side_effect = ValueError("read_response failed")
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client()
+ self.run_loop_once()
+
+ @with_server()
+ @unittest.mock.patch("websockets.legacy.client.build_request")
+ def test_client_sends_invalid_handshake_request(self, _build_request):
+ def wrong_build_request(headers):
+ return "42"
+
+ _build_request.side_effect = wrong_build_request
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client()
+
+ @with_server()
+ @unittest.mock.patch("websockets.legacy.server.build_response")
+ def test_server_sends_invalid_handshake_response(self, _build_response):
+ def wrong_build_response(headers, key):
+ return build_response(headers, "42")
+
+ _build_response.side_effect = wrong_build_response
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client()
+
+ @with_server()
+ @unittest.mock.patch("websockets.legacy.client.read_response")
+ def test_server_does_not_switch_protocols(self, _read_response):
+ async def wrong_read_response(stream):
+ status_code, reason, headers = await read_response(stream)
+ return 400, "Bad Request", headers
+
+ _read_response.side_effect = wrong_read_response
+
+ with self.assertRaises(InvalidStatusCode):
+ self.start_client()
+ self.run_loop_once()
+
+ @with_server()
+ @unittest.mock.patch(
+ "websockets.legacy.server.WebSocketServerProtocol.process_request"
+ )
+ def test_server_error_in_handshake(self, _process_request):
+ _process_request.side_effect = Exception("process_request crashed")
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client()
+
+ @with_server(create_protocol=SlowOpeningHandshakeProtocol)
+ def test_client_connect_canceled_during_handshake(self):
+ sock = socket.create_connection(get_server_address(self.server))
+ sock.send(b"") # socket is connected
+
+ async def cancelled_client():
+ start_client = connect(get_server_uri(self.server), sock=sock)
+ async with asyncio_timeout(5 * MS):
+ await start_client
+
+ with self.assertRaises(asyncio.TimeoutError):
+ self.loop.run_until_complete(cancelled_client())
+
+ with self.assertRaises(OSError):
+ sock.send(b"") # socket is closed
+
+ @with_server()
+ @unittest.mock.patch("websockets.legacy.server.WebSocketServerProtocol.send")
+ def test_server_handler_crashes(self, send):
+ send.side_effect = ValueError("send failed")
+
+ with self.temp_client():
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.client.recv())
+
+ # Connection ends with an unexpected error.
+ self.assertEqual(self.client.close_code, CloseCode.INTERNAL_ERROR)
+
+ @with_server()
+ @unittest.mock.patch("websockets.legacy.server.WebSocketServerProtocol.close")
+ def test_server_close_crashes(self, close):
+ close.side_effect = ValueError("close failed")
+
+ with self.temp_client():
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ # Connection ends with an abnormal closure.
+ self.assertEqual(self.client.close_code, CloseCode.ABNORMAL_CLOSURE)
+
+ @with_server()
+ @with_client()
+ @unittest.mock.patch.object(WebSocketClientProtocol, "handshake")
+ def test_client_closes_connection_before_handshake(self, handshake):
+ # We have mocked the handshake() method to prevent the client from
+ # performing the opening handshake. Force it to close the connection.
+ self.client.transport.close()
+ # The server should stop properly anyway. It used to hang because the
+ # task handling the connection was waiting for the opening handshake.
+
+ @with_server(create_protocol=SlowOpeningHandshakeProtocol)
+ def test_server_shuts_down_during_opening_handshake(self):
+ self.loop.call_later(5 * MS, self.server.close)
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client()
+ exception = raised.exception
+ self.assertEqual(
+ str(exception), "server rejected WebSocket connection: HTTP 503"
+ )
+ self.assertEqual(exception.status_code, 503)
+
+ @with_server()
+ def test_server_shuts_down_during_connection_handling(self):
+ with self.temp_client():
+ server_ws = next(iter(self.server.websockets))
+ self.server.close()
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.loop.run_until_complete(self.client.recv())
+
+ # Server closed the connection with 1001 Going Away.
+ self.assertEqual(self.client.close_code, CloseCode.GOING_AWAY)
+ self.assertEqual(server_ws.close_code, CloseCode.GOING_AWAY)
+
+ @with_server()
+ def test_server_shuts_down_gracefully_during_connection_handling(self):
+ with self.temp_client():
+ server_ws = next(iter(self.server.websockets))
+ self.server.close(close_connections=False)
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.loop.run_until_complete(self.client.recv())
+
+ # Client closed the connection with 1000 OK.
+ self.assertEqual(self.client.close_code, CloseCode.NORMAL_CLOSURE)
+ self.assertEqual(server_ws.close_code, CloseCode.NORMAL_CLOSURE)
+
+ @with_server()
+ def test_server_shuts_down_and_waits_until_handlers_terminate(self):
+ # This handler waits a bit after the connection is closed in order
+ # to test that wait_closed() really waits for handlers to complete.
+ self.start_client("/slow_stop")
+ server_ws = next(iter(self.server.websockets))
+
+ # Test that the handler task keeps running after close().
+ self.server.close()
+ self.loop.run_until_complete(asyncio.sleep(MS))
+ self.assertFalse(server_ws.handler_task.done())
+
+ # Test that the handler task terminates before wait_closed() returns.
+ self.loop.run_until_complete(self.server.wait_closed())
+ self.assertTrue(server_ws.handler_task.done())
+
+ @with_server(create_protocol=ForbiddenServerProtocol)
+ def test_invalid_status_error_during_client_connect(self):
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client()
+ exception = raised.exception
+ self.assertEqual(
+ str(exception), "server rejected WebSocket connection: HTTP 403"
+ )
+ self.assertEqual(exception.status_code, 403)
+
+ @with_server()
+ @unittest.mock.patch(
+ "websockets.legacy.server.WebSocketServerProtocol.write_http_response"
+ )
+ @unittest.mock.patch(
+ "websockets.legacy.server.WebSocketServerProtocol.read_http_request"
+ )
+ def test_connection_error_during_opening_handshake(
+ self, _read_http_request, _write_http_response
+ ):
+ _read_http_request.side_effect = ConnectionError
+
+ # This exception is currently platform-dependent. It was observed to
+ # be ConnectionResetError on Linux in the non-TLS case, and
+ # InvalidMessage otherwise (including both Linux and macOS). This
+ # doesn't matter though since this test is primarily for testing a
+ # code path on the server side.
+ with self.assertRaises(Exception):
+ self.start_client()
+
+ # No response must not be written if the network connection is broken.
+ _write_http_response.assert_not_called()
+
+ @with_server()
+ @unittest.mock.patch("websockets.legacy.server.WebSocketServerProtocol.close")
+ def test_connection_error_during_closing_handshake(self, close):
+ close.side_effect = ConnectionError
+
+ with self.temp_client():
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ # Connection ends with an abnormal closure.
+ self.assertEqual(self.client.close_code, CloseCode.ABNORMAL_CLOSURE)
+
+
+class ClientServerTests(
+ CommonClientServerTests, ClientServerTestsMixin, AsyncioTestCase
+):
+ pass
+
+
+class SecureClientServerTests(
+ CommonClientServerTests, SecureClientServerTestsMixin, AsyncioTestCase
+):
+ # The implementation of this test makes it hard to run it over TLS.
+ test_client_connect_canceled_during_handshake = None
+
+ # TLS over Unix sockets doesn't make sense.
+ test_unix_socket = None
+
+ # This test fails under PyPy due to a difference with CPython.
+ if platform.python_implementation() == "PyPy": # pragma: no cover
+ test_http_request_ws_endpoint = None
+
+ @with_server()
+ def test_ws_uri_is_rejected(self):
+ with self.assertRaises(ValueError):
+ self.start_client(
+ uri=get_server_uri(self.server, secure=False), ssl=self.client_context
+ )
+
+ def test_redirect_insecure(self):
+ with temp_test_redirecting_server(self):
+ with self.assertRaises(InvalidHandshake):
+ with self.temp_client("/force_insecure"):
+ self.fail("did not raise")
+
+
+class ClientServerOriginTests(ClientServerTestsMixin, AsyncioTestCase):
+ @with_server(origins=["http://localhost"])
+ @with_client(origin="http://localhost")
+ def test_checking_origin_succeeds(self):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.assertEqual(self.loop.run_until_complete(self.client.recv()), "Hello!")
+
+ @with_server(origins=["http://localhost"])
+ def test_checking_origin_fails(self):
+ with self.assertRaisesRegex(
+ InvalidHandshake, "server rejected WebSocket connection: HTTP 403"
+ ):
+ self.start_client(origin="http://otherhost")
+
+ @with_server(origins=["http://localhost"])
+ def test_checking_origins_fails_with_multiple_headers(self):
+ with self.assertRaisesRegex(
+ InvalidHandshake, "server rejected WebSocket connection: HTTP 400"
+ ):
+ self.start_client(
+ origin="http://localhost",
+ extra_headers=[("Origin", "http://otherhost")],
+ )
+
+ @with_server(origins=[None])
+ @with_client()
+ def test_checking_lack_of_origin_succeeds(self):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.assertEqual(self.loop.run_until_complete(self.client.recv()), "Hello!")
+
+ @with_server(origins=[""])
+ # The deprecation warning is raised when a client connects to the server.
+ @with_client(deprecation_warnings=["use None instead of '' in origins"])
+ def test_checking_lack_of_origin_succeeds_backwards_compatibility(self):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.assertEqual(self.loop.run_until_complete(self.client.recv()), "Hello!")
+
+
+@unittest.skipIf(
+ sys.version_info[:2] >= (3, 11), "asyncio.coroutine has been removed in Python 3.11"
+)
+class YieldFromTests(ClientServerTestsMixin, AsyncioTestCase): # pragma: no cover
+ @with_server()
+ def test_client(self):
+ # @asyncio.coroutine is deprecated on Python ≥ 3.8
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+
+ @asyncio.coroutine
+ def run_client():
+ # Yield from connect.
+ client = yield from connect(get_server_uri(self.server))
+ self.assertEqual(client.state, State.OPEN)
+ yield from client.close()
+ self.assertEqual(client.state, State.CLOSED)
+
+ self.loop.run_until_complete(run_client())
+
+ def test_server(self):
+ # @asyncio.coroutine is deprecated on Python ≥ 3.8
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+
+ @asyncio.coroutine
+ def run_server():
+ # Yield from serve.
+ server = yield from serve(default_handler, "localhost", 0)
+ self.assertTrue(server.sockets)
+ server.close()
+ yield from server.wait_closed()
+ self.assertFalse(server.sockets)
+
+ self.loop.run_until_complete(run_server())
+
+
+class AsyncAwaitTests(ClientServerTestsMixin, AsyncioTestCase):
+ @with_server()
+ def test_client(self):
+ async def run_client():
+ # Await connect.
+ client = await connect(get_server_uri(self.server))
+ self.assertEqual(client.state, State.OPEN)
+ await client.close()
+ self.assertEqual(client.state, State.CLOSED)
+
+ self.loop.run_until_complete(run_client())
+
+ def test_server(self):
+ async def run_server():
+ # Await serve.
+ server = await serve(default_handler, "localhost", 0)
+ self.assertTrue(server.sockets)
+ server.close()
+ await server.wait_closed()
+ self.assertFalse(server.sockets)
+
+ self.loop.run_until_complete(run_server())
+
+
+class ContextManagerTests(ClientServerTestsMixin, AsyncioTestCase):
+ @with_server()
+ def test_client(self):
+ async def run_client():
+ # Use connect as an asynchronous context manager.
+ async with connect(get_server_uri(self.server)) as client:
+ self.assertEqual(client.state, State.OPEN)
+
+ # Check that exiting the context manager closed the connection.
+ self.assertEqual(client.state, State.CLOSED)
+
+ self.loop.run_until_complete(run_client())
+
+ def test_server(self):
+ async def run_server():
+ # Use serve as an asynchronous context manager.
+ async with serve(default_handler, "localhost", 0) as server:
+ self.assertTrue(server.sockets)
+
+ # Check that exiting the context manager closed the server.
+ self.assertFalse(server.sockets)
+
+ self.loop.run_until_complete(run_server())
+
+ @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets")
+ def test_unix_server(self):
+ async def run_server(path):
+ async with unix_serve(default_handler, path) as server:
+ self.assertTrue(server.sockets)
+
+ # Check that exiting the context manager closed the server.
+ self.assertFalse(server.sockets)
+
+ with temp_unix_socket_path() as path:
+ self.loop.run_until_complete(run_server(path))
+
+
+class AsyncIteratorTests(ClientServerTestsMixin, AsyncioTestCase):
+ # This is a protocol-level feature, but since it's a high-level API, it is
+ # much easier to exercise at the client or server level.
+
+ MESSAGES = ["3", "2", "1", "Fire!"]
+
+ async def echo_handler(ws):
+ for message in AsyncIteratorTests.MESSAGES:
+ await ws.send(message)
+
+ @with_server(handler=echo_handler)
+ def test_iterate_on_messages(self):
+ messages = []
+
+ async def run_client():
+ nonlocal messages
+ async with connect(get_server_uri(self.server)) as ws:
+ async for message in ws:
+ messages.append(message)
+
+ self.loop.run_until_complete(run_client())
+
+ self.assertEqual(messages, self.MESSAGES)
+
+ async def echo_handler_going_away(ws):
+ for message in AsyncIteratorTests.MESSAGES:
+ await ws.send(message)
+ await ws.close(CloseCode.GOING_AWAY)
+
+ @with_server(handler=echo_handler_going_away)
+ def test_iterate_on_messages_going_away_exit_ok(self):
+ messages = []
+
+ async def run_client():
+ nonlocal messages
+ async with connect(get_server_uri(self.server)) as ws:
+ async for message in ws:
+ messages.append(message)
+
+ self.loop.run_until_complete(run_client())
+
+ self.assertEqual(messages, self.MESSAGES)
+
+ async def echo_handler_internal_error(ws):
+ for message in AsyncIteratorTests.MESSAGES:
+ await ws.send(message)
+ await ws.close(CloseCode.INTERNAL_ERROR)
+
+ @with_server(handler=echo_handler_internal_error)
+ def test_iterate_on_messages_internal_error_exit_not_ok(self):
+ messages = []
+
+ async def run_client():
+ nonlocal messages
+ async with connect(get_server_uri(self.server)) as ws:
+ async for message in ws:
+ messages.append(message)
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(run_client())
+
+ self.assertEqual(messages, self.MESSAGES)
+
+
+class ReconnectionTests(ClientServerTestsMixin, AsyncioTestCase):
+ async def echo_handler(ws):
+ async for msg in ws:
+ await ws.send(msg)
+
+ service_available = True
+
+ async def maybe_service_unavailable(path, headers):
+ if not ReconnectionTests.service_available:
+ return http.HTTPStatus.SERVICE_UNAVAILABLE, [], b""
+
+ async def disable_server(self, duration):
+ ReconnectionTests.service_available = False
+ await asyncio.sleep(duration)
+ ReconnectionTests.service_available = True
+
+ @with_server(handler=echo_handler, process_request=maybe_service_unavailable)
+ def test_reconnect(self):
+ # Big, ugly integration test :-(
+
+ async def run_client():
+ iteration = 0
+ connect_inst = connect(get_server_uri(self.server))
+ connect_inst.BACKOFF_MIN = 10 * MS
+ connect_inst.BACKOFF_MAX = 99 * MS
+ connect_inst.BACKOFF_INITIAL = 0
+ # coverage has a hard time dealing with this code - I give up.
+ async for ws in connect_inst: # pragma: no cover
+ await ws.send("spam")
+ msg = await ws.recv()
+ self.assertEqual(msg, "spam")
+
+ iteration += 1
+ if iteration == 1:
+ # Exit block normally.
+ pass
+ elif iteration == 2:
+ # Disable server for a little bit
+ asyncio.create_task(self.disable_server(50 * MS))
+ await asyncio.sleep(0)
+ elif iteration == 3:
+ # Exit block after catching connection error.
+ server_ws = next(iter(self.server.websockets))
+ await server_ws.close()
+ with self.assertRaises(ConnectionClosed):
+ await ws.recv()
+ else:
+ # Exit block with an exception.
+ raise Exception("BOOM")
+ pass # work around bug in coverage
+
+ with self.assertLogs("websockets", logging.INFO) as logs:
+ with self.assertRaisesRegex(Exception, "BOOM"):
+ self.loop.run_until_complete(run_client())
+
+ # Iteration 1
+ self.assertEqual(
+ [record.getMessage() for record in logs.records][:2],
+ [
+ "connection open",
+ "connection closed",
+ ],
+ )
+ # Iteration 2
+ self.assertEqual(
+ [record.getMessage() for record in logs.records][2:4],
+ [
+ "connection open",
+ "connection closed",
+ ],
+ )
+ # Iteration 3
+ self.assertEqual(
+ [record.getMessage() for record in logs.records][4:-1],
+ [
+ "connection rejected (503 Service Unavailable)",
+ "connection closed",
+ "! connect failed; reconnecting in 0.0 seconds",
+ ]
+ + [
+ "connection rejected (503 Service Unavailable)",
+ "connection closed",
+ "! connect failed again; retrying in 0 seconds",
+ ]
+ * ((len(logs.records) - 8) // 3)
+ + [
+ "connection open",
+ "connection closed",
+ ],
+ )
+ # Iteration 4
+ self.assertEqual(
+ [record.getMessage() for record in logs.records][-1:],
+ [
+ "connection open",
+ ],
+ )
+
+
+class LoggerTests(ClientServerTestsMixin, AsyncioTestCase):
+ def test_logger_client(self):
+ with self.assertLogs("test.server", logging.DEBUG) as server_logs:
+ self.start_server(logger=logging.getLogger("test.server"))
+ with self.assertLogs("test.client", logging.DEBUG) as client_logs:
+ self.start_client(logger=logging.getLogger("test.client"))
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.loop.run_until_complete(self.client.recv())
+ self.stop_client()
+ self.stop_server()
+
+ self.assertGreater(len(server_logs.records), 0)
+ self.assertGreater(len(client_logs.records), 0)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_framing.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_framing.py
new file mode 100644
index 0000000000..e1e4c891b0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_framing.py
@@ -0,0 +1,206 @@
+import asyncio
+import codecs
+import dataclasses
+import unittest
+import unittest.mock
+import warnings
+
+from websockets.exceptions import PayloadTooBig, ProtocolError
+from websockets.frames import OP_BINARY, OP_CLOSE, OP_PING, OP_PONG, OP_TEXT, CloseCode
+from websockets.legacy.framing import *
+
+from .utils import AsyncioTestCase
+
+
+class FramingTests(AsyncioTestCase):
+ def decode(self, message, mask=False, max_size=None, extensions=None):
+ stream = asyncio.StreamReader(loop=self.loop)
+ stream.feed_data(message)
+ stream.feed_eof()
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ frame = self.loop.run_until_complete(
+ Frame.read(
+ stream.readexactly,
+ mask=mask,
+ max_size=max_size,
+ extensions=extensions,
+ )
+ )
+ # Make sure all the data was consumed.
+ self.assertTrue(stream.at_eof())
+ return frame
+
+ def encode(self, frame, mask=False, extensions=None):
+ write = unittest.mock.Mock()
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ frame.write(write, mask=mask, extensions=extensions)
+ # Ensure the entire frame is sent with a single call to write().
+ # Multiple calls cause TCP fragmentation and degrade performance.
+ self.assertEqual(write.call_count, 1)
+ # The frame data is the single positional argument of that call.
+ self.assertEqual(len(write.call_args[0]), 1)
+ self.assertEqual(len(write.call_args[1]), 0)
+ return write.call_args[0][0]
+
+ def round_trip(self, message, expected, mask=False, extensions=None):
+ decoded = self.decode(message, mask, extensions=extensions)
+ decoded.check()
+ self.assertEqual(decoded, expected)
+ encoded = self.encode(decoded, mask, extensions=extensions)
+ if mask: # non-deterministic encoding
+ decoded = self.decode(encoded, mask, extensions=extensions)
+ self.assertEqual(decoded, expected)
+ else: # deterministic encoding
+ self.assertEqual(encoded, message)
+
+ def test_text(self):
+ self.round_trip(b"\x81\x04Spam", Frame(True, OP_TEXT, b"Spam"))
+
+ def test_text_masked(self):
+ self.round_trip(
+ b"\x81\x84\x5b\xfb\xe1\xa8\x08\x8b\x80\xc5",
+ Frame(True, OP_TEXT, b"Spam"),
+ mask=True,
+ )
+
+ def test_binary(self):
+ self.round_trip(b"\x82\x04Eggs", Frame(True, OP_BINARY, b"Eggs"))
+
+ def test_binary_masked(self):
+ self.round_trip(
+ b"\x82\x84\x53\xcd\xe2\x89\x16\xaa\x85\xfa",
+ Frame(True, OP_BINARY, b"Eggs"),
+ mask=True,
+ )
+
+ def test_non_ascii_text(self):
+ self.round_trip(
+ b"\x81\x05caf\xc3\xa9", Frame(True, OP_TEXT, "café".encode("utf-8"))
+ )
+
+ def test_non_ascii_text_masked(self):
+ self.round_trip(
+ b"\x81\x85\x64\xbe\xee\x7e\x07\xdf\x88\xbd\xcd",
+ Frame(True, OP_TEXT, "café".encode("utf-8")),
+ mask=True,
+ )
+
+ def test_close(self):
+ self.round_trip(b"\x88\x00", Frame(True, OP_CLOSE, b""))
+
+ def test_ping(self):
+ self.round_trip(b"\x89\x04ping", Frame(True, OP_PING, b"ping"))
+
+ def test_pong(self):
+ self.round_trip(b"\x8a\x04pong", Frame(True, OP_PONG, b"pong"))
+
+ def test_long(self):
+ self.round_trip(
+ b"\x82\x7e\x00\x7e" + 126 * b"a", Frame(True, OP_BINARY, 126 * b"a")
+ )
+
+ def test_very_long(self):
+ self.round_trip(
+ b"\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x00" + 65536 * b"a",
+ Frame(True, OP_BINARY, 65536 * b"a"),
+ )
+
+ def test_payload_too_big(self):
+ with self.assertRaises(PayloadTooBig):
+ self.decode(b"\x82\x7e\x04\x01" + 1025 * b"a", max_size=1024)
+
+ def test_bad_reserved_bits(self):
+ for encoded in [b"\xc0\x00", b"\xa0\x00", b"\x90\x00"]:
+ with self.subTest(encoded=encoded):
+ with self.assertRaises(ProtocolError):
+ self.decode(encoded)
+
+ def test_good_opcode(self):
+ for opcode in list(range(0x00, 0x03)) + list(range(0x08, 0x0B)):
+ encoded = bytes([0x80 | opcode, 0])
+ with self.subTest(encoded=encoded):
+ self.decode(encoded) # does not raise an exception
+
+ def test_bad_opcode(self):
+ for opcode in list(range(0x03, 0x08)) + list(range(0x0B, 0x10)):
+ encoded = bytes([0x80 | opcode, 0])
+ with self.subTest(encoded=encoded):
+ with self.assertRaises(ProtocolError):
+ self.decode(encoded)
+
+ def test_mask_flag(self):
+ # Mask flag correctly set.
+ self.decode(b"\x80\x80\x00\x00\x00\x00", mask=True)
+ # Mask flag incorrectly unset.
+ with self.assertRaises(ProtocolError):
+ self.decode(b"\x80\x80\x00\x00\x00\x00")
+ # Mask flag correctly unset.
+ self.decode(b"\x80\x00")
+ # Mask flag incorrectly set.
+ with self.assertRaises(ProtocolError):
+ self.decode(b"\x80\x00", mask=True)
+
+ def test_control_frame_max_length(self):
+ # At maximum allowed length.
+ self.decode(b"\x88\x7e\x00\x7d" + 125 * b"a")
+ # Above maximum allowed length.
+ with self.assertRaises(ProtocolError):
+ self.decode(b"\x88\x7e\x00\x7e" + 126 * b"a")
+
+ def test_fragmented_control_frame(self):
+ # Fin bit correctly set.
+ self.decode(b"\x88\x00")
+ # Fin bit incorrectly unset.
+ with self.assertRaises(ProtocolError):
+ self.decode(b"\x08\x00")
+
+ def test_extensions(self):
+ class Rot13:
+ @staticmethod
+ def encode(frame):
+ assert frame.opcode == OP_TEXT
+ text = frame.data.decode()
+ data = codecs.encode(text, "rot13").encode()
+ return dataclasses.replace(frame, data=data)
+
+ # This extensions is symmetrical.
+ @staticmethod
+ def decode(frame, *, max_size=None):
+ return Rot13.encode(frame)
+
+ self.round_trip(
+ b"\x81\x05uryyb", Frame(True, OP_TEXT, b"hello"), extensions=[Rot13()]
+ )
+
+
+class ParseAndSerializeCloseTests(unittest.TestCase):
+ def assertCloseData(self, code, reason, data):
+ """
+ Serializing code / reason yields data. Parsing data yields code / reason.
+
+ """
+ serialized = serialize_close(code, reason)
+ self.assertEqual(serialized, data)
+ parsed = parse_close(data)
+ self.assertEqual(parsed, (code, reason))
+
+ def test_parse_close_and_serialize_close(self):
+ self.assertCloseData(CloseCode.NORMAL_CLOSURE, "", b"\x03\xe8")
+ self.assertCloseData(CloseCode.NORMAL_CLOSURE, "OK", b"\x03\xe8OK")
+
+ def test_parse_close_empty(self):
+ self.assertEqual(parse_close(b""), (CloseCode.NO_STATUS_RCVD, ""))
+
+ def test_parse_close_errors(self):
+ with self.assertRaises(ProtocolError):
+ parse_close(b"\x03")
+ with self.assertRaises(ProtocolError):
+ parse_close(b"\x03\xe7")
+ with self.assertRaises(UnicodeDecodeError):
+ parse_close(b"\x03\xe8\xff\xff")
+
+ def test_serialize_close_errors(self):
+ with self.assertRaises(ProtocolError):
+ serialize_close(999, "")
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_handshake.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_handshake.py
new file mode 100644
index 0000000000..661ae64fc4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_handshake.py
@@ -0,0 +1,184 @@
+import contextlib
+import unittest
+
+from websockets.datastructures import Headers
+from websockets.exceptions import (
+ InvalidHandshake,
+ InvalidHeader,
+ InvalidHeaderValue,
+ InvalidUpgrade,
+)
+from websockets.legacy.handshake import *
+from websockets.utils import accept_key
+
+
+class HandshakeTests(unittest.TestCase):
+ def test_round_trip(self):
+ request_headers = Headers()
+ request_key = build_request(request_headers)
+ response_key = check_request(request_headers)
+ self.assertEqual(request_key, response_key)
+ response_headers = Headers()
+ build_response(response_headers, response_key)
+ check_response(response_headers, request_key)
+
+ @contextlib.contextmanager
+ def assertValidRequestHeaders(self):
+ """
+ Provide request headers for modification.
+
+ Assert that the transformation kept them valid.
+
+ """
+ headers = Headers()
+ build_request(headers)
+ yield headers
+ check_request(headers)
+
+ @contextlib.contextmanager
+ def assertInvalidRequestHeaders(self, exc_type):
+ """
+ Provide request headers for modification.
+
+ Assert that the transformation made them invalid.
+
+ """
+ headers = Headers()
+ build_request(headers)
+ yield headers
+ assert issubclass(exc_type, InvalidHandshake)
+ with self.assertRaises(exc_type):
+ check_request(headers)
+
+ def test_request_invalid_connection(self):
+ with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers:
+ del headers["Connection"]
+ headers["Connection"] = "Downgrade"
+
+ def test_request_missing_connection(self):
+ with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers:
+ del headers["Connection"]
+
+ def test_request_additional_connection(self):
+ with self.assertValidRequestHeaders() as headers:
+ headers["Connection"] = "close"
+
+ def test_request_invalid_upgrade(self):
+ with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers:
+ del headers["Upgrade"]
+ headers["Upgrade"] = "socketweb"
+
+ def test_request_missing_upgrade(self):
+ with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers:
+ del headers["Upgrade"]
+
+ def test_request_additional_upgrade(self):
+ with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers:
+ headers["Upgrade"] = "socketweb"
+
+ def test_request_invalid_key_not_base64(self):
+ with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers:
+ del headers["Sec-WebSocket-Key"]
+ headers["Sec-WebSocket-Key"] = "!@#$%^&*()"
+
+ def test_request_invalid_key_not_well_padded(self):
+ with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers:
+ del headers["Sec-WebSocket-Key"]
+ headers["Sec-WebSocket-Key"] = "CSIRmL8dWYxeAdr/XpEHRw"
+
+ def test_request_invalid_key_not_16_bytes_long(self):
+ with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers:
+ del headers["Sec-WebSocket-Key"]
+ headers["Sec-WebSocket-Key"] = "ZLpprpvK4PE="
+
+ def test_request_missing_key(self):
+ with self.assertInvalidRequestHeaders(InvalidHeader) as headers:
+ del headers["Sec-WebSocket-Key"]
+
+ def test_request_additional_key(self):
+ with self.assertInvalidRequestHeaders(InvalidHeader) as headers:
+ # This duplicates the Sec-WebSocket-Key header.
+ headers["Sec-WebSocket-Key"] = headers["Sec-WebSocket-Key"]
+
+ def test_request_invalid_version(self):
+ with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers:
+ del headers["Sec-WebSocket-Version"]
+ headers["Sec-WebSocket-Version"] = "42"
+
+ def test_request_missing_version(self):
+ with self.assertInvalidRequestHeaders(InvalidHeader) as headers:
+ del headers["Sec-WebSocket-Version"]
+
+ def test_request_additional_version(self):
+ with self.assertInvalidRequestHeaders(InvalidHeader) as headers:
+ # This duplicates the Sec-WebSocket-Version header.
+ headers["Sec-WebSocket-Version"] = headers["Sec-WebSocket-Version"]
+
+ @contextlib.contextmanager
+ def assertValidResponseHeaders(self, key="CSIRmL8dWYxeAdr/XpEHRw=="):
+ """
+ Provide response headers for modification.
+
+ Assert that the transformation kept them valid.
+
+ """
+ headers = Headers()
+ build_response(headers, key)
+ yield headers
+ check_response(headers, key)
+
+ @contextlib.contextmanager
+ def assertInvalidResponseHeaders(self, exc_type, key="CSIRmL8dWYxeAdr/XpEHRw=="):
+ """
+ Provide response headers for modification.
+
+ Assert that the transformation made them invalid.
+
+ """
+ headers = Headers()
+ build_response(headers, key)
+ yield headers
+ assert issubclass(exc_type, InvalidHandshake)
+ with self.assertRaises(exc_type):
+ check_response(headers, key)
+
+ def test_response_invalid_connection(self):
+ with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers:
+ del headers["Connection"]
+ headers["Connection"] = "Downgrade"
+
+ def test_response_missing_connection(self):
+ with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers:
+ del headers["Connection"]
+
+ def test_response_additional_connection(self):
+ with self.assertValidResponseHeaders() as headers:
+ headers["Connection"] = "close"
+
+ def test_response_invalid_upgrade(self):
+ with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers:
+ del headers["Upgrade"]
+ headers["Upgrade"] = "socketweb"
+
+ def test_response_missing_upgrade(self):
+ with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers:
+ del headers["Upgrade"]
+
+ def test_response_additional_upgrade(self):
+ with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers:
+ headers["Upgrade"] = "socketweb"
+
+ def test_response_invalid_accept(self):
+ with self.assertInvalidResponseHeaders(InvalidHeaderValue) as headers:
+ del headers["Sec-WebSocket-Accept"]
+ other_key = "1Eq4UDEFQYg3YspNgqxv5g=="
+ headers["Sec-WebSocket-Accept"] = accept_key(other_key)
+
+ def test_response_missing_accept(self):
+ with self.assertInvalidResponseHeaders(InvalidHeader) as headers:
+ del headers["Sec-WebSocket-Accept"]
+
+ def test_response_additional_accept(self):
+ with self.assertInvalidResponseHeaders(InvalidHeader) as headers:
+ # This duplicates the Sec-WebSocket-Accept header.
+ headers["Sec-WebSocket-Accept"] = headers["Sec-WebSocket-Accept"]
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_http.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_http.py
new file mode 100644
index 0000000000..15d53e08d2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_http.py
@@ -0,0 +1,135 @@
+import asyncio
+
+from websockets.exceptions import SecurityError
+from websockets.legacy.http import *
+from websockets.legacy.http import read_headers
+
+from .utils import AsyncioTestCase
+
+
+class HTTPAsyncTests(AsyncioTestCase):
+ def setUp(self):
+ super().setUp()
+ self.stream = asyncio.StreamReader(loop=self.loop)
+
+ async def test_read_request(self):
+ # Example from the protocol overview in RFC 6455
+ self.stream.feed_data(
+ b"GET /chat HTTP/1.1\r\n"
+ b"Host: server.example.com\r\n"
+ b"Upgrade: websocket\r\n"
+ b"Connection: Upgrade\r\n"
+ b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ b"Origin: http://example.com\r\n"
+ b"Sec-WebSocket-Protocol: chat, superchat\r\n"
+ b"Sec-WebSocket-Version: 13\r\n"
+ b"\r\n"
+ )
+ path, headers = await read_request(self.stream)
+ self.assertEqual(path, "/chat")
+ self.assertEqual(headers["Upgrade"], "websocket")
+
+ async def test_read_request_empty(self):
+ self.stream.feed_eof()
+ with self.assertRaisesRegex(
+ EOFError, "connection closed while reading HTTP request line"
+ ):
+ await read_request(self.stream)
+
+ async def test_read_request_invalid_request_line(self):
+ self.stream.feed_data(b"GET /\r\n\r\n")
+ with self.assertRaisesRegex(ValueError, "invalid HTTP request line: GET /"):
+ await read_request(self.stream)
+
+ async def test_read_request_unsupported_method(self):
+ self.stream.feed_data(b"OPTIONS * HTTP/1.1\r\n\r\n")
+ with self.assertRaisesRegex(ValueError, "unsupported HTTP method: OPTIONS"):
+ await read_request(self.stream)
+
+ async def test_read_request_unsupported_version(self):
+ self.stream.feed_data(b"GET /chat HTTP/1.0\r\n\r\n")
+ with self.assertRaisesRegex(ValueError, "unsupported HTTP version: HTTP/1.0"):
+ await read_request(self.stream)
+
+ async def test_read_request_invalid_header(self):
+ self.stream.feed_data(b"GET /chat HTTP/1.1\r\nOops\r\n")
+ with self.assertRaisesRegex(ValueError, "invalid HTTP header line: Oops"):
+ await read_request(self.stream)
+
+ async def test_read_response(self):
+ # Example from the protocol overview in RFC 6455
+ self.stream.feed_data(
+ b"HTTP/1.1 101 Switching Protocols\r\n"
+ b"Upgrade: websocket\r\n"
+ b"Connection: Upgrade\r\n"
+ b"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"
+ b"Sec-WebSocket-Protocol: chat\r\n"
+ b"\r\n"
+ )
+ status_code, reason, headers = await read_response(self.stream)
+ self.assertEqual(status_code, 101)
+ self.assertEqual(reason, "Switching Protocols")
+ self.assertEqual(headers["Upgrade"], "websocket")
+
+ async def test_read_response_empty(self):
+ self.stream.feed_eof()
+ with self.assertRaisesRegex(
+ EOFError, "connection closed while reading HTTP status line"
+ ):
+ await read_response(self.stream)
+
+ async def test_read_request_invalid_status_line(self):
+ self.stream.feed_data(b"Hello!\r\n")
+ with self.assertRaisesRegex(ValueError, "invalid HTTP status line: Hello!"):
+ await read_response(self.stream)
+
+ async def test_read_response_unsupported_version(self):
+ self.stream.feed_data(b"HTTP/1.0 400 Bad Request\r\n\r\n")
+ with self.assertRaisesRegex(ValueError, "unsupported HTTP version: HTTP/1.0"):
+ await read_response(self.stream)
+
+ async def test_read_response_invalid_status(self):
+ self.stream.feed_data(b"HTTP/1.1 OMG WTF\r\n\r\n")
+ with self.assertRaisesRegex(ValueError, "invalid HTTP status code: OMG"):
+ await read_response(self.stream)
+
+ async def test_read_response_unsupported_status(self):
+ self.stream.feed_data(b"HTTP/1.1 007 My name is Bond\r\n\r\n")
+ with self.assertRaisesRegex(ValueError, "unsupported HTTP status code: 007"):
+ await read_response(self.stream)
+
+ async def test_read_response_invalid_reason(self):
+ self.stream.feed_data(b"HTTP/1.1 200 \x7f\r\n\r\n")
+ with self.assertRaisesRegex(ValueError, "invalid HTTP reason phrase: \\x7f"):
+ await read_response(self.stream)
+
+ async def test_read_response_invalid_header(self):
+ self.stream.feed_data(b"HTTP/1.1 500 Internal Server Error\r\nOops\r\n")
+ with self.assertRaisesRegex(ValueError, "invalid HTTP header line: Oops"):
+ await read_response(self.stream)
+
+ async def test_header_name(self):
+ self.stream.feed_data(b"foo bar: baz qux\r\n\r\n")
+ with self.assertRaises(ValueError):
+ await read_headers(self.stream)
+
+ async def test_header_value(self):
+ self.stream.feed_data(b"foo: \x00\x00\x0f\r\n\r\n")
+ with self.assertRaises(ValueError):
+ await read_headers(self.stream)
+
+ async def test_headers_limit(self):
+ self.stream.feed_data(b"foo: bar\r\n" * 129 + b"\r\n")
+ with self.assertRaises(SecurityError):
+ await read_headers(self.stream)
+
+ async def test_line_limit(self):
+ # Header line contains 5 + 8186 + 2 = 8193 bytes.
+ self.stream.feed_data(b"foo: " + b"a" * 8186 + b"\r\n\r\n")
+ with self.assertRaises(SecurityError):
+ await read_headers(self.stream)
+
+ async def test_line_ending(self):
+ self.stream.feed_data(b"foo: bar\n\n")
+ with self.assertRaises(EOFError):
+ await read_headers(self.stream)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_protocol.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_protocol.py
new file mode 100644
index 0000000000..f2eb0fea03
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_protocol.py
@@ -0,0 +1,1708 @@
+import asyncio
+import contextlib
+import logging
+import sys
+import unittest
+import unittest.mock
+import warnings
+
+from websockets.exceptions import ConnectionClosed, InvalidState
+from websockets.frames import (
+ OP_BINARY,
+ OP_CLOSE,
+ OP_CONT,
+ OP_PING,
+ OP_PONG,
+ OP_TEXT,
+ Close,
+ CloseCode,
+)
+from websockets.legacy.framing import Frame
+from websockets.legacy.protocol import WebSocketCommonProtocol, broadcast
+from websockets.protocol import State
+
+from ..utils import MS
+from .utils import AsyncioTestCase
+
+
+async def async_iterable(iterable):
+ for item in iterable:
+ yield item
+
+
+class TransportMock(unittest.mock.Mock):
+ """
+ Transport mock to control the protocol's inputs and outputs in tests.
+
+ It calls the protocol's connection_made and connection_lost methods like
+ actual transports.
+
+ It also calls the protocol's connection_open method to bypass the
+ WebSocket handshake.
+
+ To simulate incoming data, tests call the protocol's data_received and
+ eof_received methods directly.
+
+ They could also pause_writing and resume_writing to test flow control.
+
+ """
+
+ # This should happen in __init__ but overriding Mock.__init__ is hard.
+ def setup_mock(self, loop, protocol):
+ self.loop = loop
+ self.protocol = protocol
+ self._eof = False
+ self._closing = False
+ # Simulate a successful TCP handshake.
+ self.protocol.connection_made(self)
+ # Simulate a successful WebSocket handshake.
+ self.protocol.connection_open()
+
+ def can_write_eof(self):
+ return True
+
+ def write_eof(self):
+ # When the protocol half-closes the TCP connection, it expects the
+ # other end to close it. Simulate that.
+ if not self._eof:
+ self.loop.call_soon(self.close)
+ self._eof = True
+
+ def close(self):
+ # Simulate how actual transports drop the connection.
+ if not self._closing:
+ self.loop.call_soon(self.protocol.connection_lost, None)
+ self._closing = True
+
+ def abort(self):
+ # Change this to an `if` if tests call abort() multiple times.
+ assert self.protocol.state is not State.CLOSED
+ self.loop.call_soon(self.protocol.connection_lost, None)
+
+
+class CommonTests:
+ """
+ Mixin that defines most tests but doesn't inherit unittest.TestCase.
+
+ Tests are run by the ServerTests and ClientTests subclasses.
+
+ """
+
+ def setUp(self):
+ super().setUp()
+
+ # This logic is encapsulated in a coroutine to prevent it from executing
+ # before the event loop is running which causes asyncio.get_event_loop()
+ # to raise a DeprecationWarning on Python ≥ 3.10.
+
+ async def create_protocol():
+ # Disable pings to make it easier to test what frames are sent exactly.
+ return WebSocketCommonProtocol(ping_interval=None)
+
+ self.protocol = self.loop.run_until_complete(create_protocol())
+ self.transport = TransportMock()
+ self.transport.setup_mock(self.loop, self.protocol)
+
+ def tearDown(self):
+ self.transport.close()
+ self.loop.run_until_complete(self.protocol.close())
+ super().tearDown()
+
+ # Utilities for writing tests.
+
+ def make_drain_slow(self, delay=MS):
+ # Process connection_made in order to initialize self.protocol.transport.
+ self.run_loop_once()
+
+ original_drain = self.protocol._drain
+
+ async def delayed_drain():
+ await asyncio.sleep(delay)
+ await original_drain()
+
+ self.protocol._drain = delayed_drain
+
+ close_frame = Frame(
+ True,
+ OP_CLOSE,
+ Close(CloseCode.NORMAL_CLOSURE, "close").serialize(),
+ )
+ local_close = Frame(
+ True,
+ OP_CLOSE,
+ Close(CloseCode.NORMAL_CLOSURE, "local").serialize(),
+ )
+ remote_close = Frame(
+ True,
+ OP_CLOSE,
+ Close(CloseCode.NORMAL_CLOSURE, "remote").serialize(),
+ )
+
+ def receive_frame(self, frame):
+ """
+ Make the protocol receive a frame.
+
+ """
+ write = self.protocol.data_received
+ mask = not self.protocol.is_client
+ frame.write(write, mask=mask)
+
+ def receive_eof(self):
+ """
+ Make the protocol receive the end of the data stream.
+
+ Since ``WebSocketCommonProtocol.eof_received`` returns ``None``, an
+ actual transport would close itself after calling it. This function
+ emulates that behavior.
+
+ """
+ self.protocol.eof_received()
+ self.loop.call_soon(self.transport.close)
+
+ def receive_eof_if_client(self):
+ """
+ Like receive_eof, but only if this is the client side.
+
+ Since the server is supposed to initiate the termination of the TCP
+ connection, this method helps making tests work for both sides.
+
+ """
+ if self.protocol.is_client:
+ self.receive_eof()
+
+ def close_connection(self, code=CloseCode.NORMAL_CLOSURE, reason="close"):
+ """
+ Execute a closing handshake.
+
+ This puts the connection in the CLOSED state.
+
+ """
+ close_frame_data = Close(code, reason).serialize()
+ # Prepare the response to the closing handshake from the remote side.
+ self.receive_frame(Frame(True, OP_CLOSE, close_frame_data))
+ self.receive_eof_if_client()
+ # Trigger the closing handshake from the local side and complete it.
+ self.loop.run_until_complete(self.protocol.close(code, reason))
+ # Empty the outgoing data stream so we can make assertions later on.
+ self.assertOneFrameSent(True, OP_CLOSE, close_frame_data)
+
+ assert self.protocol.state is State.CLOSED
+
+ def half_close_connection_local(
+ self,
+ code=CloseCode.NORMAL_CLOSURE,
+ reason="close",
+ ):
+ """
+ Start a closing handshake but do not complete it.
+
+ The main difference with `close_connection` is that the connection is
+ left in the CLOSING state until the event loop runs again.
+
+ The current implementation returns a task that must be awaited or
+ canceled, else asyncio complains about destroying a pending task.
+
+ """
+ close_frame_data = Close(code, reason).serialize()
+ # Trigger the closing handshake from the local endpoint.
+ close_task = self.loop.create_task(self.protocol.close(code, reason))
+ self.run_loop_once() # write_frame executes
+ # Empty the outgoing data stream so we can make assertions later on.
+ self.assertOneFrameSent(True, OP_CLOSE, close_frame_data)
+
+ assert self.protocol.state is State.CLOSING
+
+ # Complete the closing sequence at 1ms intervals so the test can run
+ # at each point even it goes back to the event loop several times.
+ self.loop.call_later(
+ MS, self.receive_frame, Frame(True, OP_CLOSE, close_frame_data)
+ )
+ self.loop.call_later(2 * MS, self.receive_eof_if_client)
+
+ # This task must be awaited or canceled by the caller.
+ return close_task
+
+ def half_close_connection_remote(
+ self,
+ code=CloseCode.NORMAL_CLOSURE,
+ reason="close",
+ ):
+ """
+ Receive a closing handshake but do not complete it.
+
+ The main difference with `close_connection` is that the connection is
+ left in the CLOSING state until the event loop runs again.
+
+ """
+ # On the server side, websockets completes the closing handshake and
+ # closes the TCP connection immediately. Yield to the event loop after
+ # sending the close frame to run the test while the connection is in
+ # the CLOSING state.
+ if not self.protocol.is_client:
+ self.make_drain_slow()
+
+ close_frame_data = Close(code, reason).serialize()
+ # Trigger the closing handshake from the remote endpoint.
+ self.receive_frame(Frame(True, OP_CLOSE, close_frame_data))
+ self.run_loop_once() # read_frame executes
+ # Empty the outgoing data stream so we can make assertions later on.
+ self.assertOneFrameSent(True, OP_CLOSE, close_frame_data)
+
+ assert self.protocol.state is State.CLOSING
+
+ # Complete the closing sequence at 1ms intervals so the test can run
+ # at each point even it goes back to the event loop several times.
+ self.loop.call_later(2 * MS, self.receive_eof_if_client)
+
+ def process_invalid_frames(self):
+ """
+ Make the protocol fail quickly after simulating invalid data.
+
+ To achieve this, this function triggers the protocol's eof_received,
+ which interrupts pending reads waiting for more data.
+
+ """
+ self.run_loop_once()
+ self.receive_eof()
+ self.loop.run_until_complete(self.protocol.close_connection_task)
+
+ def sent_frames(self):
+ """
+ Read all frames sent to the transport.
+
+ """
+ stream = asyncio.StreamReader(loop=self.loop)
+
+ for (data,), kw in self.transport.write.call_args_list:
+ stream.feed_data(data)
+ self.transport.write.call_args_list = []
+ stream.feed_eof()
+
+ frames = []
+ while not stream.at_eof():
+ frames.append(
+ self.loop.run_until_complete(
+ Frame.read(stream.readexactly, mask=self.protocol.is_client)
+ )
+ )
+ return frames
+
+ def last_sent_frame(self):
+ """
+ Read the last frame sent to the transport.
+
+ This method assumes that at most one frame was sent. It raises an
+ AssertionError otherwise.
+
+ """
+ frames = self.sent_frames()
+ if frames:
+ assert len(frames) == 1
+ return frames[0]
+
+ def assertFramesSent(self, *frames):
+ self.assertEqual(self.sent_frames(), [Frame(*args) for args in frames])
+
+ def assertOneFrameSent(self, *args):
+ self.assertEqual(self.last_sent_frame(), Frame(*args))
+
+ def assertNoFrameSent(self):
+ self.assertIsNone(self.last_sent_frame())
+
+ def assertConnectionClosed(self, code, message):
+ # The following line guarantees that connection_lost was called.
+ self.assertEqual(self.protocol.state, State.CLOSED)
+ # A close frame was received.
+ self.assertEqual(self.protocol.close_code, code)
+ self.assertEqual(self.protocol.close_reason, message)
+
+ def assertConnectionFailed(self, code, message):
+ # The following line guarantees that connection_lost was called.
+ self.assertEqual(self.protocol.state, State.CLOSED)
+ # No close frame was received.
+ self.assertEqual(self.protocol.close_code, CloseCode.ABNORMAL_CLOSURE)
+ self.assertEqual(self.protocol.close_reason, "")
+ # A close frame was sent -- unless the connection was already lost.
+ if code == CloseCode.ABNORMAL_CLOSURE:
+ self.assertNoFrameSent()
+ else:
+ self.assertOneFrameSent(True, OP_CLOSE, Close(code, message).serialize())
+
+ @contextlib.contextmanager
+ def assertCompletesWithin(self, min_time, max_time):
+ t0 = self.loop.time()
+ yield
+ t1 = self.loop.time()
+ dt = t1 - t0
+ self.assertGreaterEqual(dt, min_time, f"Too fast: {dt} < {min_time}")
+ self.assertLess(dt, max_time, f"Too slow: {dt} >= {max_time}")
+
+ # Test constructor.
+
+ def test_timeout_backwards_compatibility(self):
+ async def create_protocol():
+ return WebSocketCommonProtocol(ping_interval=None, timeout=5)
+
+ with warnings.catch_warnings(record=True) as recorded:
+ warnings.simplefilter("always")
+ protocol = self.loop.run_until_complete(create_protocol())
+
+ self.assertEqual(protocol.close_timeout, 5)
+ self.assertDeprecationWarnings(recorded, ["rename timeout to close_timeout"])
+
+ def test_loop_backwards_compatibility(self):
+ loop = asyncio.new_event_loop()
+ self.addCleanup(loop.close)
+
+ with warnings.catch_warnings(record=True) as recorded:
+ warnings.simplefilter("always")
+ protocol = WebSocketCommonProtocol(ping_interval=None, loop=loop)
+
+ self.assertEqual(protocol.loop, loop)
+ self.assertDeprecationWarnings(recorded, ["remove loop argument"])
+
+ # Test public attributes.
+
+ def test_local_address(self):
+ get_extra_info = unittest.mock.Mock(return_value=("host", 4312))
+ self.transport.get_extra_info = get_extra_info
+
+ self.assertEqual(self.protocol.local_address, ("host", 4312))
+ get_extra_info.assert_called_with("sockname")
+
+ def test_local_address_before_connection(self):
+ # Emulate the situation before connection_open() runs.
+ _transport = self.protocol.transport
+ del self.protocol.transport
+ try:
+ self.assertEqual(self.protocol.local_address, None)
+ finally:
+ self.protocol.transport = _transport
+
+ def test_remote_address(self):
+ get_extra_info = unittest.mock.Mock(return_value=("host", 4312))
+ self.transport.get_extra_info = get_extra_info
+
+ self.assertEqual(self.protocol.remote_address, ("host", 4312))
+ get_extra_info.assert_called_with("peername")
+
+ def test_remote_address_before_connection(self):
+ # Emulate the situation before connection_open() runs.
+ _transport = self.protocol.transport
+ del self.protocol.transport
+ try:
+ self.assertEqual(self.protocol.remote_address, None)
+ finally:
+ self.protocol.transport = _transport
+
+ def test_open(self):
+ self.assertTrue(self.protocol.open)
+ self.close_connection()
+ self.assertFalse(self.protocol.open)
+
+ def test_closed(self):
+ self.assertFalse(self.protocol.closed)
+ self.close_connection()
+ self.assertTrue(self.protocol.closed)
+
+ def test_wait_closed(self):
+ wait_closed = self.loop.create_task(self.protocol.wait_closed())
+ self.assertFalse(wait_closed.done())
+ self.close_connection()
+ self.assertTrue(wait_closed.done())
+
+ def test_close_code(self):
+ self.close_connection(CloseCode.GOING_AWAY, "Bye!")
+ self.assertEqual(self.protocol.close_code, CloseCode.GOING_AWAY)
+
+ def test_close_reason(self):
+ self.close_connection(CloseCode.GOING_AWAY, "Bye!")
+ self.assertEqual(self.protocol.close_reason, "Bye!")
+
+ def test_close_code_not_set(self):
+ self.assertIsNone(self.protocol.close_code)
+
+ def test_close_reason_not_set(self):
+ self.assertIsNone(self.protocol.close_reason)
+
+ # Test the recv coroutine.
+
+ def test_recv_text(self):
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8")))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, "café")
+
+ def test_recv_binary(self):
+ self.receive_frame(Frame(True, OP_BINARY, b"tea"))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, b"tea")
+
+ def test_recv_on_closing_connection_local(self):
+ close_task = self.half_close_connection_local()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.recv())
+
+ self.loop.run_until_complete(close_task) # cleanup
+
+ def test_recv_on_closing_connection_remote(self):
+ self.half_close_connection_remote()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.recv())
+
+ def test_recv_on_closed_connection(self):
+ self.close_connection()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.recv())
+
+ def test_recv_protocol_error(self):
+ self.receive_frame(Frame(True, OP_CONT, "café".encode("utf-8")))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(CloseCode.PROTOCOL_ERROR, "")
+
+ def test_recv_unicode_error(self):
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("latin-1")))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(CloseCode.INVALID_DATA, "")
+
+ def test_recv_text_payload_too_big(self):
+ self.protocol.max_size = 1024
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8") * 205))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(CloseCode.MESSAGE_TOO_BIG, "")
+
+ def test_recv_binary_payload_too_big(self):
+ self.protocol.max_size = 1024
+ self.receive_frame(Frame(True, OP_BINARY, b"tea" * 342))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(CloseCode.MESSAGE_TOO_BIG, "")
+
+ def test_recv_text_no_max_size(self):
+ self.protocol.max_size = None # for test coverage
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8") * 205))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, "café" * 205)
+
+ def test_recv_binary_no_max_size(self):
+ self.protocol.max_size = None # for test coverage
+ self.receive_frame(Frame(True, OP_BINARY, b"tea" * 342))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, b"tea" * 342)
+
+ def test_recv_queue_empty(self):
+ recv = self.loop.create_task(self.protocol.recv())
+ with self.assertRaises(asyncio.TimeoutError):
+ self.loop.run_until_complete(
+ asyncio.wait_for(asyncio.shield(recv), timeout=MS)
+ )
+
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8")))
+ data = self.loop.run_until_complete(recv)
+ self.assertEqual(data, "café")
+
+ def test_recv_queue_full(self):
+ self.protocol.max_queue = 2
+ # Test internals because it's hard to verify buffers from the outside.
+ self.assertEqual(list(self.protocol.messages), [])
+
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8")))
+ self.run_loop_once()
+ self.assertEqual(list(self.protocol.messages), ["café"])
+
+ self.receive_frame(Frame(True, OP_BINARY, b"tea"))
+ self.run_loop_once()
+ self.assertEqual(list(self.protocol.messages), ["café", b"tea"])
+
+ self.receive_frame(Frame(True, OP_BINARY, b"milk"))
+ self.run_loop_once()
+ self.assertEqual(list(self.protocol.messages), ["café", b"tea"])
+
+ self.loop.run_until_complete(self.protocol.recv())
+ self.run_loop_once()
+ self.assertEqual(list(self.protocol.messages), [b"tea", b"milk"])
+
+ self.loop.run_until_complete(self.protocol.recv())
+ self.run_loop_once()
+ self.assertEqual(list(self.protocol.messages), [b"milk"])
+
+ self.loop.run_until_complete(self.protocol.recv())
+ self.run_loop_once()
+ self.assertEqual(list(self.protocol.messages), [])
+
+ def test_recv_queue_no_limit(self):
+ self.protocol.max_queue = None
+
+ for _ in range(100):
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8")))
+ self.run_loop_once()
+
+ # Incoming message queue can contain at least 100 messages.
+ self.assertEqual(list(self.protocol.messages), ["café"] * 100)
+
+ for _ in range(100):
+ self.loop.run_until_complete(self.protocol.recv())
+
+ self.assertEqual(list(self.protocol.messages), [])
+
+ def test_recv_other_error(self):
+ async def read_message():
+ raise Exception("BOOM")
+
+ self.protocol.read_message = read_message
+ self.process_invalid_frames()
+ self.assertConnectionFailed(CloseCode.INTERNAL_ERROR, "")
+
+ def test_recv_canceled(self):
+ recv = self.loop.create_task(self.protocol.recv())
+ self.loop.call_soon(recv.cancel)
+
+ with self.assertRaises(asyncio.CancelledError):
+ self.loop.run_until_complete(recv)
+
+ # The next frame doesn't disappear in a vacuum (it used to).
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8")))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, "café")
+
+ def test_recv_canceled_race_condition(self):
+ recv = self.loop.create_task(
+ asyncio.wait_for(self.protocol.recv(), timeout=0.000_001)
+ )
+ self.loop.call_soon(
+ self.receive_frame, Frame(True, OP_TEXT, "café".encode("utf-8"))
+ )
+
+ with self.assertRaises(asyncio.TimeoutError):
+ self.loop.run_until_complete(recv)
+
+ # The previous frame doesn't disappear in a vacuum (it used to).
+ self.receive_frame(Frame(True, OP_TEXT, "tea".encode("utf-8")))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ # If we're getting "tea" there, it means "café" was swallowed (ha, ha).
+ self.assertEqual(data, "café")
+
+ def test_recv_when_transfer_data_cancelled(self):
+ # Clog incoming queue.
+ self.protocol.max_queue = 1
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8")))
+ self.receive_frame(Frame(True, OP_BINARY, b"tea"))
+ self.run_loop_once()
+
+ # Flow control kicks in (check with an implementation detail).
+ self.assertFalse(self.protocol._put_message_waiter.done())
+
+ # Schedule recv().
+ recv = self.loop.create_task(self.protocol.recv())
+
+ # Cancel transfer_data_task (again, implementation detail).
+ self.protocol.fail_connection()
+ self.run_loop_once()
+ self.assertTrue(self.protocol.transfer_data_task.cancelled())
+
+ # recv() completes properly.
+ self.assertEqual(self.loop.run_until_complete(recv), "café")
+
+ def test_recv_prevents_concurrent_calls(self):
+ recv = self.loop.create_task(self.protocol.recv())
+
+ with self.assertRaises(RuntimeError) as raised:
+ self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(
+ str(raised.exception),
+ "cannot call recv while another coroutine "
+ "is already waiting for the next message",
+ )
+ recv.cancel()
+
+ # Test the send coroutine.
+
+ def test_send_text(self):
+ self.loop.run_until_complete(self.protocol.send("café"))
+ self.assertOneFrameSent(True, OP_TEXT, "café".encode("utf-8"))
+
+ def test_send_binary(self):
+ self.loop.run_until_complete(self.protocol.send(b"tea"))
+ self.assertOneFrameSent(True, OP_BINARY, b"tea")
+
+ def test_send_binary_from_bytearray(self):
+ self.loop.run_until_complete(self.protocol.send(bytearray(b"tea")))
+ self.assertOneFrameSent(True, OP_BINARY, b"tea")
+
+ def test_send_binary_from_memoryview(self):
+ self.loop.run_until_complete(self.protocol.send(memoryview(b"tea")))
+ self.assertOneFrameSent(True, OP_BINARY, b"tea")
+
+ def test_send_dict(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(self.protocol.send({"not": "encoded"}))
+ self.assertNoFrameSent()
+
+ def test_send_type_error(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(self.protocol.send(42))
+ self.assertNoFrameSent()
+
+ def test_send_iterable_text(self):
+ self.loop.run_until_complete(self.protocol.send(["ca", "fé"]))
+ self.assertFramesSent(
+ (False, OP_TEXT, "ca".encode("utf-8")),
+ (False, OP_CONT, "fé".encode("utf-8")),
+ (True, OP_CONT, "".encode("utf-8")),
+ )
+
+ def test_send_iterable_binary(self):
+ self.loop.run_until_complete(self.protocol.send([b"te", b"a"]))
+ self.assertFramesSent(
+ (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"")
+ )
+
+ def test_send_iterable_binary_from_bytearray(self):
+ self.loop.run_until_complete(
+ self.protocol.send([bytearray(b"te"), bytearray(b"a")])
+ )
+ self.assertFramesSent(
+ (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"")
+ )
+
+ def test_send_iterable_binary_from_memoryview(self):
+ self.loop.run_until_complete(
+ self.protocol.send([memoryview(b"te"), memoryview(b"a")])
+ )
+ self.assertFramesSent(
+ (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"")
+ )
+
+ def test_send_empty_iterable(self):
+ self.loop.run_until_complete(self.protocol.send([]))
+ self.assertNoFrameSent()
+
+ def test_send_iterable_type_error(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(self.protocol.send([42]))
+ self.assertNoFrameSent()
+
+ def test_send_iterable_mixed_type_error(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(self.protocol.send(["café", b"tea"]))
+ self.assertFramesSent(
+ (False, OP_TEXT, "café".encode("utf-8")),
+ (True, OP_CLOSE, Close(CloseCode.INTERNAL_ERROR, "").serialize()),
+ )
+
+ def test_send_iterable_prevents_concurrent_send(self):
+ self.make_drain_slow(2 * MS)
+
+ async def send_iterable():
+ await self.protocol.send(["ca", "fé"])
+
+ async def send_concurrent():
+ await asyncio.sleep(MS)
+ await self.protocol.send(b"tea")
+
+ async def run_concurrently():
+ await asyncio.gather(
+ send_iterable(),
+ send_concurrent(),
+ )
+
+ self.loop.run_until_complete(run_concurrently())
+
+ self.assertFramesSent(
+ (False, OP_TEXT, "ca".encode("utf-8")),
+ (False, OP_CONT, "fé".encode("utf-8")),
+ (True, OP_CONT, "".encode("utf-8")),
+ (True, OP_BINARY, b"tea"),
+ )
+
+ def test_send_async_iterable_text(self):
+ self.loop.run_until_complete(self.protocol.send(async_iterable(["ca", "fé"])))
+ self.assertFramesSent(
+ (False, OP_TEXT, "ca".encode("utf-8")),
+ (False, OP_CONT, "fé".encode("utf-8")),
+ (True, OP_CONT, "".encode("utf-8")),
+ )
+
+ def test_send_async_iterable_binary(self):
+ self.loop.run_until_complete(self.protocol.send(async_iterable([b"te", b"a"])))
+ self.assertFramesSent(
+ (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"")
+ )
+
+ def test_send_async_iterable_binary_from_bytearray(self):
+ self.loop.run_until_complete(
+ self.protocol.send(async_iterable([bytearray(b"te"), bytearray(b"a")]))
+ )
+ self.assertFramesSent(
+ (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"")
+ )
+
+ def test_send_async_iterable_binary_from_memoryview(self):
+ self.loop.run_until_complete(
+ self.protocol.send(async_iterable([memoryview(b"te"), memoryview(b"a")]))
+ )
+ self.assertFramesSent(
+ (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"")
+ )
+
+ def test_send_empty_async_iterable(self):
+ self.loop.run_until_complete(self.protocol.send(async_iterable([])))
+ self.assertNoFrameSent()
+
+ def test_send_async_iterable_type_error(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(self.protocol.send(async_iterable([42])))
+ self.assertNoFrameSent()
+
+ def test_send_async_iterable_mixed_type_error(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(
+ self.protocol.send(async_iterable(["café", b"tea"]))
+ )
+ self.assertFramesSent(
+ (False, OP_TEXT, "café".encode("utf-8")),
+ (True, OP_CLOSE, Close(CloseCode.INTERNAL_ERROR, "").serialize()),
+ )
+
+ def test_send_async_iterable_prevents_concurrent_send(self):
+ self.make_drain_slow(2 * MS)
+
+ async def send_async_iterable():
+ await self.protocol.send(async_iterable(["ca", "fé"]))
+
+ async def send_concurrent():
+ await asyncio.sleep(MS)
+ await self.protocol.send(b"tea")
+
+ async def run_concurrently():
+ await asyncio.gather(
+ send_async_iterable(),
+ send_concurrent(),
+ )
+
+ self.loop.run_until_complete(run_concurrently())
+
+ self.assertFramesSent(
+ (False, OP_TEXT, "ca".encode("utf-8")),
+ (False, OP_CONT, "fé".encode("utf-8")),
+ (True, OP_CONT, "".encode("utf-8")),
+ (True, OP_BINARY, b"tea"),
+ )
+
+ def test_send_on_closing_connection_local(self):
+ close_task = self.half_close_connection_local()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.send("foobar"))
+
+ self.assertNoFrameSent()
+
+ self.loop.run_until_complete(close_task) # cleanup
+
+ def test_send_on_closing_connection_remote(self):
+ self.half_close_connection_remote()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.send("foobar"))
+
+ self.assertNoFrameSent()
+
+ def test_send_on_closed_connection(self):
+ self.close_connection()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.send("foobar"))
+
+ self.assertNoFrameSent()
+
+ # Test the ping coroutine.
+
+ def test_ping_default(self):
+ self.loop.run_until_complete(self.protocol.ping())
+ # With our testing tools, it's more convenient to extract the expected
+ # ping data from the library's internals than from the frame sent.
+ ping_data = next(iter(self.protocol.pings))
+ self.assertIsInstance(ping_data, bytes)
+ self.assertEqual(len(ping_data), 4)
+ self.assertOneFrameSent(True, OP_PING, ping_data)
+
+ def test_ping_text(self):
+ self.loop.run_until_complete(self.protocol.ping("café"))
+ self.assertOneFrameSent(True, OP_PING, "café".encode("utf-8"))
+
+ def test_ping_binary(self):
+ self.loop.run_until_complete(self.protocol.ping(b"tea"))
+ self.assertOneFrameSent(True, OP_PING, b"tea")
+
+ def test_ping_binary_from_bytearray(self):
+ self.loop.run_until_complete(self.protocol.ping(bytearray(b"tea")))
+ self.assertOneFrameSent(True, OP_PING, b"tea")
+
+ def test_ping_binary_from_memoryview(self):
+ self.loop.run_until_complete(self.protocol.ping(memoryview(b"tea")))
+ self.assertOneFrameSent(True, OP_PING, b"tea")
+
+ def test_ping_type_error(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(self.protocol.ping(42))
+ self.assertNoFrameSent()
+
+ def test_ping_on_closing_connection_local(self):
+ close_task = self.half_close_connection_local()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.ping())
+
+ self.assertNoFrameSent()
+
+ self.loop.run_until_complete(close_task) # cleanup
+
+ def test_ping_on_closing_connection_remote(self):
+ self.half_close_connection_remote()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.ping())
+
+ self.assertNoFrameSent()
+
+ def test_ping_on_closed_connection(self):
+ self.close_connection()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.ping())
+
+ self.assertNoFrameSent()
+
+ # Test the pong coroutine.
+
+ def test_pong_default(self):
+ self.loop.run_until_complete(self.protocol.pong())
+ self.assertOneFrameSent(True, OP_PONG, b"")
+
+ def test_pong_text(self):
+ self.loop.run_until_complete(self.protocol.pong("café"))
+ self.assertOneFrameSent(True, OP_PONG, "café".encode("utf-8"))
+
+ def test_pong_binary(self):
+ self.loop.run_until_complete(self.protocol.pong(b"tea"))
+ self.assertOneFrameSent(True, OP_PONG, b"tea")
+
+ def test_pong_binary_from_bytearray(self):
+ self.loop.run_until_complete(self.protocol.pong(bytearray(b"tea")))
+ self.assertOneFrameSent(True, OP_PONG, b"tea")
+
+ def test_pong_binary_from_memoryview(self):
+ self.loop.run_until_complete(self.protocol.pong(memoryview(b"tea")))
+ self.assertOneFrameSent(True, OP_PONG, b"tea")
+
+ def test_pong_type_error(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(self.protocol.pong(42))
+ self.assertNoFrameSent()
+
+ def test_pong_on_closing_connection_local(self):
+ close_task = self.half_close_connection_local()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.pong())
+
+ self.assertNoFrameSent()
+
+ self.loop.run_until_complete(close_task) # cleanup
+
+ def test_pong_on_closing_connection_remote(self):
+ self.half_close_connection_remote()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.pong())
+
+ self.assertNoFrameSent()
+
+ def test_pong_on_closed_connection(self):
+ self.close_connection()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.pong())
+
+ self.assertNoFrameSent()
+
+ # Test the protocol's logic for acknowledging pings with pongs.
+
+ def test_answer_ping(self):
+ self.receive_frame(Frame(True, OP_PING, b"test"))
+ self.run_loop_once()
+ self.assertOneFrameSent(True, OP_PONG, b"test")
+
+ def test_answer_ping_does_not_crash_if_connection_closing(self):
+ close_task = self.half_close_connection_local()
+
+ self.receive_frame(Frame(True, OP_PING, b"test"))
+ self.run_loop_once()
+
+ with self.assertNoLogs():
+ self.loop.run_until_complete(self.protocol.close())
+
+ self.loop.run_until_complete(close_task) # cleanup
+
+ def test_answer_ping_does_not_crash_if_connection_closed(self):
+ self.make_drain_slow()
+ # Drop the connection right after receiving a ping frame,
+ # which prevents responding with a pong frame properly.
+ self.receive_frame(Frame(True, OP_PING, b"test"))
+ self.receive_eof()
+ self.run_loop_once()
+
+ with self.assertNoLogs():
+ self.loop.run_until_complete(self.protocol.close())
+
+ def test_ignore_pong(self):
+ self.receive_frame(Frame(True, OP_PONG, b"test"))
+ self.run_loop_once()
+ self.assertNoFrameSent()
+
+ def test_acknowledge_ping(self):
+ pong_waiter = self.loop.run_until_complete(self.protocol.ping())
+ self.assertFalse(pong_waiter.done())
+ ping_frame = self.last_sent_frame()
+ pong_frame = Frame(True, OP_PONG, ping_frame.data)
+ self.receive_frame(pong_frame)
+ self.run_loop_once()
+ self.run_loop_once()
+ self.assertTrue(pong_waiter.done())
+
+ def test_abort_ping(self):
+ pong_waiter = self.loop.run_until_complete(self.protocol.ping())
+ # Remove the frame from the buffer, else close_connection() complains.
+ self.last_sent_frame()
+ self.assertFalse(pong_waiter.done())
+ self.close_connection()
+ self.assertTrue(pong_waiter.done())
+ self.assertIsInstance(pong_waiter.exception(), ConnectionClosed)
+
+ def test_abort_ping_does_not_log_exception_if_not_retreived(self):
+ self.loop.run_until_complete(self.protocol.ping())
+ # Get the internal Future, which isn't directly returned by ping().
+ ((pong_waiter, _timestamp),) = self.protocol.pings.values()
+ # Remove the frame from the buffer, else close_connection() complains.
+ self.last_sent_frame()
+ self.close_connection()
+ # Check a private attribute, for lack of a better solution.
+ self.assertFalse(pong_waiter._log_traceback)
+
+ def test_acknowledge_previous_pings(self):
+ pings = [
+ (self.loop.run_until_complete(self.protocol.ping()), self.last_sent_frame())
+ for i in range(3)
+ ]
+ # Unsolicited pong doesn't acknowledge pings
+ self.receive_frame(Frame(True, OP_PONG, b""))
+ self.run_loop_once()
+ self.run_loop_once()
+ self.assertFalse(pings[0][0].done())
+ self.assertFalse(pings[1][0].done())
+ self.assertFalse(pings[2][0].done())
+ # Pong acknowledges all previous pings
+ self.receive_frame(Frame(True, OP_PONG, pings[1][1].data))
+ self.run_loop_once()
+ self.run_loop_once()
+ self.assertTrue(pings[0][0].done())
+ self.assertTrue(pings[1][0].done())
+ self.assertFalse(pings[2][0].done())
+
+ def test_acknowledge_aborted_ping(self):
+ pong_waiter = self.loop.run_until_complete(self.protocol.ping())
+ ping_frame = self.last_sent_frame()
+ # Clog incoming queue. This lets connection_lost() abort pending pings
+ # with a ConnectionClosed exception before transfer_data_task
+ # terminates and close_connection cancels keepalive_ping_task.
+ self.protocol.max_queue = 1
+ self.receive_frame(Frame(True, OP_TEXT, b"1"))
+ self.receive_frame(Frame(True, OP_TEXT, b"2"))
+ # Add pong frame to the queue.
+ pong_frame = Frame(True, OP_PONG, ping_frame.data)
+ self.receive_frame(pong_frame)
+ # Connection drops.
+ self.receive_eof()
+ self.loop.run_until_complete(self.protocol.wait_closed())
+ # Ping receives a ConnectionClosed exception.
+ with self.assertRaises(ConnectionClosed):
+ pong_waiter.result()
+
+ # transfer_data doesn't crash, which would be logged.
+ with self.assertNoLogs():
+ # Unclog incoming queue.
+ self.loop.run_until_complete(self.protocol.recv())
+ self.loop.run_until_complete(self.protocol.recv())
+
+ def test_canceled_ping(self):
+ pong_waiter = self.loop.run_until_complete(self.protocol.ping())
+ ping_frame = self.last_sent_frame()
+ pong_waiter.cancel()
+ pong_frame = Frame(True, OP_PONG, ping_frame.data)
+ self.receive_frame(pong_frame)
+ self.run_loop_once()
+ self.run_loop_once()
+ self.assertTrue(pong_waiter.cancelled())
+
+ def test_duplicate_ping(self):
+ self.loop.run_until_complete(self.protocol.ping(b"foobar"))
+ self.assertOneFrameSent(True, OP_PING, b"foobar")
+ with self.assertRaises(RuntimeError):
+ self.loop.run_until_complete(self.protocol.ping(b"foobar"))
+ self.assertNoFrameSent()
+
+ # Test the protocol's logic for measuring latency
+
+ def test_record_latency_on_pong(self):
+ self.assertEqual(self.protocol.latency, 0)
+ self.loop.run_until_complete(self.protocol.ping(b"test"))
+ self.receive_frame(Frame(True, OP_PONG, b"test"))
+ self.run_loop_once()
+ self.assertGreater(self.protocol.latency, 0)
+
+ def test_return_latency_on_pong(self):
+ pong_waiter = self.loop.run_until_complete(self.protocol.ping())
+ ping_frame = self.last_sent_frame()
+ pong_frame = Frame(True, OP_PONG, ping_frame.data)
+ self.receive_frame(pong_frame)
+ latency = self.loop.run_until_complete(pong_waiter)
+ self.assertGreater(latency, 0)
+
+ # Test the protocol's logic for rebuilding fragmented messages.
+
+ def test_fragmented_text(self):
+ self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8")))
+ self.receive_frame(Frame(True, OP_CONT, "fé".encode("utf-8")))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, "café")
+
+ def test_fragmented_binary(self):
+ self.receive_frame(Frame(False, OP_BINARY, b"t"))
+ self.receive_frame(Frame(False, OP_CONT, b"e"))
+ self.receive_frame(Frame(True, OP_CONT, b"a"))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, b"tea")
+
+ def test_fragmented_text_payload_too_big(self):
+ self.protocol.max_size = 1024
+ self.receive_frame(Frame(False, OP_TEXT, "café".encode("utf-8") * 100))
+ self.receive_frame(Frame(True, OP_CONT, "café".encode("utf-8") * 105))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(CloseCode.MESSAGE_TOO_BIG, "")
+
+ def test_fragmented_binary_payload_too_big(self):
+ self.protocol.max_size = 1024
+ self.receive_frame(Frame(False, OP_BINARY, b"tea" * 171))
+ self.receive_frame(Frame(True, OP_CONT, b"tea" * 171))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(CloseCode.MESSAGE_TOO_BIG, "")
+
+ def test_fragmented_text_no_max_size(self):
+ self.protocol.max_size = None # for test coverage
+ self.receive_frame(Frame(False, OP_TEXT, "café".encode("utf-8") * 100))
+ self.receive_frame(Frame(True, OP_CONT, "café".encode("utf-8") * 105))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, "café" * 205)
+
+ def test_fragmented_binary_no_max_size(self):
+ self.protocol.max_size = None # for test coverage
+ self.receive_frame(Frame(False, OP_BINARY, b"tea" * 171))
+ self.receive_frame(Frame(True, OP_CONT, b"tea" * 171))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, b"tea" * 342)
+
+ def test_control_frame_within_fragmented_text(self):
+ self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8")))
+ self.receive_frame(Frame(True, OP_PING, b""))
+ self.receive_frame(Frame(True, OP_CONT, "fé".encode("utf-8")))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, "café")
+ self.assertOneFrameSent(True, OP_PONG, b"")
+
+ def test_unterminated_fragmented_text(self):
+ self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8")))
+ # Missing the second part of the fragmented frame.
+ self.receive_frame(Frame(True, OP_BINARY, b"tea"))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(CloseCode.PROTOCOL_ERROR, "")
+
+ def test_close_handshake_in_fragmented_text(self):
+ self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8")))
+ self.receive_frame(Frame(True, OP_CLOSE, b""))
+ self.process_invalid_frames()
+ # The RFC may have overlooked this case: it says that control frames
+ # can be interjected in the middle of a fragmented message and that a
+ # close frame must be echoed. Even though there's an unterminated
+ # message, technically, the closing handshake was successful.
+ self.assertConnectionClosed(CloseCode.NO_STATUS_RCVD, "")
+
+ def test_connection_close_in_fragmented_text(self):
+ self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8")))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(CloseCode.ABNORMAL_CLOSURE, "")
+
+ # Test miscellaneous code paths to ensure full coverage.
+
+ def test_connection_lost(self):
+ # Test calling connection_lost without going through close_connection.
+ self.protocol.connection_lost(None)
+
+ self.assertConnectionFailed(CloseCode.ABNORMAL_CLOSURE, "")
+
+ def test_ensure_open_before_opening_handshake(self):
+ # Simulate a bug by forcibly reverting the protocol state.
+ self.protocol.state = State.CONNECTING
+
+ with self.assertRaises(InvalidState):
+ self.loop.run_until_complete(self.protocol.ensure_open())
+
+ def test_ensure_open_during_unclean_close(self):
+ # Process connection_made in order to start transfer_data_task.
+ self.run_loop_once()
+
+ # Ensure the test terminates quickly.
+ self.loop.call_later(MS, self.receive_eof_if_client)
+
+ # Simulate the case when close() times out sending a close frame.
+ self.protocol.fail_connection()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.ensure_open())
+
+ def test_legacy_recv(self):
+ # By default legacy_recv in disabled.
+ self.assertEqual(self.protocol.legacy_recv, False)
+
+ self.close_connection()
+
+ # Enable legacy_recv.
+ self.protocol.legacy_recv = True
+
+ # Now recv() returns None instead of raising ConnectionClosed.
+ self.assertIsNone(self.loop.run_until_complete(self.protocol.recv()))
+
+ def test_connection_closed_attributes(self):
+ self.close_connection()
+
+ with self.assertRaises(ConnectionClosed) as context:
+ self.loop.run_until_complete(self.protocol.recv())
+
+ connection_closed_exc = context.exception
+ self.assertEqual(connection_closed_exc.code, CloseCode.NORMAL_CLOSURE)
+ self.assertEqual(connection_closed_exc.reason, "close")
+
+ # Test the protocol logic for sending keepalive pings.
+
+ def restart_protocol_with_keepalive_ping(
+ self,
+ ping_interval=3 * MS,
+ ping_timeout=3 * MS,
+ ):
+ initial_protocol = self.protocol
+
+ # copied from tearDown
+
+ self.transport.close()
+ self.loop.run_until_complete(self.protocol.close())
+
+ # copied from setUp, but enables keepalive pings
+
+ async def create_protocol():
+ return WebSocketCommonProtocol(
+ ping_interval=ping_interval,
+ ping_timeout=ping_timeout,
+ )
+
+ self.protocol = self.loop.run_until_complete(create_protocol())
+
+ self.transport = TransportMock()
+ self.transport.setup_mock(self.loop, self.protocol)
+ self.protocol.is_client = initial_protocol.is_client
+ self.protocol.side = initial_protocol.side
+
+ def test_keepalive_ping(self):
+ self.restart_protocol_with_keepalive_ping()
+
+ # Ping is sent at 3ms and acknowledged at 4ms.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ (ping_1,) = tuple(self.protocol.pings)
+ self.assertOneFrameSent(True, OP_PING, ping_1)
+ self.receive_frame(Frame(True, OP_PONG, ping_1))
+
+ # Next ping is sent at 7ms.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ (ping_2,) = tuple(self.protocol.pings)
+ self.assertOneFrameSent(True, OP_PING, ping_2)
+
+ # The keepalive ping task goes on.
+ self.assertFalse(self.protocol.keepalive_ping_task.done())
+
+ def test_keepalive_ping_not_acknowledged_closes_connection(self):
+ self.restart_protocol_with_keepalive_ping()
+
+ # Ping is sent at 3ms and not acknowledged.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ (ping_1,) = tuple(self.protocol.pings)
+ self.assertOneFrameSent(True, OP_PING, ping_1)
+
+ # Connection is closed at 6ms.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ self.assertOneFrameSent(
+ True,
+ OP_CLOSE,
+ Close(CloseCode.INTERNAL_ERROR, "keepalive ping timeout").serialize(),
+ )
+
+ # The keepalive ping task is complete.
+ self.assertEqual(self.protocol.keepalive_ping_task.result(), None)
+
+ def test_keepalive_ping_stops_when_connection_closing(self):
+ self.restart_protocol_with_keepalive_ping()
+ close_task = self.half_close_connection_local()
+
+ # No ping sent at 3ms because the closing handshake is in progress.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ self.assertNoFrameSent()
+
+ # The keepalive ping task terminated.
+ self.assertTrue(self.protocol.keepalive_ping_task.cancelled())
+
+ self.loop.run_until_complete(close_task) # cleanup
+
+ def test_keepalive_ping_stops_when_connection_closed(self):
+ self.restart_protocol_with_keepalive_ping()
+ self.close_connection()
+
+ # The keepalive ping task terminated.
+ self.assertTrue(self.protocol.keepalive_ping_task.cancelled())
+
+ def test_keepalive_ping_does_not_crash_when_connection_lost(self):
+ self.restart_protocol_with_keepalive_ping()
+ # Clog incoming queue. This lets connection_lost() abort pending pings
+ # with a ConnectionClosed exception before transfer_data_task
+ # terminates and close_connection cancels keepalive_ping_task.
+ self.protocol.max_queue = 1
+ self.receive_frame(Frame(True, OP_TEXT, b"1"))
+ self.receive_frame(Frame(True, OP_TEXT, b"2"))
+ # Ping is sent at 3ms.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ ((pong_waiter, _timestamp),) = self.protocol.pings.values()
+ # Connection drops.
+ self.receive_eof()
+ self.loop.run_until_complete(self.protocol.wait_closed())
+
+ # The ping waiter receives a ConnectionClosed exception.
+ with self.assertRaises(ConnectionClosed):
+ pong_waiter.result()
+ # The keepalive ping task terminated properly.
+ self.assertIsNone(self.protocol.keepalive_ping_task.result())
+
+ # Unclog incoming queue to terminate the test quickly.
+ self.loop.run_until_complete(self.protocol.recv())
+ self.loop.run_until_complete(self.protocol.recv())
+
+ def test_keepalive_ping_with_no_ping_interval(self):
+ self.restart_protocol_with_keepalive_ping(ping_interval=None)
+
+ # No ping is sent at 3ms.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ self.assertNoFrameSent()
+
+ def test_keepalive_ping_with_no_ping_timeout(self):
+ self.restart_protocol_with_keepalive_ping(ping_timeout=None)
+
+ # Ping is sent at 3ms and not acknowledged.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ (ping_1,) = tuple(self.protocol.pings)
+ self.assertOneFrameSent(True, OP_PING, ping_1)
+
+ # Next ping is sent at 7ms anyway.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ ping_1_again, ping_2 = tuple(self.protocol.pings)
+ self.assertEqual(ping_1, ping_1_again)
+ self.assertOneFrameSent(True, OP_PING, ping_2)
+
+ # The keepalive ping task goes on.
+ self.assertFalse(self.protocol.keepalive_ping_task.done())
+
+ def test_keepalive_ping_unexpected_error(self):
+ self.restart_protocol_with_keepalive_ping()
+
+ async def ping():
+ raise Exception("BOOM")
+
+ self.protocol.ping = ping
+
+ # The keepalive ping task fails when sending a ping at 3ms.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+
+ # The keepalive ping task is complete.
+ # It logs and swallows the exception.
+ self.assertEqual(self.protocol.keepalive_ping_task.result(), None)
+
+ # Test the protocol logic for closing the connection.
+
+ def test_local_close(self):
+ # Emulate how the remote endpoint answers the closing handshake.
+ self.loop.call_later(MS, self.receive_frame, self.close_frame)
+ self.loop.call_later(MS, self.receive_eof_if_client)
+
+ # Run the closing handshake.
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+
+ self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close")
+ self.assertOneFrameSent(*self.close_frame)
+
+ # Closing the connection again is a no-op.
+ self.loop.run_until_complete(self.protocol.close(reason="oh noes!"))
+
+ self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close")
+ self.assertNoFrameSent()
+
+ def test_remote_close(self):
+ # Emulate how the remote endpoint initiates the closing handshake.
+ self.loop.call_later(MS, self.receive_frame, self.close_frame)
+ self.loop.call_later(MS, self.receive_eof_if_client)
+
+ # Wait for some data in order to process the handshake.
+ # After recv() raises ConnectionClosed, the connection is closed.
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.recv())
+
+ self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close")
+ self.assertOneFrameSent(*self.close_frame)
+
+ # Closing the connection again is a no-op.
+ self.loop.run_until_complete(self.protocol.close(reason="oh noes!"))
+
+ self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close")
+ self.assertNoFrameSent()
+
+ def test_remote_close_and_connection_lost(self):
+ self.make_drain_slow()
+ # Drop the connection right after receiving a close frame,
+ # which prevents echoing the close frame properly.
+ self.receive_frame(self.close_frame)
+ self.receive_eof()
+ self.run_loop_once()
+
+ with self.assertNoLogs():
+ self.loop.run_until_complete(self.protocol.close(reason="oh noes!"))
+
+ self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close")
+ self.assertOneFrameSent(*self.close_frame)
+
+ def test_simultaneous_close(self):
+ # Receive the incoming close frame right after self.protocol.close()
+ # starts executing. This reproduces the error described in:
+ # https://github.com/python-websockets/websockets/issues/339
+ self.loop.call_soon(self.receive_frame, self.remote_close)
+ self.loop.call_soon(self.receive_eof_if_client)
+ self.run_loop_once()
+
+ self.loop.run_until_complete(self.protocol.close(reason="local"))
+
+ self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "remote")
+ # The current implementation sends a close frame in response to the
+ # close frame received from the remote end. It skips the close frame
+ # that should be sent as a result of calling close().
+ self.assertOneFrameSent(*self.remote_close)
+
+ def test_close_preserves_incoming_frames(self):
+ self.receive_frame(Frame(True, OP_TEXT, b"hello"))
+ self.run_loop_once()
+
+ self.loop.call_later(MS, self.receive_frame, self.close_frame)
+ self.loop.call_later(MS, self.receive_eof_if_client)
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+
+ self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close")
+ self.assertOneFrameSent(*self.close_frame)
+
+ next_message = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(next_message, "hello")
+
+ def test_close_protocol_error(self):
+ invalid_close_frame = Frame(True, OP_CLOSE, b"\x00")
+ self.receive_frame(invalid_close_frame)
+ self.receive_eof_if_client()
+ self.run_loop_once()
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+
+ self.assertConnectionFailed(CloseCode.PROTOCOL_ERROR, "")
+
+ def test_close_connection_lost(self):
+ self.receive_eof()
+ self.run_loop_once()
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+
+ self.assertConnectionFailed(CloseCode.ABNORMAL_CLOSURE, "")
+
+ def test_local_close_during_recv(self):
+ recv = self.loop.create_task(self.protocol.recv())
+
+ self.loop.call_later(MS, self.receive_frame, self.close_frame)
+ self.loop.call_later(MS, self.receive_eof_if_client)
+
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(recv)
+
+ self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close")
+
+ # There is no test_remote_close_during_recv because it would be identical
+ # to test_remote_close.
+
+ def test_remote_close_during_send(self):
+ self.make_drain_slow()
+ send = self.loop.create_task(self.protocol.send("hello"))
+
+ self.receive_frame(self.close_frame)
+ self.receive_eof()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(send)
+
+ self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close")
+
+ # There is no test_local_close_during_send because this cannot really
+ # happen, considering that writes are serialized.
+
+ def test_broadcast_text(self):
+ broadcast([self.protocol], "café")
+ self.assertOneFrameSent(True, OP_TEXT, "café".encode("utf-8"))
+
+ def test_broadcast_binary(self):
+ broadcast([self.protocol], b"tea")
+ self.assertOneFrameSent(True, OP_BINARY, b"tea")
+
+ def test_broadcast_type_error(self):
+ with self.assertRaises(TypeError):
+ broadcast([self.protocol], ["ca", "fé"])
+
+ def test_broadcast_no_clients(self):
+ broadcast([], "café")
+ self.assertNoFrameSent()
+
+ def test_broadcast_two_clients(self):
+ broadcast([self.protocol, self.protocol], "café")
+ self.assertFramesSent(
+ (True, OP_TEXT, "café".encode("utf-8")),
+ (True, OP_TEXT, "café".encode("utf-8")),
+ )
+
+ def test_broadcast_skips_closed_connection(self):
+ self.close_connection()
+
+ with self.assertNoLogs():
+ broadcast([self.protocol], "café")
+ self.assertNoFrameSent()
+
+ def test_broadcast_skips_closing_connection(self):
+ close_task = self.half_close_connection_local()
+
+ with self.assertNoLogs():
+ broadcast([self.protocol], "café")
+ self.assertNoFrameSent()
+
+ self.loop.run_until_complete(close_task) # cleanup
+
+ def test_broadcast_skips_connection_sending_fragmented_text(self):
+ self.make_drain_slow()
+ self.loop.create_task(self.protocol.send(["ca", "fé"]))
+ self.run_loop_once()
+ self.assertOneFrameSent(False, OP_TEXT, "ca".encode("utf-8"))
+
+ with self.assertLogs("websockets", logging.WARNING) as logs:
+ broadcast([self.protocol], "café")
+
+ self.assertEqual(
+ [record.getMessage() for record in logs.records][:2],
+ ["skipped broadcast: sending a fragmented message"],
+ )
+
+ @unittest.skipIf(
+ sys.version_info[:2] < (3, 11), "raise_exceptions requires Python 3.11+"
+ )
+ def test_broadcast_reports_connection_sending_fragmented_text(self):
+ self.make_drain_slow()
+ self.loop.create_task(self.protocol.send(["ca", "fé"]))
+ self.run_loop_once()
+ self.assertOneFrameSent(False, OP_TEXT, "ca".encode("utf-8"))
+
+ with self.assertRaises(ExceptionGroup) as raised:
+ broadcast([self.protocol], "café", raise_exceptions=True)
+
+ self.assertEqual(str(raised.exception), "skipped broadcast (1 sub-exception)")
+ self.assertEqual(
+ str(raised.exception.exceptions[0]), "sending a fragmented message"
+ )
+
+ def test_broadcast_skips_connection_failing_to_send(self):
+ # Configure mock to raise an exception when writing to the network.
+ self.protocol.transport.write.side_effect = RuntimeError
+
+ with self.assertLogs("websockets", logging.WARNING) as logs:
+ broadcast([self.protocol], "café")
+
+ self.assertEqual(
+ [record.getMessage() for record in logs.records][:2],
+ ["skipped broadcast: failed to write message"],
+ )
+
+ @unittest.skipIf(
+ sys.version_info[:2] < (3, 11), "raise_exceptions requires Python 3.11+"
+ )
+ def test_broadcast_reports_connection_failing_to_send(self):
+ # Configure mock to raise an exception when writing to the network.
+ self.protocol.transport.write.side_effect = RuntimeError("BOOM")
+
+ with self.assertRaises(ExceptionGroup) as raised:
+ broadcast([self.protocol], "café", raise_exceptions=True)
+
+ self.assertEqual(str(raised.exception), "skipped broadcast (1 sub-exception)")
+ self.assertEqual(str(raised.exception.exceptions[0]), "failed to write message")
+ self.assertEqual(str(raised.exception.exceptions[0].__cause__), "BOOM")
+
+
+class ServerTests(CommonTests, AsyncioTestCase):
+ def setUp(self):
+ super().setUp()
+ self.protocol.is_client = False
+ self.protocol.side = "server"
+
+ def test_local_close_send_close_frame_timeout(self):
+ self.protocol.close_timeout = 10 * MS
+ self.make_drain_slow(50 * MS)
+ # If we can't send a close frame, time out in 10ms.
+ # Check the timing within -1/+9ms for robustness.
+ with self.assertCompletesWithin(9 * MS, 19 * MS):
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ self.assertConnectionClosed(CloseCode.ABNORMAL_CLOSURE, "")
+
+ def test_local_close_receive_close_frame_timeout(self):
+ self.protocol.close_timeout = 10 * MS
+ # If the client doesn't send a close frame, time out in 10ms.
+ # Check the timing within -1/+9ms for robustness.
+ with self.assertCompletesWithin(9 * MS, 19 * MS):
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ self.assertConnectionClosed(CloseCode.ABNORMAL_CLOSURE, "")
+
+ def test_local_close_connection_lost_timeout_after_write_eof(self):
+ self.protocol.close_timeout = 10 * MS
+ # If the client doesn't close its side of the TCP connection after we
+ # half-close our side with write_eof(), time out in 10ms.
+ # Check the timing within -1/+9ms for robustness.
+ with self.assertCompletesWithin(9 * MS, 19 * MS):
+ # HACK: disable write_eof => other end drops connection emulation.
+ self.transport._eof = True
+ self.receive_frame(self.close_frame)
+ self.run_loop_once()
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ # Due to a bug in coverage, this is erroneously reported as not covered.
+ self.assertConnectionClosed( # pragma: no cover
+ CloseCode.NORMAL_CLOSURE,
+ "close",
+ )
+
+ def test_local_close_connection_lost_timeout_after_close(self):
+ self.protocol.close_timeout = 10 * MS
+ # If the client doesn't close its side of the TCP connection after we
+ # half-close our side with write_eof() and close it with close(), time
+ # out in 20ms.
+ # Check the timing within -1/+9ms for robustness.
+ # Add another 10ms because this test is flaky and I don't understand.
+ with self.assertCompletesWithin(19 * MS, 39 * MS):
+ # HACK: disable write_eof => other end drops connection emulation.
+ self.transport._eof = True
+ # HACK: disable close => other end drops connection emulation.
+ self.transport._closing = True
+ self.receive_frame(self.close_frame)
+ self.run_loop_once()
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ # Due to a bug in coverage, this is erroneously reported as not covered.
+ self.assertConnectionClosed( # pragma: no cover
+ CloseCode.NORMAL_CLOSURE,
+ "close",
+ )
+
+
+class ClientTests(CommonTests, AsyncioTestCase):
+ def setUp(self):
+ super().setUp()
+ self.protocol.is_client = True
+ self.protocol.side = "client"
+
+ def test_local_close_send_close_frame_timeout(self):
+ self.protocol.close_timeout = 10 * MS
+ self.make_drain_slow(50 * MS)
+ # If we can't send a close frame, time out in 20ms.
+ # - 10ms waiting for sending a close frame
+ # - 10ms waiting for receiving a half-close
+ # Check the timing within -1/+9ms for robustness.
+ with self.assertCompletesWithin(19 * MS, 29 * MS):
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ # Due to a bug in coverage, this is erroneously reported as not covered.
+ self.assertConnectionClosed( # pragma: no cover
+ CloseCode.ABNORMAL_CLOSURE,
+ "",
+ )
+
+ def test_local_close_receive_close_frame_timeout(self):
+ self.protocol.close_timeout = 10 * MS
+ # If the server doesn't send a close frame, time out in 20ms:
+ # - 10ms waiting for receiving a close frame
+ # - 10ms waiting for receiving a half-close
+ # Check the timing within -1/+9ms for robustness.
+ with self.assertCompletesWithin(19 * MS, 29 * MS):
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ # Due to a bug in coverage, this is erroneously reported as not covered.
+ self.assertConnectionClosed( # pragma: no cover
+ CloseCode.ABNORMAL_CLOSURE,
+ "",
+ )
+
+ def test_local_close_connection_lost_timeout_after_write_eof(self):
+ self.protocol.close_timeout = 10 * MS
+ # If the server doesn't half-close its side of the TCP connection
+ # after we send a close frame, time out in 20ms:
+ # - 10ms waiting for receiving a half-close
+ # - 10ms waiting for receiving a close after write_eof
+ # Check the timing within -1/+9ms for robustness.
+ with self.assertCompletesWithin(19 * MS, 29 * MS):
+ # HACK: disable write_eof => other end drops connection emulation.
+ self.transport._eof = True
+ self.receive_frame(self.close_frame)
+ self.run_loop_once()
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ # Due to a bug in coverage, this is erroneously reported as not covered.
+ self.assertConnectionClosed( # pragma: no cover
+ CloseCode.NORMAL_CLOSURE,
+ "close",
+ )
+
+ def test_local_close_connection_lost_timeout_after_close(self):
+ self.protocol.close_timeout = 10 * MS
+ # If the client doesn't close its side of the TCP connection after we
+ # half-close our side with write_eof() and close it with close(), time
+ # out in 30ms.
+ # - 10ms waiting for receiving a half-close
+ # - 10ms waiting for receiving a close after write_eof
+ # - 10ms waiting for receiving a close after close
+ # Check the timing within -1/+9ms for robustness.
+ # Add another 10ms because this test is flaky and I don't understand.
+ with self.assertCompletesWithin(29 * MS, 49 * MS):
+ # HACK: disable write_eof => other end drops connection emulation.
+ self.transport._eof = True
+ # HACK: disable close => other end drops connection emulation.
+ self.transport._closing = True
+ self.receive_frame(self.close_frame)
+ self.run_loop_once()
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ # Due to a bug in coverage, this is erroneously reported as not covered.
+ self.assertConnectionClosed( # pragma: no cover
+ CloseCode.NORMAL_CLOSURE,
+ "close",
+ )
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/utils.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/utils.py
new file mode 100644
index 0000000000..4a21dcaeb5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/utils.py
@@ -0,0 +1,84 @@
+import asyncio
+import contextlib
+import functools
+import logging
+import unittest
+
+
+class AsyncioTestCase(unittest.TestCase):
+ """
+ Base class for tests that sets up an isolated event loop for each test.
+
+ IsolatedAsyncioTestCase was introduced in Python 3.8 for similar purposes
+ but isn't a drop-in replacement.
+
+ """
+
+ def __init_subclass__(cls, **kwargs):
+ """
+ Convert test coroutines to test functions.
+
+ This supports asynchronous tests transparently.
+
+ """
+ super().__init_subclass__(**kwargs)
+ for name in unittest.defaultTestLoader.getTestCaseNames(cls):
+ test = getattr(cls, name)
+ if asyncio.iscoroutinefunction(test):
+ setattr(cls, name, cls.convert_async_to_sync(test))
+
+ @staticmethod
+ def convert_async_to_sync(test):
+ """
+ Convert a test coroutine to a test function.
+
+ """
+
+ @functools.wraps(test)
+ def test_func(self, *args, **kwargs):
+ return self.loop.run_until_complete(test(self, *args, **kwargs))
+
+ return test_func
+
+ def setUp(self):
+ super().setUp()
+ self.loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self.loop)
+
+ def tearDown(self):
+ self.loop.close()
+ super().tearDown()
+
+ def run_loop_once(self):
+ # Process callbacks scheduled with call_soon by appending a callback
+ # to stop the event loop then running it until it hits that callback.
+ self.loop.call_soon(self.loop.stop)
+ self.loop.run_forever()
+
+ # Remove when dropping Python < 3.10
+ @contextlib.contextmanager
+ def assertNoLogs(self, logger="websockets", level=logging.ERROR):
+ """
+ No message is logged on the given logger with at least the given level.
+
+ """
+ with self.assertLogs(logger, level) as logs:
+ # We want to test that no log message is emitted
+ # but assertLogs expects at least one log message.
+ logging.getLogger(logger).log(level, "dummy")
+ yield
+
+ level_name = logging.getLevelName(level)
+ self.assertEqual(logs.output, [f"{level_name}:{logger}:dummy"])
+
+ def assertDeprecationWarnings(self, recorded_warnings, expected_warnings):
+ """
+ Check recorded deprecation warnings match a list of expected messages.
+
+ """
+ for recorded in recorded_warnings:
+ self.assertEqual(type(recorded.message), DeprecationWarning)
+ self.assertEqual(
+ set(str(recorded.message) for recorded in recorded_warnings),
+ set(expected_warnings),
+ )
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/maxi_cov.py b/testing/web-platform/tests/tools/third_party/websockets/tests/maxi_cov.py
new file mode 100644
index 0000000000..2568dcf18b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/maxi_cov.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python
+
+"""Measure coverage of each module by its test module."""
+
+import glob
+import os.path
+import subprocess
+import sys
+
+
+UNMAPPED_SRC_FILES = ["websockets/version.py"]
+UNMAPPED_TEST_FILES = ["tests/test_exports.py"]
+
+
+def check_environment():
+ """Check that prerequisites for running this script are met."""
+ try:
+ import websockets # noqa: F401
+ except ImportError:
+ print("failed to import websockets; is src on PYTHONPATH?")
+ return False
+ try:
+ import coverage # noqa: F401
+ except ImportError:
+ print("failed to locate Coverage.py; is it installed?")
+ return False
+ return True
+
+
+def get_mapping(src_dir="src"):
+ """Return a dict mapping each source file to its test file."""
+
+ # List source and test files.
+
+ src_files = glob.glob(
+ os.path.join(src_dir, "websockets/**/*.py"),
+ recursive=True,
+ )
+ test_files = glob.glob(
+ "tests/**/*.py",
+ recursive=True,
+ )
+
+ src_files = [
+ os.path.relpath(src_file, src_dir)
+ for src_file in sorted(src_files)
+ if "legacy" not in os.path.dirname(src_file)
+ if os.path.basename(src_file) != "__init__.py"
+ and os.path.basename(src_file) != "__main__.py"
+ and os.path.basename(src_file) != "compatibility.py"
+ ]
+ test_files = [
+ test_file
+ for test_file in sorted(test_files)
+ if "legacy" not in os.path.dirname(test_file)
+ and os.path.basename(test_file) != "__init__.py"
+ and os.path.basename(test_file).startswith("test_")
+ ]
+
+ # Map source files to test files.
+
+ mapping = {}
+ unmapped_test_files = []
+
+ for test_file in test_files:
+ dir_name, file_name = os.path.split(test_file)
+ assert dir_name.startswith("tests")
+ assert file_name.startswith("test_")
+ src_file = os.path.join(
+ "websockets" + dir_name[len("tests") :],
+ file_name[len("test_") :],
+ )
+ if src_file in src_files:
+ mapping[src_file] = test_file
+ else:
+ unmapped_test_files.append(test_file)
+
+ unmapped_src_files = list(set(src_files) - set(mapping))
+
+ # Ensure that all files are mapped.
+
+ assert unmapped_src_files == UNMAPPED_SRC_FILES
+ assert unmapped_test_files == UNMAPPED_TEST_FILES
+
+ return mapping
+
+
+def get_ignored_files(src_dir="src"):
+ """Return the list of files to exclude from coverage measurement."""
+
+ return [
+ # */websockets matches src/websockets and .tox/**/site-packages/websockets.
+ # There are no tests for the __main__ module and for compatibility modules.
+ "*/websockets/__main__.py",
+ "*/websockets/*/compatibility.py",
+ # This approach isn't applicable to the test suite of the legacy
+ # implementation, due to the huge test_client_server test module.
+ "*/websockets/legacy/*",
+ "tests/legacy/*",
+ ] + [
+ # Exclude test utilities that are shared between several test modules.
+ # Also excludes this script.
+ test_file
+ for test_file in sorted(glob.glob("tests/**/*.py", recursive=True))
+ if "legacy" not in os.path.dirname(test_file)
+ and os.path.basename(test_file) != "__init__.py"
+ and not os.path.basename(test_file).startswith("test_")
+ ]
+
+
+def run_coverage(mapping, src_dir="src"):
+ # Initialize a new coverage measurement session. The --source option
+ # includes all files in the report, even if they're never imported.
+ print("\nInitializing session\n", flush=True)
+ subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "--source",
+ ",".join([os.path.join(src_dir, "websockets"), "tests"]),
+ "--omit",
+ ",".join(get_ignored_files(src_dir)),
+ "-m",
+ "unittest",
+ ]
+ + UNMAPPED_TEST_FILES,
+ check=True,
+ )
+ # Append coverage of each source module by the corresponding test module.
+ for src_file, test_file in mapping.items():
+ print(f"\nTesting {src_file} with {test_file}\n", flush=True)
+ subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "--append",
+ "--include",
+ ",".join([os.path.join(src_dir, src_file), test_file]),
+ "-m",
+ "unittest",
+ test_file,
+ ],
+ check=True,
+ )
+
+
+if __name__ == "__main__":
+ if not check_environment():
+ sys.exit(1)
+ src_dir = sys.argv[1] if len(sys.argv) == 2 else "src"
+ mapping = get_mapping(src_dir)
+ run_coverage(mapping, src_dir)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/protocol.py b/testing/web-platform/tests/tools/third_party/websockets/tests/protocol.py
new file mode 100644
index 0000000000..4e843daab3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/protocol.py
@@ -0,0 +1,29 @@
+from websockets.protocol import Protocol
+
+
+class RecordingProtocol(Protocol):
+ """
+ Protocol subclass that records incoming frames.
+
+ By interfacing with this protocol, you can check easily what the component
+ being testing sends during a test.
+
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.frames_rcvd = []
+
+ def get_frames_rcvd(self):
+ """
+ Get incoming frames received up to this point.
+
+ Calling this method clears the list. Each frame is returned only once.
+
+ """
+ frames_rcvd, self.frames_rcvd = self.frames_rcvd, []
+ return frames_rcvd
+
+ def recv_frame(self, frame):
+ self.frames_rcvd.append(frame)
+ super().recv_frame(frame)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/client.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/client.py
new file mode 100644
index 0000000000..683893e88c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/client.py
@@ -0,0 +1,39 @@
+import contextlib
+import ssl
+
+from websockets.sync.client import *
+from websockets.sync.server import WebSocketServer
+
+from ..utils import CERTIFICATE
+
+
+__all__ = [
+ "CLIENT_CONTEXT",
+ "run_client",
+ "run_unix_client",
+]
+
+
+CLIENT_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+CLIENT_CONTEXT.load_verify_locations(CERTIFICATE)
+
+
+@contextlib.contextmanager
+def run_client(wsuri_or_server, secure=None, resource_name="/", **kwargs):
+ if isinstance(wsuri_or_server, str):
+ wsuri = wsuri_or_server
+ else:
+ assert isinstance(wsuri_or_server, WebSocketServer)
+ if secure is None:
+ secure = "ssl_context" in kwargs
+ protocol = "wss" if secure else "ws"
+ host, port = wsuri_or_server.socket.getsockname()
+ wsuri = f"{protocol}://{host}:{port}{resource_name}"
+ with connect(wsuri, **kwargs) as client:
+ yield client
+
+
+@contextlib.contextmanager
+def run_unix_client(path, **kwargs):
+ with unix_connect(path, **kwargs) as client:
+ yield client
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/connection.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/connection.py
new file mode 100644
index 0000000000..89d4909ee1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/connection.py
@@ -0,0 +1,109 @@
+import contextlib
+import time
+
+from websockets.sync.connection import Connection
+
+
+class InterceptingConnection(Connection):
+ """
+ Connection subclass that can intercept outgoing packets.
+
+ By interfacing with this connection, you can simulate network conditions
+ affecting what the component being tested receives during a test.
+
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.socket = InterceptingSocket(self.socket)
+
+ @contextlib.contextmanager
+ def delay_frames_sent(self, delay):
+ """
+ Add a delay before sending frames.
+
+ Delays cumulate: they're added before every frame or before EOF.
+
+ """
+ assert self.socket.delay_sendall is None
+ self.socket.delay_sendall = delay
+ try:
+ yield
+ finally:
+ self.socket.delay_sendall = None
+
+ @contextlib.contextmanager
+ def delay_eof_sent(self, delay):
+ """
+ Add a delay before sending EOF.
+
+ Delays cumulate: they're added before every frame or before EOF.
+
+ """
+ assert self.socket.delay_shutdown is None
+ self.socket.delay_shutdown = delay
+ try:
+ yield
+ finally:
+ self.socket.delay_shutdown = None
+
+ @contextlib.contextmanager
+ def drop_frames_sent(self):
+ """
+ Prevent frames from being sent.
+
+ Since TCP is reliable, sending frames or EOF afterwards is unrealistic.
+
+ """
+ assert not self.socket.drop_sendall
+ self.socket.drop_sendall = True
+ try:
+ yield
+ finally:
+ self.socket.drop_sendall = False
+
+ @contextlib.contextmanager
+ def drop_eof_sent(self):
+ """
+ Prevent EOF from being sent.
+
+ Since TCP is reliable, sending frames or EOF afterwards is unrealistic.
+
+ """
+ assert not self.socket.drop_shutdown
+ self.socket.drop_shutdown = True
+ try:
+ yield
+ finally:
+ self.socket.drop_shutdown = False
+
+
+class InterceptingSocket:
+ """
+ Socket wrapper that intercepts calls to sendall and shutdown.
+
+ This is coupled to the implementation, which relies on these two methods.
+
+ """
+
+ def __init__(self, socket):
+ self.socket = socket
+ self.delay_sendall = None
+ self.delay_shutdown = None
+ self.drop_sendall = False
+ self.drop_shutdown = False
+
+ def __getattr__(self, name):
+ return getattr(self.socket, name)
+
+ def sendall(self, bytes, flags=0):
+ if self.delay_sendall is not None:
+ time.sleep(self.delay_sendall)
+ if not self.drop_sendall:
+ self.socket.sendall(bytes, flags)
+
+ def shutdown(self, how):
+ if self.delay_shutdown is not None:
+ time.sleep(self.delay_shutdown)
+ if not self.drop_shutdown:
+ self.socket.shutdown(how)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/server.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/server.py
new file mode 100644
index 0000000000..a9a77438ca
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/server.py
@@ -0,0 +1,65 @@
+import contextlib
+import ssl
+import threading
+
+from websockets.sync.server import *
+
+from ..utils import CERTIFICATE
+
+
+SERVER_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+SERVER_CONTEXT.load_cert_chain(CERTIFICATE)
+
+# Work around https://github.com/openssl/openssl/issues/7967
+
+# This bug causes connect() to hang in tests for the client. Including this
+# workaround acknowledges that the issue could happen outside of the test suite.
+
+# It shouldn't happen too often, or else OpenSSL 1.1.1 would be unusable. If it
+# happens, we can look for a library-level fix, but it won't be easy.
+
+SERVER_CONTEXT.num_tickets = 0
+
+
+def crash(ws):
+ raise RuntimeError
+
+
+def do_nothing(ws):
+ pass
+
+
+def eval_shell(ws):
+ for expr in ws:
+ value = eval(expr)
+ ws.send(str(value))
+
+
+class EvalShellMixin:
+ def assertEval(self, client, expr, value):
+ client.send(expr)
+ self.assertEqual(client.recv(), value)
+
+
+@contextlib.contextmanager
+def run_server(ws_handler=eval_shell, host="localhost", port=0, **kwargs):
+ with serve(ws_handler, host, port, **kwargs) as server:
+ thread = threading.Thread(target=server.serve_forever)
+ thread.start()
+ try:
+ yield server
+ finally:
+ server.shutdown()
+ thread.join()
+
+
+@contextlib.contextmanager
+def run_unix_server(path, ws_handler=eval_shell, **kwargs):
+ with unix_serve(ws_handler, path, **kwargs) as server:
+ thread = threading.Thread(target=server.serve_forever)
+ thread.start()
+ try:
+ yield server
+ finally:
+ server.shutdown()
+ thread.join()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_client.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_client.py
new file mode 100644
index 0000000000..c900f3b0fe
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_client.py
@@ -0,0 +1,274 @@
+import socket
+import ssl
+import threading
+import unittest
+
+from websockets.exceptions import InvalidHandshake
+from websockets.extensions.permessage_deflate import PerMessageDeflate
+from websockets.sync.client import *
+
+from ..utils import MS, temp_unix_socket_path
+from .client import CLIENT_CONTEXT, run_client, run_unix_client
+from .server import SERVER_CONTEXT, do_nothing, run_server, run_unix_server
+
+
+class ClientTests(unittest.TestCase):
+ def test_connection(self):
+ """Client connects to server and the handshake succeeds."""
+ with run_server() as server:
+ with run_client(server) as client:
+ self.assertEqual(client.protocol.state.name, "OPEN")
+
+ def test_connection_fails(self):
+ """Client connects to server but the handshake fails."""
+
+ def remove_accept_header(self, request, response):
+ del response.headers["Sec-WebSocket-Accept"]
+
+ # The connection will be open for the server but failed for the client.
+ # Use a connection handler that exits immediately to avoid an exception.
+ with run_server(do_nothing, process_response=remove_accept_header) as server:
+ with self.assertRaisesRegex(
+ InvalidHandshake,
+ "missing Sec-WebSocket-Accept header",
+ ):
+ with run_client(server, close_timeout=MS):
+ self.fail("did not raise")
+
+ def test_tcp_connection_fails(self):
+ """Client fails to connect to server."""
+ with self.assertRaises(OSError):
+ with run_client("ws://localhost:54321"): # invalid port
+ self.fail("did not raise")
+
+ def test_existing_socket(self):
+ """Client connects using a pre-existing socket."""
+ with run_server() as server:
+ with socket.create_connection(server.socket.getsockname()) as sock:
+ # Use a non-existing domain to ensure we connect to the right socket.
+ with run_client("ws://invalid/", sock=sock) as client:
+ self.assertEqual(client.protocol.state.name, "OPEN")
+
+ def test_additional_headers(self):
+ """Client can set additional headers with additional_headers."""
+ with run_server() as server:
+ with run_client(
+ server, additional_headers={"Authorization": "Bearer ..."}
+ ) as client:
+ self.assertEqual(client.request.headers["Authorization"], "Bearer ...")
+
+ def test_override_user_agent(self):
+ """Client can override User-Agent header with user_agent_header."""
+ with run_server() as server:
+ with run_client(server, user_agent_header="Smith") as client:
+ self.assertEqual(client.request.headers["User-Agent"], "Smith")
+
+ def test_remove_user_agent(self):
+ """Client can remove User-Agent header with user_agent_header."""
+ with run_server() as server:
+ with run_client(server, user_agent_header=None) as client:
+ self.assertNotIn("User-Agent", client.request.headers)
+
+ def test_compression_is_enabled(self):
+ """Client enables compression by default."""
+ with run_server() as server:
+ with run_client(server) as client:
+ self.assertEqual(
+ [type(ext) for ext in client.protocol.extensions],
+ [PerMessageDeflate],
+ )
+
+ def test_disable_compression(self):
+ """Client disables compression."""
+ with run_server() as server:
+ with run_client(server, compression=None) as client:
+ self.assertEqual(client.protocol.extensions, [])
+
+ def test_custom_connection_factory(self):
+ """Client runs ClientConnection factory provided in create_connection."""
+
+ def create_connection(*args, **kwargs):
+ client = ClientConnection(*args, **kwargs)
+ client.create_connection_ran = True
+ return client
+
+ with run_server() as server:
+ with run_client(server, create_connection=create_connection) as client:
+ self.assertTrue(client.create_connection_ran)
+
+ def test_timeout_during_handshake(self):
+ """Client times out before receiving handshake response from server."""
+ gate = threading.Event()
+
+ def stall_connection(self, request):
+ gate.wait()
+
+ # The connection will be open for the server but failed for the client.
+ # Use a connection handler that exits immediately to avoid an exception.
+ with run_server(do_nothing, process_request=stall_connection) as server:
+ try:
+ with self.assertRaisesRegex(
+ TimeoutError,
+ "timed out during handshake",
+ ):
+ # While it shouldn't take 50ms to open a connection, this
+ # test becomes flaky in CI when setting a smaller timeout,
+ # even after increasing WEBSOCKETS_TESTS_TIMEOUT_FACTOR.
+ with run_client(server, open_timeout=5 * MS):
+ self.fail("did not raise")
+ finally:
+ gate.set()
+
+ def test_connection_closed_during_handshake(self):
+ """Client reads EOF before receiving handshake response from server."""
+
+ def close_connection(self, request):
+ self.close_socket()
+
+ with run_server(process_request=close_connection) as server:
+ with self.assertRaisesRegex(
+ ConnectionError,
+ "connection closed during handshake",
+ ):
+ with run_client(server):
+ self.fail("did not raise")
+
+
+class SecureClientTests(unittest.TestCase):
+ def test_connection(self):
+ """Client connects to server securely."""
+ with run_server(ssl_context=SERVER_CONTEXT) as server:
+ with run_client(server, ssl_context=CLIENT_CONTEXT) as client:
+ self.assertEqual(client.protocol.state.name, "OPEN")
+ self.assertEqual(client.socket.version()[:3], "TLS")
+
+ def test_set_server_hostname_implicitly(self):
+ """Client sets server_hostname to the host in the WebSocket URI."""
+ with temp_unix_socket_path() as path:
+ with run_unix_server(path, ssl_context=SERVER_CONTEXT):
+ with run_unix_client(
+ path,
+ ssl_context=CLIENT_CONTEXT,
+ uri="wss://overridden/",
+ ) as client:
+ self.assertEqual(client.socket.server_hostname, "overridden")
+
+ def test_set_server_hostname_explicitly(self):
+ """Client sets server_hostname to the value provided in argument."""
+ with temp_unix_socket_path() as path:
+ with run_unix_server(path, ssl_context=SERVER_CONTEXT):
+ with run_unix_client(
+ path,
+ ssl_context=CLIENT_CONTEXT,
+ server_hostname="overridden",
+ ) as client:
+ self.assertEqual(client.socket.server_hostname, "overridden")
+
+ def test_reject_invalid_server_certificate(self):
+ """Client rejects certificate where server certificate isn't trusted."""
+ with run_server(ssl_context=SERVER_CONTEXT) as server:
+ with self.assertRaisesRegex(
+ ssl.SSLCertVerificationError,
+ r"certificate verify failed: self[ -]signed certificate",
+ ):
+ # The test certificate isn't trusted system-wide.
+ with run_client(server, secure=True):
+ self.fail("did not raise")
+
+ def test_reject_invalid_server_hostname(self):
+ """Client rejects certificate where server hostname doesn't match."""
+ with run_server(ssl_context=SERVER_CONTEXT) as server:
+ with self.assertRaisesRegex(
+ ssl.SSLCertVerificationError,
+ r"certificate verify failed: Hostname mismatch",
+ ):
+ # This hostname isn't included in the test certificate.
+ with run_client(
+ server, ssl_context=CLIENT_CONTEXT, server_hostname="invalid"
+ ):
+ self.fail("did not raise")
+
+
+@unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets")
+class UnixClientTests(unittest.TestCase):
+ def test_connection(self):
+ """Client connects to server over a Unix socket."""
+ with temp_unix_socket_path() as path:
+ with run_unix_server(path):
+ with run_unix_client(path) as client:
+ self.assertEqual(client.protocol.state.name, "OPEN")
+
+ def test_set_host_header(self):
+ """Client sets the Host header to the host in the WebSocket URI."""
+ # This is part of the documented behavior of unix_connect().
+ with temp_unix_socket_path() as path:
+ with run_unix_server(path):
+ with run_unix_client(path, uri="ws://overridden/") as client:
+ self.assertEqual(client.request.headers["Host"], "overridden")
+
+
+@unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets")
+class SecureUnixClientTests(unittest.TestCase):
+ def test_connection(self):
+ """Client connects to server securely over a Unix socket."""
+ with temp_unix_socket_path() as path:
+ with run_unix_server(path, ssl_context=SERVER_CONTEXT):
+ with run_unix_client(path, ssl_context=CLIENT_CONTEXT) as client:
+ self.assertEqual(client.protocol.state.name, "OPEN")
+ self.assertEqual(client.socket.version()[:3], "TLS")
+
+ def test_set_server_hostname(self):
+ """Client sets server_hostname to the host in the WebSocket URI."""
+ # This is part of the documented behavior of unix_connect().
+ with temp_unix_socket_path() as path:
+ with run_unix_server(path, ssl_context=SERVER_CONTEXT):
+ with run_unix_client(
+ path,
+ ssl_context=CLIENT_CONTEXT,
+ uri="wss://overridden/",
+ ) as client:
+ self.assertEqual(client.socket.server_hostname, "overridden")
+
+
+class ClientUsageErrorsTests(unittest.TestCase):
+ def test_ssl_context_without_secure_uri(self):
+ """Client rejects ssl_context when URI isn't secure."""
+ with self.assertRaisesRegex(
+ TypeError,
+ "ssl_context argument is incompatible with a ws:// URI",
+ ):
+ connect("ws://localhost/", ssl_context=CLIENT_CONTEXT)
+
+ def test_unix_without_path_or_sock(self):
+ """Unix client requires path when sock isn't provided."""
+ with self.assertRaisesRegex(
+ TypeError,
+ "missing path argument",
+ ):
+ unix_connect()
+
+ def test_unix_with_path_and_sock(self):
+ """Unix client rejects path when sock is provided."""
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.addCleanup(sock.close)
+ with self.assertRaisesRegex(
+ TypeError,
+ "path and sock arguments are incompatible",
+ ):
+ unix_connect(path="/", sock=sock)
+
+ def test_invalid_subprotocol(self):
+ """Client rejects single value of subprotocols."""
+ with self.assertRaisesRegex(
+ TypeError,
+ "subprotocols must be a list",
+ ):
+ connect("ws://localhost/", subprotocols="chat")
+
+ def test_unsupported_compression(self):
+ """Client rejects incorrect value of compression."""
+ with self.assertRaisesRegex(
+ ValueError,
+ "unsupported compression: False",
+ ):
+ connect("ws://localhost/", compression=False)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_connection.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_connection.py
new file mode 100644
index 0000000000..63544d4add
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_connection.py
@@ -0,0 +1,752 @@
+import contextlib
+import logging
+import platform
+import socket
+import sys
+import threading
+import time
+import unittest
+import uuid
+from unittest.mock import patch
+
+from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
+from websockets.frames import CloseCode, Frame, Opcode
+from websockets.protocol import CLIENT, SERVER, Protocol
+from websockets.sync.connection import *
+
+from ..protocol import RecordingProtocol
+from ..utils import MS
+from .connection import InterceptingConnection
+
+
+# Connection implements symmetrical behavior between clients and servers.
+# All tests run on the client side and the server side to validate this.
+
+
+class ClientConnectionTests(unittest.TestCase):
+ LOCAL = CLIENT
+ REMOTE = SERVER
+
+ def setUp(self):
+ socket_, remote_socket = socket.socketpair()
+ protocol = Protocol(self.LOCAL)
+ remote_protocol = RecordingProtocol(self.REMOTE)
+ self.connection = Connection(socket_, protocol, close_timeout=2 * MS)
+ self.remote_connection = InterceptingConnection(remote_socket, remote_protocol)
+
+ def tearDown(self):
+ self.remote_connection.close()
+ self.connection.close()
+
+ # Test helpers built upon RecordingProtocol and InterceptingConnection.
+
+ def assertFrameSent(self, frame):
+ """Check that a single frame was sent."""
+ time.sleep(MS) # let the remote side process messages
+ self.assertEqual(self.remote_connection.protocol.get_frames_rcvd(), [frame])
+
+ def assertNoFrameSent(self):
+ """Check that no frame was sent."""
+ time.sleep(MS) # let the remote side process messages
+ self.assertEqual(self.remote_connection.protocol.get_frames_rcvd(), [])
+
+ @contextlib.contextmanager
+ def delay_frames_rcvd(self, delay):
+ """Delay frames before they're received by the connection."""
+ with self.remote_connection.delay_frames_sent(delay):
+ yield
+ time.sleep(MS) # let the remote side process messages
+
+ @contextlib.contextmanager
+ def delay_eof_rcvd(self, delay):
+ """Delay EOF before it's received by the connection."""
+ with self.remote_connection.delay_eof_sent(delay):
+ yield
+ time.sleep(MS) # let the remote side process messages
+
+ @contextlib.contextmanager
+ def drop_frames_rcvd(self):
+ """Drop frames before they're received by the connection."""
+ with self.remote_connection.drop_frames_sent():
+ yield
+ time.sleep(MS) # let the remote side process messages
+
+ @contextlib.contextmanager
+ def drop_eof_rcvd(self):
+ """Drop EOF before it's received by the connection."""
+ with self.remote_connection.drop_eof_sent():
+ yield
+ time.sleep(MS) # let the remote side process messages
+
+ # Test __enter__ and __exit__.
+
+ def test_enter(self):
+ """__enter__ returns the connection itself."""
+ with self.connection as connection:
+ self.assertIs(connection, self.connection)
+
+ def test_exit(self):
+ """__exit__ closes the connection with code 1000."""
+ with self.connection:
+ self.assertNoFrameSent()
+ self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe8"))
+
+ def test_exit_with_exception(self):
+ """__exit__ with an exception closes the connection with code 1011."""
+ with self.assertRaises(RuntimeError):
+ with self.connection:
+ raise RuntimeError
+ self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xf3"))
+
+ # Test __iter__.
+
+ def test_iter_text(self):
+ """__iter__ yields text messages."""
+ iterator = iter(self.connection)
+ self.remote_connection.send("😀")
+ self.assertEqual(next(iterator), "😀")
+ self.remote_connection.send("😀")
+ self.assertEqual(next(iterator), "😀")
+
+ def test_iter_binary(self):
+ """__iter__ yields binary messages."""
+ iterator = iter(self.connection)
+ self.remote_connection.send(b"\x01\x02\xfe\xff")
+ self.assertEqual(next(iterator), b"\x01\x02\xfe\xff")
+ self.remote_connection.send(b"\x01\x02\xfe\xff")
+ self.assertEqual(next(iterator), b"\x01\x02\xfe\xff")
+
+ def test_iter_mixed(self):
+ """__iter__ yields a mix of text and binary messages."""
+ iterator = iter(self.connection)
+ self.remote_connection.send("😀")
+ self.assertEqual(next(iterator), "😀")
+ self.remote_connection.send(b"\x01\x02\xfe\xff")
+ self.assertEqual(next(iterator), b"\x01\x02\xfe\xff")
+
+ def test_iter_connection_closed_ok(self):
+ """__iter__ terminates after a normal closure."""
+ iterator = iter(self.connection)
+ self.remote_connection.close()
+ with self.assertRaises(StopIteration):
+ next(iterator)
+
+ def test_iter_connection_closed_error(self):
+ """__iter__ raises ConnnectionClosedError after an error."""
+ iterator = iter(self.connection)
+ self.remote_connection.close(code=CloseCode.INTERNAL_ERROR)
+ with self.assertRaises(ConnectionClosedError):
+ next(iterator)
+
+ # Test recv.
+
+ def test_recv_text(self):
+ """recv receives a text message."""
+ self.remote_connection.send("😀")
+ self.assertEqual(self.connection.recv(), "😀")
+
+ def test_recv_binary(self):
+ """recv receives a binary message."""
+ self.remote_connection.send(b"\x01\x02\xfe\xff")
+ self.assertEqual(self.connection.recv(), b"\x01\x02\xfe\xff")
+
+ def test_recv_fragmented_text(self):
+ """recv receives a fragmented text message."""
+ self.remote_connection.send(["😀", "😀"])
+ self.assertEqual(self.connection.recv(), "😀😀")
+
+ def test_recv_fragmented_binary(self):
+ """recv receives a fragmented binary message."""
+ self.remote_connection.send([b"\x01\x02", b"\xfe\xff"])
+ self.assertEqual(self.connection.recv(), b"\x01\x02\xfe\xff")
+
+ def test_recv_connection_closed_ok(self):
+ """recv raises ConnectionClosedOK after a normal closure."""
+ self.remote_connection.close()
+ with self.assertRaises(ConnectionClosedOK):
+ self.connection.recv()
+
+ def test_recv_connection_closed_error(self):
+ """recv raises ConnectionClosedError after an error."""
+ self.remote_connection.close(code=CloseCode.INTERNAL_ERROR)
+ with self.assertRaises(ConnectionClosedError):
+ self.connection.recv()
+
+ def test_recv_during_recv(self):
+ """recv raises RuntimeError when called concurrently with itself."""
+ recv_thread = threading.Thread(target=self.connection.recv)
+ recv_thread.start()
+
+ with self.assertRaisesRegex(
+ RuntimeError,
+ "cannot call recv while another thread "
+ "is already running recv or recv_streaming",
+ ):
+ self.connection.recv()
+
+ self.remote_connection.send("")
+ recv_thread.join()
+
+ def test_recv_during_recv_streaming(self):
+ """recv raises RuntimeError when called concurrently with recv_streaming."""
+ recv_streaming_thread = threading.Thread(
+ target=lambda: list(self.connection.recv_streaming())
+ )
+ recv_streaming_thread.start()
+
+ with self.assertRaisesRegex(
+ RuntimeError,
+ "cannot call recv while another thread "
+ "is already running recv or recv_streaming",
+ ):
+ self.connection.recv()
+
+ self.remote_connection.send("")
+ recv_streaming_thread.join()
+
+ # Test recv_streaming.
+
+ def test_recv_streaming_text(self):
+ """recv_streaming receives a text message."""
+ self.remote_connection.send("😀")
+ self.assertEqual(
+ list(self.connection.recv_streaming()),
+ ["😀"],
+ )
+
+ def test_recv_streaming_binary(self):
+ """recv_streaming receives a binary message."""
+ self.remote_connection.send(b"\x01\x02\xfe\xff")
+ self.assertEqual(
+ list(self.connection.recv_streaming()),
+ [b"\x01\x02\xfe\xff"],
+ )
+
+ def test_recv_streaming_fragmented_text(self):
+ """recv_streaming receives a fragmented text message."""
+ self.remote_connection.send(["😀", "😀"])
+ # websockets sends an trailing empty fragment. That's an implementation detail.
+ self.assertEqual(
+ list(self.connection.recv_streaming()),
+ ["😀", "😀", ""],
+ )
+
+ def test_recv_streaming_fragmented_binary(self):
+ """recv_streaming receives a fragmented binary message."""
+ self.remote_connection.send([b"\x01\x02", b"\xfe\xff"])
+ # websockets sends an trailing empty fragment. That's an implementation detail.
+ self.assertEqual(
+ list(self.connection.recv_streaming()),
+ [b"\x01\x02", b"\xfe\xff", b""],
+ )
+
+ def test_recv_streaming_connection_closed_ok(self):
+ """recv_streaming raises ConnectionClosedOK after a normal closure."""
+ self.remote_connection.close()
+ with self.assertRaises(ConnectionClosedOK):
+ list(self.connection.recv_streaming())
+
+ def test_recv_streaming_connection_closed_error(self):
+ """recv_streaming raises ConnectionClosedError after an error."""
+ self.remote_connection.close(code=CloseCode.INTERNAL_ERROR)
+ with self.assertRaises(ConnectionClosedError):
+ list(self.connection.recv_streaming())
+
+ def test_recv_streaming_during_recv(self):
+ """recv_streaming raises RuntimeError when called concurrently with recv."""
+ recv_thread = threading.Thread(target=self.connection.recv)
+ recv_thread.start()
+
+ with self.assertRaisesRegex(
+ RuntimeError,
+ "cannot call recv_streaming while another thread "
+ "is already running recv or recv_streaming",
+ ):
+ list(self.connection.recv_streaming())
+
+ self.remote_connection.send("")
+ recv_thread.join()
+
+ def test_recv_streaming_during_recv_streaming(self):
+ """recv_streaming raises RuntimeError when called concurrently with itself."""
+ recv_streaming_thread = threading.Thread(
+ target=lambda: list(self.connection.recv_streaming())
+ )
+ recv_streaming_thread.start()
+
+ with self.assertRaisesRegex(
+ RuntimeError,
+ r"cannot call recv_streaming while another thread "
+ r"is already running recv or recv_streaming",
+ ):
+ list(self.connection.recv_streaming())
+
+ self.remote_connection.send("")
+ recv_streaming_thread.join()
+
+ # Test send.
+
+ def test_send_text(self):
+ """send sends a text message."""
+ self.connection.send("😀")
+ self.assertEqual(self.remote_connection.recv(), "😀")
+
+ def test_send_binary(self):
+ """send sends a binary message."""
+ self.connection.send(b"\x01\x02\xfe\xff")
+ self.assertEqual(self.remote_connection.recv(), b"\x01\x02\xfe\xff")
+
+ def test_send_fragmented_text(self):
+ """send sends a fragmented text message."""
+ self.connection.send(["😀", "😀"])
+ # websockets sends an trailing empty fragment. That's an implementation detail.
+ self.assertEqual(
+ list(self.remote_connection.recv_streaming()),
+ ["😀", "😀", ""],
+ )
+
+ def test_send_fragmented_binary(self):
+ """send sends a fragmented binary message."""
+ self.connection.send([b"\x01\x02", b"\xfe\xff"])
+ # websockets sends an trailing empty fragment. That's an implementation detail.
+ self.assertEqual(
+ list(self.remote_connection.recv_streaming()),
+ [b"\x01\x02", b"\xfe\xff", b""],
+ )
+
+ def test_send_connection_closed_ok(self):
+ """send raises ConnectionClosedOK after a normal closure."""
+ self.remote_connection.close()
+ with self.assertRaises(ConnectionClosedOK):
+ self.connection.send("😀")
+
+ def test_send_connection_closed_error(self):
+ """send raises ConnectionClosedError after an error."""
+ self.remote_connection.close(code=CloseCode.INTERNAL_ERROR)
+ with self.assertRaises(ConnectionClosedError):
+ self.connection.send("😀")
+
+ def test_send_during_send(self):
+ """send raises RuntimeError when called concurrently with itself."""
+ recv_thread = threading.Thread(target=self.remote_connection.recv)
+ recv_thread.start()
+
+ send_gate = threading.Event()
+ exit_gate = threading.Event()
+
+ def fragments():
+ yield "😀"
+ send_gate.set()
+ exit_gate.wait()
+ yield "😀"
+
+ send_thread = threading.Thread(
+ target=self.connection.send,
+ args=(fragments(),),
+ )
+ send_thread.start()
+
+ send_gate.wait()
+ # The check happens in four code paths, depending on the argument.
+ for message in [
+ "😀",
+ b"\x01\x02\xfe\xff",
+ ["😀", "😀"],
+ [b"\x01\x02", b"\xfe\xff"],
+ ]:
+ with self.subTest(message=message):
+ with self.assertRaisesRegex(
+ RuntimeError,
+ "cannot call send while another thread is already running send",
+ ):
+ self.connection.send(message)
+
+ exit_gate.set()
+ send_thread.join()
+ recv_thread.join()
+
+ def test_send_empty_iterable(self):
+ """send does nothing when called with an empty iterable."""
+ self.connection.send([])
+ self.connection.close()
+ self.assertEqual(list(iter(self.remote_connection)), [])
+
+ def test_send_mixed_iterable(self):
+ """send raises TypeError when called with an iterable of inconsistent types."""
+ with self.assertRaises(TypeError):
+ self.connection.send(["😀", b"\xfe\xff"])
+
+ def test_send_unsupported_iterable(self):
+ """send raises TypeError when called with an iterable of unsupported type."""
+ with self.assertRaises(TypeError):
+ self.connection.send([None])
+
+ def test_send_dict(self):
+ """send raises TypeError when called with a dict."""
+ with self.assertRaises(TypeError):
+ self.connection.send({"type": "object"})
+
+ def test_send_unsupported_type(self):
+ """send raises TypeError when called with an unsupported type."""
+ with self.assertRaises(TypeError):
+ self.connection.send(None)
+
+ # Test close.
+
+ def test_close(self):
+ """close sends a close frame."""
+ self.connection.close()
+ self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe8"))
+
+ def test_close_explicit_code_reason(self):
+ """close sends a close frame with a given code and reason."""
+ self.connection.close(CloseCode.GOING_AWAY, "bye!")
+ self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe9bye!"))
+
+ def test_close_waits_for_close_frame(self):
+ """close waits for a close frame (then EOF) before returning."""
+ with self.delay_frames_rcvd(MS):
+ self.connection.close()
+
+ with self.assertRaises(ConnectionClosedOK) as raised:
+ self.connection.recv()
+
+ exc = raised.exception
+ self.assertEqual(str(exc), "sent 1000 (OK); then received 1000 (OK)")
+ self.assertIsNone(exc.__cause__)
+
+ def test_close_waits_for_connection_closed(self):
+ """close waits for EOF before returning."""
+ if self.LOCAL is SERVER:
+ self.skipTest("only relevant on the client-side")
+
+ with self.delay_eof_rcvd(MS):
+ self.connection.close()
+
+ with self.assertRaises(ConnectionClosedOK) as raised:
+ self.connection.recv()
+
+ exc = raised.exception
+ self.assertEqual(str(exc), "sent 1000 (OK); then received 1000 (OK)")
+ self.assertIsNone(exc.__cause__)
+
+ def test_close_timeout_waiting_for_close_frame(self):
+ """close times out if no close frame is received."""
+ with self.drop_eof_rcvd(), self.drop_frames_rcvd():
+ self.connection.close()
+
+ with self.assertRaises(ConnectionClosedError) as raised:
+ self.connection.recv()
+
+ exc = raised.exception
+ self.assertEqual(str(exc), "sent 1000 (OK); no close frame received")
+ self.assertIsInstance(exc.__cause__, TimeoutError)
+
+ def test_close_timeout_waiting_for_connection_closed(self):
+ """close times out if EOF isn't received."""
+ if self.LOCAL is SERVER:
+ self.skipTest("only relevant on the client-side")
+
+ with self.drop_eof_rcvd():
+ self.connection.close()
+
+ with self.assertRaises(ConnectionClosedOK) as raised:
+ self.connection.recv()
+
+ exc = raised.exception
+ self.assertEqual(str(exc), "sent 1000 (OK); then received 1000 (OK)")
+ # Remove socket.timeout when dropping Python < 3.10.
+ self.assertIsInstance(exc.__cause__, (socket.timeout, TimeoutError))
+
+ def test_close_waits_for_recv(self):
+ self.remote_connection.send("😀")
+
+ close_thread = threading.Thread(target=self.connection.close)
+ close_thread.start()
+
+ # Let close() initiate the closing handshake and send a close frame.
+ time.sleep(MS)
+ self.assertTrue(close_thread.is_alive())
+
+ # Connection isn't closed yet.
+ self.connection.recv()
+
+ # Let close() receive a close frame and finish the closing handshake.
+ time.sleep(MS)
+ self.assertFalse(close_thread.is_alive())
+
+ # Connection is closed now.
+ with self.assertRaises(ConnectionClosedOK) as raised:
+ self.connection.recv()
+
+ exc = raised.exception
+ self.assertEqual(str(exc), "sent 1000 (OK); then received 1000 (OK)")
+ self.assertIsNone(exc.__cause__)
+
+ def test_close_timeout_waiting_for_recv(self):
+ self.remote_connection.send("😀")
+
+ close_thread = threading.Thread(target=self.connection.close)
+ close_thread.start()
+
+ # Let close() time out during the closing handshake.
+ time.sleep(3 * MS)
+ self.assertFalse(close_thread.is_alive())
+
+ # Connection is closed now.
+ with self.assertRaises(ConnectionClosedError) as raised:
+ self.connection.recv()
+
+ exc = raised.exception
+ self.assertEqual(str(exc), "sent 1000 (OK); no close frame received")
+ self.assertIsInstance(exc.__cause__, TimeoutError)
+
+ def test_close_idempotency(self):
+ """close does nothing if the connection is already closed."""
+ self.connection.close()
+ self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe8"))
+
+ self.connection.close()
+ self.assertNoFrameSent()
+
+ @unittest.skipIf(
+ platform.python_implementation() == "PyPy",
+ "this test fails randomly due to a bug in PyPy", # see #1314 for details
+ )
+ def test_close_idempotency_race_condition(self):
+ """close waits if the connection is already closing."""
+
+ self.connection.close_timeout = 5 * MS
+
+ def closer():
+ with self.delay_frames_rcvd(3 * MS):
+ self.connection.close()
+
+ close_thread = threading.Thread(target=closer)
+ close_thread.start()
+
+ # Let closer() initiate the closing handshake and send a close frame.
+ time.sleep(MS)
+ self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe8"))
+
+ # Connection isn't closed yet.
+ with self.assertRaises(TimeoutError):
+ self.connection.recv(timeout=0)
+
+ self.connection.close()
+ self.assertNoFrameSent()
+
+ # Connection is closed now.
+ with self.assertRaises(ConnectionClosedOK):
+ self.connection.recv(timeout=0)
+
+ close_thread.join()
+
+ def test_close_during_send(self):
+ """close fails the connection when called concurrently with send."""
+ close_gate = threading.Event()
+ exit_gate = threading.Event()
+
+ def closer():
+ close_gate.wait()
+ self.connection.close()
+ exit_gate.set()
+
+ def fragments():
+ yield "😀"
+ close_gate.set()
+ exit_gate.wait()
+ yield "😀"
+
+ close_thread = threading.Thread(target=closer)
+ close_thread.start()
+
+ with self.assertRaises(ConnectionClosedError) as raised:
+ self.connection.send(fragments())
+
+ exc = raised.exception
+ self.assertEqual(
+ str(exc),
+ "sent 1011 (internal error) close during fragmented message; "
+ "no close frame received",
+ )
+ self.assertIsNone(exc.__cause__)
+
+ close_thread.join()
+
+ # Test ping.
+
+ @patch("random.getrandbits")
+ def test_ping(self, getrandbits):
+ """ping sends a ping frame with a random payload."""
+ getrandbits.return_value = 1918987876
+ self.connection.ping()
+ getrandbits.assert_called_once_with(32)
+ self.assertFrameSent(Frame(Opcode.PING, b"rand"))
+
+ def test_ping_explicit_text(self):
+ """ping sends a ping frame with a payload provided as text."""
+ self.connection.ping("ping")
+ self.assertFrameSent(Frame(Opcode.PING, b"ping"))
+
+ def test_ping_explicit_binary(self):
+ """ping sends a ping frame with a payload provided as binary."""
+ self.connection.ping(b"ping")
+ self.assertFrameSent(Frame(Opcode.PING, b"ping"))
+
+ def test_ping_duplicate_payload(self):
+ """ping rejects the same payload until receiving the pong."""
+ with self.remote_connection.protocol_mutex: # block response to ping
+ pong_waiter = self.connection.ping("idem")
+ with self.assertRaisesRegex(
+ RuntimeError,
+ "already waiting for a pong with the same data",
+ ):
+ self.connection.ping("idem")
+ self.assertTrue(pong_waiter.wait(MS))
+ self.connection.ping("idem") # doesn't raise an exception
+
+ def test_acknowledge_ping(self):
+ """ping is acknowledged by a pong with the same payload."""
+ with self.drop_frames_rcvd():
+ pong_waiter = self.connection.ping("this")
+ self.assertFalse(pong_waiter.wait(MS))
+ self.remote_connection.pong("this")
+ self.assertTrue(pong_waiter.wait(MS))
+
+ def test_acknowledge_ping_non_matching_pong(self):
+ """ping isn't acknowledged by a pong with a different payload."""
+ with self.drop_frames_rcvd():
+ pong_waiter = self.connection.ping("this")
+ self.remote_connection.pong("that")
+ self.assertFalse(pong_waiter.wait(MS))
+
+ def test_acknowledge_previous_ping(self):
+ """ping is acknowledged by a pong with the same payload as a later ping."""
+ with self.drop_frames_rcvd():
+ pong_waiter = self.connection.ping("this")
+ self.connection.ping("that")
+ self.remote_connection.pong("that")
+ self.assertTrue(pong_waiter.wait(MS))
+
+ # Test pong.
+
+ def test_pong(self):
+ """pong sends a pong frame."""
+ self.connection.pong()
+ self.assertFrameSent(Frame(Opcode.PONG, b""))
+
+ def test_pong_explicit_text(self):
+ """pong sends a pong frame with a payload provided as text."""
+ self.connection.pong("pong")
+ self.assertFrameSent(Frame(Opcode.PONG, b"pong"))
+
+ def test_pong_explicit_binary(self):
+ """pong sends a pong frame with a payload provided as binary."""
+ self.connection.pong(b"pong")
+ self.assertFrameSent(Frame(Opcode.PONG, b"pong"))
+
+ # Test attributes.
+
+ def test_id(self):
+ """Connection has an id attribute."""
+ self.assertIsInstance(self.connection.id, uuid.UUID)
+
+ def test_logger(self):
+ """Connection has a logger attribute."""
+ self.assertIsInstance(self.connection.logger, logging.LoggerAdapter)
+
+ def test_local_address(self):
+ """Connection has a local_address attribute."""
+ self.assertIsNotNone(self.connection.local_address)
+
+ def test_remote_address(self):
+ """Connection has a remote_address attribute."""
+ self.assertIsNotNone(self.connection.remote_address)
+
+ def test_request(self):
+ """Connection has a request attribute."""
+ self.assertIsNone(self.connection.request)
+
+ def test_response(self):
+ """Connection has a response attribute."""
+ self.assertIsNone(self.connection.response)
+
+ def test_subprotocol(self):
+ """Connection has a subprotocol attribute."""
+ self.assertIsNone(self.connection.subprotocol)
+
+ # Test reporting of network errors.
+
+ @unittest.skipUnless(sys.platform == "darwin", "works only on BSD")
+ def test_reading_in_recv_events_fails(self):
+ """Error when reading incoming frames is correctly reported."""
+ # Inject a fault by closing the socket. This works only on BSD.
+ # I cannot find a way to achieve the same effect on Linux.
+ self.connection.socket.close()
+ # The connection closed exception reports the injected fault.
+ with self.assertRaises(ConnectionClosedError) as raised:
+ self.connection.recv()
+ self.assertIsInstance(raised.exception.__cause__, IOError)
+
+ def test_writing_in_recv_events_fails(self):
+ """Error when responding to incoming frames is correctly reported."""
+ # Inject a fault by shutting down the socket for writing — but not by
+ # closing it because that would terminate the connection.
+ self.connection.socket.shutdown(socket.SHUT_WR)
+ # Receive a ping. Responding with a pong will fail.
+ self.remote_connection.ping()
+ # The connection closed exception reports the injected fault.
+ with self.assertRaises(ConnectionClosedError) as raised:
+ self.connection.recv()
+ self.assertIsInstance(raised.exception.__cause__, BrokenPipeError)
+
+ def test_writing_in_send_context_fails(self):
+ """Error when sending outgoing frame is correctly reported."""
+ # Inject a fault by shutting down the socket for writing — but not by
+ # closing it because that would terminate the connection.
+ self.connection.socket.shutdown(socket.SHUT_WR)
+ # Sending a pong will fail.
+ # The connection closed exception reports the injected fault.
+ with self.assertRaises(ConnectionClosedError) as raised:
+ self.connection.pong()
+ self.assertIsInstance(raised.exception.__cause__, BrokenPipeError)
+
+ # Test safety nets — catching all exceptions in case of bugs.
+
+ @patch("websockets.protocol.Protocol.events_received")
+ def test_unexpected_failure_in_recv_events(self, events_received):
+ """Unexpected internal error in recv_events() is correctly reported."""
+ # Inject a fault in a random call in recv_events().
+ # This test is tightly coupled to the implementation.
+ events_received.side_effect = AssertionError
+ # Receive a message to trigger the fault.
+ self.remote_connection.send("😀")
+
+ with self.assertRaises(ConnectionClosedError) as raised:
+ self.connection.recv()
+
+ exc = raised.exception
+ self.assertEqual(str(exc), "no close frame received or sent")
+ self.assertIsInstance(exc.__cause__, AssertionError)
+
+ @patch("websockets.protocol.Protocol.send_text")
+ def test_unexpected_failure_in_send_context(self, send_text):
+ """Unexpected internal error in send_context() is correctly reported."""
+ # Inject a fault in a random call in send_context().
+ # This test is tightly coupled to the implementation.
+ send_text.side_effect = AssertionError
+
+ # Send a message to trigger the fault.
+ # The connection closed exception reports the injected fault.
+ with self.assertRaises(ConnectionClosedError) as raised:
+ self.connection.send("😀")
+
+ exc = raised.exception
+ self.assertEqual(str(exc), "no close frame received or sent")
+ self.assertIsInstance(exc.__cause__, AssertionError)
+
+
+class ServerConnectionTests(ClientConnectionTests):
+ LOCAL = SERVER
+ REMOTE = CLIENT
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_messages.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_messages.py
new file mode 100644
index 0000000000..825eb87974
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_messages.py
@@ -0,0 +1,479 @@
+import time
+
+from websockets.frames import OP_BINARY, OP_CONT, OP_PING, OP_PONG, OP_TEXT, Frame
+from websockets.sync.messages import *
+
+from ..utils import MS
+from .utils import ThreadTestCase
+
+
+class AssemblerTests(ThreadTestCase):
+ """
+ Tests in this class interact a lot with hidden synchronization mechanisms:
+
+ - get() / get_iter() and put() must run in separate threads when a final
+ frame is set because put() waits for get() / get_iter() to fetch the
+ message before returning.
+
+ - run_in_thread() lets its target run before yielding back control on entry,
+ which guarantees the intended execution order of test cases.
+
+ - run_in_thread() waits for its target to finish running before yielding
+ back control on exit, which allows making assertions immediately.
+
+ - When the main thread performs actions that let another thread progress, it
+ must wait before making assertions, to avoid depending on scheduling.
+
+ """
+
+ def setUp(self):
+ self.assembler = Assembler()
+
+ def tearDown(self):
+ """
+ Check that the assembler goes back to its default state after each test.
+
+ This removes the need for testing various sequences.
+
+ """
+ self.assertFalse(self.assembler.mutex.locked())
+ self.assertFalse(self.assembler.get_in_progress)
+ self.assertFalse(self.assembler.put_in_progress)
+ if not self.assembler.closed:
+ self.assertFalse(self.assembler.message_complete.is_set())
+ self.assertFalse(self.assembler.message_fetched.is_set())
+ self.assertIsNone(self.assembler.decoder)
+ self.assertEqual(self.assembler.chunks, [])
+ self.assertIsNone(self.assembler.chunks_queue)
+
+ # Test get
+
+ def test_get_text_message_already_received(self):
+ """get returns a text message that is already received."""
+
+ def putter():
+ self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9"))
+
+ with self.run_in_thread(putter):
+ message = self.assembler.get()
+
+ self.assertEqual(message, "café")
+
+ def test_get_binary_message_already_received(self):
+ """get returns a binary message that is already received."""
+
+ def putter():
+ self.assembler.put(Frame(OP_BINARY, b"tea"))
+
+ with self.run_in_thread(putter):
+ message = self.assembler.get()
+
+ self.assertEqual(message, b"tea")
+
+ def test_get_text_message_not_received_yet(self):
+ """get returns a text message when it is received."""
+ message = None
+
+ def getter():
+ nonlocal message
+ message = self.assembler.get()
+
+ with self.run_in_thread(getter):
+ self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9"))
+
+ self.assertEqual(message, "café")
+
+ def test_get_binary_message_not_received_yet(self):
+ """get returns a binary message when it is received."""
+ message = None
+
+ def getter():
+ nonlocal message
+ message = self.assembler.get()
+
+ with self.run_in_thread(getter):
+ self.assembler.put(Frame(OP_BINARY, b"tea"))
+
+ self.assertEqual(message, b"tea")
+
+ def test_get_fragmented_text_message_already_received(self):
+ """get reassembles a fragmented a text message that is already received."""
+
+ def putter():
+ self.assembler.put(Frame(OP_TEXT, b"ca", fin=False))
+ self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False))
+ self.assembler.put(Frame(OP_CONT, b"\xa9"))
+
+ with self.run_in_thread(putter):
+ message = self.assembler.get()
+
+ self.assertEqual(message, "café")
+
+ def test_get_fragmented_binary_message_already_received(self):
+ """get reassembles a fragmented binary message that is already received."""
+
+ def putter():
+ self.assembler.put(Frame(OP_BINARY, b"t", fin=False))
+ self.assembler.put(Frame(OP_CONT, b"e", fin=False))
+ self.assembler.put(Frame(OP_CONT, b"a"))
+
+ with self.run_in_thread(putter):
+ message = self.assembler.get()
+
+ self.assertEqual(message, b"tea")
+
+ def test_get_fragmented_text_message_being_received(self):
+ """get reassembles a fragmented text message that is partially received."""
+ message = None
+
+ def getter():
+ nonlocal message
+ message = self.assembler.get()
+
+ self.assembler.put(Frame(OP_TEXT, b"ca", fin=False))
+ with self.run_in_thread(getter):
+ self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False))
+ self.assembler.put(Frame(OP_CONT, b"\xa9"))
+
+ self.assertEqual(message, "café")
+
+ def test_get_fragmented_binary_message_being_received(self):
+ """get reassembles a fragmented binary message that is partially received."""
+ message = None
+
+ def getter():
+ nonlocal message
+ message = self.assembler.get()
+
+ self.assembler.put(Frame(OP_BINARY, b"t", fin=False))
+ with self.run_in_thread(getter):
+ self.assembler.put(Frame(OP_CONT, b"e", fin=False))
+ self.assembler.put(Frame(OP_CONT, b"a"))
+
+ self.assertEqual(message, b"tea")
+
+ def test_get_fragmented_text_message_not_received_yet(self):
+ """get reassembles a fragmented text message when it is received."""
+ message = None
+
+ def getter():
+ nonlocal message
+ message = self.assembler.get()
+
+ with self.run_in_thread(getter):
+ self.assembler.put(Frame(OP_TEXT, b"ca", fin=False))
+ self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False))
+ self.assembler.put(Frame(OP_CONT, b"\xa9"))
+
+ self.assertEqual(message, "café")
+
+ def test_get_fragmented_binary_message_not_received_yet(self):
+ """get reassembles a fragmented binary message when it is received."""
+ message = None
+
+ def getter():
+ nonlocal message
+ message = self.assembler.get()
+
+ with self.run_in_thread(getter):
+ self.assembler.put(Frame(OP_BINARY, b"t", fin=False))
+ self.assembler.put(Frame(OP_CONT, b"e", fin=False))
+ self.assembler.put(Frame(OP_CONT, b"a"))
+
+ self.assertEqual(message, b"tea")
+
+ # Test get_iter
+
+ def test_get_iter_text_message_already_received(self):
+ """get_iter yields a text message that is already received."""
+
+ def putter():
+ self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9"))
+
+ with self.run_in_thread(putter):
+ fragments = list(self.assembler.get_iter())
+
+ self.assertEqual(fragments, ["café"])
+
+ def test_get_iter_binary_message_already_received(self):
+ """get_iter yields a binary message that is already received."""
+
+ def putter():
+ self.assembler.put(Frame(OP_BINARY, b"tea"))
+
+ with self.run_in_thread(putter):
+ fragments = list(self.assembler.get_iter())
+
+ self.assertEqual(fragments, [b"tea"])
+
+ def test_get_iter_text_message_not_received_yet(self):
+ """get_iter yields a text message when it is received."""
+ fragments = []
+
+ def getter():
+ for fragment in self.assembler.get_iter():
+ fragments.append(fragment)
+
+ with self.run_in_thread(getter):
+ self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9"))
+
+ self.assertEqual(fragments, ["café"])
+
+ def test_get_iter_binary_message_not_received_yet(self):
+ """get_iter yields a binary message when it is received."""
+ fragments = []
+
+ def getter():
+ for fragment in self.assembler.get_iter():
+ fragments.append(fragment)
+
+ with self.run_in_thread(getter):
+ self.assembler.put(Frame(OP_BINARY, b"tea"))
+
+ self.assertEqual(fragments, [b"tea"])
+
+ def test_get_iter_fragmented_text_message_already_received(self):
+ """get_iter yields a fragmented text message that is already received."""
+
+ def putter():
+ self.assembler.put(Frame(OP_TEXT, b"ca", fin=False))
+ self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False))
+ self.assembler.put(Frame(OP_CONT, b"\xa9"))
+
+ with self.run_in_thread(putter):
+ fragments = list(self.assembler.get_iter())
+
+ self.assertEqual(fragments, ["ca", "f", "é"])
+
+ def test_get_iter_fragmented_binary_message_already_received(self):
+ """get_iter yields a fragmented binary message that is already received."""
+
+ def putter():
+ self.assembler.put(Frame(OP_BINARY, b"t", fin=False))
+ self.assembler.put(Frame(OP_CONT, b"e", fin=False))
+ self.assembler.put(Frame(OP_CONT, b"a"))
+
+ with self.run_in_thread(putter):
+ fragments = list(self.assembler.get_iter())
+
+ self.assertEqual(fragments, [b"t", b"e", b"a"])
+
+ def test_get_iter_fragmented_text_message_being_received(self):
+ """get_iter yields a fragmented text message that is partially received."""
+ fragments = []
+
+ def getter():
+ for fragment in self.assembler.get_iter():
+ fragments.append(fragment)
+
+ self.assembler.put(Frame(OP_TEXT, b"ca", fin=False))
+ with self.run_in_thread(getter):
+ self.assertEqual(fragments, ["ca"])
+ self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False))
+ time.sleep(MS)
+ self.assertEqual(fragments, ["ca", "f"])
+ self.assembler.put(Frame(OP_CONT, b"\xa9"))
+
+ self.assertEqual(fragments, ["ca", "f", "é"])
+
+ def test_get_iter_fragmented_binary_message_being_received(self):
+ """get_iter yields a fragmented binary message that is partially received."""
+ fragments = []
+
+ def getter():
+ for fragment in self.assembler.get_iter():
+ fragments.append(fragment)
+
+ self.assembler.put(Frame(OP_BINARY, b"t", fin=False))
+ with self.run_in_thread(getter):
+ self.assertEqual(fragments, [b"t"])
+ self.assembler.put(Frame(OP_CONT, b"e", fin=False))
+ time.sleep(MS)
+ self.assertEqual(fragments, [b"t", b"e"])
+ self.assembler.put(Frame(OP_CONT, b"a"))
+
+ self.assertEqual(fragments, [b"t", b"e", b"a"])
+
+ def test_get_iter_fragmented_text_message_not_received_yet(self):
+ """get_iter yields a fragmented text message when it is received."""
+ fragments = []
+
+ def getter():
+ for fragment in self.assembler.get_iter():
+ fragments.append(fragment)
+
+ with self.run_in_thread(getter):
+ self.assembler.put(Frame(OP_TEXT, b"ca", fin=False))
+ time.sleep(MS)
+ self.assertEqual(fragments, ["ca"])
+ self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False))
+ time.sleep(MS)
+ self.assertEqual(fragments, ["ca", "f"])
+ self.assembler.put(Frame(OP_CONT, b"\xa9"))
+
+ self.assertEqual(fragments, ["ca", "f", "é"])
+
+ def test_get_iter_fragmented_binary_message_not_received_yet(self):
+ """get_iter yields a fragmented binary message when it is received."""
+ fragments = []
+
+ def getter():
+ for fragment in self.assembler.get_iter():
+ fragments.append(fragment)
+
+ with self.run_in_thread(getter):
+ self.assembler.put(Frame(OP_BINARY, b"t", fin=False))
+ time.sleep(MS)
+ self.assertEqual(fragments, [b"t"])
+ self.assembler.put(Frame(OP_CONT, b"e", fin=False))
+ time.sleep(MS)
+ self.assertEqual(fragments, [b"t", b"e"])
+ self.assembler.put(Frame(OP_CONT, b"a"))
+
+ self.assertEqual(fragments, [b"t", b"e", b"a"])
+
+ # Test timeouts
+
+ def test_get_with_timeout_completes(self):
+ """get returns a message when it is received before the timeout."""
+
+ def putter():
+ self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9"))
+
+ with self.run_in_thread(putter):
+ message = self.assembler.get(MS)
+
+ self.assertEqual(message, "café")
+
+ def test_get_with_timeout_times_out(self):
+ """get raises TimeoutError when no message is received before the timeout."""
+ with self.assertRaises(TimeoutError):
+ self.assembler.get(MS)
+
+ # Test control frames
+
+ def test_control_frame_before_message_is_ignored(self):
+ """get ignores control frames between messages."""
+
+ def putter():
+ self.assembler.put(Frame(OP_PING, b""))
+ self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9"))
+
+ with self.run_in_thread(putter):
+ message = self.assembler.get()
+
+ self.assertEqual(message, "café")
+
+ def test_control_frame_in_fragmented_message_is_ignored(self):
+ """get ignores control frames within fragmented messages."""
+
+ def putter():
+ self.assembler.put(Frame(OP_BINARY, b"t", fin=False))
+ self.assembler.put(Frame(OP_PING, b""))
+ self.assembler.put(Frame(OP_CONT, b"e", fin=False))
+ self.assembler.put(Frame(OP_PONG, b""))
+ self.assembler.put(Frame(OP_CONT, b"a"))
+
+ with self.run_in_thread(putter):
+ message = self.assembler.get()
+
+ self.assertEqual(message, b"tea")
+
+ # Test concurrency
+
+ def test_get_fails_when_get_is_running(self):
+ """get cannot be called concurrently with itself."""
+ with self.run_in_thread(self.assembler.get):
+ with self.assertRaises(RuntimeError):
+ self.assembler.get()
+ self.assembler.put(Frame(OP_TEXT, b"")) # unlock other thread
+
+ def test_get_fails_when_get_iter_is_running(self):
+ """get cannot be called concurrently with get_iter."""
+ with self.run_in_thread(lambda: list(self.assembler.get_iter())):
+ with self.assertRaises(RuntimeError):
+ self.assembler.get()
+ self.assembler.put(Frame(OP_TEXT, b"")) # unlock other thread
+
+ def test_get_iter_fails_when_get_is_running(self):
+ """get_iter cannot be called concurrently with get."""
+ with self.run_in_thread(self.assembler.get):
+ with self.assertRaises(RuntimeError):
+ list(self.assembler.get_iter())
+ self.assembler.put(Frame(OP_TEXT, b"")) # unlock other thread
+
+ def test_get_iter_fails_when_get_iter_is_running(self):
+ """get_iter cannot be called concurrently with itself."""
+ with self.run_in_thread(lambda: list(self.assembler.get_iter())):
+ with self.assertRaises(RuntimeError):
+ list(self.assembler.get_iter())
+ self.assembler.put(Frame(OP_TEXT, b"")) # unlock other thread
+
+ def test_put_fails_when_put_is_running(self):
+ """put cannot be called concurrently with itself."""
+
+ def putter():
+ self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9"))
+
+ with self.run_in_thread(putter):
+ with self.assertRaises(RuntimeError):
+ self.assembler.put(Frame(OP_BINARY, b"tea"))
+ self.assembler.get() # unblock other thread
+
+ # Test termination
+
+ def test_get_fails_when_interrupted_by_close(self):
+ """get raises EOFError when close is called."""
+
+ def closer():
+ time.sleep(2 * MS)
+ self.assembler.close()
+
+ with self.run_in_thread(closer):
+ with self.assertRaises(EOFError):
+ self.assembler.get()
+
+ def test_get_iter_fails_when_interrupted_by_close(self):
+ """get_iter raises EOFError when close is called."""
+
+ def closer():
+ time.sleep(2 * MS)
+ self.assembler.close()
+
+ with self.run_in_thread(closer):
+ with self.assertRaises(EOFError):
+ list(self.assembler.get_iter())
+
+ def test_put_fails_when_interrupted_by_close(self):
+ """put raises EOFError when close is called."""
+
+ def closer():
+ time.sleep(2 * MS)
+ self.assembler.close()
+
+ with self.run_in_thread(closer):
+ with self.assertRaises(EOFError):
+ self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9"))
+
+ def test_get_fails_after_close(self):
+ """get raises EOFError after close is called."""
+ self.assembler.close()
+ with self.assertRaises(EOFError):
+ self.assembler.get()
+
+ def test_get_iter_fails_after_close(self):
+ """get_iter raises EOFError after close is called."""
+ self.assembler.close()
+ with self.assertRaises(EOFError):
+ list(self.assembler.get_iter())
+
+ def test_put_fails_after_close(self):
+ """put raises EOFError after close is called."""
+ self.assembler.close()
+ with self.assertRaises(EOFError):
+ self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9"))
+
+ def test_close_is_idempotent(self):
+ """close can be called multiple times safely."""
+ self.assembler.close()
+ self.assembler.close()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_server.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_server.py
new file mode 100644
index 0000000000..f9db842468
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_server.py
@@ -0,0 +1,388 @@
+import dataclasses
+import http
+import logging
+import socket
+import threading
+import unittest
+
+from websockets.exceptions import (
+ ConnectionClosedError,
+ ConnectionClosedOK,
+ InvalidStatus,
+ NegotiationError,
+)
+from websockets.http11 import Request, Response
+from websockets.sync.server import *
+
+from ..utils import MS, temp_unix_socket_path
+from .client import CLIENT_CONTEXT, run_client, run_unix_client
+from .server import (
+ SERVER_CONTEXT,
+ EvalShellMixin,
+ crash,
+ do_nothing,
+ eval_shell,
+ run_server,
+ run_unix_server,
+)
+
+
+class ServerTests(EvalShellMixin, unittest.TestCase):
+ def test_connection(self):
+ """Server receives connection from client and the handshake succeeds."""
+ with run_server() as server:
+ with run_client(server) as client:
+ self.assertEval(client, "ws.protocol.state.name", "OPEN")
+
+ def test_connection_fails(self):
+ """Server receives connection from client but the handshake fails."""
+
+ def remove_key_header(self, request):
+ del request.headers["Sec-WebSocket-Key"]
+
+ with run_server(process_request=remove_key_header) as server:
+ with self.assertRaisesRegex(
+ InvalidStatus,
+ "server rejected WebSocket connection: HTTP 400",
+ ):
+ with run_client(server):
+ self.fail("did not raise")
+
+ def test_connection_handler_returns(self):
+ """Connection handler returns."""
+ with run_server(do_nothing) as server:
+ with run_client(server) as client:
+ with self.assertRaisesRegex(
+ ConnectionClosedOK,
+ r"received 1000 \(OK\); then sent 1000 \(OK\)",
+ ):
+ client.recv()
+
+ def test_connection_handler_raises_exception(self):
+ """Connection handler raises an exception."""
+ with run_server(crash) as server:
+ with run_client(server) as client:
+ with self.assertRaisesRegex(
+ ConnectionClosedError,
+ r"received 1011 \(internal error\); "
+ r"then sent 1011 \(internal error\)",
+ ):
+ client.recv()
+
+ def test_existing_socket(self):
+ """Server receives connection using a pre-existing socket."""
+ with socket.create_server(("localhost", 0)) as sock:
+ with run_server(sock=sock):
+ # Build WebSocket URI to ensure we connect to the right socket.
+ with run_client("ws://{}:{}/".format(*sock.getsockname())) as client:
+ self.assertEval(client, "ws.protocol.state.name", "OPEN")
+
+ def test_select_subprotocol(self):
+ """Server selects a subprotocol with the select_subprotocol callable."""
+
+ def select_subprotocol(ws, subprotocols):
+ ws.select_subprotocol_ran = True
+ assert "chat" in subprotocols
+ return "chat"
+
+ with run_server(
+ subprotocols=["chat"],
+ select_subprotocol=select_subprotocol,
+ ) as server:
+ with run_client(server, subprotocols=["chat"]) as client:
+ self.assertEval(client, "ws.select_subprotocol_ran", "True")
+ self.assertEval(client, "ws.subprotocol", "chat")
+
+ def test_select_subprotocol_rejects_handshake(self):
+ """Server rejects handshake if select_subprotocol raises NegotiationError."""
+
+ def select_subprotocol(ws, subprotocols):
+ raise NegotiationError
+
+ with run_server(select_subprotocol=select_subprotocol) as server:
+ with self.assertRaisesRegex(
+ InvalidStatus,
+ "server rejected WebSocket connection: HTTP 400",
+ ):
+ with run_client(server):
+ self.fail("did not raise")
+
+ def test_select_subprotocol_raises_exception(self):
+ """Server returns an error if select_subprotocol raises an exception."""
+
+ def select_subprotocol(ws, subprotocols):
+ raise RuntimeError
+
+ with run_server(select_subprotocol=select_subprotocol) as server:
+ with self.assertRaisesRegex(
+ InvalidStatus,
+ "server rejected WebSocket connection: HTTP 500",
+ ):
+ with run_client(server):
+ self.fail("did not raise")
+
+ def test_process_request(self):
+ """Server runs process_request before processing the handshake."""
+
+ def process_request(ws, request):
+ self.assertIsInstance(request, Request)
+ ws.process_request_ran = True
+
+ with run_server(process_request=process_request) as server:
+ with run_client(server) as client:
+ self.assertEval(client, "ws.process_request_ran", "True")
+
+ def test_process_request_abort_handshake(self):
+ """Server aborts handshake if process_request returns a response."""
+
+ def process_request(ws, request):
+ return ws.protocol.reject(http.HTTPStatus.FORBIDDEN, "Forbidden")
+
+ with run_server(process_request=process_request) as server:
+ with self.assertRaisesRegex(
+ InvalidStatus,
+ "server rejected WebSocket connection: HTTP 403",
+ ):
+ with run_client(server):
+ self.fail("did not raise")
+
+ def test_process_request_raises_exception(self):
+ """Server returns an error if process_request raises an exception."""
+
+ def process_request(ws, request):
+ raise RuntimeError
+
+ with run_server(process_request=process_request) as server:
+ with self.assertRaisesRegex(
+ InvalidStatus,
+ "server rejected WebSocket connection: HTTP 500",
+ ):
+ with run_client(server):
+ self.fail("did not raise")
+
+ def test_process_response(self):
+ """Server runs process_response after processing the handshake."""
+
+ def process_response(ws, request, response):
+ self.assertIsInstance(request, Request)
+ self.assertIsInstance(response, Response)
+ ws.process_response_ran = True
+
+ with run_server(process_response=process_response) as server:
+ with run_client(server) as client:
+ self.assertEval(client, "ws.process_response_ran", "True")
+
+ def test_process_response_override_response(self):
+ """Server runs process_response after processing the handshake."""
+
+ def process_response(ws, request, response):
+ headers = response.headers.copy()
+ headers["X-ProcessResponse-Ran"] = "true"
+ return dataclasses.replace(response, headers=headers)
+
+ with run_server(process_response=process_response) as server:
+ with run_client(server) as client:
+ self.assertEqual(
+ client.response.headers["X-ProcessResponse-Ran"], "true"
+ )
+
+ def test_process_response_raises_exception(self):
+ """Server returns an error if process_response raises an exception."""
+
+ def process_response(ws, request, response):
+ raise RuntimeError
+
+ with run_server(process_response=process_response) as server:
+ with self.assertRaisesRegex(
+ InvalidStatus,
+ "server rejected WebSocket connection: HTTP 500",
+ ):
+ with run_client(server):
+ self.fail("did not raise")
+
+ def test_override_server(self):
+ """Server can override Server header with server_header."""
+ with run_server(server_header="Neo") as server:
+ with run_client(server) as client:
+ self.assertEval(client, "ws.response.headers['Server']", "Neo")
+
+ def test_remove_server(self):
+ """Server can remove Server header with server_header."""
+ with run_server(server_header=None) as server:
+ with run_client(server) as client:
+ self.assertEval(client, "'Server' in ws.response.headers", "False")
+
+ def test_compression_is_enabled(self):
+ """Server enables compression by default."""
+ with run_server() as server:
+ with run_client(server) as client:
+ self.assertEval(
+ client,
+ "[type(ext).__name__ for ext in ws.protocol.extensions]",
+ "['PerMessageDeflate']",
+ )
+
+ def test_disable_compression(self):
+ """Server disables compression."""
+ with run_server(compression=None) as server:
+ with run_client(server) as client:
+ self.assertEval(client, "ws.protocol.extensions", "[]")
+
+ def test_custom_connection_factory(self):
+ """Server runs ServerConnection factory provided in create_connection."""
+
+ def create_connection(*args, **kwargs):
+ server = ServerConnection(*args, **kwargs)
+ server.create_connection_ran = True
+ return server
+
+ with run_server(create_connection=create_connection) as server:
+ with run_client(server) as client:
+ self.assertEval(client, "ws.create_connection_ran", "True")
+
+ def test_timeout_during_handshake(self):
+ """Server times out before receiving handshake request from client."""
+ with run_server(open_timeout=MS) as server:
+ with socket.create_connection(server.socket.getsockname()) as sock:
+ self.assertEqual(sock.recv(4096), b"")
+
+ def test_connection_closed_during_handshake(self):
+ """Server reads EOF before receiving handshake request from client."""
+ with run_server() as server:
+ # Patch handler to record a reference to the thread running it.
+ server_thread = None
+ conn_received = threading.Event()
+ original_handler = server.handler
+
+ def handler(sock, addr):
+ nonlocal server_thread
+ server_thread = threading.current_thread()
+ nonlocal conn_received
+ conn_received.set()
+ original_handler(sock, addr)
+
+ server.handler = handler
+
+ with socket.create_connection(server.socket.getsockname()):
+ # Wait for the server to receive the connection, then close it.
+ conn_received.wait()
+
+ # Wait for the server thread to terminate.
+ server_thread.join()
+
+
+class SecureServerTests(EvalShellMixin, unittest.TestCase):
+ def test_connection(self):
+ """Server receives secure connection from client."""
+ with run_server(ssl_context=SERVER_CONTEXT) as server:
+ with run_client(server, ssl_context=CLIENT_CONTEXT) as client:
+ self.assertEval(client, "ws.protocol.state.name", "OPEN")
+ self.assertEval(client, "ws.socket.version()[:3]", "TLS")
+
+ def test_timeout_during_tls_handshake(self):
+ """Server times out before receiving TLS handshake request from client."""
+ with run_server(ssl_context=SERVER_CONTEXT, open_timeout=MS) as server:
+ with socket.create_connection(server.socket.getsockname()) as sock:
+ self.assertEqual(sock.recv(4096), b"")
+
+ def test_connection_closed_during_tls_handshake(self):
+ """Server reads EOF before receiving TLS handshake request from client."""
+ with run_server(ssl_context=SERVER_CONTEXT) as server:
+ # Patch handler to record a reference to the thread running it.
+ server_thread = None
+ conn_received = threading.Event()
+ original_handler = server.handler
+
+ def handler(sock, addr):
+ nonlocal server_thread
+ server_thread = threading.current_thread()
+ nonlocal conn_received
+ conn_received.set()
+ original_handler(sock, addr)
+
+ server.handler = handler
+
+ with socket.create_connection(server.socket.getsockname()):
+ # Wait for the server to receive the connection, then close it.
+ conn_received.wait()
+
+ # Wait for the server thread to terminate.
+ server_thread.join()
+
+
+@unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets")
+class UnixServerTests(EvalShellMixin, unittest.TestCase):
+ def test_connection(self):
+ """Server receives connection from client over a Unix socket."""
+ with temp_unix_socket_path() as path:
+ with run_unix_server(path):
+ with run_unix_client(path) as client:
+ self.assertEval(client, "ws.protocol.state.name", "OPEN")
+
+
+@unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets")
+class SecureUnixServerTests(EvalShellMixin, unittest.TestCase):
+ def test_connection(self):
+ """Server receives secure connection from client over a Unix socket."""
+ with temp_unix_socket_path() as path:
+ with run_unix_server(path, ssl_context=SERVER_CONTEXT):
+ with run_unix_client(path, ssl_context=CLIENT_CONTEXT) as client:
+ self.assertEval(client, "ws.protocol.state.name", "OPEN")
+ self.assertEval(client, "ws.socket.version()[:3]", "TLS")
+
+
+class ServerUsageErrorsTests(unittest.TestCase):
+ def test_unix_without_path_or_sock(self):
+ """Unix server requires path when sock isn't provided."""
+ with self.assertRaisesRegex(
+ TypeError,
+ "missing path argument",
+ ):
+ unix_serve(eval_shell)
+
+ def test_unix_with_path_and_sock(self):
+ """Unix server rejects path when sock is provided."""
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.addCleanup(sock.close)
+ with self.assertRaisesRegex(
+ TypeError,
+ "path and sock arguments are incompatible",
+ ):
+ unix_serve(eval_shell, path="/", sock=sock)
+
+ def test_invalid_subprotocol(self):
+ """Server rejects single value of subprotocols."""
+ with self.assertRaisesRegex(
+ TypeError,
+ "subprotocols must be a list",
+ ):
+ serve(eval_shell, subprotocols="chat")
+
+ def test_unsupported_compression(self):
+ """Server rejects incorrect value of compression."""
+ with self.assertRaisesRegex(
+ ValueError,
+ "unsupported compression: False",
+ ):
+ serve(eval_shell, compression=False)
+
+
+class WebSocketServerTests(unittest.TestCase):
+ def test_logger(self):
+ """WebSocketServer accepts a logger argument."""
+ logger = logging.getLogger("test")
+ with run_server(logger=logger) as server:
+ self.assertIs(server.logger, logger)
+
+ def test_fileno(self):
+ """WebSocketServer provides a fileno attribute."""
+ with run_server() as server:
+ self.assertIsInstance(server.fileno(), int)
+
+ def test_shutdown(self):
+ """WebSocketServer provides a shutdown method."""
+ with run_server() as server:
+ server.shutdown()
+ # Check that the server socket is closed.
+ with self.assertRaises(OSError):
+ server.socket.accept()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_utils.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_utils.py
new file mode 100644
index 0000000000..2980a97b42
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_utils.py
@@ -0,0 +1,33 @@
+import unittest
+
+from websockets.sync.utils import *
+
+from ..utils import MS
+
+
+class DeadlineTests(unittest.TestCase):
+ def test_timeout_pending(self):
+ """timeout returns remaining time if deadline is in the future."""
+ deadline = Deadline(MS)
+ timeout = deadline.timeout()
+ self.assertGreater(timeout, 0)
+ self.assertLess(timeout, MS)
+
+ def test_timeout_elapsed_exception(self):
+ """timeout raises TimeoutError if deadline is in the past."""
+ deadline = Deadline(-MS)
+ with self.assertRaises(TimeoutError):
+ deadline.timeout()
+
+ def test_timeout_elapsed_no_exception(self):
+ """timeout doesn't raise TimeoutError when raise_if_elapsed is disabled."""
+ deadline = Deadline(-MS)
+ timeout = deadline.timeout(raise_if_elapsed=False)
+ self.assertGreater(timeout, -2 * MS)
+ self.assertLess(timeout, -MS)
+
+ def test_no_timeout(self):
+ """timeout returns None when no deadline is set."""
+ deadline = Deadline(None)
+ timeout = deadline.timeout()
+ self.assertIsNone(timeout, None)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/utils.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/utils.py
new file mode 100644
index 0000000000..8903cd3499
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/utils.py
@@ -0,0 +1,26 @@
+import contextlib
+import threading
+import time
+import unittest
+
+from ..utils import MS
+
+
+class ThreadTestCase(unittest.TestCase):
+ @contextlib.contextmanager
+ def run_in_thread(self, target):
+ """
+ Run ``target`` function without arguments in a thread.
+
+ In order to facilitate writing tests, this helper lets the thread run
+ for 1ms on entry and joins the thread with a 1ms timeout on exit.
+
+ """
+ thread = threading.Thread(target=target)
+ thread.start()
+ time.sleep(MS)
+ try:
+ yield
+ finally:
+ thread.join(MS)
+ self.assertFalse(thread.is_alive())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_auth.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_auth.py
new file mode 100644
index 0000000000..28db931552
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_auth.py
@@ -0,0 +1 @@
+from websockets.auth import *
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_client.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_client.py
new file mode 100644
index 0000000000..c83c87038f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_client.py
@@ -0,0 +1,614 @@
+import logging
+import unittest
+import unittest.mock
+
+from websockets.client import *
+from websockets.datastructures import Headers
+from websockets.exceptions import InvalidHandshake, InvalidHeader
+from websockets.frames import OP_TEXT, Frame
+from websockets.http11 import Request, Response
+from websockets.protocol import CONNECTING, OPEN
+from websockets.uri import parse_uri
+from websockets.utils import accept_key
+
+from .extensions.utils import (
+ ClientOpExtensionFactory,
+ ClientRsv2ExtensionFactory,
+ OpExtension,
+ Rsv2Extension,
+)
+from .test_utils import ACCEPT, KEY
+from .utils import DATE, DeprecationTestCase
+
+
+class ConnectTests(unittest.TestCase):
+ def test_send_connect(self):
+ with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
+ client = ClientProtocol(parse_uri("wss://example.com/test"))
+ request = client.connect()
+ self.assertIsInstance(request, Request)
+ client.send_request(request)
+ self.assertEqual(
+ client.data_to_send(),
+ [
+ f"GET /test HTTP/1.1\r\n"
+ f"Host: example.com\r\n"
+ f"Upgrade: websocket\r\n"
+ f"Connection: Upgrade\r\n"
+ f"Sec-WebSocket-Key: {KEY}\r\n"
+ f"Sec-WebSocket-Version: 13\r\n"
+ f"\r\n".encode()
+ ],
+ )
+ self.assertFalse(client.close_expected())
+
+ def test_connect_request(self):
+ with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
+ client = ClientProtocol(parse_uri("wss://example.com/test"))
+ request = client.connect()
+ self.assertEqual(request.path, "/test")
+ self.assertEqual(
+ request.headers,
+ Headers(
+ {
+ "Host": "example.com",
+ "Upgrade": "websocket",
+ "Connection": "Upgrade",
+ "Sec-WebSocket-Key": KEY,
+ "Sec-WebSocket-Version": "13",
+ }
+ ),
+ )
+
+ def test_path(self):
+ client = ClientProtocol(parse_uri("wss://example.com/endpoint?test=1"))
+ request = client.connect()
+
+ self.assertEqual(request.path, "/endpoint?test=1")
+
+ def test_port(self):
+ for uri, host in [
+ ("ws://example.com/", "example.com"),
+ ("ws://example.com:80/", "example.com"),
+ ("ws://example.com:8080/", "example.com:8080"),
+ ("wss://example.com/", "example.com"),
+ ("wss://example.com:443/", "example.com"),
+ ("wss://example.com:8443/", "example.com:8443"),
+ ]:
+ with self.subTest(uri=uri):
+ client = ClientProtocol(parse_uri(uri))
+ request = client.connect()
+
+ self.assertEqual(request.headers["Host"], host)
+
+ def test_user_info(self):
+ client = ClientProtocol(parse_uri("wss://hello:iloveyou@example.com/"))
+ request = client.connect()
+
+ self.assertEqual(request.headers["Authorization"], "Basic aGVsbG86aWxvdmV5b3U=")
+
+ def test_origin(self):
+ client = ClientProtocol(
+ parse_uri("wss://example.com/"),
+ origin="https://example.com",
+ )
+ request = client.connect()
+
+ self.assertEqual(request.headers["Origin"], "https://example.com")
+
+ def test_extensions(self):
+ client = ClientProtocol(
+ parse_uri("wss://example.com/"),
+ extensions=[ClientOpExtensionFactory()],
+ )
+ request = client.connect()
+
+ self.assertEqual(request.headers["Sec-WebSocket-Extensions"], "x-op; op")
+
+ def test_subprotocols(self):
+ client = ClientProtocol(
+ parse_uri("wss://example.com/"),
+ subprotocols=["chat"],
+ )
+ request = client.connect()
+
+ self.assertEqual(request.headers["Sec-WebSocket-Protocol"], "chat")
+
+
+class AcceptRejectTests(unittest.TestCase):
+ def test_receive_accept(self):
+ with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
+ client = ClientProtocol(parse_uri("ws://example.com/test"))
+ client.connect()
+ client.receive_data(
+ (
+ f"HTTP/1.1 101 Switching Protocols\r\n"
+ f"Upgrade: websocket\r\n"
+ f"Connection: Upgrade\r\n"
+ f"Sec-WebSocket-Accept: {ACCEPT}\r\n"
+ f"Date: {DATE}\r\n"
+ f"\r\n"
+ ).encode(),
+ )
+ [response] = client.events_received()
+ self.assertIsInstance(response, Response)
+ self.assertEqual(client.data_to_send(), [])
+ self.assertFalse(client.close_expected())
+ self.assertEqual(client.state, OPEN)
+
+ def test_receive_reject(self):
+ with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
+ client = ClientProtocol(parse_uri("ws://example.com/test"))
+ client.connect()
+ client.receive_data(
+ (
+ f"HTTP/1.1 404 Not Found\r\n"
+ f"Date: {DATE}\r\n"
+ f"Content-Length: 13\r\n"
+ f"Content-Type: text/plain; charset=utf-8\r\n"
+ f"Connection: close\r\n"
+ f"\r\n"
+ f"Sorry folks.\n"
+ ).encode(),
+ )
+ [response] = client.events_received()
+ self.assertIsInstance(response, Response)
+ self.assertEqual(client.data_to_send(), [])
+ self.assertTrue(client.close_expected())
+ self.assertEqual(client.state, CONNECTING)
+
+ def test_accept_response(self):
+ with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
+ client = ClientProtocol(parse_uri("ws://example.com/test"))
+ client.connect()
+ client.receive_data(
+ (
+ f"HTTP/1.1 101 Switching Protocols\r\n"
+ f"Upgrade: websocket\r\n"
+ f"Connection: Upgrade\r\n"
+ f"Sec-WebSocket-Accept: {ACCEPT}\r\n"
+ f"Date: {DATE}\r\n"
+ f"\r\n"
+ ).encode(),
+ )
+ [response] = client.events_received()
+ self.assertEqual(response.status_code, 101)
+ self.assertEqual(response.reason_phrase, "Switching Protocols")
+ self.assertEqual(
+ response.headers,
+ Headers(
+ {
+ "Upgrade": "websocket",
+ "Connection": "Upgrade",
+ "Sec-WebSocket-Accept": ACCEPT,
+ "Date": DATE,
+ }
+ ),
+ )
+ self.assertIsNone(response.body)
+
+ def test_reject_response(self):
+ with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
+ client = ClientProtocol(parse_uri("ws://example.com/test"))
+ client.connect()
+ client.receive_data(
+ (
+ f"HTTP/1.1 404 Not Found\r\n"
+ f"Date: {DATE}\r\n"
+ f"Content-Length: 13\r\n"
+ f"Content-Type: text/plain; charset=utf-8\r\n"
+ f"Connection: close\r\n"
+ f"\r\n"
+ f"Sorry folks.\n"
+ ).encode(),
+ )
+ [response] = client.events_received()
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(response.reason_phrase, "Not Found")
+ self.assertEqual(
+ response.headers,
+ Headers(
+ {
+ "Date": DATE,
+ "Content-Length": "13",
+ "Content-Type": "text/plain; charset=utf-8",
+ "Connection": "close",
+ }
+ ),
+ )
+ self.assertEqual(response.body, b"Sorry folks.\n")
+
+ def test_no_response(self):
+ with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
+ client = ClientProtocol(parse_uri("ws://example.com/test"))
+ client.connect()
+ client.receive_eof()
+ self.assertEqual(client.events_received(), [])
+
+ def test_partial_response(self):
+ with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
+ client = ClientProtocol(parse_uri("ws://example.com/test"))
+ client.connect()
+ client.receive_data(b"HTTP/1.1 101 Switching Protocols\r\n")
+ client.receive_eof()
+ self.assertEqual(client.events_received(), [])
+
+ def test_random_response(self):
+ with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
+ client = ClientProtocol(parse_uri("ws://example.com/test"))
+ client.connect()
+ client.receive_data(b"220 smtp.invalid\r\n")
+ client.receive_data(b"250 Hello relay.invalid\r\n")
+ client.receive_data(b"250 Ok\r\n")
+ client.receive_data(b"250 Ok\r\n")
+ client.receive_eof()
+ self.assertEqual(client.events_received(), [])
+
+ def make_accept_response(self, client):
+ request = client.connect()
+ return Response(
+ status_code=101,
+ reason_phrase="Switching Protocols",
+ headers=Headers(
+ {
+ "Upgrade": "websocket",
+ "Connection": "Upgrade",
+ "Sec-WebSocket-Accept": accept_key(
+ request.headers["Sec-WebSocket-Key"]
+ ),
+ }
+ ),
+ )
+
+ def test_basic(self):
+ client = ClientProtocol(parse_uri("wss://example.com/"))
+ response = self.make_accept_response(client)
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, OPEN)
+
+ def test_missing_connection(self):
+ client = ClientProtocol(parse_uri("wss://example.com/"))
+ response = self.make_accept_response(client)
+ del response.headers["Connection"]
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, CONNECTING)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise client.handshake_exc
+ self.assertEqual(str(raised.exception), "missing Connection header")
+
+ def test_invalid_connection(self):
+ client = ClientProtocol(parse_uri("wss://example.com/"))
+ response = self.make_accept_response(client)
+ del response.headers["Connection"]
+ response.headers["Connection"] = "close"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, CONNECTING)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise client.handshake_exc
+ self.assertEqual(str(raised.exception), "invalid Connection header: close")
+
+ def test_missing_upgrade(self):
+ client = ClientProtocol(parse_uri("wss://example.com/"))
+ response = self.make_accept_response(client)
+ del response.headers["Upgrade"]
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, CONNECTING)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise client.handshake_exc
+ self.assertEqual(str(raised.exception), "missing Upgrade header")
+
+ def test_invalid_upgrade(self):
+ client = ClientProtocol(parse_uri("wss://example.com/"))
+ response = self.make_accept_response(client)
+ del response.headers["Upgrade"]
+ response.headers["Upgrade"] = "h2c"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, CONNECTING)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise client.handshake_exc
+ self.assertEqual(str(raised.exception), "invalid Upgrade header: h2c")
+
+ def test_missing_accept(self):
+ client = ClientProtocol(parse_uri("wss://example.com/"))
+ response = self.make_accept_response(client)
+ del response.headers["Sec-WebSocket-Accept"]
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, CONNECTING)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise client.handshake_exc
+ self.assertEqual(str(raised.exception), "missing Sec-WebSocket-Accept header")
+
+ def test_multiple_accept(self):
+ client = ClientProtocol(parse_uri("wss://example.com/"))
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Accept"] = ACCEPT
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, CONNECTING)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise client.handshake_exc
+ self.assertEqual(
+ str(raised.exception),
+ "invalid Sec-WebSocket-Accept header: "
+ "more than one Sec-WebSocket-Accept header found",
+ )
+
+ def test_invalid_accept(self):
+ client = ClientProtocol(parse_uri("wss://example.com/"))
+ response = self.make_accept_response(client)
+ del response.headers["Sec-WebSocket-Accept"]
+ response.headers["Sec-WebSocket-Accept"] = ACCEPT
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, CONNECTING)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise client.handshake_exc
+ self.assertEqual(
+ str(raised.exception), f"invalid Sec-WebSocket-Accept header: {ACCEPT}"
+ )
+
+ def test_no_extensions(self):
+ client = ClientProtocol(parse_uri("wss://example.com/"))
+ response = self.make_accept_response(client)
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, OPEN)
+ self.assertEqual(client.extensions, [])
+
+ def test_no_extension(self):
+ client = ClientProtocol(
+ parse_uri("wss://example.com/"),
+ extensions=[ClientOpExtensionFactory()],
+ )
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Extensions"] = "x-op; op"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, OPEN)
+ self.assertEqual(client.extensions, [OpExtension()])
+
+ def test_extension(self):
+ client = ClientProtocol(
+ parse_uri("wss://example.com/"),
+ extensions=[ClientRsv2ExtensionFactory()],
+ )
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Extensions"] = "x-rsv2"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, OPEN)
+ self.assertEqual(client.extensions, [Rsv2Extension()])
+
+ def test_unexpected_extension(self):
+ client = ClientProtocol(parse_uri("wss://example.com/"))
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Extensions"] = "x-op; op"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, CONNECTING)
+ with self.assertRaises(InvalidHandshake) as raised:
+ raise client.handshake_exc
+ self.assertEqual(str(raised.exception), "no extensions supported")
+
+ def test_unsupported_extension(self):
+ client = ClientProtocol(
+ parse_uri("wss://example.com/"),
+ extensions=[ClientRsv2ExtensionFactory()],
+ )
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Extensions"] = "x-op; op"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, CONNECTING)
+ with self.assertRaises(InvalidHandshake) as raised:
+ raise client.handshake_exc
+ self.assertEqual(
+ str(raised.exception),
+ "Unsupported extension: name = x-op, params = [('op', None)]",
+ )
+
+ def test_supported_extension_parameters(self):
+ client = ClientProtocol(
+ parse_uri("wss://example.com/"),
+ extensions=[ClientOpExtensionFactory("this")],
+ )
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Extensions"] = "x-op; op=this"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, OPEN)
+ self.assertEqual(client.extensions, [OpExtension("this")])
+
+ def test_unsupported_extension_parameters(self):
+ client = ClientProtocol(
+ parse_uri("wss://example.com/"),
+ extensions=[ClientOpExtensionFactory("this")],
+ )
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Extensions"] = "x-op; op=that"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, CONNECTING)
+ with self.assertRaises(InvalidHandshake) as raised:
+ raise client.handshake_exc
+ self.assertEqual(
+ str(raised.exception),
+ "Unsupported extension: name = x-op, params = [('op', 'that')]",
+ )
+
+ def test_multiple_supported_extension_parameters(self):
+ client = ClientProtocol(
+ parse_uri("wss://example.com/"),
+ extensions=[
+ ClientOpExtensionFactory("this"),
+ ClientOpExtensionFactory("that"),
+ ],
+ )
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Extensions"] = "x-op; op=that"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, OPEN)
+ self.assertEqual(client.extensions, [OpExtension("that")])
+
+ def test_multiple_extensions(self):
+ client = ClientProtocol(
+ parse_uri("wss://example.com/"),
+ extensions=[ClientOpExtensionFactory(), ClientRsv2ExtensionFactory()],
+ )
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Extensions"] = "x-op; op"
+ response.headers["Sec-WebSocket-Extensions"] = "x-rsv2"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, OPEN)
+ self.assertEqual(client.extensions, [OpExtension(), Rsv2Extension()])
+
+ def test_multiple_extensions_order(self):
+ client = ClientProtocol(
+ parse_uri("wss://example.com/"),
+ extensions=[ClientOpExtensionFactory(), ClientRsv2ExtensionFactory()],
+ )
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Extensions"] = "x-rsv2"
+ response.headers["Sec-WebSocket-Extensions"] = "x-op; op"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, OPEN)
+ self.assertEqual(client.extensions, [Rsv2Extension(), OpExtension()])
+
+ def test_no_subprotocols(self):
+ client = ClientProtocol(parse_uri("wss://example.com/"))
+ response = self.make_accept_response(client)
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, OPEN)
+ self.assertIsNone(client.subprotocol)
+
+ def test_no_subprotocol(self):
+ client = ClientProtocol(parse_uri("wss://example.com/"), subprotocols=["chat"])
+ response = self.make_accept_response(client)
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, OPEN)
+ self.assertIsNone(client.subprotocol)
+
+ def test_subprotocol(self):
+ client = ClientProtocol(parse_uri("wss://example.com/"), subprotocols=["chat"])
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Protocol"] = "chat"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, OPEN)
+ self.assertEqual(client.subprotocol, "chat")
+
+ def test_unexpected_subprotocol(self):
+ client = ClientProtocol(parse_uri("wss://example.com/"))
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Protocol"] = "chat"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, CONNECTING)
+ with self.assertRaises(InvalidHandshake) as raised:
+ raise client.handshake_exc
+ self.assertEqual(str(raised.exception), "no subprotocols supported")
+
+ def test_multiple_subprotocols(self):
+ client = ClientProtocol(
+ parse_uri("wss://example.com/"),
+ subprotocols=["superchat", "chat"],
+ )
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Protocol"] = "superchat"
+ response.headers["Sec-WebSocket-Protocol"] = "chat"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, CONNECTING)
+ with self.assertRaises(InvalidHandshake) as raised:
+ raise client.handshake_exc
+ self.assertEqual(
+ str(raised.exception), "multiple subprotocols: superchat, chat"
+ )
+
+ def test_supported_subprotocol(self):
+ client = ClientProtocol(
+ parse_uri("wss://example.com/"),
+ subprotocols=["superchat", "chat"],
+ )
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Protocol"] = "chat"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, OPEN)
+ self.assertEqual(client.subprotocol, "chat")
+
+ def test_unsupported_subprotocol(self):
+ client = ClientProtocol(
+ parse_uri("wss://example.com/"),
+ subprotocols=["superchat", "chat"],
+ )
+ response = self.make_accept_response(client)
+ response.headers["Sec-WebSocket-Protocol"] = "otherchat"
+ client.receive_data(response.serialize())
+ [response] = client.events_received()
+
+ self.assertEqual(client.state, CONNECTING)
+ with self.assertRaises(InvalidHandshake) as raised:
+ raise client.handshake_exc
+ self.assertEqual(str(raised.exception), "unsupported subprotocol: otherchat")
+
+
+class MiscTests(unittest.TestCase):
+ def test_bypass_handshake(self):
+ client = ClientProtocol(parse_uri("ws://example.com/test"), state=OPEN)
+ client.receive_data(b"\x81\x06Hello!")
+ [frame] = client.events_received()
+ self.assertEqual(frame, Frame(OP_TEXT, b"Hello!"))
+
+ def test_custom_logger(self):
+ logger = logging.getLogger("test")
+ with self.assertLogs("test", logging.DEBUG) as logs:
+ ClientProtocol(parse_uri("wss://example.com/test"), logger=logger)
+ self.assertEqual(len(logs.records), 1)
+
+
+class BackwardsCompatibilityTests(DeprecationTestCase):
+ def test_client_connection_class(self):
+ with self.assertDeprecationWarning(
+ "ClientConnection was renamed to ClientProtocol"
+ ):
+ from websockets.client import ClientConnection
+
+ client = ClientConnection("ws://localhost/")
+
+ self.assertIsInstance(client, ClientProtocol)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_connection.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_connection.py
new file mode 100644
index 0000000000..6592d67d0d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_connection.py
@@ -0,0 +1,14 @@
+from websockets.protocol import Protocol
+
+from .utils import DeprecationTestCase
+
+
+class BackwardsCompatibilityTests(DeprecationTestCase):
+ def test_connection_class(self):
+ with self.assertDeprecationWarning(
+ "websockets.connection was renamed to websockets.protocol "
+ "and Connection was renamed to Protocol"
+ ):
+ from websockets.connection import Connection
+
+ self.assertIs(Connection, Protocol)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_datastructures.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_datastructures.py
new file mode 100644
index 0000000000..32b79817ae
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_datastructures.py
@@ -0,0 +1,236 @@
+import unittest
+
+from websockets.datastructures import *
+
+
+class MultipleValuesErrorTests(unittest.TestCase):
+ def test_multiple_values_error_str(self):
+ self.assertEqual(str(MultipleValuesError("Connection")), "'Connection'")
+ self.assertEqual(str(MultipleValuesError()), "")
+
+
+class HeadersTests(unittest.TestCase):
+ def setUp(self):
+ self.headers = Headers([("Connection", "Upgrade"), ("Server", "websockets")])
+
+ def test_init(self):
+ self.assertEqual(
+ Headers(),
+ Headers(),
+ )
+
+ def test_init_from_kwargs(self):
+ self.assertEqual(
+ Headers(connection="Upgrade", server="websockets"),
+ self.headers,
+ )
+
+ def test_init_from_headers(self):
+ self.assertEqual(
+ Headers(self.headers),
+ self.headers,
+ )
+
+ def test_init_from_headers_and_kwargs(self):
+ self.assertEqual(
+ Headers(Headers(connection="Upgrade"), server="websockets"),
+ self.headers,
+ )
+
+ def test_init_from_mapping(self):
+ self.assertEqual(
+ Headers({"Connection": "Upgrade", "Server": "websockets"}),
+ self.headers,
+ )
+
+ def test_init_from_mapping_and_kwargs(self):
+ self.assertEqual(
+ Headers({"Connection": "Upgrade"}, server="websockets"),
+ self.headers,
+ )
+
+ def test_init_from_iterable(self):
+ self.assertEqual(
+ Headers([("Connection", "Upgrade"), ("Server", "websockets")]),
+ self.headers,
+ )
+
+ def test_init_from_iterable_and_kwargs(self):
+ self.assertEqual(
+ Headers([("Connection", "Upgrade")], server="websockets"),
+ self.headers,
+ )
+
+ def test_init_multiple_positional_arguments(self):
+ with self.assertRaises(TypeError):
+ Headers(Headers(connection="Upgrade"), Headers(server="websockets"))
+
+ def test_str(self):
+ self.assertEqual(
+ str(self.headers), "Connection: Upgrade\r\nServer: websockets\r\n\r\n"
+ )
+
+ def test_repr(self):
+ self.assertEqual(
+ repr(self.headers),
+ "Headers([('Connection', 'Upgrade'), ('Server', 'websockets')])",
+ )
+
+ def test_copy(self):
+ self.assertEqual(repr(self.headers.copy()), repr(self.headers))
+
+ def test_serialize(self):
+ self.assertEqual(
+ self.headers.serialize(),
+ b"Connection: Upgrade\r\nServer: websockets\r\n\r\n",
+ )
+
+ def test_contains(self):
+ self.assertIn("Server", self.headers)
+
+ def test_contains_case_insensitive(self):
+ self.assertIn("server", self.headers)
+
+ def test_contains_not_found(self):
+ self.assertNotIn("Date", self.headers)
+
+ def test_contains_non_string_key(self):
+ self.assertNotIn(42, self.headers)
+
+ def test_iter(self):
+ self.assertEqual(set(iter(self.headers)), {"connection", "server"})
+
+ def test_len(self):
+ self.assertEqual(len(self.headers), 2)
+
+ def test_getitem(self):
+ self.assertEqual(self.headers["Server"], "websockets")
+
+ def test_getitem_case_insensitive(self):
+ self.assertEqual(self.headers["server"], "websockets")
+
+ def test_getitem_key_error(self):
+ with self.assertRaises(KeyError):
+ self.headers["Upgrade"]
+
+ def test_setitem(self):
+ self.headers["Upgrade"] = "websocket"
+ self.assertEqual(self.headers["Upgrade"], "websocket")
+
+ def test_setitem_case_insensitive(self):
+ self.headers["upgrade"] = "websocket"
+ self.assertEqual(self.headers["Upgrade"], "websocket")
+
+ def test_delitem(self):
+ del self.headers["Connection"]
+ with self.assertRaises(KeyError):
+ self.headers["Connection"]
+
+ def test_delitem_case_insensitive(self):
+ del self.headers["connection"]
+ with self.assertRaises(KeyError):
+ self.headers["Connection"]
+
+ def test_eq(self):
+ other_headers = Headers([("Connection", "Upgrade"), ("Server", "websockets")])
+ self.assertEqual(self.headers, other_headers)
+
+ def test_eq_case_insensitive(self):
+ other_headers = Headers(connection="Upgrade", server="websockets")
+ self.assertEqual(self.headers, other_headers)
+
+ def test_eq_not_equal(self):
+ other_headers = Headers([("Connection", "close"), ("Server", "websockets")])
+ self.assertNotEqual(self.headers, other_headers)
+
+ def test_eq_other_type(self):
+ self.assertNotEqual(
+ self.headers, "Connection: Upgrade\r\nServer: websockets\r\n\r\n"
+ )
+
+ def test_clear(self):
+ self.headers.clear()
+ self.assertFalse(self.headers)
+ self.assertEqual(self.headers, Headers())
+
+ def test_get_all(self):
+ self.assertEqual(self.headers.get_all("Connection"), ["Upgrade"])
+
+ def test_get_all_case_insensitive(self):
+ self.assertEqual(self.headers.get_all("connection"), ["Upgrade"])
+
+ def test_get_all_no_values(self):
+ self.assertEqual(self.headers.get_all("Upgrade"), [])
+
+ def test_raw_items(self):
+ self.assertEqual(
+ list(self.headers.raw_items()),
+ [("Connection", "Upgrade"), ("Server", "websockets")],
+ )
+
+
+class MultiValueHeadersTests(unittest.TestCase):
+ def setUp(self):
+ self.headers = Headers([("Server", "Python"), ("Server", "websockets")])
+
+ def test_init_from_headers(self):
+ self.assertEqual(
+ Headers(self.headers),
+ self.headers,
+ )
+
+ def test_init_from_headers_and_kwargs(self):
+ self.assertEqual(
+ Headers(Headers(server="Python"), server="websockets"),
+ self.headers,
+ )
+
+ def test_str(self):
+ self.assertEqual(
+ str(self.headers), "Server: Python\r\nServer: websockets\r\n\r\n"
+ )
+
+ def test_repr(self):
+ self.assertEqual(
+ repr(self.headers),
+ "Headers([('Server', 'Python'), ('Server', 'websockets')])",
+ )
+
+ def test_copy(self):
+ self.assertEqual(repr(self.headers.copy()), repr(self.headers))
+
+ def test_serialize(self):
+ self.assertEqual(
+ self.headers.serialize(),
+ b"Server: Python\r\nServer: websockets\r\n\r\n",
+ )
+
+ def test_iter(self):
+ self.assertEqual(set(iter(self.headers)), {"server"})
+
+ def test_len(self):
+ self.assertEqual(len(self.headers), 1)
+
+ def test_getitem_multiple_values_error(self):
+ with self.assertRaises(MultipleValuesError):
+ self.headers["Server"]
+
+ def test_setitem(self):
+ self.headers["Server"] = "redux"
+ self.assertEqual(
+ self.headers.get_all("Server"), ["Python", "websockets", "redux"]
+ )
+
+ def test_delitem(self):
+ del self.headers["Server"]
+ with self.assertRaises(KeyError):
+ self.headers["Server"]
+
+ def test_get_all(self):
+ self.assertEqual(self.headers.get_all("Server"), ["Python", "websockets"])
+
+ def test_raw_items(self):
+ self.assertEqual(
+ list(self.headers.raw_items()),
+ [("Server", "Python"), ("Server", "websockets")],
+ )
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_exceptions.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_exceptions.py
new file mode 100644
index 0000000000..1e6f58fad5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_exceptions.py
@@ -0,0 +1,196 @@
+import unittest
+
+from websockets.datastructures import Headers
+from websockets.exceptions import *
+from websockets.frames import Close, CloseCode
+from websockets.http11 import Response
+
+
+class ExceptionsTests(unittest.TestCase):
+ def test_str(self):
+ for exception, exception_str in [
+ (
+ WebSocketException("something went wrong"),
+ "something went wrong",
+ ),
+ (
+ ConnectionClosed(
+ Close(CloseCode.NORMAL_CLOSURE, ""),
+ Close(CloseCode.NORMAL_CLOSURE, ""),
+ True,
+ ),
+ "received 1000 (OK); then sent 1000 (OK)",
+ ),
+ (
+ ConnectionClosed(
+ Close(CloseCode.GOING_AWAY, "Bye!"),
+ Close(CloseCode.GOING_AWAY, "Bye!"),
+ False,
+ ),
+ "sent 1001 (going away) Bye!; then received 1001 (going away) Bye!",
+ ),
+ (
+ ConnectionClosed(
+ Close(CloseCode.NORMAL_CLOSURE, "race"),
+ Close(CloseCode.NORMAL_CLOSURE, "cond"),
+ True,
+ ),
+ "received 1000 (OK) race; then sent 1000 (OK) cond",
+ ),
+ (
+ ConnectionClosed(
+ Close(CloseCode.NORMAL_CLOSURE, "cond"),
+ Close(CloseCode.NORMAL_CLOSURE, "race"),
+ False,
+ ),
+ "sent 1000 (OK) race; then received 1000 (OK) cond",
+ ),
+ (
+ ConnectionClosed(
+ None,
+ Close(CloseCode.MESSAGE_TOO_BIG, ""),
+ None,
+ ),
+ "sent 1009 (message too big); no close frame received",
+ ),
+ (
+ ConnectionClosed(
+ Close(CloseCode.PROTOCOL_ERROR, ""),
+ None,
+ None,
+ ),
+ "received 1002 (protocol error); no close frame sent",
+ ),
+ (
+ ConnectionClosedOK(
+ Close(CloseCode.NORMAL_CLOSURE, ""),
+ Close(CloseCode.NORMAL_CLOSURE, ""),
+ True,
+ ),
+ "received 1000 (OK); then sent 1000 (OK)",
+ ),
+ (
+ ConnectionClosedError(
+ None,
+ None,
+ None,
+ ),
+ "no close frame received or sent",
+ ),
+ (
+ InvalidHandshake("invalid request"),
+ "invalid request",
+ ),
+ (
+ SecurityError("redirect from WSS to WS"),
+ "redirect from WSS to WS",
+ ),
+ (
+ InvalidMessage("malformed HTTP message"),
+ "malformed HTTP message",
+ ),
+ (
+ InvalidHeader("Name"),
+ "missing Name header",
+ ),
+ (
+ InvalidHeader("Name", None),
+ "missing Name header",
+ ),
+ (
+ InvalidHeader("Name", ""),
+ "empty Name header",
+ ),
+ (
+ InvalidHeader("Name", "Value"),
+ "invalid Name header: Value",
+ ),
+ (
+ InvalidHeaderFormat("Sec-WebSocket-Protocol", "exp. token", "a=|", 3),
+ "invalid Sec-WebSocket-Protocol header: exp. token at 3 in a=|",
+ ),
+ (
+ InvalidHeaderValue("Sec-WebSocket-Version", "42"),
+ "invalid Sec-WebSocket-Version header: 42",
+ ),
+ (
+ InvalidOrigin("http://bad.origin"),
+ "invalid Origin header: http://bad.origin",
+ ),
+ (
+ InvalidUpgrade("Upgrade"),
+ "missing Upgrade header",
+ ),
+ (
+ InvalidUpgrade("Connection", "websocket"),
+ "invalid Connection header: websocket",
+ ),
+ (
+ InvalidStatus(Response(401, "Unauthorized", Headers())),
+ "server rejected WebSocket connection: HTTP 401",
+ ),
+ (
+ InvalidStatusCode(403, Headers()),
+ "server rejected WebSocket connection: HTTP 403",
+ ),
+ (
+ NegotiationError("unsupported subprotocol: spam"),
+ "unsupported subprotocol: spam",
+ ),
+ (
+ DuplicateParameter("a"),
+ "duplicate parameter: a",
+ ),
+ (
+ InvalidParameterName("|"),
+ "invalid parameter name: |",
+ ),
+ (
+ InvalidParameterValue("a", None),
+ "missing value for parameter a",
+ ),
+ (
+ InvalidParameterValue("a", ""),
+ "empty value for parameter a",
+ ),
+ (
+ InvalidParameterValue("a", "|"),
+ "invalid value for parameter a: |",
+ ),
+ (
+ AbortHandshake(200, Headers(), b"OK\n"),
+ "HTTP 200, 0 headers, 3 bytes",
+ ),
+ (
+ RedirectHandshake("wss://example.com"),
+ "redirect to wss://example.com",
+ ),
+ (
+ InvalidState("WebSocket connection isn't established yet"),
+ "WebSocket connection isn't established yet",
+ ),
+ (
+ InvalidURI("|", "not at all!"),
+ "| isn't a valid URI: not at all!",
+ ),
+ (
+ PayloadTooBig("payload length exceeds limit: 2 > 1 bytes"),
+ "payload length exceeds limit: 2 > 1 bytes",
+ ),
+ (
+ ProtocolError("invalid opcode: 7"),
+ "invalid opcode: 7",
+ ),
+ ]:
+ with self.subTest(exception=exception):
+ self.assertEqual(str(exception), exception_str)
+
+ def test_connection_closed_attributes_backwards_compatibility(self):
+ exception = ConnectionClosed(Close(CloseCode.NORMAL_CLOSURE, "OK"), None, None)
+ self.assertEqual(exception.code, CloseCode.NORMAL_CLOSURE)
+ self.assertEqual(exception.reason, "OK")
+
+ def test_connection_closed_attributes_backwards_compatibility_defaults(self):
+ exception = ConnectionClosed(None, None, None)
+ self.assertEqual(exception.code, CloseCode.ABNORMAL_CLOSURE)
+ self.assertEqual(exception.reason, "")
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_exports.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_exports.py
new file mode 100644
index 0000000000..67a1a6f994
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_exports.py
@@ -0,0 +1,30 @@
+import unittest
+
+import websockets
+import websockets.auth
+import websockets.client
+import websockets.datastructures
+import websockets.exceptions
+import websockets.legacy.protocol
+import websockets.server
+import websockets.typing
+import websockets.uri
+
+
+combined_exports = (
+ websockets.auth.__all__
+ + websockets.client.__all__
+ + websockets.datastructures.__all__
+ + websockets.exceptions.__all__
+ + websockets.legacy.protocol.__all__
+ + websockets.server.__all__
+ + websockets.typing.__all__
+)
+
+
+class ExportsTests(unittest.TestCase):
+ def test_top_level_module_reexports_all_submodule_exports(self):
+ self.assertEqual(set(combined_exports), set(websockets.__all__))
+
+ def test_submodule_exports_are_globally_unique(self):
+ self.assertEqual(len(set(combined_exports)), len(combined_exports))
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_frames.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_frames.py
new file mode 100644
index 0000000000..e323b3b57c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_frames.py
@@ -0,0 +1,495 @@
+import codecs
+import dataclasses
+import unittest
+import unittest.mock
+
+from websockets.exceptions import PayloadTooBig, ProtocolError
+from websockets.frames import *
+from websockets.frames import CloseCode
+from websockets.streams import StreamReader
+
+from .utils import GeneratorTestCase
+
+
+class FramesTestCase(GeneratorTestCase):
+ def enforce_mask(self, mask):
+ return unittest.mock.patch("secrets.token_bytes", return_value=mask)
+
+ def parse(self, data, mask, max_size=None, extensions=None):
+ """
+ Parse a frame from a bytestring.
+
+ """
+ reader = StreamReader()
+ reader.feed_data(data)
+ reader.feed_eof()
+ parser = Frame.parse(
+ reader.read_exact, mask=mask, max_size=max_size, extensions=extensions
+ )
+ return self.assertGeneratorReturns(parser)
+
+ def assertFrameData(self, frame, data, mask, extensions=None):
+ """
+ Serializing frame yields data. Parsing data yields frame.
+
+ """
+ # Compare frames first, because test failures are easier to read,
+ # especially when mask = True.
+ parsed = self.parse(data, mask=mask, extensions=extensions)
+ self.assertEqual(parsed, frame)
+
+ # Make masking deterministic by reusing the same "random" mask.
+ # This has an effect only when mask is True.
+ mask_bytes = data[2:6] if mask else b""
+ with self.enforce_mask(mask_bytes):
+ serialized = frame.serialize(mask=mask, extensions=extensions)
+ self.assertEqual(serialized, data)
+
+
+class FrameTests(FramesTestCase):
+ def test_text_unmasked(self):
+ self.assertFrameData(
+ Frame(OP_TEXT, b"Spam"),
+ b"\x81\x04Spam",
+ mask=False,
+ )
+
+ def test_text_masked(self):
+ self.assertFrameData(
+ Frame(OP_TEXT, b"Spam"),
+ b"\x81\x84\x5b\xfb\xe1\xa8\x08\x8b\x80\xc5",
+ mask=True,
+ )
+
+ def test_binary_unmasked(self):
+ self.assertFrameData(
+ Frame(OP_BINARY, b"Eggs"),
+ b"\x82\x04Eggs",
+ mask=False,
+ )
+
+ def test_binary_masked(self):
+ self.assertFrameData(
+ Frame(OP_BINARY, b"Eggs"),
+ b"\x82\x84\x53\xcd\xe2\x89\x16\xaa\x85\xfa",
+ mask=True,
+ )
+
+ def test_non_ascii_text_unmasked(self):
+ self.assertFrameData(
+ Frame(OP_TEXT, "café".encode("utf-8")),
+ b"\x81\x05caf\xc3\xa9",
+ mask=False,
+ )
+
+ def test_non_ascii_text_masked(self):
+ self.assertFrameData(
+ Frame(OP_TEXT, "café".encode("utf-8")),
+ b"\x81\x85\x64\xbe\xee\x7e\x07\xdf\x88\xbd\xcd",
+ mask=True,
+ )
+
+ def test_close(self):
+ self.assertFrameData(
+ Frame(OP_CLOSE, b""),
+ b"\x88\x00",
+ mask=False,
+ )
+
+ def test_ping(self):
+ self.assertFrameData(
+ Frame(OP_PING, b"ping"),
+ b"\x89\x04ping",
+ mask=False,
+ )
+
+ def test_pong(self):
+ self.assertFrameData(
+ Frame(OP_PONG, b"pong"),
+ b"\x8a\x04pong",
+ mask=False,
+ )
+
+ def test_long(self):
+ self.assertFrameData(
+ Frame(OP_BINARY, 126 * b"a"),
+ b"\x82\x7e\x00\x7e" + 126 * b"a",
+ mask=False,
+ )
+
+ def test_very_long(self):
+ self.assertFrameData(
+ Frame(OP_BINARY, 65536 * b"a"),
+ b"\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x00" + 65536 * b"a",
+ mask=False,
+ )
+
+ def test_payload_too_big(self):
+ with self.assertRaises(PayloadTooBig):
+ self.parse(b"\x82\x7e\x04\x01" + 1025 * b"a", mask=False, max_size=1024)
+
+ def test_bad_reserved_bits(self):
+ for data in [b"\xc0\x00", b"\xa0\x00", b"\x90\x00"]:
+ with self.subTest(data=data):
+ with self.assertRaises(ProtocolError):
+ self.parse(data, mask=False)
+
+ def test_good_opcode(self):
+ for opcode in list(range(0x00, 0x03)) + list(range(0x08, 0x0B)):
+ data = bytes([0x80 | opcode, 0])
+ with self.subTest(data=data):
+ self.parse(data, mask=False) # does not raise an exception
+
+ def test_bad_opcode(self):
+ for opcode in list(range(0x03, 0x08)) + list(range(0x0B, 0x10)):
+ data = bytes([0x80 | opcode, 0])
+ with self.subTest(data=data):
+ with self.assertRaises(ProtocolError):
+ self.parse(data, mask=False)
+
+ def test_mask_flag(self):
+ # Mask flag correctly set.
+ self.parse(b"\x80\x80\x00\x00\x00\x00", mask=True)
+ # Mask flag incorrectly unset.
+ with self.assertRaises(ProtocolError):
+ self.parse(b"\x80\x80\x00\x00\x00\x00", mask=False)
+ # Mask flag correctly unset.
+ self.parse(b"\x80\x00", mask=False)
+ # Mask flag incorrectly set.
+ with self.assertRaises(ProtocolError):
+ self.parse(b"\x80\x00", mask=True)
+
+ def test_control_frame_max_length(self):
+ # At maximum allowed length.
+ self.parse(b"\x88\x7e\x00\x7d" + 125 * b"a", mask=False)
+ # Above maximum allowed length.
+ with self.assertRaises(ProtocolError):
+ self.parse(b"\x88\x7e\x00\x7e" + 126 * b"a", mask=False)
+
+ def test_fragmented_control_frame(self):
+ # Fin bit correctly set.
+ self.parse(b"\x88\x00", mask=False)
+ # Fin bit incorrectly unset.
+ with self.assertRaises(ProtocolError):
+ self.parse(b"\x08\x00", mask=False)
+
+ def test_extensions(self):
+ class Rot13:
+ @staticmethod
+ def encode(frame):
+ assert frame.opcode == OP_TEXT
+ text = frame.data.decode()
+ data = codecs.encode(text, "rot13").encode()
+ return dataclasses.replace(frame, data=data)
+
+ # This extensions is symmetrical.
+ @staticmethod
+ def decode(frame, *, max_size=None):
+ return Rot13.encode(frame)
+
+ self.assertFrameData(
+ Frame(OP_TEXT, b"hello"),
+ b"\x81\x05uryyb",
+ mask=False,
+ extensions=[Rot13()],
+ )
+
+
+class StrTests(unittest.TestCase):
+ def test_cont_text(self):
+ self.assertEqual(
+ str(Frame(OP_CONT, b" cr\xc3\xa8me", fin=False)),
+ "CONT ' crème' [text, 7 bytes, continued]",
+ )
+
+ def test_cont_binary(self):
+ self.assertEqual(
+ str(Frame(OP_CONT, b"\xfc\xfd\xfe\xff", fin=False)),
+ "CONT fc fd fe ff [binary, 4 bytes, continued]",
+ )
+
+ def test_cont_binary_from_memoryview(self):
+ self.assertEqual(
+ str(Frame(OP_CONT, memoryview(b"\xfc\xfd\xfe\xff"), fin=False)),
+ "CONT fc fd fe ff [binary, 4 bytes, continued]",
+ )
+
+ def test_cont_final_text(self):
+ self.assertEqual(
+ str(Frame(OP_CONT, b" cr\xc3\xa8me")),
+ "CONT ' crème' [text, 7 bytes]",
+ )
+
+ def test_cont_final_binary(self):
+ self.assertEqual(
+ str(Frame(OP_CONT, b"\xfc\xfd\xfe\xff")),
+ "CONT fc fd fe ff [binary, 4 bytes]",
+ )
+
+ def test_cont_final_binary_from_memoryview(self):
+ self.assertEqual(
+ str(Frame(OP_CONT, memoryview(b"\xfc\xfd\xfe\xff"))),
+ "CONT fc fd fe ff [binary, 4 bytes]",
+ )
+
+ def test_cont_text_truncated(self):
+ self.assertEqual(
+ str(Frame(OP_CONT, b"caf\xc3\xa9 " * 16, fin=False)),
+ "CONT 'café café café café café café café café café ca..."
+ "fé café café café café ' [text, 96 bytes, continued]",
+ )
+
+ def test_cont_binary_truncated(self):
+ self.assertEqual(
+ str(Frame(OP_CONT, bytes(range(256)), fin=False)),
+ "CONT 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ..."
+ " f8 f9 fa fb fc fd fe ff [binary, 256 bytes, continued]",
+ )
+
+ def test_cont_binary_truncated_from_memoryview(self):
+ self.assertEqual(
+ str(Frame(OP_CONT, memoryview(bytes(range(256))), fin=False)),
+ "CONT 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ..."
+ " f8 f9 fa fb fc fd fe ff [binary, 256 bytes, continued]",
+ )
+
+ def test_text(self):
+ self.assertEqual(
+ str(Frame(OP_TEXT, b"caf\xc3\xa9")),
+ "TEXT 'café' [5 bytes]",
+ )
+
+ def test_text_non_final(self):
+ self.assertEqual(
+ str(Frame(OP_TEXT, b"caf\xc3\xa9", fin=False)),
+ "TEXT 'café' [5 bytes, continued]",
+ )
+
+ def test_text_truncated(self):
+ self.assertEqual(
+ str(Frame(OP_TEXT, b"caf\xc3\xa9 " * 16)),
+ "TEXT 'café café café café café café café café café ca..."
+ "fé café café café café ' [96 bytes]",
+ )
+
+ def test_text_with_newline(self):
+ self.assertEqual(
+ str(Frame(OP_TEXT, b"Hello\nworld!")),
+ "TEXT 'Hello\\nworld!' [12 bytes]",
+ )
+
+ def test_binary(self):
+ self.assertEqual(
+ str(Frame(OP_BINARY, b"\x00\x01\x02\x03")),
+ "BINARY 00 01 02 03 [4 bytes]",
+ )
+
+ def test_binary_from_memoryview(self):
+ self.assertEqual(
+ str(Frame(OP_BINARY, memoryview(b"\x00\x01\x02\x03"))),
+ "BINARY 00 01 02 03 [4 bytes]",
+ )
+
+ def test_binary_non_final(self):
+ self.assertEqual(
+ str(Frame(OP_BINARY, b"\x00\x01\x02\x03", fin=False)),
+ "BINARY 00 01 02 03 [4 bytes, continued]",
+ )
+
+ def test_binary_non_final_from_memoryview(self):
+ self.assertEqual(
+ str(Frame(OP_BINARY, memoryview(b"\x00\x01\x02\x03"), fin=False)),
+ "BINARY 00 01 02 03 [4 bytes, continued]",
+ )
+
+ def test_binary_truncated(self):
+ self.assertEqual(
+ str(Frame(OP_BINARY, bytes(range(256)))),
+ "BINARY 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ..."
+ " f8 f9 fa fb fc fd fe ff [256 bytes]",
+ )
+
+ def test_binary_truncated_from_memoryview(self):
+ self.assertEqual(
+ str(Frame(OP_BINARY, memoryview(bytes(range(256))))),
+ "BINARY 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ..."
+ " f8 f9 fa fb fc fd fe ff [256 bytes]",
+ )
+
+ def test_close(self):
+ self.assertEqual(
+ str(Frame(OP_CLOSE, b"\x03\xe8")),
+ "CLOSE 1000 (OK) [2 bytes]",
+ )
+
+ def test_close_reason(self):
+ self.assertEqual(
+ str(Frame(OP_CLOSE, b"\x03\xe9Bye!")),
+ "CLOSE 1001 (going away) Bye! [6 bytes]",
+ )
+
+ def test_ping(self):
+ self.assertEqual(
+ str(Frame(OP_PING, b"")),
+ "PING '' [0 bytes]",
+ )
+
+ def test_ping_text(self):
+ self.assertEqual(
+ str(Frame(OP_PING, b"ping")),
+ "PING 'ping' [text, 4 bytes]",
+ )
+
+ def test_ping_text_with_newline(self):
+ self.assertEqual(
+ str(Frame(OP_PING, b"ping\n")),
+ "PING 'ping\\n' [text, 5 bytes]",
+ )
+
+ def test_ping_binary(self):
+ self.assertEqual(
+ str(Frame(OP_PING, b"\xff\x00\xff\x00")),
+ "PING ff 00 ff 00 [binary, 4 bytes]",
+ )
+
+ def test_pong(self):
+ self.assertEqual(
+ str(Frame(OP_PONG, b"")),
+ "PONG '' [0 bytes]",
+ )
+
+ def test_pong_text(self):
+ self.assertEqual(
+ str(Frame(OP_PONG, b"pong")),
+ "PONG 'pong' [text, 4 bytes]",
+ )
+
+ def test_pong_text_with_newline(self):
+ self.assertEqual(
+ str(Frame(OP_PONG, b"pong\n")),
+ "PONG 'pong\\n' [text, 5 bytes]",
+ )
+
+ def test_pong_binary(self):
+ self.assertEqual(
+ str(Frame(OP_PONG, b"\xff\x00\xff\x00")),
+ "PONG ff 00 ff 00 [binary, 4 bytes]",
+ )
+
+
+class PrepareDataTests(unittest.TestCase):
+ def test_prepare_data_str(self):
+ self.assertEqual(
+ prepare_data("café"),
+ (OP_TEXT, b"caf\xc3\xa9"),
+ )
+
+ def test_prepare_data_bytes(self):
+ self.assertEqual(
+ prepare_data(b"tea"),
+ (OP_BINARY, b"tea"),
+ )
+
+ def test_prepare_data_bytearray(self):
+ self.assertEqual(
+ prepare_data(bytearray(b"tea")),
+ (OP_BINARY, bytearray(b"tea")),
+ )
+
+ def test_prepare_data_memoryview(self):
+ self.assertEqual(
+ prepare_data(memoryview(b"tea")),
+ (OP_BINARY, memoryview(b"tea")),
+ )
+
+ def test_prepare_data_list(self):
+ with self.assertRaises(TypeError):
+ prepare_data([])
+
+ def test_prepare_data_none(self):
+ with self.assertRaises(TypeError):
+ prepare_data(None)
+
+
+class PrepareCtrlTests(unittest.TestCase):
+ def test_prepare_ctrl_str(self):
+ self.assertEqual(prepare_ctrl("café"), b"caf\xc3\xa9")
+
+ def test_prepare_ctrl_bytes(self):
+ self.assertEqual(prepare_ctrl(b"tea"), b"tea")
+
+ def test_prepare_ctrl_bytearray(self):
+ self.assertEqual(prepare_ctrl(bytearray(b"tea")), b"tea")
+
+ def test_prepare_ctrl_memoryview(self):
+ self.assertEqual(prepare_ctrl(memoryview(b"tea")), b"tea")
+
+ def test_prepare_ctrl_list(self):
+ with self.assertRaises(TypeError):
+ prepare_ctrl([])
+
+ def test_prepare_ctrl_none(self):
+ with self.assertRaises(TypeError):
+ prepare_ctrl(None)
+
+
+class CloseTests(unittest.TestCase):
+ def assertCloseData(self, close, data):
+ """
+ Serializing close yields data. Parsing data yields close.
+
+ """
+ serialized = close.serialize()
+ self.assertEqual(serialized, data)
+ parsed = Close.parse(data)
+ self.assertEqual(parsed, close)
+
+ def test_str(self):
+ self.assertEqual(
+ str(Close(CloseCode.NORMAL_CLOSURE, "")),
+ "1000 (OK)",
+ )
+ self.assertEqual(
+ str(Close(CloseCode.GOING_AWAY, "Bye!")),
+ "1001 (going away) Bye!",
+ )
+ self.assertEqual(
+ str(Close(3000, "")),
+ "3000 (registered)",
+ )
+ self.assertEqual(
+ str(Close(4000, "")),
+ "4000 (private use)",
+ )
+ self.assertEqual(
+ str(Close(5000, "")),
+ "5000 (unknown)",
+ )
+
+ def test_parse_and_serialize(self):
+ self.assertCloseData(
+ Close(CloseCode.NORMAL_CLOSURE, "OK"),
+ b"\x03\xe8OK",
+ )
+ self.assertCloseData(
+ Close(CloseCode.GOING_AWAY, ""),
+ b"\x03\xe9",
+ )
+
+ def test_parse_empty(self):
+ self.assertEqual(
+ Close.parse(b""),
+ Close(CloseCode.NO_STATUS_RCVD, ""),
+ )
+
+ def test_parse_errors(self):
+ with self.assertRaises(ProtocolError):
+ Close.parse(b"\x03")
+ with self.assertRaises(ProtocolError):
+ Close.parse(b"\x03\xe7")
+ with self.assertRaises(UnicodeDecodeError):
+ Close.parse(b"\x03\xe8\xff\xff")
+
+ def test_serialize_errors(self):
+ with self.assertRaises(ProtocolError):
+ Close(999, "").serialize()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_headers.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_headers.py
new file mode 100644
index 0000000000..4ebd8b90cf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_headers.py
@@ -0,0 +1,222 @@
+import unittest
+
+from websockets.exceptions import InvalidHeaderFormat, InvalidHeaderValue
+from websockets.headers import *
+
+
+class HeadersTests(unittest.TestCase):
+ def test_build_host(self):
+ for (host, port, secure), result in [
+ (("localhost", 80, False), "localhost"),
+ (("localhost", 8000, False), "localhost:8000"),
+ (("localhost", 443, True), "localhost"),
+ (("localhost", 8443, True), "localhost:8443"),
+ (("example.com", 80, False), "example.com"),
+ (("example.com", 8000, False), "example.com:8000"),
+ (("example.com", 443, True), "example.com"),
+ (("example.com", 8443, True), "example.com:8443"),
+ (("127.0.0.1", 80, False), "127.0.0.1"),
+ (("127.0.0.1", 8000, False), "127.0.0.1:8000"),
+ (("127.0.0.1", 443, True), "127.0.0.1"),
+ (("127.0.0.1", 8443, True), "127.0.0.1:8443"),
+ (("::1", 80, False), "[::1]"),
+ (("::1", 8000, False), "[::1]:8000"),
+ (("::1", 443, True), "[::1]"),
+ (("::1", 8443, True), "[::1]:8443"),
+ ]:
+ with self.subTest(host=host, port=port, secure=secure):
+ self.assertEqual(build_host(host, port, secure), result)
+
+ def test_parse_connection(self):
+ for header, parsed in [
+ # Realistic use cases
+ ("Upgrade", ["Upgrade"]), # Safari, Chrome
+ ("keep-alive, Upgrade", ["keep-alive", "Upgrade"]), # Firefox
+ # Pathological example
+ (",,\t, , ,Upgrade ,,", ["Upgrade"]),
+ ]:
+ with self.subTest(header=header):
+ self.assertEqual(parse_connection(header), parsed)
+
+ def test_parse_connection_invalid_header_format(self):
+ for header in ["???", "keep-alive; Upgrade"]:
+ with self.subTest(header=header):
+ with self.assertRaises(InvalidHeaderFormat):
+ parse_connection(header)
+
+ def test_parse_upgrade(self):
+ for header, parsed in [
+ # Realistic use case
+ ("websocket", ["websocket"]),
+ # Synthetic example
+ ("http/3.0, websocket", ["http/3.0", "websocket"]),
+ # Pathological example
+ (",, WebSocket, \t,,", ["WebSocket"]),
+ ]:
+ with self.subTest(header=header):
+ self.assertEqual(parse_upgrade(header), parsed)
+
+ def test_parse_upgrade_invalid_header_format(self):
+ for header in ["???", "websocket 2", "http/3.0; websocket"]:
+ with self.subTest(header=header):
+ with self.assertRaises(InvalidHeaderFormat):
+ parse_upgrade(header)
+
+ def test_parse_extension(self):
+ for header, parsed in [
+ # Synthetic examples
+ ("foo", [("foo", [])]),
+ ("foo, bar", [("foo", []), ("bar", [])]),
+ (
+ 'foo; name; token=token; quoted-string="quoted-string", '
+ "bar; quux; quuux",
+ [
+ (
+ "foo",
+ [
+ ("name", None),
+ ("token", "token"),
+ ("quoted-string", "quoted-string"),
+ ],
+ ),
+ ("bar", [("quux", None), ("quuux", None)]),
+ ],
+ ),
+ # Pathological example
+ (
+ ",\t, , ,foo ;bar = 42,, baz,,",
+ [("foo", [("bar", "42")]), ("baz", [])],
+ ),
+ # Realistic use cases for permessage-deflate
+ ("permessage-deflate", [("permessage-deflate", [])]),
+ (
+ "permessage-deflate; client_max_window_bits",
+ [("permessage-deflate", [("client_max_window_bits", None)])],
+ ),
+ (
+ "permessage-deflate; server_max_window_bits=10",
+ [("permessage-deflate", [("server_max_window_bits", "10")])],
+ ),
+ ]:
+ with self.subTest(header=header):
+ self.assertEqual(parse_extension(header), parsed)
+ # Also ensure that build_extension round-trips cleanly.
+ unparsed = build_extension(parsed)
+ self.assertEqual(parse_extension(unparsed), parsed)
+
+ def test_parse_extension_invalid_header_format(self):
+ for header in [
+ # Truncated examples
+ "",
+ ",\t,",
+ "foo;",
+ "foo; bar;",
+ "foo; bar=",
+ 'foo; bar="baz',
+ # Wrong delimiter
+ "foo, bar, baz=quux; quuux",
+ # Value in quoted string parameter that isn't a token
+ 'foo; bar=" "',
+ ]:
+ with self.subTest(header=header):
+ with self.assertRaises(InvalidHeaderFormat):
+ parse_extension(header)
+
+ def test_parse_subprotocol(self):
+ for header, parsed in [
+ # Synthetic examples
+ ("foo", ["foo"]),
+ ("foo, bar", ["foo", "bar"]),
+ # Pathological example
+ (",\t, , ,foo ,, bar,baz,,", ["foo", "bar", "baz"]),
+ ]:
+ with self.subTest(header=header):
+ self.assertEqual(parse_subprotocol(header), parsed)
+ # Also ensure that build_subprotocol round-trips cleanly.
+ unparsed = build_subprotocol(parsed)
+ self.assertEqual(parse_subprotocol(unparsed), parsed)
+
+ def test_parse_subprotocol_invalid_header(self):
+ for header in [
+ # Truncated examples
+ "",
+ ",\t,",
+ # Wrong delimiter
+ "foo; bar",
+ ]:
+ with self.subTest(header=header):
+ with self.assertRaises(InvalidHeaderFormat):
+ parse_subprotocol(header)
+
+ def test_validate_subprotocols(self):
+ for subprotocols in [[], ["sip"], ["v1.usp"], ["sip", "v1.usp"]]:
+ with self.subTest(subprotocols=subprotocols):
+ validate_subprotocols(subprotocols)
+
+ def test_validate_subprotocols_invalid(self):
+ for subprotocols, exception in [
+ ({"sip": None}, TypeError),
+ ("sip", TypeError),
+ ([""], ValueError),
+ ]:
+ with self.subTest(subprotocols=subprotocols):
+ with self.assertRaises(exception):
+ validate_subprotocols(subprotocols)
+
+ def test_build_www_authenticate_basic(self):
+ # Test vector from RFC 7617
+ self.assertEqual(
+ build_www_authenticate_basic("foo"), 'Basic realm="foo", charset="UTF-8"'
+ )
+
+ def test_build_www_authenticate_basic_invalid_realm(self):
+ # Realm contains a control character forbidden in quoted-string encoding
+ with self.assertRaises(ValueError):
+ build_www_authenticate_basic("\u0007")
+
+ def test_build_authorization_basic(self):
+ # Test vector from RFC 7617
+ self.assertEqual(
+ build_authorization_basic("Aladdin", "open sesame"),
+ "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
+ )
+
+ def test_build_authorization_basic_utf8(self):
+ # Test vector from RFC 7617
+ self.assertEqual(
+ build_authorization_basic("test", "123£"), "Basic dGVzdDoxMjPCow=="
+ )
+
+ def test_parse_authorization_basic(self):
+ for header, parsed in [
+ ("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", ("Aladdin", "open sesame")),
+ # Password contains non-ASCII character
+ ("Basic dGVzdDoxMjPCow==", ("test", "123£")),
+ # Password contains a colon
+ ("Basic YWxhZGRpbjpvcGVuOnNlc2FtZQ==", ("aladdin", "open:sesame")),
+ # Scheme name must be case insensitive
+ ("basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", ("Aladdin", "open sesame")),
+ ]:
+ with self.subTest(header=header):
+ self.assertEqual(parse_authorization_basic(header), parsed)
+
+ def test_parse_authorization_basic_invalid_header_format(self):
+ for header in [
+ "// Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
+ "Basic\tQWxhZGRpbjpvcGVuIHNlc2FtZQ==",
+ "Basic ****************************",
+ "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== //",
+ ]:
+ with self.subTest(header=header):
+ with self.assertRaises(InvalidHeaderFormat):
+ parse_authorization_basic(header)
+
+ def test_parse_authorization_basic_invalid_header_value(self):
+ for header in [
+ "Digest ...",
+ "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ",
+ "Basic QWxhZGNlc2FtZQ==",
+ ]:
+ with self.subTest(header=header):
+ with self.assertRaises(InvalidHeaderValue):
+ parse_authorization_basic(header)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_http.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_http.py
new file mode 100644
index 0000000000..036bc14102
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_http.py
@@ -0,0 +1 @@
+from websockets.http import *
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_http11.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_http11.py
new file mode 100644
index 0000000000..d2e5e04627
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_http11.py
@@ -0,0 +1,344 @@
+from websockets.datastructures import Headers
+from websockets.exceptions import SecurityError
+from websockets.http11 import *
+from websockets.http11 import parse_headers
+from websockets.streams import StreamReader
+
+from .utils import GeneratorTestCase
+
+
+class RequestTests(GeneratorTestCase):
+ def setUp(self):
+ super().setUp()
+ self.reader = StreamReader()
+
+ def parse(self):
+ return Request.parse(self.reader.read_line)
+
+ def test_parse(self):
+ # Example from the protocol overview in RFC 6455
+ self.reader.feed_data(
+ b"GET /chat HTTP/1.1\r\n"
+ b"Host: server.example.com\r\n"
+ b"Upgrade: websocket\r\n"
+ b"Connection: Upgrade\r\n"
+ b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ b"Origin: http://example.com\r\n"
+ b"Sec-WebSocket-Protocol: chat, superchat\r\n"
+ b"Sec-WebSocket-Version: 13\r\n"
+ b"\r\n"
+ )
+ request = self.assertGeneratorReturns(self.parse())
+ self.assertEqual(request.path, "/chat")
+ self.assertEqual(request.headers["Upgrade"], "websocket")
+
+ def test_parse_empty(self):
+ self.reader.feed_eof()
+ with self.assertRaises(EOFError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "connection closed while reading HTTP request line",
+ )
+
+ def test_parse_invalid_request_line(self):
+ self.reader.feed_data(b"GET /\r\n\r\n")
+ with self.assertRaises(ValueError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "invalid HTTP request line: GET /",
+ )
+
+ def test_parse_unsupported_method(self):
+ self.reader.feed_data(b"OPTIONS * HTTP/1.1\r\n\r\n")
+ with self.assertRaises(ValueError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "unsupported HTTP method: OPTIONS",
+ )
+
+ def test_parse_unsupported_version(self):
+ self.reader.feed_data(b"GET /chat HTTP/1.0\r\n\r\n")
+ with self.assertRaises(ValueError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "unsupported HTTP version: HTTP/1.0",
+ )
+
+ def test_parse_invalid_header(self):
+ self.reader.feed_data(b"GET /chat HTTP/1.1\r\nOops\r\n")
+ with self.assertRaises(ValueError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "invalid HTTP header line: Oops",
+ )
+
+ def test_parse_body(self):
+ self.reader.feed_data(b"GET / HTTP/1.1\r\nContent-Length: 3\r\n\r\nYo\n")
+ with self.assertRaises(ValueError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "unsupported request body",
+ )
+
+ def test_parse_body_with_transfer_encoding(self):
+ self.reader.feed_data(b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n")
+ with self.assertRaises(NotImplementedError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "transfer codings aren't supported",
+ )
+
+ def test_serialize(self):
+ # Example from the protocol overview in RFC 6455
+ request = Request(
+ "/chat",
+ Headers(
+ [
+ ("Host", "server.example.com"),
+ ("Upgrade", "websocket"),
+ ("Connection", "Upgrade"),
+ ("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ=="),
+ ("Origin", "http://example.com"),
+ ("Sec-WebSocket-Protocol", "chat, superchat"),
+ ("Sec-WebSocket-Version", "13"),
+ ]
+ ),
+ )
+ self.assertEqual(
+ request.serialize(),
+ b"GET /chat HTTP/1.1\r\n"
+ b"Host: server.example.com\r\n"
+ b"Upgrade: websocket\r\n"
+ b"Connection: Upgrade\r\n"
+ b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ b"Origin: http://example.com\r\n"
+ b"Sec-WebSocket-Protocol: chat, superchat\r\n"
+ b"Sec-WebSocket-Version: 13\r\n"
+ b"\r\n",
+ )
+
+
+class ResponseTests(GeneratorTestCase):
+ def setUp(self):
+ super().setUp()
+ self.reader = StreamReader()
+
+ def parse(self):
+ return Response.parse(
+ self.reader.read_line,
+ self.reader.read_exact,
+ self.reader.read_to_eof,
+ )
+
+ def test_parse(self):
+ # Example from the protocol overview in RFC 6455
+ self.reader.feed_data(
+ b"HTTP/1.1 101 Switching Protocols\r\n"
+ b"Upgrade: websocket\r\n"
+ b"Connection: Upgrade\r\n"
+ b"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"
+ b"Sec-WebSocket-Protocol: chat\r\n"
+ b"\r\n"
+ )
+ response = self.assertGeneratorReturns(self.parse())
+ self.assertEqual(response.status_code, 101)
+ self.assertEqual(response.reason_phrase, "Switching Protocols")
+ self.assertEqual(response.headers["Upgrade"], "websocket")
+ self.assertIsNone(response.body)
+
+ def test_parse_empty(self):
+ self.reader.feed_eof()
+ with self.assertRaises(EOFError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "connection closed while reading HTTP status line",
+ )
+
+ def test_parse_invalid_status_line(self):
+ self.reader.feed_data(b"Hello!\r\n")
+ with self.assertRaises(ValueError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "invalid HTTP status line: Hello!",
+ )
+
+ def test_parse_unsupported_version(self):
+ self.reader.feed_data(b"HTTP/1.0 400 Bad Request\r\n\r\n")
+ with self.assertRaises(ValueError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "unsupported HTTP version: HTTP/1.0",
+ )
+
+ def test_parse_invalid_status(self):
+ self.reader.feed_data(b"HTTP/1.1 OMG WTF\r\n\r\n")
+ with self.assertRaises(ValueError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "invalid HTTP status code: OMG",
+ )
+
+ def test_parse_unsupported_status(self):
+ self.reader.feed_data(b"HTTP/1.1 007 My name is Bond\r\n\r\n")
+ with self.assertRaises(ValueError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "unsupported HTTP status code: 007",
+ )
+
+ def test_parse_invalid_reason(self):
+ self.reader.feed_data(b"HTTP/1.1 200 \x7f\r\n\r\n")
+ with self.assertRaises(ValueError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "invalid HTTP reason phrase: \x7f",
+ )
+
+ def test_parse_invalid_header(self):
+ self.reader.feed_data(b"HTTP/1.1 500 Internal Server Error\r\nOops\r\n")
+ with self.assertRaises(ValueError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "invalid HTTP header line: Oops",
+ )
+
+ def test_parse_body_with_content_length(self):
+ self.reader.feed_data(
+ b"HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello world!\n"
+ )
+ response = self.assertGeneratorReturns(self.parse())
+ self.assertEqual(response.body, b"Hello world!\n")
+
+ def test_parse_body_without_content_length(self):
+ self.reader.feed_data(b"HTTP/1.1 200 OK\r\n\r\nHello world!\n")
+ gen = self.parse()
+ self.assertGeneratorRunning(gen)
+ self.reader.feed_eof()
+ response = self.assertGeneratorReturns(gen)
+ self.assertEqual(response.body, b"Hello world!\n")
+
+ def test_parse_body_with_content_length_too_long(self):
+ self.reader.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 1048577\r\n\r\n")
+ with self.assertRaises(SecurityError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "body too large: 1048577 bytes",
+ )
+
+ def test_parse_body_without_content_length_too_long(self):
+ self.reader.feed_data(b"HTTP/1.1 200 OK\r\n\r\n" + b"a" * 1048577)
+ with self.assertRaises(SecurityError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "body too large: over 1048576 bytes",
+ )
+
+ def test_parse_body_with_transfer_encoding(self):
+ self.reader.feed_data(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n")
+ with self.assertRaises(NotImplementedError) as raised:
+ next(self.parse())
+ self.assertEqual(
+ str(raised.exception),
+ "transfer codings aren't supported",
+ )
+
+ def test_parse_body_no_content(self):
+ self.reader.feed_data(b"HTTP/1.1 204 No Content\r\n\r\n")
+ response = self.assertGeneratorReturns(self.parse())
+ self.assertIsNone(response.body)
+
+ def test_parse_body_not_modified(self):
+ self.reader.feed_data(b"HTTP/1.1 304 Not Modified\r\n\r\n")
+ response = self.assertGeneratorReturns(self.parse())
+ self.assertIsNone(response.body)
+
+ def test_serialize(self):
+ # Example from the protocol overview in RFC 6455
+ response = Response(
+ 101,
+ "Switching Protocols",
+ Headers(
+ [
+ ("Upgrade", "websocket"),
+ ("Connection", "Upgrade"),
+ ("Sec-WebSocket-Accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="),
+ ("Sec-WebSocket-Protocol", "chat"),
+ ]
+ ),
+ )
+ self.assertEqual(
+ response.serialize(),
+ b"HTTP/1.1 101 Switching Protocols\r\n"
+ b"Upgrade: websocket\r\n"
+ b"Connection: Upgrade\r\n"
+ b"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"
+ b"Sec-WebSocket-Protocol: chat\r\n"
+ b"\r\n",
+ )
+
+ def test_serialize_with_body(self):
+ response = Response(
+ 200,
+ "OK",
+ Headers([("Content-Length", "13"), ("Content-Type", "text/plain")]),
+ b"Hello world!\n",
+ )
+ self.assertEqual(
+ response.serialize(),
+ b"HTTP/1.1 200 OK\r\n"
+ b"Content-Length: 13\r\n"
+ b"Content-Type: text/plain\r\n"
+ b"\r\n"
+ b"Hello world!\n",
+ )
+
+
+class HeadersTests(GeneratorTestCase):
+ def setUp(self):
+ super().setUp()
+ self.reader = StreamReader()
+
+ def parse_headers(self):
+ return parse_headers(self.reader.read_line)
+
+ def test_parse_invalid_name(self):
+ self.reader.feed_data(b"foo bar: baz qux\r\n\r\n")
+ with self.assertRaises(ValueError):
+ next(self.parse_headers())
+
+ def test_parse_invalid_value(self):
+ self.reader.feed_data(b"foo: \x00\x00\x0f\r\n\r\n")
+ with self.assertRaises(ValueError):
+ next(self.parse_headers())
+
+ def test_parse_too_long_value(self):
+ self.reader.feed_data(b"foo: bar\r\n" * 129 + b"\r\n")
+ with self.assertRaises(SecurityError):
+ next(self.parse_headers())
+
+ def test_parse_too_long_line(self):
+ # Header line contains 5 + 8186 + 2 = 8193 bytes.
+ self.reader.feed_data(b"foo: " + b"a" * 8186 + b"\r\n\r\n")
+ with self.assertRaises(SecurityError):
+ next(self.parse_headers())
+
+ def test_parse_invalid_line_ending(self):
+ self.reader.feed_data(b"foo: bar\n\n")
+ with self.assertRaises(EOFError):
+ next(self.parse_headers())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_imports.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_imports.py
new file mode 100644
index 0000000000..b69ed93162
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_imports.py
@@ -0,0 +1,64 @@
+import types
+import unittest
+import warnings
+
+from websockets.imports import *
+
+
+foo = object()
+
+bar = object()
+
+
+class ImportsTests(unittest.TestCase):
+ def setUp(self):
+ self.mod = types.ModuleType("tests.test_imports.test_alias")
+ self.mod.__package__ = self.mod.__name__
+
+ def test_get_alias(self):
+ lazy_import(
+ vars(self.mod),
+ aliases={"foo": "...test_imports"},
+ )
+
+ self.assertEqual(self.mod.foo, foo)
+
+ def test_get_deprecated_alias(self):
+ lazy_import(
+ vars(self.mod),
+ deprecated_aliases={"bar": "...test_imports"},
+ )
+
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ warnings.simplefilter("always")
+ self.assertEqual(self.mod.bar, bar)
+
+ self.assertEqual(len(recorded_warnings), 1)
+ warning = recorded_warnings[0].message
+ self.assertEqual(
+ str(warning), "tests.test_imports.test_alias.bar is deprecated"
+ )
+ self.assertEqual(type(warning), DeprecationWarning)
+
+ def test_dir(self):
+ lazy_import(
+ vars(self.mod),
+ aliases={"foo": "...test_imports"},
+ deprecated_aliases={"bar": "...test_imports"},
+ )
+
+ self.assertEqual(
+ [item for item in dir(self.mod) if not item[:2] == item[-2:] == "__"],
+ ["bar", "foo"],
+ )
+
+ def test_attribute_error(self):
+ lazy_import(vars(self.mod))
+
+ with self.assertRaises(AttributeError) as raised:
+ self.mod.foo
+
+ self.assertEqual(
+ str(raised.exception),
+ "module 'tests.test_imports.test_alias' has no attribute 'foo'",
+ )
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.cnf b/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.cnf
new file mode 100644
index 0000000000..4069e39670
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.cnf
@@ -0,0 +1,27 @@
+[ req ]
+
+default_md = sha256
+encrypt_key = no
+
+prompt = no
+
+distinguished_name = dn
+x509_extensions = ext
+
+[ dn ]
+
+C = "FR"
+L = "Paris"
+O = "Aymeric Augustin"
+CN = "localhost"
+
+[ ext ]
+
+subjectAltName = @san
+
+[ san ]
+
+DNS.1 = localhost
+DNS.2 = overridden
+IP.3 = 127.0.0.1
+IP.4 = ::1
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.pem b/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.pem
new file mode 100644
index 0000000000..8df63ec8f4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.pem
@@ -0,0 +1,48 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDYOOQyq8yYtn5x
+K3yRborFxTFse16JIVb4x/ZhZgGm49eARCi09fmczQxJdQpHz81Ij6z0xi7AUYH7
+9wS8T0Lh3uGFDDS1GzITUVPIqSUi0xim2T6XPzXFVQYI1D/OjUxlHm+3/up+WwbL
+sBgBO/lDmzoa3ZN7kt9HQoGc/14oQz1Qsv1QTDQs69r+o7mmBJr/hf/g7S0Csyy3
+iC6aaq+yCUyzDbjXceTI7WJqbTGNnK0/DjdFD/SJS/uSDNEg0AH53eqcCSjm+Ei/
+UF8qR5Pu4sSsNwToOW2MVgjtHFazc+kG3rzD6+3Dp+t6x6uI/npyuudOMCmOtd6z
+kX0UPQaNAgMBAAECggEAS4eMBztGC+5rusKTEAZKSY15l0h9HG/d/qdzJFDKsO6T
+/8VPZu8pk6F48kwFHFK1hexSYWq9OAcA3fBK4jDZzybZJm2+F6l5U5AsMUMMqt6M
+lPP8Tj8RXG433muuIkvvbL82DVLpvNu1Qv+vUvcNOpWFtY7DDv6eKjlMJ3h4/pzh
+89MNt26VMCYOlq1NSjuZBzFohL2u9nsFehlOpcVsqNfNfcYCq9+5yoH8fWJP90Op
+hqhvqUoGLN7DRKV1f+AWHSA4nmGgvVviV5PQgMhtk5exlN7kG+rDc3LbzhefS1Sp
+Tat1qIgm8fK2n+Q/obQPjHOGOGuvE5cIF7E275ZKgQKBgQDt87BqALKWnbkbQnb7
+GS1h6LRcKyZhFbxnO2qbviBWSo15LEF8jPGV33Dj+T56hqufa/rUkbZiUbIR9yOX
+dnOwpAVTo+ObAwZfGfHvrnufiIbHFqJBumaYLqjRZ7AC0QtS3G+kjS9dbllrr7ok
+fO4JdfKRXzBJKrkQdCn8hR22rQKBgQDon0b49Dxs1EfdSDbDode2TSwE83fI3vmR
+SKUkNY8ma6CRbomVRWijhBM458wJeuhpjPZOvjNMsnDzGwrtdAp2VfFlMIDnA8ZC
+fEWIAAH2QYKXKGmkoXOcWB2QbvbI154zCm6zFGtzvRKOCGmTXuhFajO8VPwOyJVt
+aSJA3bLrYQKBgQDJM2/tAfAAKRdW9GlUwqI8Ep9G+/l0yANJqtTnIemH7XwYhJJO
+9YJlPszfB2aMBgliQNSUHy1/jyKpzDYdITyLlPUoFwEilnkxuud2yiuf5rpH51yF
+hU6wyWtXvXv3tbkEdH42PmdZcjBMPQeBSN2hxEi6ISncBDL9tau26PwJ9QKBgQCs
+cNYl2reoXTzgtpWSNDk6NL769JjJWTFcF6QD0YhKjOI8rNpkw00sWc3+EybXqDr9
+c7dq6+gPZQAB1vwkxi6zRkZqIqiLl+qygnjwtkC+EhYCg7y8g8q2DUPtO7TJcb0e
+TQ9+xRZad8B3dZj93A8G1hF//OfU9bB/qL3xo+bsQQKBgC/9YJvgLIWA/UziLcB2
+29Ai0nbPkN5df7z4PifUHHSlbQJHKak8UKbMP+8S064Ul0F7g8UCjZMk2LzSbaNY
+XU5+2j0sIOnGUFoSlvcpdowzYrD2LN5PkKBot7AOq/v7HlcOoR8J8RGWAMpCrHsI
+a/u/dlZs+/K16RcavQwx8rag
+-----END PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIDWTCCAkGgAwIBAgIJAOL9UKiOOxupMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV
+BAYTAkZSMQ4wDAYDVQQHDAVQYXJpczEZMBcGA1UECgwQQXltZXJpYyBBdWd1c3Rp
+bjESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIyMTAxNTE5Mjg0MVoYDzIwNjQxMDE0
+MTkyODQxWjBMMQswCQYDVQQGEwJGUjEOMAwGA1UEBwwFUGFyaXMxGTAXBgNVBAoM
+EEF5bWVyaWMgQXVndXN0aW4xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBANg45DKrzJi2fnErfJFuisXFMWx7XokhVvjH
+9mFmAabj14BEKLT1+ZzNDEl1CkfPzUiPrPTGLsBRgfv3BLxPQuHe4YUMNLUbMhNR
+U8ipJSLTGKbZPpc/NcVVBgjUP86NTGUeb7f+6n5bBsuwGAE7+UObOhrdk3uS30dC
+gZz/XihDPVCy/VBMNCzr2v6juaYEmv+F/+DtLQKzLLeILppqr7IJTLMNuNdx5Mjt
+YmptMY2crT8ON0UP9IlL+5IM0SDQAfnd6pwJKOb4SL9QXypHk+7ixKw3BOg5bYxW
+CO0cVrNz6QbevMPr7cOn63rHq4j+enK6504wKY613rORfRQ9Bo0CAwEAAaM8MDow
+OAYDVR0RBDEwL4IJbG9jYWxob3N0ggpvdmVycmlkZGVuhwR/AAABhxAAAAAAAAAA
+AAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQBPNDGDdl4wsCRlDuyCHBC8o+vW
+Vb14thUw9Z6UrlsQRXLONxHOXbNAj1sYQACNwIWuNz36HXu5m8Xw/ID/bOhnIg+b
+Y6l/JU/kZQYB7SV1aR3ZdbCK0gjfkE0POBHuKOjUFIOPBCtJ4tIBUX94zlgJrR9v
+2rqJC3TIYrR7pVQumHZsI5GZEMpM5NxfreWwxcgltgxmGdm7elcizHfz7k5+szwh
+4eZ/rxK9bw1q8BIvVBWelRvUR55mIrCjzfZp5ZObSYQTZlW7PzXBe5Jk+1w31YHM
+RSBA2EpPhYlGNqPidi7bg7rnQcsc6+hE0OqzTL/hWxPm9Vbp9dj3HFTik1wa
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_protocol.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_protocol.py
new file mode 100644
index 0000000000..a64172b539
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_protocol.py
@@ -0,0 +1,1790 @@
+import logging
+import unittest.mock
+
+from websockets.exceptions import (
+ ConnectionClosedError,
+ ConnectionClosedOK,
+ InvalidState,
+ PayloadTooBig,
+ ProtocolError,
+)
+from websockets.frames import (
+ OP_BINARY,
+ OP_CLOSE,
+ OP_CONT,
+ OP_PING,
+ OP_PONG,
+ OP_TEXT,
+ Close,
+ CloseCode,
+ Frame,
+)
+from websockets.protocol import *
+from websockets.protocol import CLIENT, CLOSED, CLOSING, SERVER
+
+from .extensions.utils import Rsv2Extension
+from .test_frames import FramesTestCase
+
+
+class ProtocolTestCase(FramesTestCase):
+ def assertFrameSent(self, connection, frame, eof=False):
+ """
+ Outgoing data for ``connection`` contains the given frame.
+
+ ``frame`` may be ``None`` if no frame is expected.
+
+ When ``eof`` is ``True``, the end of the stream is also expected.
+
+ """
+ frames_sent = [
+ None
+ if write is SEND_EOF
+ else self.parse(
+ write,
+ mask=connection.side is CLIENT,
+ extensions=connection.extensions,
+ )
+ for write in connection.data_to_send()
+ ]
+ frames_expected = [] if frame is None else [frame]
+ if eof:
+ frames_expected += [None]
+ self.assertEqual(frames_sent, frames_expected)
+
+ def assertFrameReceived(self, connection, frame):
+ """
+ Incoming data for ``connection`` contains the given frame.
+
+ ``frame`` may be ``None`` if no frame is expected.
+
+ """
+ frames_received = connection.events_received()
+ frames_expected = [] if frame is None else [frame]
+ self.assertEqual(frames_received, frames_expected)
+
+ def assertConnectionClosing(self, connection, code=None, reason=""):
+ """
+ Incoming data caused the "Start the WebSocket Closing Handshake" process.
+
+ """
+ close_frame = Frame(
+ OP_CLOSE,
+ b"" if code is None else Close(code, reason).serialize(),
+ )
+ # A close frame was received.
+ self.assertFrameReceived(connection, close_frame)
+ # A close frame and possibly the end of stream were sent.
+ self.assertFrameSent(connection, close_frame, eof=connection.side is SERVER)
+
+ def assertConnectionFailing(self, connection, code=None, reason=""):
+ """
+ Incoming data caused the "Fail the WebSocket Connection" process.
+
+ """
+ close_frame = Frame(
+ OP_CLOSE,
+ b"" if code is None else Close(code, reason).serialize(),
+ )
+ # No frame was received.
+ self.assertFrameReceived(connection, None)
+ # A close frame and possibly the end of stream were sent.
+ self.assertFrameSent(connection, close_frame, eof=connection.side is SERVER)
+
+
+class MaskingTests(ProtocolTestCase):
+ """
+ Test frame masking.
+
+ 5.1. Overview
+
+ """
+
+ unmasked_text_frame_date = b"\x81\x04Spam"
+ masked_text_frame_data = b"\x81\x84\x00\xff\x00\xff\x53\x8f\x61\x92"
+
+ def test_client_sends_masked_frame(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\xff\x00\xff"):
+ client.send_text(b"Spam", True)
+ self.assertEqual(client.data_to_send(), [self.masked_text_frame_data])
+
+ def test_server_sends_unmasked_frame(self):
+ server = Protocol(SERVER)
+ server.send_text(b"Spam", True)
+ self.assertEqual(server.data_to_send(), [self.unmasked_text_frame_date])
+
+ def test_client_receives_unmasked_frame(self):
+ client = Protocol(CLIENT)
+ client.receive_data(self.unmasked_text_frame_date)
+ self.assertFrameReceived(
+ client,
+ Frame(OP_TEXT, b"Spam"),
+ )
+
+ def test_server_receives_masked_frame(self):
+ server = Protocol(SERVER)
+ server.receive_data(self.masked_text_frame_data)
+ self.assertFrameReceived(
+ server,
+ Frame(OP_TEXT, b"Spam"),
+ )
+
+ def test_client_receives_masked_frame(self):
+ client = Protocol(CLIENT)
+ client.receive_data(self.masked_text_frame_data)
+ self.assertIsInstance(client.parser_exc, ProtocolError)
+ self.assertEqual(str(client.parser_exc), "incorrect masking")
+ self.assertConnectionFailing(
+ client, CloseCode.PROTOCOL_ERROR, "incorrect masking"
+ )
+
+ def test_server_receives_unmasked_frame(self):
+ server = Protocol(SERVER)
+ server.receive_data(self.unmasked_text_frame_date)
+ self.assertIsInstance(server.parser_exc, ProtocolError)
+ self.assertEqual(str(server.parser_exc), "incorrect masking")
+ self.assertConnectionFailing(
+ server, CloseCode.PROTOCOL_ERROR, "incorrect masking"
+ )
+
+
+class ContinuationTests(ProtocolTestCase):
+ """
+ Test continuation frames without text or binary frames.
+
+ """
+
+ def test_client_sends_unexpected_continuation(self):
+ client = Protocol(CLIENT)
+ with self.assertRaises(ProtocolError) as raised:
+ client.send_continuation(b"", fin=False)
+ self.assertEqual(str(raised.exception), "unexpected continuation frame")
+
+ def test_server_sends_unexpected_continuation(self):
+ server = Protocol(SERVER)
+ with self.assertRaises(ProtocolError) as raised:
+ server.send_continuation(b"", fin=False)
+ self.assertEqual(str(raised.exception), "unexpected continuation frame")
+
+ def test_client_receives_unexpected_continuation(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x00\x00")
+ self.assertIsInstance(client.parser_exc, ProtocolError)
+ self.assertEqual(str(client.parser_exc), "unexpected continuation frame")
+ self.assertConnectionFailing(
+ client, CloseCode.PROTOCOL_ERROR, "unexpected continuation frame"
+ )
+
+ def test_server_receives_unexpected_continuation(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x00\x80\x00\x00\x00\x00")
+ self.assertIsInstance(server.parser_exc, ProtocolError)
+ self.assertEqual(str(server.parser_exc), "unexpected continuation frame")
+ self.assertConnectionFailing(
+ server, CloseCode.PROTOCOL_ERROR, "unexpected continuation frame"
+ )
+
+ def test_client_sends_continuation_after_sending_close(self):
+ client = Protocol(CLIENT)
+ # Since it isn't possible to send a close frame in a fragmented
+ # message (see test_client_send_close_in_fragmented_message), in fact,
+ # this is the same test as test_client_sends_unexpected_continuation.
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_close(CloseCode.GOING_AWAY)
+ self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"])
+ with self.assertRaises(ProtocolError) as raised:
+ client.send_continuation(b"", fin=False)
+ self.assertEqual(str(raised.exception), "unexpected continuation frame")
+
+ def test_server_sends_continuation_after_sending_close(self):
+ # Since it isn't possible to send a close frame in a fragmented
+ # message (see test_server_send_close_in_fragmented_message), in fact,
+ # this is the same test as test_server_sends_unexpected_continuation.
+ server = Protocol(SERVER)
+ server.send_close(CloseCode.NORMAL_CLOSURE)
+ self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"])
+ with self.assertRaises(ProtocolError) as raised:
+ server.send_continuation(b"", fin=False)
+ self.assertEqual(str(raised.exception), "unexpected continuation frame")
+
+ def test_client_receives_continuation_after_receiving_close(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x02\x03\xe8")
+ self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE)
+ client.receive_data(b"\x00\x00")
+ self.assertFrameReceived(client, None)
+ self.assertFrameSent(client, None)
+
+ def test_server_receives_continuation_after_receiving_close(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9")
+ self.assertConnectionClosing(server, CloseCode.GOING_AWAY)
+ server.receive_data(b"\x00\x80\x00\xff\x00\xff")
+ self.assertFrameReceived(server, None)
+ self.assertFrameSent(server, None)
+
+
+class TextTests(ProtocolTestCase):
+ """
+ Test text frames and continuation frames.
+
+ """
+
+ def test_client_sends_text(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_text("😀".encode())
+ self.assertEqual(
+ client.data_to_send(), [b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80"]
+ )
+
+ def test_server_sends_text(self):
+ server = Protocol(SERVER)
+ server.send_text("😀".encode())
+ self.assertEqual(server.data_to_send(), [b"\x81\x04\xf0\x9f\x98\x80"])
+
+ def test_client_receives_text(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x81\x04\xf0\x9f\x98\x80")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_TEXT, "😀".encode()),
+ )
+
+ def test_server_receives_text(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_TEXT, "😀".encode()),
+ )
+
+ def test_client_receives_text_over_size_limit(self):
+ client = Protocol(CLIENT, max_size=3)
+ client.receive_data(b"\x81\x04\xf0\x9f\x98\x80")
+ self.assertIsInstance(client.parser_exc, PayloadTooBig)
+ self.assertEqual(str(client.parser_exc), "over size limit (4 > 3 bytes)")
+ self.assertConnectionFailing(
+ client, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)"
+ )
+
+ def test_server_receives_text_over_size_limit(self):
+ server = Protocol(SERVER, max_size=3)
+ server.receive_data(b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80")
+ self.assertIsInstance(server.parser_exc, PayloadTooBig)
+ self.assertEqual(str(server.parser_exc), "over size limit (4 > 3 bytes)")
+ self.assertConnectionFailing(
+ server, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)"
+ )
+
+ def test_client_receives_text_without_size_limit(self):
+ client = Protocol(CLIENT, max_size=None)
+ client.receive_data(b"\x81\x04\xf0\x9f\x98\x80")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_TEXT, "😀".encode()),
+ )
+
+ def test_server_receives_text_without_size_limit(self):
+ server = Protocol(SERVER, max_size=None)
+ server.receive_data(b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_TEXT, "😀".encode()),
+ )
+
+ def test_client_sends_fragmented_text(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_text("😀".encode()[:2], fin=False)
+ self.assertEqual(client.data_to_send(), [b"\x01\x82\x00\x00\x00\x00\xf0\x9f"])
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_continuation("😀😀".encode()[2:6], fin=False)
+ self.assertEqual(
+ client.data_to_send(), [b"\x00\x84\x00\x00\x00\x00\x98\x80\xf0\x9f"]
+ )
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_continuation("😀".encode()[2:], fin=True)
+ self.assertEqual(client.data_to_send(), [b"\x80\x82\x00\x00\x00\x00\x98\x80"])
+
+ def test_server_sends_fragmented_text(self):
+ server = Protocol(SERVER)
+ server.send_text("😀".encode()[:2], fin=False)
+ self.assertEqual(server.data_to_send(), [b"\x01\x02\xf0\x9f"])
+ server.send_continuation("😀😀".encode()[2:6], fin=False)
+ self.assertEqual(server.data_to_send(), [b"\x00\x04\x98\x80\xf0\x9f"])
+ server.send_continuation("😀".encode()[2:], fin=True)
+ self.assertEqual(server.data_to_send(), [b"\x80\x02\x98\x80"])
+
+ def test_client_receives_fragmented_text(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x01\x02\xf0\x9f")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_TEXT, "😀".encode()[:2], fin=False),
+ )
+ client.receive_data(b"\x00\x04\x98\x80\xf0\x9f")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_CONT, "😀😀".encode()[2:6], fin=False),
+ )
+ client.receive_data(b"\x80\x02\x98\x80")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_CONT, "😀".encode()[2:]),
+ )
+
+ def test_server_receives_fragmented_text(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x01\x82\x00\x00\x00\x00\xf0\x9f")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_TEXT, "😀".encode()[:2], fin=False),
+ )
+ server.receive_data(b"\x00\x84\x00\x00\x00\x00\x98\x80\xf0\x9f")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_CONT, "😀😀".encode()[2:6], fin=False),
+ )
+ server.receive_data(b"\x80\x82\x00\x00\x00\x00\x98\x80")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_CONT, "😀".encode()[2:]),
+ )
+
+ def test_client_receives_fragmented_text_over_size_limit(self):
+ client = Protocol(CLIENT, max_size=3)
+ client.receive_data(b"\x01\x02\xf0\x9f")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_TEXT, "😀".encode()[:2], fin=False),
+ )
+ client.receive_data(b"\x80\x02\x98\x80")
+ self.assertIsInstance(client.parser_exc, PayloadTooBig)
+ self.assertEqual(str(client.parser_exc), "over size limit (2 > 1 bytes)")
+ self.assertConnectionFailing(
+ client, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)"
+ )
+
+ def test_server_receives_fragmented_text_over_size_limit(self):
+ server = Protocol(SERVER, max_size=3)
+ server.receive_data(b"\x01\x82\x00\x00\x00\x00\xf0\x9f")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_TEXT, "😀".encode()[:2], fin=False),
+ )
+ server.receive_data(b"\x80\x82\x00\x00\x00\x00\x98\x80")
+ self.assertIsInstance(server.parser_exc, PayloadTooBig)
+ self.assertEqual(str(server.parser_exc), "over size limit (2 > 1 bytes)")
+ self.assertConnectionFailing(
+ server, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)"
+ )
+
+ def test_client_receives_fragmented_text_without_size_limit(self):
+ client = Protocol(CLIENT, max_size=None)
+ client.receive_data(b"\x01\x02\xf0\x9f")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_TEXT, "😀".encode()[:2], fin=False),
+ )
+ client.receive_data(b"\x00\x04\x98\x80\xf0\x9f")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_CONT, "😀😀".encode()[2:6], fin=False),
+ )
+ client.receive_data(b"\x80\x02\x98\x80")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_CONT, "😀".encode()[2:]),
+ )
+
+ def test_server_receives_fragmented_text_without_size_limit(self):
+ server = Protocol(SERVER, max_size=None)
+ server.receive_data(b"\x01\x82\x00\x00\x00\x00\xf0\x9f")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_TEXT, "😀".encode()[:2], fin=False),
+ )
+ server.receive_data(b"\x00\x84\x00\x00\x00\x00\x98\x80\xf0\x9f")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_CONT, "😀😀".encode()[2:6], fin=False),
+ )
+ server.receive_data(b"\x80\x82\x00\x00\x00\x00\x98\x80")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_CONT, "😀".encode()[2:]),
+ )
+
+ def test_client_sends_unexpected_text(self):
+ client = Protocol(CLIENT)
+ client.send_text(b"", fin=False)
+ with self.assertRaises(ProtocolError) as raised:
+ client.send_text(b"", fin=False)
+ self.assertEqual(str(raised.exception), "expected a continuation frame")
+
+ def test_server_sends_unexpected_text(self):
+ server = Protocol(SERVER)
+ server.send_text(b"", fin=False)
+ with self.assertRaises(ProtocolError) as raised:
+ server.send_text(b"", fin=False)
+ self.assertEqual(str(raised.exception), "expected a continuation frame")
+
+ def test_client_receives_unexpected_text(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x01\x00")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_TEXT, b"", fin=False),
+ )
+ client.receive_data(b"\x01\x00")
+ self.assertIsInstance(client.parser_exc, ProtocolError)
+ self.assertEqual(str(client.parser_exc), "expected a continuation frame")
+ self.assertConnectionFailing(
+ client, CloseCode.PROTOCOL_ERROR, "expected a continuation frame"
+ )
+
+ def test_server_receives_unexpected_text(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x01\x80\x00\x00\x00\x00")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_TEXT, b"", fin=False),
+ )
+ server.receive_data(b"\x01\x80\x00\x00\x00\x00")
+ self.assertIsInstance(server.parser_exc, ProtocolError)
+ self.assertEqual(str(server.parser_exc), "expected a continuation frame")
+ self.assertConnectionFailing(
+ server, CloseCode.PROTOCOL_ERROR, "expected a continuation frame"
+ )
+
+ def test_client_sends_text_after_sending_close(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_close(CloseCode.GOING_AWAY)
+ self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"])
+ with self.assertRaises(InvalidState):
+ client.send_text(b"")
+
+ def test_server_sends_text_after_sending_close(self):
+ server = Protocol(SERVER)
+ server.send_close(CloseCode.NORMAL_CLOSURE)
+ self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"])
+ with self.assertRaises(InvalidState):
+ server.send_text(b"")
+
+ def test_client_receives_text_after_receiving_close(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x02\x03\xe8")
+ self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE)
+ client.receive_data(b"\x81\x00")
+ self.assertFrameReceived(client, None)
+ self.assertFrameSent(client, None)
+
+ def test_server_receives_text_after_receiving_close(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9")
+ self.assertConnectionClosing(server, CloseCode.GOING_AWAY)
+ server.receive_data(b"\x81\x80\x00\xff\x00\xff")
+ self.assertFrameReceived(server, None)
+ self.assertFrameSent(server, None)
+
+
+class BinaryTests(ProtocolTestCase):
+ """
+ Test binary frames and continuation frames.
+
+ """
+
+ def test_client_sends_binary(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_binary(b"\x01\x02\xfe\xff")
+ self.assertEqual(
+ client.data_to_send(), [b"\x82\x84\x00\x00\x00\x00\x01\x02\xfe\xff"]
+ )
+
+ def test_server_sends_binary(self):
+ server = Protocol(SERVER)
+ server.send_binary(b"\x01\x02\xfe\xff")
+ self.assertEqual(server.data_to_send(), [b"\x82\x04\x01\x02\xfe\xff"])
+
+ def test_client_receives_binary(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x82\x04\x01\x02\xfe\xff")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_BINARY, b"\x01\x02\xfe\xff"),
+ )
+
+ def test_server_receives_binary(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x82\x84\x00\x00\x00\x00\x01\x02\xfe\xff")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_BINARY, b"\x01\x02\xfe\xff"),
+ )
+
+ def test_client_receives_binary_over_size_limit(self):
+ client = Protocol(CLIENT, max_size=3)
+ client.receive_data(b"\x82\x04\x01\x02\xfe\xff")
+ self.assertIsInstance(client.parser_exc, PayloadTooBig)
+ self.assertEqual(str(client.parser_exc), "over size limit (4 > 3 bytes)")
+ self.assertConnectionFailing(
+ client, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)"
+ )
+
+ def test_server_receives_binary_over_size_limit(self):
+ server = Protocol(SERVER, max_size=3)
+ server.receive_data(b"\x82\x84\x00\x00\x00\x00\x01\x02\xfe\xff")
+ self.assertIsInstance(server.parser_exc, PayloadTooBig)
+ self.assertEqual(str(server.parser_exc), "over size limit (4 > 3 bytes)")
+ self.assertConnectionFailing(
+ server, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)"
+ )
+
+ def test_client_sends_fragmented_binary(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_binary(b"\x01\x02", fin=False)
+ self.assertEqual(client.data_to_send(), [b"\x02\x82\x00\x00\x00\x00\x01\x02"])
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_continuation(b"\xee\xff\x01\x02", fin=False)
+ self.assertEqual(
+ client.data_to_send(), [b"\x00\x84\x00\x00\x00\x00\xee\xff\x01\x02"]
+ )
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_continuation(b"\xee\xff", fin=True)
+ self.assertEqual(client.data_to_send(), [b"\x80\x82\x00\x00\x00\x00\xee\xff"])
+
+ def test_server_sends_fragmented_binary(self):
+ server = Protocol(SERVER)
+ server.send_binary(b"\x01\x02", fin=False)
+ self.assertEqual(server.data_to_send(), [b"\x02\x02\x01\x02"])
+ server.send_continuation(b"\xee\xff\x01\x02", fin=False)
+ self.assertEqual(server.data_to_send(), [b"\x00\x04\xee\xff\x01\x02"])
+ server.send_continuation(b"\xee\xff", fin=True)
+ self.assertEqual(server.data_to_send(), [b"\x80\x02\xee\xff"])
+
+ def test_client_receives_fragmented_binary(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x02\x02\x01\x02")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_BINARY, b"\x01\x02", fin=False),
+ )
+ client.receive_data(b"\x00\x04\xfe\xff\x01\x02")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_CONT, b"\xfe\xff\x01\x02", fin=False),
+ )
+ client.receive_data(b"\x80\x02\xfe\xff")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_CONT, b"\xfe\xff"),
+ )
+
+ def test_server_receives_fragmented_binary(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x02\x82\x00\x00\x00\x00\x01\x02")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_BINARY, b"\x01\x02", fin=False),
+ )
+ server.receive_data(b"\x00\x84\x00\x00\x00\x00\xee\xff\x01\x02")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_CONT, b"\xee\xff\x01\x02", fin=False),
+ )
+ server.receive_data(b"\x80\x82\x00\x00\x00\x00\xfe\xff")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_CONT, b"\xfe\xff"),
+ )
+
+ def test_client_receives_fragmented_binary_over_size_limit(self):
+ client = Protocol(CLIENT, max_size=3)
+ client.receive_data(b"\x02\x02\x01\x02")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_BINARY, b"\x01\x02", fin=False),
+ )
+ client.receive_data(b"\x80\x02\xfe\xff")
+ self.assertIsInstance(client.parser_exc, PayloadTooBig)
+ self.assertEqual(str(client.parser_exc), "over size limit (2 > 1 bytes)")
+ self.assertConnectionFailing(
+ client, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)"
+ )
+
+ def test_server_receives_fragmented_binary_over_size_limit(self):
+ server = Protocol(SERVER, max_size=3)
+ server.receive_data(b"\x02\x82\x00\x00\x00\x00\x01\x02")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_BINARY, b"\x01\x02", fin=False),
+ )
+ server.receive_data(b"\x80\x82\x00\x00\x00\x00\xfe\xff")
+ self.assertIsInstance(server.parser_exc, PayloadTooBig)
+ self.assertEqual(str(server.parser_exc), "over size limit (2 > 1 bytes)")
+ self.assertConnectionFailing(
+ server, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)"
+ )
+
+ def test_client_sends_unexpected_binary(self):
+ client = Protocol(CLIENT)
+ client.send_binary(b"", fin=False)
+ with self.assertRaises(ProtocolError) as raised:
+ client.send_binary(b"", fin=False)
+ self.assertEqual(str(raised.exception), "expected a continuation frame")
+
+ def test_server_sends_unexpected_binary(self):
+ server = Protocol(SERVER)
+ server.send_binary(b"", fin=False)
+ with self.assertRaises(ProtocolError) as raised:
+ server.send_binary(b"", fin=False)
+ self.assertEqual(str(raised.exception), "expected a continuation frame")
+
+ def test_client_receives_unexpected_binary(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x02\x00")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_BINARY, b"", fin=False),
+ )
+ client.receive_data(b"\x02\x00")
+ self.assertIsInstance(client.parser_exc, ProtocolError)
+ self.assertEqual(str(client.parser_exc), "expected a continuation frame")
+ self.assertConnectionFailing(
+ client, CloseCode.PROTOCOL_ERROR, "expected a continuation frame"
+ )
+
+ def test_server_receives_unexpected_binary(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x02\x80\x00\x00\x00\x00")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_BINARY, b"", fin=False),
+ )
+ server.receive_data(b"\x02\x80\x00\x00\x00\x00")
+ self.assertIsInstance(server.parser_exc, ProtocolError)
+ self.assertEqual(str(server.parser_exc), "expected a continuation frame")
+ self.assertConnectionFailing(
+ server, CloseCode.PROTOCOL_ERROR, "expected a continuation frame"
+ )
+
+ def test_client_sends_binary_after_sending_close(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_close(CloseCode.GOING_AWAY)
+ self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"])
+ with self.assertRaises(InvalidState):
+ client.send_binary(b"")
+
+ def test_server_sends_binary_after_sending_close(self):
+ server = Protocol(SERVER)
+ server.send_close(CloseCode.NORMAL_CLOSURE)
+ self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"])
+ with self.assertRaises(InvalidState):
+ server.send_binary(b"")
+
+ def test_client_receives_binary_after_receiving_close(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x02\x03\xe8")
+ self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE)
+ client.receive_data(b"\x82\x00")
+ self.assertFrameReceived(client, None)
+ self.assertFrameSent(client, None)
+
+ def test_server_receives_binary_after_receiving_close(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9")
+ self.assertConnectionClosing(server, CloseCode.GOING_AWAY)
+ server.receive_data(b"\x82\x80\x00\xff\x00\xff")
+ self.assertFrameReceived(server, None)
+ self.assertFrameSent(server, None)
+
+
+class CloseTests(ProtocolTestCase):
+ """
+ Test close frames.
+
+ See RFC 6544:
+
+ 5.5.1. Close
+ 7.1.6. The WebSocket Connection Close Reason
+ 7.1.7. Fail the WebSocket Connection
+
+ """
+
+ def test_close_code(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x04\x03\xe8OK")
+ client.receive_eof()
+ self.assertEqual(client.close_code, CloseCode.NORMAL_CLOSURE)
+
+ def test_close_reason(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x84\x00\x00\x00\x00\x03\xe8OK")
+ server.receive_eof()
+ self.assertEqual(server.close_reason, "OK")
+
+ def test_close_code_not_provided(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x80\x00\x00\x00\x00")
+ server.receive_eof()
+ self.assertEqual(server.close_code, CloseCode.NO_STATUS_RCVD)
+
+ def test_close_reason_not_provided(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x00")
+ client.receive_eof()
+ self.assertEqual(client.close_reason, "")
+
+ def test_close_code_not_available(self):
+ client = Protocol(CLIENT)
+ client.receive_eof()
+ self.assertEqual(client.close_code, CloseCode.ABNORMAL_CLOSURE)
+
+ def test_close_reason_not_available(self):
+ server = Protocol(SERVER)
+ server.receive_eof()
+ self.assertEqual(server.close_reason, "")
+
+ def test_close_code_not_available_yet(self):
+ server = Protocol(SERVER)
+ self.assertIsNone(server.close_code)
+
+ def test_close_reason_not_available_yet(self):
+ client = Protocol(CLIENT)
+ self.assertIsNone(client.close_reason)
+
+ def test_client_sends_close(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x3c\x3c\x3c\x3c"):
+ client.send_close()
+ self.assertEqual(client.data_to_send(), [b"\x88\x80\x3c\x3c\x3c\x3c"])
+ self.assertIs(client.state, CLOSING)
+
+ def test_server_sends_close(self):
+ server = Protocol(SERVER)
+ server.send_close()
+ self.assertEqual(server.data_to_send(), [b"\x88\x00"])
+ self.assertIs(server.state, CLOSING)
+
+ def test_client_receives_close(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x3c\x3c\x3c\x3c"):
+ client.receive_data(b"\x88\x00")
+ self.assertEqual(client.events_received(), [Frame(OP_CLOSE, b"")])
+ self.assertEqual(client.data_to_send(), [b"\x88\x80\x3c\x3c\x3c\x3c"])
+ self.assertIs(client.state, CLOSING)
+
+ def test_server_receives_close(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c")
+ self.assertEqual(server.events_received(), [Frame(OP_CLOSE, b"")])
+ self.assertEqual(server.data_to_send(), [b"\x88\x00", b""])
+ self.assertIs(server.state, CLOSING)
+
+ def test_client_sends_close_then_receives_close(self):
+ # Client-initiated close handshake on the client side.
+ client = Protocol(CLIENT)
+
+ client.send_close()
+ self.assertFrameReceived(client, None)
+ self.assertFrameSent(client, Frame(OP_CLOSE, b""))
+
+ client.receive_data(b"\x88\x00")
+ self.assertFrameReceived(client, Frame(OP_CLOSE, b""))
+ self.assertFrameSent(client, None)
+
+ client.receive_eof()
+ self.assertFrameReceived(client, None)
+ self.assertFrameSent(client, None, eof=True)
+
+ def test_server_sends_close_then_receives_close(self):
+ # Server-initiated close handshake on the server side.
+ server = Protocol(SERVER)
+
+ server.send_close()
+ self.assertFrameReceived(server, None)
+ self.assertFrameSent(server, Frame(OP_CLOSE, b""))
+
+ server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c")
+ self.assertFrameReceived(server, Frame(OP_CLOSE, b""))
+ self.assertFrameSent(server, None, eof=True)
+
+ server.receive_eof()
+ self.assertFrameReceived(server, None)
+ self.assertFrameSent(server, None)
+
+ def test_client_receives_close_then_sends_close(self):
+ # Server-initiated close handshake on the client side.
+ client = Protocol(CLIENT)
+
+ client.receive_data(b"\x88\x00")
+ self.assertFrameReceived(client, Frame(OP_CLOSE, b""))
+ self.assertFrameSent(client, Frame(OP_CLOSE, b""))
+
+ client.receive_eof()
+ self.assertFrameReceived(client, None)
+ self.assertFrameSent(client, None, eof=True)
+
+ def test_server_receives_close_then_sends_close(self):
+ # Client-initiated close handshake on the server side.
+ server = Protocol(SERVER)
+
+ server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c")
+ self.assertFrameReceived(server, Frame(OP_CLOSE, b""))
+ self.assertFrameSent(server, Frame(OP_CLOSE, b""), eof=True)
+
+ server.receive_eof()
+ self.assertFrameReceived(server, None)
+ self.assertFrameSent(server, None)
+
+ def test_client_sends_close_with_code(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_close(CloseCode.GOING_AWAY)
+ self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"])
+ self.assertIs(client.state, CLOSING)
+
+ def test_server_sends_close_with_code(self):
+ server = Protocol(SERVER)
+ server.send_close(CloseCode.NORMAL_CLOSURE)
+ self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"])
+ self.assertIs(server.state, CLOSING)
+
+ def test_client_receives_close_with_code(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x02\x03\xe8")
+ self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE, "")
+ self.assertIs(client.state, CLOSING)
+
+ def test_server_receives_close_with_code(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9")
+ self.assertConnectionClosing(server, CloseCode.GOING_AWAY, "")
+ self.assertIs(server.state, CLOSING)
+
+ def test_client_sends_close_with_code_and_reason(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_close(CloseCode.GOING_AWAY, "going away")
+ self.assertEqual(
+ client.data_to_send(), [b"\x88\x8c\x00\x00\x00\x00\x03\xe9going away"]
+ )
+ self.assertIs(client.state, CLOSING)
+
+ def test_server_sends_close_with_code_and_reason(self):
+ server = Protocol(SERVER)
+ server.send_close(CloseCode.NORMAL_CLOSURE, "OK")
+ self.assertEqual(server.data_to_send(), [b"\x88\x04\x03\xe8OK"])
+ self.assertIs(server.state, CLOSING)
+
+ def test_client_receives_close_with_code_and_reason(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x04\x03\xe8OK")
+ self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE, "OK")
+ self.assertIs(client.state, CLOSING)
+
+ def test_server_receives_close_with_code_and_reason(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x8c\x00\x00\x00\x00\x03\xe9going away")
+ self.assertConnectionClosing(server, CloseCode.GOING_AWAY, "going away")
+ self.assertIs(server.state, CLOSING)
+
+ def test_client_sends_close_with_reason_only(self):
+ client = Protocol(CLIENT)
+ with self.assertRaises(ProtocolError) as raised:
+ client.send_close(reason="going away")
+ self.assertEqual(str(raised.exception), "cannot send a reason without a code")
+
+ def test_server_sends_close_with_reason_only(self):
+ server = Protocol(SERVER)
+ with self.assertRaises(ProtocolError) as raised:
+ server.send_close(reason="OK")
+ self.assertEqual(str(raised.exception), "cannot send a reason without a code")
+
+ def test_client_receives_close_with_truncated_code(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x01\x03")
+ self.assertIsInstance(client.parser_exc, ProtocolError)
+ self.assertEqual(str(client.parser_exc), "close frame too short")
+ self.assertConnectionFailing(
+ client, CloseCode.PROTOCOL_ERROR, "close frame too short"
+ )
+ self.assertIs(client.state, CLOSING)
+
+ def test_server_receives_close_with_truncated_code(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x81\x00\x00\x00\x00\x03")
+ self.assertIsInstance(server.parser_exc, ProtocolError)
+ self.assertEqual(str(server.parser_exc), "close frame too short")
+ self.assertConnectionFailing(
+ server, CloseCode.PROTOCOL_ERROR, "close frame too short"
+ )
+ self.assertIs(server.state, CLOSING)
+
+ def test_client_receives_close_with_non_utf8_reason(self):
+ client = Protocol(CLIENT)
+
+ client.receive_data(b"\x88\x04\x03\xe8\xff\xff")
+ self.assertIsInstance(client.parser_exc, UnicodeDecodeError)
+ self.assertEqual(
+ str(client.parser_exc),
+ "'utf-8' codec can't decode byte 0xff in position 0: invalid start byte",
+ )
+ self.assertConnectionFailing(
+ client, CloseCode.INVALID_DATA, "invalid start byte at position 0"
+ )
+ self.assertIs(client.state, CLOSING)
+
+ def test_server_receives_close_with_non_utf8_reason(self):
+ server = Protocol(SERVER)
+
+ server.receive_data(b"\x88\x84\x00\x00\x00\x00\x03\xe9\xff\xff")
+ self.assertIsInstance(server.parser_exc, UnicodeDecodeError)
+ self.assertEqual(
+ str(server.parser_exc),
+ "'utf-8' codec can't decode byte 0xff in position 0: invalid start byte",
+ )
+ self.assertConnectionFailing(
+ server, CloseCode.INVALID_DATA, "invalid start byte at position 0"
+ )
+ self.assertIs(server.state, CLOSING)
+
+
+class PingTests(ProtocolTestCase):
+ """
+ Test ping. See 5.5.2. Ping in RFC 6544.
+
+ """
+
+ def test_client_sends_ping(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\x44\x88\xcc"):
+ client.send_ping(b"")
+ self.assertEqual(client.data_to_send(), [b"\x89\x80\x00\x44\x88\xcc"])
+
+ def test_server_sends_ping(self):
+ server = Protocol(SERVER)
+ server.send_ping(b"")
+ self.assertEqual(server.data_to_send(), [b"\x89\x00"])
+
+ def test_client_receives_ping(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x89\x00")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_PING, b""),
+ )
+ self.assertFrameSent(
+ client,
+ Frame(OP_PONG, b""),
+ )
+
+ def test_server_receives_ping(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x89\x80\x00\x44\x88\xcc")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_PING, b""),
+ )
+ self.assertFrameSent(
+ server,
+ Frame(OP_PONG, b""),
+ )
+
+ def test_client_sends_ping_with_data(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\x44\x88\xcc"):
+ client.send_ping(b"\x22\x66\xaa\xee")
+ self.assertEqual(
+ client.data_to_send(), [b"\x89\x84\x00\x44\x88\xcc\x22\x22\x22\x22"]
+ )
+
+ def test_server_sends_ping_with_data(self):
+ server = Protocol(SERVER)
+ server.send_ping(b"\x22\x66\xaa\xee")
+ self.assertEqual(server.data_to_send(), [b"\x89\x04\x22\x66\xaa\xee"])
+
+ def test_client_receives_ping_with_data(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x89\x04\x22\x66\xaa\xee")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_PING, b"\x22\x66\xaa\xee"),
+ )
+ self.assertFrameSent(
+ client,
+ Frame(OP_PONG, b"\x22\x66\xaa\xee"),
+ )
+
+ def test_server_receives_ping_with_data(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x89\x84\x00\x44\x88\xcc\x22\x22\x22\x22")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_PING, b"\x22\x66\xaa\xee"),
+ )
+ self.assertFrameSent(
+ server,
+ Frame(OP_PONG, b"\x22\x66\xaa\xee"),
+ )
+
+ def test_client_sends_fragmented_ping_frame(self):
+ client = Protocol(CLIENT)
+ # This is only possible through a private API.
+ with self.assertRaises(ProtocolError) as raised:
+ client.send_frame(Frame(OP_PING, b"", fin=False))
+ self.assertEqual(str(raised.exception), "fragmented control frame")
+
+ def test_server_sends_fragmented_ping_frame(self):
+ server = Protocol(SERVER)
+ # This is only possible through a private API.
+ with self.assertRaises(ProtocolError) as raised:
+ server.send_frame(Frame(OP_PING, b"", fin=False))
+ self.assertEqual(str(raised.exception), "fragmented control frame")
+
+ def test_client_receives_fragmented_ping_frame(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x09\x00")
+ self.assertIsInstance(client.parser_exc, ProtocolError)
+ self.assertEqual(str(client.parser_exc), "fragmented control frame")
+ self.assertConnectionFailing(
+ client, CloseCode.PROTOCOL_ERROR, "fragmented control frame"
+ )
+
+ def test_server_receives_fragmented_ping_frame(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x09\x80\x3c\x3c\x3c\x3c")
+ self.assertIsInstance(server.parser_exc, ProtocolError)
+ self.assertEqual(str(server.parser_exc), "fragmented control frame")
+ self.assertConnectionFailing(
+ server, CloseCode.PROTOCOL_ERROR, "fragmented control frame"
+ )
+
+ def test_client_sends_ping_after_sending_close(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_close(CloseCode.GOING_AWAY)
+ self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"])
+ # The spec says: "An endpoint MAY send a Ping frame any time (...)
+ # before the connection is closed" but websockets doesn't support
+ # sending a Ping frame after a Close frame.
+ with self.assertRaises(InvalidState) as raised:
+ client.send_ping(b"")
+ self.assertEqual(
+ str(raised.exception),
+ "cannot write to a WebSocket in the CLOSING state",
+ )
+
+ def test_server_sends_ping_after_sending_close(self):
+ server = Protocol(SERVER)
+ server.send_close(CloseCode.NORMAL_CLOSURE)
+ self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"])
+ # The spec says: "An endpoint MAY send a Ping frame any time (...)
+ # before the connection is closed" but websockets doesn't support
+ # sending a Ping frame after a Close frame.
+ with self.assertRaises(InvalidState) as raised:
+ server.send_ping(b"")
+ self.assertEqual(
+ str(raised.exception),
+ "cannot write to a WebSocket in the CLOSING state",
+ )
+
+ def test_client_receives_ping_after_receiving_close(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x02\x03\xe8")
+ self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE)
+ client.receive_data(b"\x89\x04\x22\x66\xaa\xee")
+ self.assertFrameReceived(client, None)
+ self.assertFrameSent(client, None)
+
+ def test_server_receives_ping_after_receiving_close(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9")
+ self.assertConnectionClosing(server, CloseCode.GOING_AWAY)
+ server.receive_data(b"\x89\x84\x00\x44\x88\xcc\x22\x22\x22\x22")
+ self.assertFrameReceived(server, None)
+ self.assertFrameSent(server, None)
+
+
+class PongTests(ProtocolTestCase):
+ """
+ Test pong frames. See 5.5.3. Pong in RFC 6544.
+
+ """
+
+ def test_client_sends_pong(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\x44\x88\xcc"):
+ client.send_pong(b"")
+ self.assertEqual(client.data_to_send(), [b"\x8a\x80\x00\x44\x88\xcc"])
+
+ def test_server_sends_pong(self):
+ server = Protocol(SERVER)
+ server.send_pong(b"")
+ self.assertEqual(server.data_to_send(), [b"\x8a\x00"])
+
+ def test_client_receives_pong(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x8a\x00")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_PONG, b""),
+ )
+
+ def test_server_receives_pong(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x8a\x80\x00\x44\x88\xcc")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_PONG, b""),
+ )
+
+ def test_client_sends_pong_with_data(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\x44\x88\xcc"):
+ client.send_pong(b"\x22\x66\xaa\xee")
+ self.assertEqual(
+ client.data_to_send(), [b"\x8a\x84\x00\x44\x88\xcc\x22\x22\x22\x22"]
+ )
+
+ def test_server_sends_pong_with_data(self):
+ server = Protocol(SERVER)
+ server.send_pong(b"\x22\x66\xaa\xee")
+ self.assertEqual(server.data_to_send(), [b"\x8a\x04\x22\x66\xaa\xee"])
+
+ def test_client_receives_pong_with_data(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x8a\x04\x22\x66\xaa\xee")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_PONG, b"\x22\x66\xaa\xee"),
+ )
+
+ def test_server_receives_pong_with_data(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x8a\x84\x00\x44\x88\xcc\x22\x22\x22\x22")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_PONG, b"\x22\x66\xaa\xee"),
+ )
+
+ def test_client_sends_fragmented_pong_frame(self):
+ client = Protocol(CLIENT)
+ # This is only possible through a private API.
+ with self.assertRaises(ProtocolError) as raised:
+ client.send_frame(Frame(OP_PONG, b"", fin=False))
+ self.assertEqual(str(raised.exception), "fragmented control frame")
+
+ def test_server_sends_fragmented_pong_frame(self):
+ server = Protocol(SERVER)
+ # This is only possible through a private API.
+ with self.assertRaises(ProtocolError) as raised:
+ server.send_frame(Frame(OP_PONG, b"", fin=False))
+ self.assertEqual(str(raised.exception), "fragmented control frame")
+
+ def test_client_receives_fragmented_pong_frame(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x0a\x00")
+ self.assertIsInstance(client.parser_exc, ProtocolError)
+ self.assertEqual(str(client.parser_exc), "fragmented control frame")
+ self.assertConnectionFailing(
+ client, CloseCode.PROTOCOL_ERROR, "fragmented control frame"
+ )
+
+ def test_server_receives_fragmented_pong_frame(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x0a\x80\x3c\x3c\x3c\x3c")
+ self.assertIsInstance(server.parser_exc, ProtocolError)
+ self.assertEqual(str(server.parser_exc), "fragmented control frame")
+ self.assertConnectionFailing(
+ server, CloseCode.PROTOCOL_ERROR, "fragmented control frame"
+ )
+
+ def test_client_sends_pong_after_sending_close(self):
+ client = Protocol(CLIENT)
+ with self.enforce_mask(b"\x00\x00\x00\x00"):
+ client.send_close(CloseCode.GOING_AWAY)
+ self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"])
+ # websockets doesn't support sending a Pong frame after a Close frame.
+ with self.assertRaises(InvalidState):
+ client.send_pong(b"")
+
+ def test_server_sends_pong_after_sending_close(self):
+ server = Protocol(SERVER)
+ server.send_close(CloseCode.NORMAL_CLOSURE)
+ self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"])
+ # websockets doesn't support sending a Pong frame after a Close frame.
+ with self.assertRaises(InvalidState):
+ server.send_pong(b"")
+
+ def test_client_receives_pong_after_receiving_close(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x02\x03\xe8")
+ self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE)
+ client.receive_data(b"\x8a\x04\x22\x66\xaa\xee")
+ self.assertFrameReceived(client, None)
+ self.assertFrameSent(client, None)
+
+ def test_server_receives_pong_after_receiving_close(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9")
+ self.assertConnectionClosing(server, CloseCode.GOING_AWAY)
+ server.receive_data(b"\x8a\x84\x00\x44\x88\xcc\x22\x22\x22\x22")
+ self.assertFrameReceived(server, None)
+ self.assertFrameSent(server, None)
+
+
+class FailTests(ProtocolTestCase):
+ """
+ Test failing the connection.
+
+ See 7.1.7. Fail the WebSocket Connection in RFC 6544.
+
+ """
+
+ def test_client_stops_processing_frames_after_fail(self):
+ client = Protocol(CLIENT)
+ client.fail(CloseCode.PROTOCOL_ERROR)
+ self.assertConnectionFailing(client, CloseCode.PROTOCOL_ERROR)
+ client.receive_data(b"\x88\x02\x03\xea")
+ self.assertFrameReceived(client, None)
+
+ def test_server_stops_processing_frames_after_fail(self):
+ server = Protocol(SERVER)
+ server.fail(CloseCode.PROTOCOL_ERROR)
+ self.assertConnectionFailing(server, CloseCode.PROTOCOL_ERROR)
+ server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xea")
+ self.assertFrameReceived(server, None)
+
+
+class FragmentationTests(ProtocolTestCase):
+ """
+ Test message fragmentation.
+
+ See 5.4. Fragmentation in RFC 6544.
+
+ """
+
+ def test_client_send_ping_pong_in_fragmented_message(self):
+ client = Protocol(CLIENT)
+ client.send_text(b"Spam", fin=False)
+ self.assertFrameSent(client, Frame(OP_TEXT, b"Spam", fin=False))
+ client.send_ping(b"Ping")
+ self.assertFrameSent(client, Frame(OP_PING, b"Ping"))
+ client.send_continuation(b"Ham", fin=False)
+ self.assertFrameSent(client, Frame(OP_CONT, b"Ham", fin=False))
+ client.send_pong(b"Pong")
+ self.assertFrameSent(client, Frame(OP_PONG, b"Pong"))
+ client.send_continuation(b"Eggs", fin=True)
+ self.assertFrameSent(client, Frame(OP_CONT, b"Eggs"))
+
+ def test_server_send_ping_pong_in_fragmented_message(self):
+ server = Protocol(SERVER)
+ server.send_text(b"Spam", fin=False)
+ self.assertFrameSent(server, Frame(OP_TEXT, b"Spam", fin=False))
+ server.send_ping(b"Ping")
+ self.assertFrameSent(server, Frame(OP_PING, b"Ping"))
+ server.send_continuation(b"Ham", fin=False)
+ self.assertFrameSent(server, Frame(OP_CONT, b"Ham", fin=False))
+ server.send_pong(b"Pong")
+ self.assertFrameSent(server, Frame(OP_PONG, b"Pong"))
+ server.send_continuation(b"Eggs", fin=True)
+ self.assertFrameSent(server, Frame(OP_CONT, b"Eggs"))
+
+ def test_client_receive_ping_pong_in_fragmented_message(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x01\x04Spam")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_TEXT, b"Spam", fin=False),
+ )
+ client.receive_data(b"\x89\x04Ping")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_PING, b"Ping"),
+ )
+ self.assertFrameSent(
+ client,
+ Frame(OP_PONG, b"Ping"),
+ )
+ client.receive_data(b"\x00\x03Ham")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_CONT, b"Ham", fin=False),
+ )
+ client.receive_data(b"\x8a\x04Pong")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_PONG, b"Pong"),
+ )
+ client.receive_data(b"\x80\x04Eggs")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_CONT, b"Eggs"),
+ )
+
+ def test_server_receive_ping_pong_in_fragmented_message(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x01\x84\x00\x00\x00\x00Spam")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_TEXT, b"Spam", fin=False),
+ )
+ server.receive_data(b"\x89\x84\x00\x00\x00\x00Ping")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_PING, b"Ping"),
+ )
+ self.assertFrameSent(
+ server,
+ Frame(OP_PONG, b"Ping"),
+ )
+ server.receive_data(b"\x00\x83\x00\x00\x00\x00Ham")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_CONT, b"Ham", fin=False),
+ )
+ server.receive_data(b"\x8a\x84\x00\x00\x00\x00Pong")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_PONG, b"Pong"),
+ )
+ server.receive_data(b"\x80\x84\x00\x00\x00\x00Eggs")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_CONT, b"Eggs"),
+ )
+
+ def test_client_send_close_in_fragmented_message(self):
+ client = Protocol(CLIENT)
+ client.send_text(b"Spam", fin=False)
+ self.assertFrameSent(client, Frame(OP_TEXT, b"Spam", fin=False))
+ # The spec says: "An endpoint MUST be capable of handling control
+ # frames in the middle of a fragmented message." However, since the
+ # endpoint must not send a data frame after a close frame, a close
+ # frame can't be "in the middle" of a fragmented message.
+ with self.assertRaises(ProtocolError) as raised:
+ client.send_close(CloseCode.GOING_AWAY)
+ self.assertEqual(str(raised.exception), "expected a continuation frame")
+ client.send_continuation(b"Eggs", fin=True)
+
+ def test_server_send_close_in_fragmented_message(self):
+ server = Protocol(CLIENT)
+ server.send_text(b"Spam", fin=False)
+ self.assertFrameSent(server, Frame(OP_TEXT, b"Spam", fin=False))
+ # The spec says: "An endpoint MUST be capable of handling control
+ # frames in the middle of a fragmented message." However, since the
+ # endpoint must not send a data frame after a close frame, a close
+ # frame can't be "in the middle" of a fragmented message.
+ with self.assertRaises(ProtocolError) as raised:
+ server.send_close(CloseCode.NORMAL_CLOSURE)
+ self.assertEqual(str(raised.exception), "expected a continuation frame")
+
+ def test_client_receive_close_in_fragmented_message(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x01\x04Spam")
+ self.assertFrameReceived(
+ client,
+ Frame(OP_TEXT, b"Spam", fin=False),
+ )
+ # The spec says: "An endpoint MUST be capable of handling control
+ # frames in the middle of a fragmented message." However, since the
+ # endpoint must not send a data frame after a close frame, a close
+ # frame can't be "in the middle" of a fragmented message.
+ client.receive_data(b"\x88\x02\x03\xe8")
+ self.assertIsInstance(client.parser_exc, ProtocolError)
+ self.assertEqual(str(client.parser_exc), "incomplete fragmented message")
+ self.assertConnectionFailing(
+ client, CloseCode.PROTOCOL_ERROR, "incomplete fragmented message"
+ )
+
+ def test_server_receive_close_in_fragmented_message(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x01\x84\x00\x00\x00\x00Spam")
+ self.assertFrameReceived(
+ server,
+ Frame(OP_TEXT, b"Spam", fin=False),
+ )
+ # The spec says: "An endpoint MUST be capable of handling control
+ # frames in the middle of a fragmented message." However, since the
+ # endpoint must not send a data frame after a close frame, a close
+ # frame can't be "in the middle" of a fragmented message.
+ server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9")
+ self.assertIsInstance(server.parser_exc, ProtocolError)
+ self.assertEqual(str(server.parser_exc), "incomplete fragmented message")
+ self.assertConnectionFailing(
+ server, CloseCode.PROTOCOL_ERROR, "incomplete fragmented message"
+ )
+
+
+class EOFTests(ProtocolTestCase):
+ """
+ Test half-closes on connection termination.
+
+ """
+
+ def test_client_receives_eof(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x00")
+ self.assertConnectionClosing(client)
+ client.receive_eof()
+ self.assertIs(client.state, CLOSED)
+
+ def test_server_receives_eof(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c")
+ self.assertConnectionClosing(server)
+ server.receive_eof()
+ self.assertIs(server.state, CLOSED)
+
+ def test_client_receives_eof_between_frames(self):
+ client = Protocol(CLIENT)
+ client.receive_eof()
+ self.assertIsInstance(client.parser_exc, EOFError)
+ self.assertEqual(str(client.parser_exc), "unexpected end of stream")
+ self.assertIs(client.state, CLOSED)
+
+ def test_server_receives_eof_between_frames(self):
+ server = Protocol(SERVER)
+ server.receive_eof()
+ self.assertIsInstance(server.parser_exc, EOFError)
+ self.assertEqual(str(server.parser_exc), "unexpected end of stream")
+ self.assertIs(server.state, CLOSED)
+
+ def test_client_receives_eof_inside_frame(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x81")
+ client.receive_eof()
+ self.assertIsInstance(client.parser_exc, EOFError)
+ self.assertEqual(
+ str(client.parser_exc),
+ "stream ends after 1 bytes, expected 2 bytes",
+ )
+ self.assertIs(client.state, CLOSED)
+
+ def test_server_receives_eof_inside_frame(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x81")
+ server.receive_eof()
+ self.assertIsInstance(server.parser_exc, EOFError)
+ self.assertEqual(
+ str(server.parser_exc),
+ "stream ends after 1 bytes, expected 2 bytes",
+ )
+ self.assertIs(server.state, CLOSED)
+
+ def test_client_receives_data_after_exception(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\xff\xff")
+ self.assertConnectionFailing(client, CloseCode.PROTOCOL_ERROR, "invalid opcode")
+ client.receive_data(b"\x00\x00")
+ self.assertFrameSent(client, None)
+
+ def test_server_receives_data_after_exception(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\xff\xff")
+ self.assertConnectionFailing(server, CloseCode.PROTOCOL_ERROR, "invalid opcode")
+ server.receive_data(b"\x00\x00")
+ self.assertFrameSent(server, None)
+
+ def test_client_receives_eof_after_exception(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\xff\xff")
+ self.assertConnectionFailing(client, CloseCode.PROTOCOL_ERROR, "invalid opcode")
+ client.receive_eof()
+ self.assertFrameSent(client, None, eof=True)
+
+ def test_server_receives_eof_after_exception(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\xff\xff")
+ self.assertConnectionFailing(server, CloseCode.PROTOCOL_ERROR, "invalid opcode")
+ server.receive_eof()
+ self.assertFrameSent(server, None)
+
+ def test_client_receives_data_and_eof_after_exception(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\xff\xff")
+ self.assertConnectionFailing(client, CloseCode.PROTOCOL_ERROR, "invalid opcode")
+ client.receive_data(b"\x00\x00")
+ client.receive_eof()
+ self.assertFrameSent(client, None, eof=True)
+
+ def test_server_receives_data_and_eof_after_exception(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\xff\xff")
+ self.assertConnectionFailing(server, CloseCode.PROTOCOL_ERROR, "invalid opcode")
+ server.receive_data(b"\x00\x00")
+ server.receive_eof()
+ self.assertFrameSent(server, None)
+
+ def test_client_receives_data_after_eof(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x00")
+ self.assertConnectionClosing(client)
+ client.receive_eof()
+ with self.assertRaises(EOFError) as raised:
+ client.receive_data(b"\x88\x00")
+ self.assertEqual(str(raised.exception), "stream ended")
+
+ def test_server_receives_data_after_eof(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c")
+ self.assertConnectionClosing(server)
+ server.receive_eof()
+ with self.assertRaises(EOFError) as raised:
+ server.receive_data(b"\x88\x80\x00\x00\x00\x00")
+ self.assertEqual(str(raised.exception), "stream ended")
+
+ def test_client_receives_eof_after_eof(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x00")
+ self.assertConnectionClosing(client)
+ client.receive_eof()
+ with self.assertRaises(EOFError) as raised:
+ client.receive_eof()
+ self.assertEqual(str(raised.exception), "stream ended")
+
+ def test_server_receives_eof_after_eof(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c")
+ self.assertConnectionClosing(server)
+ server.receive_eof()
+ with self.assertRaises(EOFError) as raised:
+ server.receive_eof()
+ self.assertEqual(str(raised.exception), "stream ended")
+
+
+class TCPCloseTests(ProtocolTestCase):
+ """
+ Test expectation of TCP close on connection termination.
+
+ """
+
+ def test_client_default(self):
+ client = Protocol(CLIENT)
+ self.assertFalse(client.close_expected())
+
+ def test_server_default(self):
+ server = Protocol(SERVER)
+ self.assertFalse(server.close_expected())
+
+ def test_client_sends_close(self):
+ client = Protocol(CLIENT)
+ client.send_close()
+ self.assertTrue(client.close_expected())
+
+ def test_server_sends_close(self):
+ server = Protocol(SERVER)
+ server.send_close()
+ self.assertTrue(server.close_expected())
+
+ def test_client_receives_close(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x00")
+ self.assertTrue(client.close_expected())
+
+ def test_client_receives_close_then_eof(self):
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x00")
+ client.receive_eof()
+ self.assertFalse(client.close_expected())
+
+ def test_server_receives_close_then_eof(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c")
+ server.receive_eof()
+ self.assertFalse(server.close_expected())
+
+ def test_server_receives_close(self):
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c")
+ self.assertTrue(server.close_expected())
+
+ def test_client_fails_connection(self):
+ client = Protocol(CLIENT)
+ client.fail(CloseCode.PROTOCOL_ERROR)
+ self.assertTrue(client.close_expected())
+
+ def test_server_fails_connection(self):
+ server = Protocol(SERVER)
+ server.fail(CloseCode.PROTOCOL_ERROR)
+ self.assertTrue(server.close_expected())
+
+
+class ConnectionClosedTests(ProtocolTestCase):
+ """
+ Test connection closed exception.
+
+ """
+
+ def test_client_sends_close_then_receives_close(self):
+ # Client-initiated close handshake on the client side complete.
+ client = Protocol(CLIENT)
+ client.send_close(CloseCode.NORMAL_CLOSURE, "")
+ client.receive_data(b"\x88\x02\x03\xe8")
+ client.receive_eof()
+ exc = client.close_exc
+ self.assertIsInstance(exc, ConnectionClosedOK)
+ self.assertEqual(exc.rcvd, Close(CloseCode.NORMAL_CLOSURE, ""))
+ self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, ""))
+ self.assertFalse(exc.rcvd_then_sent)
+
+ def test_server_sends_close_then_receives_close(self):
+ # Server-initiated close handshake on the server side complete.
+ server = Protocol(SERVER)
+ server.send_close(CloseCode.NORMAL_CLOSURE, "")
+ server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe8")
+ server.receive_eof()
+ exc = server.close_exc
+ self.assertIsInstance(exc, ConnectionClosedOK)
+ self.assertEqual(exc.rcvd, Close(CloseCode.NORMAL_CLOSURE, ""))
+ self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, ""))
+ self.assertFalse(exc.rcvd_then_sent)
+
+ def test_client_receives_close_then_sends_close(self):
+ # Server-initiated close handshake on the client side complete.
+ client = Protocol(CLIENT)
+ client.receive_data(b"\x88\x02\x03\xe8")
+ client.receive_eof()
+ exc = client.close_exc
+ self.assertIsInstance(exc, ConnectionClosedOK)
+ self.assertEqual(exc.rcvd, Close(CloseCode.NORMAL_CLOSURE, ""))
+ self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, ""))
+ self.assertTrue(exc.rcvd_then_sent)
+
+ def test_server_receives_close_then_sends_close(self):
+ # Client-initiated close handshake on the server side complete.
+ server = Protocol(SERVER)
+ server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe8")
+ server.receive_eof()
+ exc = server.close_exc
+ self.assertIsInstance(exc, ConnectionClosedOK)
+ self.assertEqual(exc.rcvd, Close(CloseCode.NORMAL_CLOSURE, ""))
+ self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, ""))
+ self.assertTrue(exc.rcvd_then_sent)
+
+ def test_client_sends_close_then_receives_eof(self):
+ # Client-initiated close handshake on the client side times out.
+ client = Protocol(CLIENT)
+ client.send_close(CloseCode.NORMAL_CLOSURE, "")
+ client.receive_eof()
+ exc = client.close_exc
+ self.assertIsInstance(exc, ConnectionClosedError)
+ self.assertIsNone(exc.rcvd)
+ self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, ""))
+ self.assertIsNone(exc.rcvd_then_sent)
+
+ def test_server_sends_close_then_receives_eof(self):
+ # Server-initiated close handshake on the server side times out.
+ server = Protocol(SERVER)
+ server.send_close(CloseCode.NORMAL_CLOSURE, "")
+ server.receive_eof()
+ exc = server.close_exc
+ self.assertIsInstance(exc, ConnectionClosedError)
+ self.assertIsNone(exc.rcvd)
+ self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, ""))
+ self.assertIsNone(exc.rcvd_then_sent)
+
+ def test_client_receives_eof(self):
+ # Server-initiated close handshake on the client side times out.
+ client = Protocol(CLIENT)
+ client.receive_eof()
+ exc = client.close_exc
+ self.assertIsInstance(exc, ConnectionClosedError)
+ self.assertIsNone(exc.rcvd)
+ self.assertIsNone(exc.sent)
+ self.assertIsNone(exc.rcvd_then_sent)
+
+ def test_server_receives_eof(self):
+ # Client-initiated close handshake on the server side times out.
+ server = Protocol(SERVER)
+ server.receive_eof()
+ exc = server.close_exc
+ self.assertIsInstance(exc, ConnectionClosedError)
+ self.assertIsNone(exc.rcvd)
+ self.assertIsNone(exc.sent)
+ self.assertIsNone(exc.rcvd_then_sent)
+
+
+class ErrorTests(ProtocolTestCase):
+ """
+ Test other error cases.
+
+ """
+
+ def test_client_hits_internal_error_reading_frame(self):
+ client = Protocol(CLIENT)
+ # This isn't supposed to happen, so we're simulating it.
+ with unittest.mock.patch("struct.unpack", side_effect=RuntimeError("BOOM")):
+ client.receive_data(b"\x81\x00")
+ self.assertIsInstance(client.parser_exc, RuntimeError)
+ self.assertEqual(str(client.parser_exc), "BOOM")
+ self.assertConnectionFailing(client, CloseCode.INTERNAL_ERROR, "")
+
+ def test_server_hits_internal_error_reading_frame(self):
+ server = Protocol(SERVER)
+ # This isn't supposed to happen, so we're simulating it.
+ with unittest.mock.patch("struct.unpack", side_effect=RuntimeError("BOOM")):
+ server.receive_data(b"\x81\x80\x00\x00\x00\x00")
+ self.assertIsInstance(server.parser_exc, RuntimeError)
+ self.assertEqual(str(server.parser_exc), "BOOM")
+ self.assertConnectionFailing(server, CloseCode.INTERNAL_ERROR, "")
+
+
+class ExtensionsTests(ProtocolTestCase):
+ """
+ Test how extensions affect frames.
+
+ """
+
+ def test_client_extension_encodes_frame(self):
+ client = Protocol(CLIENT)
+ client.extensions = [Rsv2Extension()]
+ with self.enforce_mask(b"\x00\x44\x88\xcc"):
+ client.send_ping(b"")
+ self.assertEqual(client.data_to_send(), [b"\xa9\x80\x00\x44\x88\xcc"])
+
+ def test_server_extension_encodes_frame(self):
+ server = Protocol(SERVER)
+ server.extensions = [Rsv2Extension()]
+ server.send_ping(b"")
+ self.assertEqual(server.data_to_send(), [b"\xa9\x00"])
+
+ def test_client_extension_decodes_frame(self):
+ client = Protocol(CLIENT)
+ client.extensions = [Rsv2Extension()]
+ client.receive_data(b"\xaa\x00")
+ self.assertEqual(client.events_received(), [Frame(OP_PONG, b"")])
+
+ def test_server_extension_decodes_frame(self):
+ server = Protocol(SERVER)
+ server.extensions = [Rsv2Extension()]
+ server.receive_data(b"\xaa\x80\x00\x44\x88\xcc")
+ self.assertEqual(server.events_received(), [Frame(OP_PONG, b"")])
+
+
+class MiscTests(unittest.TestCase):
+ def test_client_default_logger(self):
+ client = Protocol(CLIENT)
+ logger = logging.getLogger("websockets.client")
+ self.assertIs(client.logger, logger)
+
+ def test_server_default_logger(self):
+ server = Protocol(SERVER)
+ logger = logging.getLogger("websockets.server")
+ self.assertIs(server.logger, logger)
+
+ def test_client_custom_logger(self):
+ logger = logging.getLogger("test")
+ client = Protocol(CLIENT, logger=logger)
+ self.assertIs(client.logger, logger)
+
+ def test_server_custom_logger(self):
+ logger = logging.getLogger("test")
+ server = Protocol(SERVER, logger=logger)
+ self.assertIs(server.logger, logger)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_server.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_server.py
new file mode 100644
index 0000000000..b6f5e35681
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_server.py
@@ -0,0 +1,686 @@
+import http
+import logging
+import unittest
+import unittest.mock
+
+from websockets.datastructures import Headers
+from websockets.exceptions import (
+ InvalidHeader,
+ InvalidOrigin,
+ InvalidUpgrade,
+ NegotiationError,
+)
+from websockets.frames import OP_TEXT, Frame
+from websockets.http11 import Request, Response
+from websockets.protocol import CONNECTING, OPEN
+from websockets.server import *
+
+from .extensions.utils import (
+ OpExtension,
+ Rsv2Extension,
+ ServerOpExtensionFactory,
+ ServerRsv2ExtensionFactory,
+)
+from .test_utils import ACCEPT, KEY
+from .utils import DATE, DeprecationTestCase
+
+
+class ConnectTests(unittest.TestCase):
+ def test_receive_connect(self):
+ server = ServerProtocol()
+ server.receive_data(
+ (
+ f"GET /test HTTP/1.1\r\n"
+ f"Host: example.com\r\n"
+ f"Upgrade: websocket\r\n"
+ f"Connection: Upgrade\r\n"
+ f"Sec-WebSocket-Key: {KEY}\r\n"
+ f"Sec-WebSocket-Version: 13\r\n"
+ f"\r\n"
+ ).encode(),
+ )
+ [request] = server.events_received()
+ self.assertIsInstance(request, Request)
+ self.assertEqual(server.data_to_send(), [])
+ self.assertFalse(server.close_expected())
+
+ def test_connect_request(self):
+ server = ServerProtocol()
+ server.receive_data(
+ (
+ f"GET /test HTTP/1.1\r\n"
+ f"Host: example.com\r\n"
+ f"Upgrade: websocket\r\n"
+ f"Connection: Upgrade\r\n"
+ f"Sec-WebSocket-Key: {KEY}\r\n"
+ f"Sec-WebSocket-Version: 13\r\n"
+ f"\r\n"
+ ).encode(),
+ )
+ [request] = server.events_received()
+ self.assertEqual(request.path, "/test")
+ self.assertEqual(
+ request.headers,
+ Headers(
+ {
+ "Host": "example.com",
+ "Upgrade": "websocket",
+ "Connection": "Upgrade",
+ "Sec-WebSocket-Key": KEY,
+ "Sec-WebSocket-Version": "13",
+ }
+ ),
+ )
+
+ def test_no_request(self):
+ server = ServerProtocol()
+ server.receive_eof()
+ self.assertEqual(server.events_received(), [])
+
+ def test_partial_request(self):
+ server = ServerProtocol()
+ server.receive_data(b"GET /test HTTP/1.1\r\n")
+ server.receive_eof()
+ self.assertEqual(server.events_received(), [])
+
+ def test_random_request(self):
+ server = ServerProtocol()
+ server.receive_data(b"HELO relay.invalid\r\n")
+ server.receive_data(b"MAIL FROM: <alice@invalid>\r\n")
+ server.receive_data(b"RCPT TO: <bob@invalid>\r\n")
+ self.assertEqual(server.events_received(), [])
+
+
+class AcceptRejectTests(unittest.TestCase):
+ def make_request(self):
+ return Request(
+ path="/test",
+ headers=Headers(
+ {
+ "Host": "example.com",
+ "Upgrade": "websocket",
+ "Connection": "Upgrade",
+ "Sec-WebSocket-Key": KEY,
+ "Sec-WebSocket-Version": "13",
+ }
+ ),
+ )
+
+ def test_send_accept(self):
+ server = ServerProtocol()
+ with unittest.mock.patch("email.utils.formatdate", return_value=DATE):
+ response = server.accept(self.make_request())
+ self.assertIsInstance(response, Response)
+ server.send_response(response)
+ self.assertEqual(
+ server.data_to_send(),
+ [
+ f"HTTP/1.1 101 Switching Protocols\r\n"
+ f"Date: {DATE}\r\n"
+ f"Upgrade: websocket\r\n"
+ f"Connection: Upgrade\r\n"
+ f"Sec-WebSocket-Accept: {ACCEPT}\r\n"
+ f"\r\n".encode()
+ ],
+ )
+ self.assertFalse(server.close_expected())
+ self.assertEqual(server.state, OPEN)
+
+ def test_send_reject(self):
+ server = ServerProtocol()
+ with unittest.mock.patch("email.utils.formatdate", return_value=DATE):
+ response = server.reject(http.HTTPStatus.NOT_FOUND, "Sorry folks.\n")
+ self.assertIsInstance(response, Response)
+ server.send_response(response)
+ self.assertEqual(
+ server.data_to_send(),
+ [
+ f"HTTP/1.1 404 Not Found\r\n"
+ f"Date: {DATE}\r\n"
+ f"Connection: close\r\n"
+ f"Content-Length: 13\r\n"
+ f"Content-Type: text/plain; charset=utf-8\r\n"
+ f"\r\n"
+ f"Sorry folks.\n".encode(),
+ b"",
+ ],
+ )
+ self.assertTrue(server.close_expected())
+ self.assertEqual(server.state, CONNECTING)
+
+ def test_accept_response(self):
+ server = ServerProtocol()
+ with unittest.mock.patch("email.utils.formatdate", return_value=DATE):
+ response = server.accept(self.make_request())
+ self.assertIsInstance(response, Response)
+ self.assertEqual(response.status_code, 101)
+ self.assertEqual(response.reason_phrase, "Switching Protocols")
+ self.assertEqual(
+ response.headers,
+ Headers(
+ {
+ "Date": DATE,
+ "Upgrade": "websocket",
+ "Connection": "Upgrade",
+ "Sec-WebSocket-Accept": ACCEPT,
+ }
+ ),
+ )
+ self.assertIsNone(response.body)
+
+ def test_reject_response(self):
+ server = ServerProtocol()
+ with unittest.mock.patch("email.utils.formatdate", return_value=DATE):
+ response = server.reject(http.HTTPStatus.NOT_FOUND, "Sorry folks.\n")
+ self.assertIsInstance(response, Response)
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(response.reason_phrase, "Not Found")
+ self.assertEqual(
+ response.headers,
+ Headers(
+ {
+ "Date": DATE,
+ "Connection": "close",
+ "Content-Length": "13",
+ "Content-Type": "text/plain; charset=utf-8",
+ }
+ ),
+ )
+ self.assertEqual(response.body, b"Sorry folks.\n")
+
+ def test_reject_response_supports_int_status(self):
+ server = ServerProtocol()
+ response = server.reject(404, "Sorry folks.\n")
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(response.reason_phrase, "Not Found")
+
+ def test_basic(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+
+ def test_unexpected_exception(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ with unittest.mock.patch(
+ "websockets.server.ServerProtocol.process_request",
+ side_effect=Exception("BOOM"),
+ ):
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 500)
+ with self.assertRaises(Exception) as raised:
+ raise server.handshake_exc
+ self.assertEqual(str(raised.exception), "BOOM")
+
+ def test_missing_connection(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ del request.headers["Connection"]
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 426)
+ self.assertEqual(response.headers["Upgrade"], "websocket")
+ with self.assertRaises(InvalidUpgrade) as raised:
+ raise server.handshake_exc
+ self.assertEqual(str(raised.exception), "missing Connection header")
+
+ def test_invalid_connection(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ del request.headers["Connection"]
+ request.headers["Connection"] = "close"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 426)
+ self.assertEqual(response.headers["Upgrade"], "websocket")
+ with self.assertRaises(InvalidUpgrade) as raised:
+ raise server.handshake_exc
+ self.assertEqual(str(raised.exception), "invalid Connection header: close")
+
+ def test_missing_upgrade(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ del request.headers["Upgrade"]
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 426)
+ self.assertEqual(response.headers["Upgrade"], "websocket")
+ with self.assertRaises(InvalidUpgrade) as raised:
+ raise server.handshake_exc
+ self.assertEqual(str(raised.exception), "missing Upgrade header")
+
+ def test_invalid_upgrade(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ del request.headers["Upgrade"]
+ request.headers["Upgrade"] = "h2c"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 426)
+ self.assertEqual(response.headers["Upgrade"], "websocket")
+ with self.assertRaises(InvalidUpgrade) as raised:
+ raise server.handshake_exc
+ self.assertEqual(str(raised.exception), "invalid Upgrade header: h2c")
+
+ def test_missing_key(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ del request.headers["Sec-WebSocket-Key"]
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 400)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise server.handshake_exc
+ self.assertEqual(str(raised.exception), "missing Sec-WebSocket-Key header")
+
+ def test_multiple_key(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Key"] = KEY
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 400)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise server.handshake_exc
+ self.assertEqual(
+ str(raised.exception),
+ "invalid Sec-WebSocket-Key header: "
+ "more than one Sec-WebSocket-Key header found",
+ )
+
+ def test_invalid_key(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ del request.headers["Sec-WebSocket-Key"]
+ request.headers["Sec-WebSocket-Key"] = "not Base64 data!"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 400)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise server.handshake_exc
+ self.assertEqual(
+ str(raised.exception), "invalid Sec-WebSocket-Key header: not Base64 data!"
+ )
+
+ def test_truncated_key(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ del request.headers["Sec-WebSocket-Key"]
+ request.headers["Sec-WebSocket-Key"] = KEY[
+ :16
+ ] # 12 bytes instead of 16, Base64-encoded
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 400)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise server.handshake_exc
+ self.assertEqual(
+ str(raised.exception), f"invalid Sec-WebSocket-Key header: {KEY[:16]}"
+ )
+
+ def test_missing_version(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ del request.headers["Sec-WebSocket-Version"]
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 400)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise server.handshake_exc
+ self.assertEqual(str(raised.exception), "missing Sec-WebSocket-Version header")
+
+ def test_multiple_version(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Version"] = "11"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 400)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise server.handshake_exc
+ self.assertEqual(
+ str(raised.exception),
+ "invalid Sec-WebSocket-Version header: "
+ "more than one Sec-WebSocket-Version header found",
+ )
+
+ def test_invalid_version(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ del request.headers["Sec-WebSocket-Version"]
+ request.headers["Sec-WebSocket-Version"] = "11"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 400)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise server.handshake_exc
+ self.assertEqual(
+ str(raised.exception), "invalid Sec-WebSocket-Version header: 11"
+ )
+
+ def test_no_origin(self):
+ server = ServerProtocol(origins=["https://example.com"])
+ request = self.make_request()
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 403)
+ with self.assertRaises(InvalidOrigin) as raised:
+ raise server.handshake_exc
+ self.assertEqual(str(raised.exception), "missing Origin header")
+
+ def test_origin(self):
+ server = ServerProtocol(origins=["https://example.com"])
+ request = self.make_request()
+ request.headers["Origin"] = "https://example.com"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertEqual(server.origin, "https://example.com")
+
+ def test_unexpected_origin(self):
+ server = ServerProtocol(origins=["https://example.com"])
+ request = self.make_request()
+ request.headers["Origin"] = "https://other.example.com"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 403)
+ with self.assertRaises(InvalidOrigin) as raised:
+ raise server.handshake_exc
+ self.assertEqual(
+ str(raised.exception), "invalid Origin header: https://other.example.com"
+ )
+
+ def test_multiple_origin(self):
+ server = ServerProtocol(
+ origins=["https://example.com", "https://other.example.com"]
+ )
+ request = self.make_request()
+ request.headers["Origin"] = "https://example.com"
+ request.headers["Origin"] = "https://other.example.com"
+ response = server.accept(request)
+
+ # This is prohibited by the HTTP specification, so the return code is
+ # 400 Bad Request rather than 403 Forbidden.
+ self.assertEqual(response.status_code, 400)
+ with self.assertRaises(InvalidHeader) as raised:
+ raise server.handshake_exc
+ self.assertEqual(
+ str(raised.exception),
+ "invalid Origin header: more than one Origin header found",
+ )
+
+ def test_supported_origin(self):
+ server = ServerProtocol(
+ origins=["https://example.com", "https://other.example.com"]
+ )
+ request = self.make_request()
+ request.headers["Origin"] = "https://other.example.com"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertEqual(server.origin, "https://other.example.com")
+
+ def test_unsupported_origin(self):
+ server = ServerProtocol(
+ origins=["https://example.com", "https://other.example.com"]
+ )
+ request = self.make_request()
+ request.headers["Origin"] = "https://original.example.com"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 403)
+ with self.assertRaises(InvalidOrigin) as raised:
+ raise server.handshake_exc
+ self.assertEqual(
+ str(raised.exception), "invalid Origin header: https://original.example.com"
+ )
+
+ def test_no_origin_accepted(self):
+ server = ServerProtocol(origins=[None])
+ request = self.make_request()
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertIsNone(server.origin)
+
+ def test_no_extensions(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertNotIn("Sec-WebSocket-Extensions", response.headers)
+ self.assertEqual(server.extensions, [])
+
+ def test_no_extension(self):
+ server = ServerProtocol(extensions=[ServerOpExtensionFactory()])
+ request = self.make_request()
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertNotIn("Sec-WebSocket-Extensions", response.headers)
+ self.assertEqual(server.extensions, [])
+
+ def test_extension(self):
+ server = ServerProtocol(extensions=[ServerOpExtensionFactory()])
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Extensions"] = "x-op; op"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertEqual(response.headers["Sec-WebSocket-Extensions"], "x-op; op")
+ self.assertEqual(server.extensions, [OpExtension()])
+
+ def test_unexpected_extension(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Extensions"] = "x-op; op"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertNotIn("Sec-WebSocket-Extensions", response.headers)
+ self.assertEqual(server.extensions, [])
+
+ def test_unsupported_extension(self):
+ server = ServerProtocol(extensions=[ServerRsv2ExtensionFactory()])
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Extensions"] = "x-op; op"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertNotIn("Sec-WebSocket-Extensions", response.headers)
+ self.assertEqual(server.extensions, [])
+
+ def test_supported_extension_parameters(self):
+ server = ServerProtocol(extensions=[ServerOpExtensionFactory("this")])
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Extensions"] = "x-op; op=this"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertEqual(response.headers["Sec-WebSocket-Extensions"], "x-op; op=this")
+ self.assertEqual(server.extensions, [OpExtension("this")])
+
+ def test_unsupported_extension_parameters(self):
+ server = ServerProtocol(extensions=[ServerOpExtensionFactory("this")])
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Extensions"] = "x-op; op=that"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertNotIn("Sec-WebSocket-Extensions", response.headers)
+ self.assertEqual(server.extensions, [])
+
+ def test_multiple_supported_extension_parameters(self):
+ server = ServerProtocol(
+ extensions=[
+ ServerOpExtensionFactory("this"),
+ ServerOpExtensionFactory("that"),
+ ]
+ )
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Extensions"] = "x-op; op=that"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertEqual(response.headers["Sec-WebSocket-Extensions"], "x-op; op=that")
+ self.assertEqual(server.extensions, [OpExtension("that")])
+
+ def test_multiple_extensions(self):
+ server = ServerProtocol(
+ extensions=[ServerOpExtensionFactory(), ServerRsv2ExtensionFactory()]
+ )
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Extensions"] = "x-op; op"
+ request.headers["Sec-WebSocket-Extensions"] = "x-rsv2"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertEqual(
+ response.headers["Sec-WebSocket-Extensions"], "x-op; op, x-rsv2"
+ )
+ self.assertEqual(server.extensions, [OpExtension(), Rsv2Extension()])
+
+ def test_multiple_extensions_order(self):
+ server = ServerProtocol(
+ extensions=[ServerOpExtensionFactory(), ServerRsv2ExtensionFactory()]
+ )
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Extensions"] = "x-rsv2"
+ request.headers["Sec-WebSocket-Extensions"] = "x-op; op"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertEqual(
+ response.headers["Sec-WebSocket-Extensions"], "x-rsv2, x-op; op"
+ )
+ self.assertEqual(server.extensions, [Rsv2Extension(), OpExtension()])
+
+ def test_no_subprotocols(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertNotIn("Sec-WebSocket-Protocol", response.headers)
+ self.assertIsNone(server.subprotocol)
+
+ def test_no_subprotocol(self):
+ server = ServerProtocol(subprotocols=["chat"])
+ request = self.make_request()
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 400)
+ with self.assertRaisesRegex(
+ NegotiationError,
+ r"missing subprotocol",
+ ):
+ raise server.handshake_exc
+
+ def test_subprotocol(self):
+ server = ServerProtocol(subprotocols=["chat"])
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Protocol"] = "chat"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertEqual(response.headers["Sec-WebSocket-Protocol"], "chat")
+ self.assertEqual(server.subprotocol, "chat")
+
+ def test_unexpected_subprotocol(self):
+ server = ServerProtocol()
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Protocol"] = "chat"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertNotIn("Sec-WebSocket-Protocol", response.headers)
+ self.assertIsNone(server.subprotocol)
+
+ def test_multiple_subprotocols(self):
+ server = ServerProtocol(subprotocols=["superchat", "chat"])
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Protocol"] = "chat"
+ request.headers["Sec-WebSocket-Protocol"] = "superchat"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertEqual(response.headers["Sec-WebSocket-Protocol"], "superchat")
+ self.assertEqual(server.subprotocol, "superchat")
+
+ def test_supported_subprotocol(self):
+ server = ServerProtocol(subprotocols=["superchat", "chat"])
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Protocol"] = "chat"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertEqual(response.headers["Sec-WebSocket-Protocol"], "chat")
+ self.assertEqual(server.subprotocol, "chat")
+
+ def test_unsupported_subprotocol(self):
+ server = ServerProtocol(subprotocols=["superchat", "chat"])
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Protocol"] = "otherchat"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 400)
+ with self.assertRaisesRegex(
+ NegotiationError,
+ r"invalid subprotocol; expected one of superchat, chat",
+ ):
+ raise server.handshake_exc
+
+ @staticmethod
+ def optional_chat(protocol, subprotocols):
+ if "chat" in subprotocols:
+ return "chat"
+
+ def test_select_subprotocol(self):
+ server = ServerProtocol(select_subprotocol=self.optional_chat)
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Protocol"] = "chat"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertEqual(response.headers["Sec-WebSocket-Protocol"], "chat")
+ self.assertEqual(server.subprotocol, "chat")
+
+ def test_select_no_subprotocol(self):
+ server = ServerProtocol(select_subprotocol=self.optional_chat)
+ request = self.make_request()
+ request.headers["Sec-WebSocket-Protocol"] = "otherchat"
+ response = server.accept(request)
+
+ self.assertEqual(response.status_code, 101)
+ self.assertNotIn("Sec-WebSocket-Protocol", response.headers)
+ self.assertIsNone(server.subprotocol)
+
+
+class MiscTests(unittest.TestCase):
+ def test_bypass_handshake(self):
+ server = ServerProtocol(state=OPEN)
+ server.receive_data(b"\x81\x86\x00\x00\x00\x00Hello!")
+ [frame] = server.events_received()
+ self.assertEqual(frame, Frame(OP_TEXT, b"Hello!"))
+
+ def test_custom_logger(self):
+ logger = logging.getLogger("test")
+ with self.assertLogs("test", logging.DEBUG) as logs:
+ ServerProtocol(logger=logger)
+ self.assertEqual(len(logs.records), 1)
+
+
+class BackwardsCompatibilityTests(DeprecationTestCase):
+ def test_server_connection_class(self):
+ with self.assertDeprecationWarning(
+ "ServerConnection was renamed to ServerProtocol"
+ ):
+ from websockets.server import ServerConnection
+
+ server = ServerConnection()
+
+ self.assertIsInstance(server, ServerProtocol)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_streams.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_streams.py
new file mode 100644
index 0000000000..fd7c66a0bd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_streams.py
@@ -0,0 +1,198 @@
+from websockets.streams import StreamReader
+
+from .utils import GeneratorTestCase
+
+
+class StreamReaderTests(GeneratorTestCase):
+ def setUp(self):
+ self.reader = StreamReader()
+
+ def test_read_line(self):
+ self.reader.feed_data(b"spam\neggs\n")
+
+ gen = self.reader.read_line(32)
+ line = self.assertGeneratorReturns(gen)
+ self.assertEqual(line, b"spam\n")
+
+ gen = self.reader.read_line(32)
+ line = self.assertGeneratorReturns(gen)
+ self.assertEqual(line, b"eggs\n")
+
+ def test_read_line_need_more_data(self):
+ self.reader.feed_data(b"spa")
+
+ gen = self.reader.read_line(32)
+ self.assertGeneratorRunning(gen)
+ self.reader.feed_data(b"m\neg")
+ line = self.assertGeneratorReturns(gen)
+ self.assertEqual(line, b"spam\n")
+
+ gen = self.reader.read_line(32)
+ self.assertGeneratorRunning(gen)
+ self.reader.feed_data(b"gs\n")
+ line = self.assertGeneratorReturns(gen)
+ self.assertEqual(line, b"eggs\n")
+
+ def test_read_line_not_enough_data(self):
+ self.reader.feed_data(b"spa")
+ self.reader.feed_eof()
+
+ gen = self.reader.read_line(32)
+ with self.assertRaises(EOFError) as raised:
+ next(gen)
+ self.assertEqual(
+ str(raised.exception),
+ "stream ends after 3 bytes, before end of line",
+ )
+
+ def test_read_line_too_long(self):
+ self.reader.feed_data(b"spam\neggs\n")
+
+ gen = self.reader.read_line(2)
+ with self.assertRaises(RuntimeError) as raised:
+ next(gen)
+ self.assertEqual(
+ str(raised.exception),
+ "read 5 bytes, expected no more than 2 bytes",
+ )
+
+ def test_read_line_too_long_need_more_data(self):
+ self.reader.feed_data(b"spa")
+
+ gen = self.reader.read_line(2)
+ with self.assertRaises(RuntimeError) as raised:
+ next(gen)
+ self.assertEqual(
+ str(raised.exception),
+ "read 3 bytes, expected no more than 2 bytes",
+ )
+
+ def test_read_exact(self):
+ self.reader.feed_data(b"spameggs")
+
+ gen = self.reader.read_exact(4)
+ data = self.assertGeneratorReturns(gen)
+ self.assertEqual(data, b"spam")
+
+ gen = self.reader.read_exact(4)
+ data = self.assertGeneratorReturns(gen)
+ self.assertEqual(data, b"eggs")
+
+ def test_read_exact_need_more_data(self):
+ self.reader.feed_data(b"spa")
+
+ gen = self.reader.read_exact(4)
+ self.assertGeneratorRunning(gen)
+ self.reader.feed_data(b"meg")
+ data = self.assertGeneratorReturns(gen)
+ self.assertEqual(data, b"spam")
+
+ gen = self.reader.read_exact(4)
+ self.assertGeneratorRunning(gen)
+ self.reader.feed_data(b"gs")
+ data = self.assertGeneratorReturns(gen)
+ self.assertEqual(data, b"eggs")
+
+ def test_read_exact_not_enough_data(self):
+ self.reader.feed_data(b"spa")
+ self.reader.feed_eof()
+
+ gen = self.reader.read_exact(4)
+ with self.assertRaises(EOFError) as raised:
+ next(gen)
+ self.assertEqual(
+ str(raised.exception),
+ "stream ends after 3 bytes, expected 4 bytes",
+ )
+
+ def test_read_to_eof(self):
+ gen = self.reader.read_to_eof(32)
+
+ self.reader.feed_data(b"spam")
+ self.assertGeneratorRunning(gen)
+
+ self.reader.feed_eof()
+ data = self.assertGeneratorReturns(gen)
+ self.assertEqual(data, b"spam")
+
+ def test_read_to_eof_at_eof(self):
+ self.reader.feed_eof()
+
+ gen = self.reader.read_to_eof(32)
+ data = self.assertGeneratorReturns(gen)
+ self.assertEqual(data, b"")
+
+ def test_read_to_eof_too_long(self):
+ gen = self.reader.read_to_eof(2)
+
+ self.reader.feed_data(b"spam")
+ with self.assertRaises(RuntimeError) as raised:
+ next(gen)
+ self.assertEqual(
+ str(raised.exception),
+ "read 4 bytes, expected no more than 2 bytes",
+ )
+
+ def test_at_eof_after_feed_data(self):
+ gen = self.reader.at_eof()
+ self.assertGeneratorRunning(gen)
+ self.reader.feed_data(b"spam")
+ eof = self.assertGeneratorReturns(gen)
+ self.assertFalse(eof)
+
+ def test_at_eof_after_feed_eof(self):
+ gen = self.reader.at_eof()
+ self.assertGeneratorRunning(gen)
+ self.reader.feed_eof()
+ eof = self.assertGeneratorReturns(gen)
+ self.assertTrue(eof)
+
+ def test_feed_data_after_feed_data(self):
+ self.reader.feed_data(b"spam")
+ self.reader.feed_data(b"eggs")
+
+ gen = self.reader.read_exact(8)
+ data = self.assertGeneratorReturns(gen)
+ self.assertEqual(data, b"spameggs")
+ gen = self.reader.at_eof()
+ self.assertGeneratorRunning(gen)
+
+ def test_feed_eof_after_feed_data(self):
+ self.reader.feed_data(b"spam")
+ self.reader.feed_eof()
+
+ gen = self.reader.read_exact(4)
+ data = self.assertGeneratorReturns(gen)
+ self.assertEqual(data, b"spam")
+ gen = self.reader.at_eof()
+ eof = self.assertGeneratorReturns(gen)
+ self.assertTrue(eof)
+
+ def test_feed_data_after_feed_eof(self):
+ self.reader.feed_eof()
+ with self.assertRaises(EOFError) as raised:
+ self.reader.feed_data(b"spam")
+ self.assertEqual(
+ str(raised.exception),
+ "stream ended",
+ )
+
+ def test_feed_eof_after_feed_eof(self):
+ self.reader.feed_eof()
+ with self.assertRaises(EOFError) as raised:
+ self.reader.feed_eof()
+ self.assertEqual(
+ str(raised.exception),
+ "stream ended",
+ )
+
+ def test_discard(self):
+ gen = self.reader.read_to_eof(32)
+
+ self.reader.feed_data(b"spam")
+ self.reader.discard()
+ self.assertGeneratorRunning(gen)
+
+ self.reader.feed_eof()
+ data = self.assertGeneratorReturns(gen)
+ self.assertEqual(data, b"")
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_typing.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_typing.py
new file mode 100644
index 0000000000..202de840f3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_typing.py
@@ -0,0 +1 @@
+from websockets.typing import *
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_uri.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_uri.py
new file mode 100644
index 0000000000..8acc01c187
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_uri.py
@@ -0,0 +1,96 @@
+import unittest
+
+from websockets.exceptions import InvalidURI
+from websockets.uri import *
+
+
+VALID_URIS = [
+ (
+ "ws://localhost/",
+ WebSocketURI(False, "localhost", 80, "/", "", None, None),
+ ),
+ (
+ "wss://localhost/",
+ WebSocketURI(True, "localhost", 443, "/", "", None, None),
+ ),
+ (
+ "ws://localhost",
+ WebSocketURI(False, "localhost", 80, "", "", None, None),
+ ),
+ (
+ "ws://localhost/path?query",
+ WebSocketURI(False, "localhost", 80, "/path", "query", None, None),
+ ),
+ (
+ "ws://localhost/path;params",
+ WebSocketURI(False, "localhost", 80, "/path;params", "", None, None),
+ ),
+ (
+ "WS://LOCALHOST/PATH?QUERY",
+ WebSocketURI(False, "localhost", 80, "/PATH", "QUERY", None, None),
+ ),
+ (
+ "ws://user:pass@localhost/",
+ WebSocketURI(False, "localhost", 80, "/", "", "user", "pass"),
+ ),
+ (
+ "ws://høst/",
+ WebSocketURI(False, "xn--hst-0na", 80, "/", "", None, None),
+ ),
+ (
+ "ws://üser:påss@høst/πass?qùéry",
+ WebSocketURI(
+ False,
+ "xn--hst-0na",
+ 80,
+ "/%CF%80ass",
+ "q%C3%B9%C3%A9ry",
+ "%C3%BCser",
+ "p%C3%A5ss",
+ ),
+ ),
+]
+
+INVALID_URIS = [
+ "http://localhost/",
+ "https://localhost/",
+ "ws://localhost/path#fragment",
+ "ws://user@localhost/",
+ "ws:///path",
+]
+
+RESOURCE_NAMES = [
+ ("ws://localhost/", "/"),
+ ("ws://localhost", "/"),
+ ("ws://localhost/path?query", "/path?query"),
+ ("ws://høst/πass?qùéry", "/%CF%80ass?q%C3%B9%C3%A9ry"),
+]
+
+USER_INFOS = [
+ ("ws://localhost/", None),
+ ("ws://user:pass@localhost/", ("user", "pass")),
+ ("ws://üser:påss@høst/", ("%C3%BCser", "p%C3%A5ss")),
+]
+
+
+class URITests(unittest.TestCase):
+ def test_success(self):
+ for uri, parsed in VALID_URIS:
+ with self.subTest(uri=uri):
+ self.assertEqual(parse_uri(uri), parsed)
+
+ def test_error(self):
+ for uri in INVALID_URIS:
+ with self.subTest(uri=uri):
+ with self.assertRaises(InvalidURI):
+ parse_uri(uri)
+
+ def test_resource_name(self):
+ for uri, resource_name in RESOURCE_NAMES:
+ with self.subTest(uri=uri):
+ self.assertEqual(parse_uri(uri).resource_name, resource_name)
+
+ def test_user_info(self):
+ for uri, user_info in USER_INFOS:
+ with self.subTest(uri=uri):
+ self.assertEqual(parse_uri(uri).user_info, user_info)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_utils.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_utils.py
new file mode 100644
index 0000000000..678fcfe798
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_utils.py
@@ -0,0 +1,103 @@
+import base64
+import itertools
+import platform
+import unittest
+
+from websockets.utils import accept_key, apply_mask as py_apply_mask, generate_key
+
+
+# Test vector from RFC 6455
+KEY = "dGhlIHNhbXBsZSBub25jZQ=="
+ACCEPT = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
+
+
+class UtilsTests(unittest.TestCase):
+ def test_generate_key(self):
+ key = generate_key()
+ self.assertEqual(len(base64.b64decode(key.encode())), 16)
+
+ def test_accept_key(self):
+ self.assertEqual(accept_key(KEY), ACCEPT)
+
+
+class ApplyMaskTests(unittest.TestCase):
+ @staticmethod
+ def apply_mask(*args, **kwargs):
+ return py_apply_mask(*args, **kwargs)
+
+ apply_mask_type_combos = list(itertools.product([bytes, bytearray], repeat=2))
+
+ apply_mask_test_values = [
+ (b"", b"1234", b""),
+ (b"aBcDe", b"\x00\x00\x00\x00", b"aBcDe"),
+ (b"abcdABCD", b"1234", b"PPPPpppp"),
+ (b"abcdABCD" * 10, b"1234", b"PPPPpppp" * 10),
+ ]
+
+ def test_apply_mask(self):
+ for data_type, mask_type in self.apply_mask_type_combos:
+ for data_in, mask, data_out in self.apply_mask_test_values:
+ data_in, mask = data_type(data_in), mask_type(mask)
+
+ with self.subTest(data_in=data_in, mask=mask):
+ result = self.apply_mask(data_in, mask)
+ self.assertEqual(result, data_out)
+
+ def test_apply_mask_memoryview(self):
+ for mask_type in [bytes, bytearray]:
+ for data_in, mask, data_out in self.apply_mask_test_values:
+ data_in, mask = memoryview(data_in), mask_type(mask)
+
+ with self.subTest(data_in=data_in, mask=mask):
+ result = self.apply_mask(data_in, mask)
+ self.assertEqual(result, data_out)
+
+ def test_apply_mask_non_contiguous_memoryview(self):
+ for mask_type in [bytes, bytearray]:
+ for data_in, mask, data_out in self.apply_mask_test_values:
+ data_in, mask = memoryview(data_in)[::-1], mask_type(mask)[::-1]
+ data_out = data_out[::-1]
+
+ with self.subTest(data_in=data_in, mask=mask):
+ result = self.apply_mask(data_in, mask)
+ self.assertEqual(result, data_out)
+
+ def test_apply_mask_check_input_types(self):
+ for data_in, mask in [(None, None), (b"abcd", None), (None, b"abcd")]:
+ with self.subTest(data_in=data_in, mask=mask):
+ with self.assertRaises(TypeError):
+ self.apply_mask(data_in, mask)
+
+ def test_apply_mask_check_mask_length(self):
+ for data_in, mask in [
+ (b"", b""),
+ (b"abcd", b"123"),
+ (b"", b"aBcDe"),
+ (b"12345678", b"12345678"),
+ ]:
+ with self.subTest(data_in=data_in, mask=mask):
+ with self.assertRaises(ValueError):
+ self.apply_mask(data_in, mask)
+
+
+try:
+ from websockets.speedups import apply_mask as c_apply_mask
+except ImportError:
+ pass
+else:
+
+ class SpeedupsTests(ApplyMaskTests):
+ @staticmethod
+ def apply_mask(*args, **kwargs):
+ try:
+ return c_apply_mask(*args, **kwargs)
+ except NotImplementedError as exc: # pragma: no cover
+ # PyPy doesn't implement creating contiguous readonly buffer
+ # from non-contiguous. We don't care about this edge case.
+ if (
+ platform.python_implementation() == "PyPy"
+ and "not implemented yet" in str(exc)
+ ):
+ raise unittest.SkipTest(str(exc))
+ else:
+ raise
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/utils.py b/testing/web-platform/tests/tools/third_party/websockets/tests/utils.py
new file mode 100644
index 0000000000..2937a2f15e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/utils.py
@@ -0,0 +1,88 @@
+import contextlib
+import email.utils
+import os
+import pathlib
+import platform
+import tempfile
+import time
+import unittest
+import warnings
+
+
+# Generate TLS certificate with:
+# $ openssl req -x509 -config test_localhost.cnf -days 15340 -newkey rsa:2048 \
+# -out test_localhost.crt -keyout test_localhost.key
+# $ cat test_localhost.key test_localhost.crt > test_localhost.pem
+# $ rm test_localhost.key test_localhost.crt
+
+CERTIFICATE = bytes(pathlib.Path(__file__).with_name("test_localhost.pem"))
+
+
+DATE = email.utils.formatdate(usegmt=True)
+
+
+# Unit for timeouts. May be increased on slow machines by setting the
+# WEBSOCKETS_TESTS_TIMEOUT_FACTOR environment variable.
+MS = 0.001 * float(os.environ.get("WEBSOCKETS_TESTS_TIMEOUT_FACTOR", "1"))
+
+# PyPy has a performance penalty for this test suite.
+if platform.python_implementation() == "PyPy": # pragma: no cover
+ MS *= 5
+
+# asyncio's debug mode has a 10x performance penalty for this test suite.
+if os.environ.get("PYTHONASYNCIODEBUG"): # pragma: no cover
+ MS *= 10
+
+# Ensure that timeouts are larger than the clock's resolution (for Windows).
+MS = max(MS, 2.5 * time.get_clock_info("monotonic").resolution)
+
+
+class GeneratorTestCase(unittest.TestCase):
+ """
+ Base class for testing generator-based coroutines.
+
+ """
+
+ def assertGeneratorRunning(self, gen):
+ """
+ Check that a generator-based coroutine hasn't completed yet.
+
+ """
+ next(gen)
+
+ def assertGeneratorReturns(self, gen):
+ """
+ Check that a generator-based coroutine completes and return its value.
+
+ """
+ with self.assertRaises(StopIteration) as raised:
+ next(gen)
+ return raised.exception.value
+
+
+class DeprecationTestCase(unittest.TestCase):
+ """
+ Base class for testing deprecations.
+
+ """
+
+ @contextlib.contextmanager
+ def assertDeprecationWarning(self, message):
+ """
+ Check that a deprecation warning was raised with the given message.
+
+ """
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ warnings.simplefilter("always")
+ yield
+
+ self.assertEqual(len(recorded_warnings), 1)
+ warning = recorded_warnings[0]
+ self.assertEqual(warning.category, DeprecationWarning)
+ self.assertEqual(str(warning.message), message)
+
+
+@contextlib.contextmanager
+def temp_unix_socket_path():
+ with tempfile.TemporaryDirectory() as temp_dir:
+ yield str(pathlib.Path(temp_dir) / "websockets")
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tox.ini b/testing/web-platform/tests/tools/third_party/websockets/tox.ini
new file mode 100644
index 0000000000..939d8c0cd8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tox.ini
@@ -0,0 +1,39 @@
+[tox]
+envlist =
+ py37
+ py38
+ py39
+ py310
+ py311
+ coverage
+ black
+ ruff
+ mypy
+
+[testenv]
+commands = python -W error::DeprecationWarning -W error::PendingDeprecationWarning -m unittest {posargs}
+
+[testenv:coverage]
+commands =
+ python -m coverage erase
+ python -m coverage run --source {envsitepackagesdir}/websockets,tests -m unittest {posargs}
+ python -m coverage report --show-missing --fail-under=100
+deps = coverage
+
+[testenv:maxi_cov]
+commands =
+ python tests/maxi_cov.py {envsitepackagesdir}
+ python -m coverage report --show-missing --fail-under=100
+deps = coverage
+
+[testenv:black]
+commands = black --check src tests
+deps = black
+
+[testenv:ruff]
+commands = ruff src tests
+deps = ruff
+
+[testenv:mypy]
+commands = mypy --strict src
+deps = mypy
diff --git a/testing/web-platform/tests/tools/third_party_modified/mozlog/mozlog/formatters/html/html.py b/testing/web-platform/tests/tools/third_party_modified/mozlog/mozlog/formatters/html/html.py
index 040de1ae53..87fa4a638a 100644
--- a/testing/web-platform/tests/tools/third_party_modified/mozlog/mozlog/formatters/html/html.py
+++ b/testing/web-platform/tests/tools/third_party_modified/mozlog/mozlog/formatters/html/html.py
@@ -6,7 +6,7 @@ import base64
import json
import os
from collections import defaultdict
-from datetime import datetime
+from datetime import datetime, timezone
from .. import base
@@ -242,7 +242,7 @@ class HTMLFormatter(base.BaseFormatter):
)
def generate_html(self):
- generated = datetime.utcnow()
+ generated = datetime.now(timezone.utc)
with open(os.path.join(base_path, "main.js")) as main_f:
doc = html.html(
self.head,
diff --git a/testing/web-platform/tests/tools/tox.ini b/testing/web-platform/tests/tools/tox.ini
index a4b747ac4f..61442932f3 100644
--- a/testing/web-platform/tests/tools/tox.ini
+++ b/testing/web-platform/tests/tools/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py37,py38,py39,py310,py311,{py37,py38,py39,py310,py311}-{flake8,mypy}
+envlist = py38,py39,py310,py311,py312,{py38,py39,py310,py311,py312}-{flake8,mypy}
skipsdist=True
skip_missing_interpreters=False
diff --git a/testing/web-platform/tests/tools/wave/requirements.txt b/testing/web-platform/tests/tools/wave/requirements.txt
index b2c151b8fe..3bb476fd96 100644
--- a/testing/web-platform/tests/tools/wave/requirements.txt
+++ b/testing/web-platform/tests/tools/wave/requirements.txt
@@ -1,2 +1,2 @@
ua-parser==0.18.0
-python-dateutil==2.8.2
+python-dateutil==2.9.0.post0
diff --git a/testing/web-platform/tests/tools/wave/tox.ini b/testing/web-platform/tests/tools/wave/tox.ini
index 06bdfcd674..88c76096f4 100644
--- a/testing/web-platform/tests/tools/wave/tox.ini
+++ b/testing/web-platform/tests/tools/wave/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py37,py38,py39,py310,py311
+envlist = py38,py39,py310,py311,py312
skipsdist=True
skip_missing_interpreters = False
diff --git a/testing/web-platform/tests/tools/webtransport/h3/capsule.py b/testing/web-platform/tests/tools/webtransport/h3/capsule.py
index 74ca71ade9..fc8183a65f 100644
--- a/testing/web-platform/tests/tools/webtransport/h3/capsule.py
+++ b/testing/web-platform/tests/tools/webtransport/h3/capsule.py
@@ -108,7 +108,7 @@ class H3CapsuleDecoder:
if self._final:
raise e
if not self._buffer:
- return 0
+ return
size = self._buffer.capacity - self._buffer.tell()
if size >= UINT_VAR_MAX_SIZE:
raise e
diff --git a/testing/web-platform/tests/tools/wpt/browser.py b/testing/web-platform/tests/tools/wpt/browser.py
index ea71499ec4..4c42ffa4e8 100644
--- a/testing/web-platform/tests/tools/wpt/browser.py
+++ b/testing/web-platform/tests/tools/wpt/browser.py
@@ -1452,7 +1452,7 @@ class ChromeAndroidBase(Browser):
if browser_binary is None:
browser_binary = self.find_binary(channel)
chrome = Chrome(self.logger)
- return chrome.install_webdriver_by_version(self.version(browser_binary), dest)
+ return chrome.install_webdriver_by_version(self.version(browser_binary), dest, channel)
def version(self, binary=None, webdriver_binary=None):
if not binary:
@@ -1489,20 +1489,6 @@ class ChromeAndroid(ChromeAndroidBase):
return "com.android.chrome"
-# TODO(aluo): This is largely copied from the AndroidWebView implementation.
-# Tests are not running for weblayer yet (crbug/1019521), this initial
-# implementation will help to reproduce and debug any issues.
-class AndroidWeblayer(ChromeAndroidBase):
- """Weblayer-specific interface for Android."""
-
- product = "android_weblayer"
- # TODO(aluo): replace this with weblayer version after tests are working.
- requirements = "requirements_chromium.txt"
-
- def find_binary(self, venv_path=None, channel=None):
- return "org.chromium.weblayer.shell"
-
-
class AndroidWebview(ChromeAndroidBase):
"""Webview-specific interface for Android.
@@ -1656,10 +1642,10 @@ class Opera(Browser):
return m.group(0)
-class EdgeChromium(Browser):
+class Edge(Browser):
"""Microsoft Edge Chromium Browser class."""
- product = "edgechromium"
+ product = "edge"
requirements = "requirements_chromium.txt"
platform = {
"Linux": "linux",
diff --git a/testing/web-platform/tests/tools/wpt/install.py b/testing/web-platform/tests/tools/wpt/install.py
index 382c1e2eb8..1e6408b0be 100644
--- a/testing/web-platform/tests/tools/wpt/install.py
+++ b/testing/web-platform/tests/tools/wpt/install.py
@@ -4,14 +4,13 @@ import argparse
from . import browser
latest_channels = {
- 'android_weblayer': 'dev',
'android_webview': 'dev',
'firefox': 'nightly',
'firefox_android': 'nightly',
'chrome': 'canary',
'chrome_android': 'dev',
'chromium': 'nightly',
- 'edgechromium': 'dev',
+ 'edge': 'dev',
'safari': 'preview',
'servo': 'nightly',
'webkitgtk_minibrowser': 'nightly',
diff --git a/testing/web-platform/tests/tools/wpt/requirements_android.txt b/testing/web-platform/tests/tools/wpt/requirements_android.txt
index 17672383cb..e8205caa70 100644
--- a/testing/web-platform/tests/tools/wpt/requirements_android.txt
+++ b/testing/web-platform/tests/tools/wpt/requirements_android.txt
@@ -1 +1 @@
-mozrunner==8.3.0
+mozrunner==8.3.1
diff --git a/testing/web-platform/tests/tools/wpt/requirements_install.txt b/testing/web-platform/tests/tools/wpt/requirements_install.txt
index 55bed99f8c..f7df06548c 100644
--- a/testing/web-platform/tests/tools/wpt/requirements_install.txt
+++ b/testing/web-platform/tests/tools/wpt/requirements_install.txt
@@ -1,2 +1,2 @@
mozinstall==2.1.0
-packaging==23.1
+packaging==24.0
diff --git a/testing/web-platform/tests/tools/wpt/run.py b/testing/web-platform/tests/tools/wpt/run.py
index b9db082517..c84cdb442a 100644
--- a/testing/web-platform/tests/tools/wpt/run.py
+++ b/testing/web-platform/tests/tools/wpt/run.py
@@ -111,10 +111,9 @@ otherwise install OpenSSL and ensure that it's on your $PATH.""")
def check_environ(product):
- if product not in ("android_weblayer", "android_webview", "chrome",
- "chrome_android", "chrome_ios", "content_shell",
- "edgechromium", "firefox", "firefox_android", "ladybird", "servo",
- "wktr"):
+ if product not in ("android_webview", "chrome", "chrome_android", "chrome_ios",
+ "content_shell", "edge", "firefox", "firefox_android",
+ "ladybird", "servo", "wktr"):
config_builder = serve.build_config(os.path.join(wpt_root, "config.json"))
# Override the ports to avoid looking for free ports
config_builder.ssl = {"type": "none"}
@@ -568,6 +567,8 @@ class ChromeAndroidBase(BrowserSetup):
if kwargs["package_name"] is None:
kwargs["package_name"] = self.browser.find_binary(
channel=browser_channel)
+ if not kwargs["device_serial"]:
+ kwargs["device_serial"] = ["emulator-5554"]
if kwargs["webdriver_binary"] is None:
webdriver_binary = None
if not kwargs["install_webdriver"]:
@@ -615,17 +616,6 @@ class ChromeiOS(BrowserSetup):
raise WptrunError("Unable to locate or install chromedriver binary")
-class AndroidWeblayer(ChromeAndroidBase):
- name = "android_weblayer"
- browser_cls = browser.AndroidWeblayer
-
- def setup_kwargs(self, kwargs):
- super().setup_kwargs(kwargs)
- if kwargs["browser_channel"] in self.experimental_channels and kwargs["enable_experimental"] is None:
- logger.info("Automatically turning on experimental features for WebLayer Dev/Canary")
- kwargs["enable_experimental"] = True
-
-
class AndroidWebview(ChromeAndroidBase):
name = "android_webview"
browser_cls = browser.AndroidWebview
@@ -663,9 +653,9 @@ class Opera(BrowserSetup):
raise WptrunError("Unable to locate or install operadriver binary")
-class EdgeChromium(BrowserSetup):
+class Edge(BrowserSetup):
name = "MicrosoftEdge"
- browser_cls = browser.EdgeChromium
+ browser_cls = browser.Edge
experimental_channels: ClassVar[Tuple[str, ...]] = ("dev", "canary")
def setup_kwargs(self, kwargs):
@@ -872,7 +862,6 @@ class Epiphany(BrowserSetup):
product_setup = {
- "android_weblayer": AndroidWeblayer,
"android_webview": AndroidWebview,
"firefox": Firefox,
"firefox_android": FirefoxAndroid,
@@ -881,7 +870,7 @@ product_setup = {
"chrome_ios": ChromeiOS,
"chromium": Chromium,
"content_shell": ContentShell,
- "edgechromium": EdgeChromium,
+ "edge": Edge,
"safari": Safari,
"servo": Servo,
"servodriver": ServoWebDriver,
@@ -924,6 +913,9 @@ def setup_wptrunner(venv, **kwargs):
args_general(kwargs)
if kwargs["product"] not in product_setup:
+ if kwargs["product"] == "edgechromium":
+ raise WptrunError("edgechromium has been renamed to edge.")
+
raise WptrunError("Unsupported product %s" % kwargs["product"])
setup_cls = product_setup[kwargs["product"]](venv, kwargs["prompt"])
diff --git a/testing/web-platform/tests/tools/wpt/tests/test_browser.py b/testing/web-platform/tests/tools/wpt/tests/test_browser.py
index 95094e376d..3a45dab16e 100644
--- a/testing/web-platform/tests/tools/wpt/tests/test_browser.py
+++ b/testing/web-platform/tests/tools/wpt/tests/test_browser.py
@@ -30,33 +30,33 @@ def test_all_browser_abc():
assert not inspect.isabstract(cls), "%s is abstract" % name
-def test_edgechromium_webdriver_supports_browser():
+def test_edge_webdriver_supports_browser():
# MSEdgeDriver binary cannot be called.
- edge = browser.EdgeChromium(logger)
+ edge = browser.Edge(logger)
edge.webdriver_version = mock.MagicMock(return_value=None)
assert not edge.webdriver_supports_browser('/usr/bin/edgedriver', '/usr/bin/edge', 'stable')
# Browser binary cannot be called.
- edge = browser.EdgeChromium(logger)
+ edge = browser.Edge(logger)
edge.webdriver_version = mock.MagicMock(return_value='70.0.1')
edge.version = mock.MagicMock(return_value=None)
assert edge.webdriver_supports_browser('/usr/bin/edgedriver', '/usr/bin/edge', 'stable')
# Browser version matches.
- edge = browser.EdgeChromium(logger)
+ edge = browser.Edge(logger)
# Versions should be an exact match to be compatible.
edge.webdriver_version = mock.MagicMock(return_value='70.1.5')
edge.version = mock.MagicMock(return_value='70.1.5')
assert edge.webdriver_supports_browser('/usr/bin/edgedriver', '/usr/bin/edge', 'stable')
# Browser version doesn't match.
- edge = browser.EdgeChromium(logger)
+ edge = browser.Edge(logger)
edge.webdriver_version = mock.MagicMock(return_value='70.0.1')
edge.version = mock.MagicMock(return_value='69.0.1')
assert not edge.webdriver_supports_browser('/usr/bin/edgedriver', '/usr/bin/edge', 'stable')
# MSEdgeDriver version should match for MAJOR.MINOR.BUILD version.
- edge = browser.EdgeChromium(logger)
+ edge = browser.Edge(logger)
edge.webdriver_version = mock.MagicMock(return_value='70.0.1.0')
edge.version = mock.MagicMock(return_value='70.0.1.1 dev')
assert edge.webdriver_supports_browser('/usr/bin/edgedriver', '/usr/bin/edge', 'dev')
@@ -68,8 +68,8 @@ def test_edgechromium_webdriver_supports_browser():
# logic to test there.
@pytest.mark.skipif(sys.platform.startswith('win'), reason='just uses _get_fileversion on Windows')
@mock.patch('tools.wpt.browser.call')
-def test_edgechromium_webdriver_version(mocked_call):
- edge = browser.EdgeChromium(logger)
+def test_edge_webdriver_version(mocked_call):
+ edge = browser.Edge(logger)
webdriver_binary = '/usr/bin/edgedriver'
# Working cases.
diff --git a/testing/web-platform/tests/tools/wpt/tox.ini b/testing/web-platform/tests/tools/wpt/tox.ini
index 4068f70898..b6e3dab231 100644
--- a/testing/web-platform/tests/tools/wpt/tox.ini
+++ b/testing/web-platform/tests/tools/wpt/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py37,py38,py39,py310,py311
+envlist = py38,py39,py310,py311,py312
skipsdist=True
skip_missing_interpreters = False
diff --git a/testing/web-platform/tests/tools/wpt/utils.py b/testing/web-platform/tests/tools/wpt/utils.py
index 5899dc3f3a..51bb3f55dc 100644
--- a/testing/web-platform/tests/tools/wpt/utils.py
+++ b/testing/web-platform/tests/tools/wpt/utils.py
@@ -46,8 +46,11 @@ def untar(fileobj, dest="."):
"""Extract tar archive."""
logger.debug("untar")
fileobj = seekable(fileobj)
+ kwargs = {}
+ if sys.version_info.major >= 3 and sys.version_info.minor >= 12:
+ kwargs["filter"] = "tar"
with tarfile.open(fileobj=fileobj) as tar_data:
- tar_data.extractall(path=dest)
+ tar_data.extractall(path=dest, **kwargs)
def unzip(fileobj, dest=None, limit=None):
diff --git a/testing/web-platform/tests/tools/wptrunner/docs/expectation.rst b/testing/web-platform/tests/tools/wptrunner/docs/expectation.rst
index fea676565b..76f088dd8f 100644
--- a/testing/web-platform/tests/tools/wptrunner/docs/expectation.rst
+++ b/testing/web-platform/tests/tools/wptrunner/docs/expectation.rst
@@ -153,7 +153,7 @@ When used for expectation data, metadata files have the following format:
:implementation-status:
One of the values ``implementing``,
- ``not-implementing`` or ``default``. This is used in conjunction
+ ``not-implementing`` or ``backlog``. This is used in conjunction
with the ``--skip-implementation-status`` command line argument to
``wptrunner`` to ignore certain features where running the test is
low value.
diff --git a/testing/web-platform/tests/tools/wptrunner/requirements.txt b/testing/web-platform/tests/tools/wptrunner/requirements.txt
index a7face3bd0..bb9b4ac77c 100644
--- a/testing/web-platform/tests/tools/wptrunner/requirements.txt
+++ b/testing/web-platform/tests/tools/wptrunner/requirements.txt
@@ -1,11 +1,11 @@
html5lib==1.1
-mozdebug==0.3.0
+mozdebug==0.3.1
mozinfo==1.2.3 # https://bugzilla.mozilla.org/show_bug.cgi?id=1621226
mozlog==8.0.0
mozprocess==1.3.1
-packaging==23.1
-pillow==9.5.0
+packaging==24.0
+pillow==10.3.0
requests==2.31.0
six==1.16.0
-urllib3==2.0.7
-aioquic==0.9.19
+urllib3==2.2.1
+aioquic==0.9.21
diff --git a/testing/web-platform/tests/tools/wptrunner/requirements_firefox.txt b/testing/web-platform/tests/tools/wptrunner/requirements_firefox.txt
index 3ba4731494..ed377b9c95 100644
--- a/testing/web-platform/tests/tools/wptrunner/requirements_firefox.txt
+++ b/testing/web-platform/tests/tools/wptrunner/requirements_firefox.txt
@@ -1,10 +1,10 @@
marionette_driver==3.4.0
mozcrash==2.2.0
-mozdevice==4.1.1
+mozdevice==4.1.2
mozinstall==2.1.0
mozleak==0.2
-mozprofile==2.6.1
-mozrunner==8.3.0
+mozprofile==3.0.0
+mozrunner==8.3.1
mozversion==2.4.0
-psutil==5.9.5
+psutil==5.9.8
redo==2.0.4
diff --git a/testing/web-platform/tests/tools/wptrunner/requirements_opera.txt b/testing/web-platform/tests/tools/wptrunner/requirements_opera.txt
index db0c5dd992..6c2425f337 100644
--- a/testing/web-platform/tests/tools/wptrunner/requirements_opera.txt
+++ b/testing/web-platform/tests/tools/wptrunner/requirements_opera.txt
@@ -1,2 +1,2 @@
mozprocess==1.3.1
-selenium==4.18.1
+selenium==4.20.0
diff --git a/testing/web-platform/tests/tools/wptrunner/requirements_safari.txt b/testing/web-platform/tests/tools/wptrunner/requirements_safari.txt
index bcce11aed8..0704b2dbf6 100644
--- a/testing/web-platform/tests/tools/wptrunner/requirements_safari.txt
+++ b/testing/web-platform/tests/tools/wptrunner/requirements_safari.txt
@@ -1 +1 @@
-psutil==5.9.5
+psutil==5.9.8
diff --git a/testing/web-platform/tests/tools/wptrunner/requirements_sauce.txt b/testing/web-platform/tests/tools/wptrunner/requirements_sauce.txt
index c9e42346ce..806352e87e 100644
--- a/testing/web-platform/tests/tools/wptrunner/requirements_sauce.txt
+++ b/testing/web-platform/tests/tools/wptrunner/requirements_sauce.txt
@@ -1,2 +1,2 @@
-selenium==4.18.1
+selenium==4.20.0
requests==2.31.0
diff --git a/testing/web-platform/tests/tools/wptrunner/tox.ini b/testing/web-platform/tests/tools/wptrunner/tox.ini
index 82d3ac6f55..c380be1252 100644
--- a/testing/web-platform/tests/tools/wptrunner/tox.ini
+++ b/testing/web-platform/tests/tools/wptrunner/tox.ini
@@ -2,7 +2,7 @@
xfail_strict=true
[tox]
-envlist = py311-{base,chrome,firefox,opera,safari,sauce,servo,webkit,webkitgtk_minibrowser,epiphany},{py37,py38,py39,py310}-base
+envlist = py312-{base,chrome,firefox,opera,safari,sauce,servo,webkit,webkitgtk_minibrowser,epiphany},{py38,py39,py310,py311}-base
skip_missing_interpreters = False
[testenv]
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/__init__.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/__init__.py
index 81dc549d73..d54a9be943 100644
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/__init__.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/__init__.py
@@ -22,14 +22,13 @@ All classes and functions named in the above dict must be imported into the
module global scope.
"""
-product_list = ["android_weblayer",
- "android_webview",
+product_list = ["android_webview",
"chrome",
"chrome_android",
"chrome_ios",
"chromium",
"content_shell",
- "edgechromium",
+ "edge",
"firefox",
"firefox_android",
"safari",
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/android_weblayer.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/android_weblayer.py
deleted file mode 100644
index db23b64793..0000000000
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/android_weblayer.py
+++ /dev/null
@@ -1,105 +0,0 @@
-# mypy: allow-untyped-defs
-
-from .base import NullBrowser # noqa: F401
-from .base import require_arg
-from .base import get_timeout_multiplier # noqa: F401
-from .chrome import executor_kwargs as chrome_executor_kwargs
-from .chrome_android import ChromeAndroidBrowserBase
-from ..executors.base import WdspecExecutor # noqa: F401
-from ..executors.executorchrome import ChromeDriverPrintRefTestExecutor # noqa: F401
-from ..executors.executorwebdriver import (WebDriverCrashtestExecutor, # noqa: F401
- WebDriverTestharnessExecutor, # noqa: F401
- WebDriverRefTestExecutor) # noqa: F401
-
-
-__wptrunner__ = {"product": "android_weblayer",
- "check_args": "check_args",
- "browser": {None: "WeblayerShell",
- "wdspec": "NullBrowser"},
- "executor": {"testharness": "WebDriverTestharnessExecutor",
- "reftest": "WebDriverRefTestExecutor",
- "print-reftest": "ChromeDriverPrintRefTestExecutor",
- "wdspec": "WdspecExecutor",
- "crashtest": "WebDriverCrashtestExecutor"},
- "browser_kwargs": "browser_kwargs",
- "executor_kwargs": "executor_kwargs",
- "env_extras": "env_extras",
- "env_options": "env_options",
- "timeout_multiplier": "get_timeout_multiplier"}
-
-_wptserve_ports = set()
-
-
-def check_args(**kwargs):
- require_arg(kwargs, "webdriver_binary")
-
-
-def browser_kwargs(logger, test_type, run_info_data, config, **kwargs):
- return {"binary": kwargs["binary"],
- "adb_binary": kwargs["adb_binary"],
- "device_serial": kwargs["device_serial"],
- "webdriver_binary": kwargs["webdriver_binary"],
- "webdriver_args": kwargs.get("webdriver_args"),
- "stackwalk_binary": kwargs.get("stackwalk_binary"),
- "symbols_path": kwargs.get("symbols_path")}
-
-
-def executor_kwargs(logger, test_type, test_environment, run_info_data,
- **kwargs):
- # Use update() to modify the global list in place.
- _wptserve_ports.update(set(
- test_environment.config['ports']['http'] + test_environment.config['ports']['https'] +
- test_environment.config['ports']['ws'] + test_environment.config['ports']['wss']
- ))
-
- executor_kwargs = chrome_executor_kwargs(logger, test_type, test_environment, run_info_data,
- **kwargs)
- del executor_kwargs["capabilities"]["goog:chromeOptions"]["prefs"]
- capabilities = executor_kwargs["capabilities"]
- # Note that for WebLayer, we launch a test shell and have the test shell use
- # WebLayer.
- # https://cs.chromium.org/chromium/src/weblayer/shell/android/shell_apk/
- capabilities["goog:chromeOptions"]["androidPackage"] = \
- "org.chromium.weblayer.shell"
- capabilities["goog:chromeOptions"]["androidActivity"] = ".WebLayerShellActivity"
- capabilities["goog:chromeOptions"]["androidKeepAppDataDir"] = \
- kwargs.get("keep_app_data_directory")
-
- # Workaround: driver.quit() cannot quit WeblayerShell.
- executor_kwargs["pause_after_test"] = False
- # Workaround: driver.close() is not supported.
- executor_kwargs["restart_after_test"] = True
- executor_kwargs["close_after_done"] = False
- return executor_kwargs
-
-
-def env_extras(**kwargs):
- return []
-
-
-def env_options():
- # allow the use of host-resolver-rules in lieu of modifying /etc/hosts file
- return {"server_host": "127.0.0.1"}
-
-
-class WeblayerShell(ChromeAndroidBrowserBase):
- """Chrome is backed by chromedriver, which is supplied through
- ``wptrunner.webdriver.ChromeDriverServer``.
- """
-
- def __init__(self, logger, binary,
- webdriver_binary="chromedriver",
- adb_binary=None,
- remote_queue=None,
- device_serial=None,
- webdriver_args=None,
- stackwalk_binary=None,
- symbols_path=None):
- """Creates a new representation of Chrome. The `binary` argument gives
- the browser binary to use for testing."""
- super().__init__(logger,
- webdriver_binary, adb_binary, remote_queue,
- device_serial, webdriver_args, stackwalk_binary,
- symbols_path)
- self.binary = binary
- self.wptserver_ports = _wptserve_ports
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome.py
index 8198bfe11d..c0a176743d 100644
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome.py
@@ -53,13 +53,13 @@ def browser_kwargs(logger, test_type, run_info_data, config, **kwargs):
"webdriver_args": kwargs.get("webdriver_args")}
-def executor_kwargs(logger, test_type, test_environment, run_info_data,
+def executor_kwargs(logger, test_type, test_environment, run_info_data, subsuite,
**kwargs):
sanitizer_enabled = kwargs.get("sanitizer_enabled")
if sanitizer_enabled:
test_type = "crashtest"
executor_kwargs = base_executor_kwargs(test_type, test_environment, run_info_data,
- **kwargs)
+ subsuite, **kwargs)
executor_kwargs["close_after_done"] = True
executor_kwargs["sanitizer_enabled"] = sanitizer_enabled
executor_kwargs["reuse_window"] = kwargs.get("reuse_window", False)
@@ -115,6 +115,10 @@ def executor_kwargs(logger, test_type, test_environment, run_info_data,
# The GenericSensorExtraClasses flag enables the browser-side
# implementation of sensors such as Ambient Light Sensor.
chrome_options["args"].append("--enable-features=GenericSensorExtraClasses")
+ # Do not show Chrome for Testing infobar. For other Chromium build this
+ # flag is no-op. Required to avoid flakiness in tests, as the infobar
+ # changes the viewport, which can happen during the test run.
+ chrome_options["args"].append("--disable-infobars")
# Classify `http-private`, `http-public` and https variants in the
# appropriate IP address spaces.
@@ -134,8 +138,14 @@ def executor_kwargs(logger, test_type, test_environment, run_info_data,
chrome_options["args"].append(
"--ip-address-space-overrides=" + address_space_overrides_arg)
+ # Always disable antialiasing on the Ahem font.
+ blink_features = ['DisableAhemAntialias']
+
if kwargs["enable_mojojs"]:
- chrome_options["args"].append("--enable-blink-features=MojoJS,MojoJSTest")
+ blink_features.append('MojoJS')
+ blink_features.append('MojoJSTest')
+
+ chrome_options["args"].append("--enable-blink-features=" + ','.join(blink_features))
if kwargs["enable_swiftshader"]:
# https://chromium.googlesource.com/chromium/src/+/HEAD/docs/gpu/swiftshader.md
@@ -149,6 +159,10 @@ def executor_kwargs(logger, test_type, test_environment, run_info_data,
if arg not in chrome_options["args"]:
chrome_options["args"].append(arg)
+ for arg in subsuite.config.get("binary_args", []):
+ if arg not in chrome_options["args"]:
+ chrome_options["args"].append(arg)
+
# Pass the --headless=new flag to Chrome if WPT's own --headless flag was
# set. '--headless' should always mean the new headless mode, as the old
# headless mode is not used anyway.
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/edgechromium.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/edge.py
index 4f5bffa06c..82597c9312 100644
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/edgechromium.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/edge.py
@@ -6,18 +6,18 @@ from .chrome import executor_kwargs as chrome_executor_kwargs
from ..executors.executorwebdriver import WebDriverCrashtestExecutor # noqa: F401
from ..executors.base import WdspecExecutor # noqa: F401
from ..executors.executoredge import ( # noqa: F401
- EdgeChromiumDriverPrintRefTestExecutor,
- EdgeChromiumDriverRefTestExecutor,
- EdgeChromiumDriverTestharnessExecutor,
+ EdgeDriverPrintRefTestExecutor,
+ EdgeDriverRefTestExecutor,
+ EdgeDriverTestharnessExecutor,
)
-__wptrunner__ = {"product": "edgechromium",
+__wptrunner__ = {"product": "edge",
"check_args": "check_args",
- "browser": "EdgeChromiumBrowser",
- "executor": {"testharness": "EdgeChromiumDriverTestharnessExecutor",
- "reftest": "EdgeChromiumDriverRefTestExecutor",
- "print-reftest": "EdgeChromiumDriverPrintRefTestExecutor",
+ "browser": "EdgeBrowser",
+ "executor": {"testharness": "EdgeDriverTestharnessExecutor",
+ "reftest": "EdgeDriverRefTestExecutor",
+ "print-reftest": "EdgeDriverPrintRefTestExecutor",
"wdspec": "WdspecExecutor",
"crashtest": "WebDriverCrashtestExecutor"},
"browser_kwargs": "browser_kwargs",
@@ -58,9 +58,9 @@ def update_properties():
return (["debug", "os", "processor"], {"os": ["version"], "processor": ["bits"]})
-class EdgeChromiumBrowser(WebDriverBrowser):
+class EdgeBrowser(WebDriverBrowser):
"""MicrosoftEdge is backed by MSEdgeDriver, which is supplied through
- ``wptrunner.webdriver.EdgeChromiumDriverServer``.
+ ``wptrunner.webdriver.EdgeDriverServer``.
"""
def make_command(self):
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py
index 814b8b8d75..d977930a28 100644
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py
@@ -132,6 +132,7 @@ def browser_kwargs(logger, test_type, run_info_data, config, subsuite, **kwargs)
"headless": kwargs["headless"],
"preload_browser": kwargs["preload_browser"] and not kwargs["pause_after_test"] and not kwargs["num_test_groups"] == 1,
"specialpowers_path": kwargs["specialpowers_path"],
+ "allow_list_paths": kwargs["allow_list_paths"],
"debug_test": kwargs["debug_test"]}
if test_type == "wdspec" and kwargs["binary"]:
browser_kwargs["webdriver_args"].extend(["--binary", kwargs["binary"]])
@@ -644,7 +645,8 @@ class GeckodriverOutputHandler(FirefoxOutputHandler):
class ProfileCreator:
def __init__(self, logger, prefs_root, config, test_type, extra_prefs,
disable_fission, debug_test, browser_channel, binary,
- package_name, certutil_binary, ca_certificate_path):
+ package_name, certutil_binary, ca_certificate_path,
+ allow_list_paths):
self.logger = logger
self.prefs_root = prefs_root
self.config = config
@@ -658,6 +660,7 @@ class ProfileCreator:
self.package_name = package_name
self.certutil_binary = certutil_binary
self.ca_certificate_path = ca_certificate_path
+ self.allow_list_paths = allow_list_paths
def create(self, **kwargs):
"""Create a Firefox profile and return the mozprofile Profile object pointing at that
@@ -669,6 +672,7 @@ class ProfileCreator:
profile = FirefoxProfile(preferences=preferences,
restore=False,
+ allowlistpaths=self.allow_list_paths,
**kwargs)
self._set_required_prefs(profile)
if self.ca_certificate_path is not None:
@@ -795,7 +799,7 @@ class FirefoxBrowser(Browser):
stackfix_dir=None, binary_args=None, timeout_multiplier=None, leak_check=False,
asan=False, chaos_mode_flags=None, config=None,
browser_channel="nightly", headless=None, preload_browser=False,
- specialpowers_path=None, debug_test=False, **kwargs):
+ specialpowers_path=None, debug_test=False, allow_list_paths=None, **kwargs):
Browser.__init__(self, logger)
self.logger = logger
@@ -826,7 +830,8 @@ class FirefoxBrowser(Browser):
binary,
package_name,
certutil_binary,
- ca_certificate_path)
+ ca_certificate_path,
+ allow_list_paths)
if preload_browser:
instance_manager_cls = PreloadInstanceManager
@@ -899,7 +904,7 @@ class FirefoxWdSpecBrowser(WebDriverBrowser):
disable_fission=False, stackfix_dir=None, leak_check=False,
asan=False, chaos_mode_flags=None, config=None, browser_channel="nightly",
headless=None, debug_test=False, profile_creator_cls=ProfileCreator,
- **kwargs):
+ allow_list_paths=None, **kwargs):
super().__init__(logger, binary, webdriver_binary, webdriver_args)
self.binary = binary
@@ -927,7 +932,8 @@ class FirefoxWdSpecBrowser(WebDriverBrowser):
binary,
package_name,
certutil_binary,
- ca_certificate_path)
+ ca_certificate_path,
+ allow_list_paths)
self.profile = profile_creator.create()
self.marionette_port = None
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox_android.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox_android.py
index 7c158902e1..526f83d595 100644
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox_android.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox_android.py
@@ -148,11 +148,13 @@ def get_environ(chaos_mode_flags, env_extras=None):
class ProfileCreator(FirefoxProfileCreator):
def __init__(self, logger, prefs_root, config, test_type, extra_prefs,
disable_fission, debug_test, browser_channel, binary,
- package_name, certutil_binary, ca_certificate_path):
+ package_name, certutil_binary, ca_certificate_path,
+ allow_list_paths=None):
super().__init__(logger, prefs_root, config, test_type, extra_prefs,
disable_fission, debug_test, browser_channel, None,
- package_name, certutil_binary, ca_certificate_path)
+ package_name, certutil_binary, ca_certificate_path,
+ allow_list_paths)
def _set_required_prefs(self, profile):
profile.set_preferences({
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/actions.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/actions.py
index cb9c1a1508..6e0c081b48 100644
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/actions.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/actions.py
@@ -443,6 +443,26 @@ class GetVirtualSensorInformationAction:
self.logger.debug("Requesting information from %s sensor" % sensor_type)
return self.protocol.virtual_sensor.get_virtual_sensor_information(sensor_type)
+class SetDevicePostureAction:
+ name = "set_device_posture"
+
+ def __init__(self, logger, protocol):
+ self.logger = logger
+ self.protocol = protocol
+
+ def __call__(self, payload):
+ posture = payload["posture"]
+ return self.protocol.device_posture.set_device_posture(posture)
+
+class ClearDevicePostureAction:
+ name = "clear_device_posture"
+
+ def __init__(self, logger, protocol):
+ self.logger = logger
+ self.protocol = protocol
+
+ def __call__(self, payload):
+ return self.protocol.device_posture.clear_device_posture()
actions = [ClickAction,
DeleteAllCookiesAction,
@@ -477,4 +497,6 @@ actions = [ClickAction,
CreateVirtualSensorAction,
UpdateVirtualSensorAction,
RemoveVirtualSensorAction,
- GetVirtualSensorInformationAction]
+ GetVirtualSensorInformationAction,
+ SetDevicePostureAction,
+ ClearDevicePostureAction]
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/base.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/base.py
index 763b6fcb19..92a782e835 100644
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/base.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/base.py
@@ -313,7 +313,7 @@ class TestExecutor:
result = self.do_test(test)
except Exception as e:
exception_string = traceback.format_exc()
- message = f"Exception in TextExecutor.run:\n{exception_string}"
+ message = f"Exception in TestExecutor.run:\n{exception_string}"
self.logger.warning(message)
result = self.result_from_exception(test, e, exception_string)
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executoredge.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executoredge.py
index cbe5eadf9a..3b62cb7477 100644
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executoredge.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executoredge.py
@@ -20,42 +20,42 @@ here = os.path.dirname(__file__)
_SanitizerMixin = make_sanitizer_mixin(WebDriverCrashtestExecutor)
-class EdgeChromiumDriverTestharnessProtocolPart(ChromeDriverTestharnessProtocolPart):
+class EdgeDriverTestharnessProtocolPart(ChromeDriverTestharnessProtocolPart):
def setup(self):
super().setup()
self.cdp_company_prefix = "ms"
-class EdgeChromiumDriverPrintProtocolPart(ChromeDriverPrintProtocolPart):
+class EdgeDriverPrintProtocolPart(ChromeDriverPrintProtocolPart):
def setup(self):
super().setup()
self.cdp_company_prefix = "ms"
-class EdgeChromiumDriverProtocol(WebDriverProtocol):
+class EdgeDriverProtocol(WebDriverProtocol):
implements = [
- EdgeChromiumDriverPrintProtocolPart,
- EdgeChromiumDriverTestharnessProtocolPart,
+ EdgeDriverPrintProtocolPart,
+ EdgeDriverTestharnessProtocolPart,
*(part for part in WebDriverProtocol.implements
- if part.name != EdgeChromiumDriverTestharnessProtocolPart.name)
+ if part.name != EdgeDriverTestharnessProtocolPart.name)
]
reuse_window = False
-class EdgeChromiumDriverRefTestExecutor(WebDriverRefTestExecutor, _SanitizerMixin): # type: ignore
- protocol_cls = EdgeChromiumDriverProtocol
+class EdgeDriverRefTestExecutor(WebDriverRefTestExecutor, _SanitizerMixin): # type: ignore
+ protocol_cls = EdgeDriverProtocol
-class EdgeChromiumDriverTestharnessExecutor(WebDriverTestharnessExecutor, _SanitizerMixin): # type: ignore
- protocol_cls = EdgeChromiumDriverProtocol
+class EdgeDriverTestharnessExecutor(WebDriverTestharnessExecutor, _SanitizerMixin): # type: ignore
+ protocol_cls = EdgeDriverProtocol
def __init__(self, *args, reuse_window=False, **kwargs):
super().__init__(*args, **kwargs)
self.protocol.reuse_window = reuse_window
-class EdgeChromiumDriverPrintRefTestExecutor(EdgeChromiumDriverRefTestExecutor):
- protocol_cls = EdgeChromiumDriverProtocol
+class EdgeDriverPrintRefTestExecutor(EdgeDriverRefTestExecutor):
+ protocol_cls = EdgeDriverProtocol
def setup(self, runner):
super().setup(runner)
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py
index 0f640d7741..05a9fc1ae4 100644
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py
@@ -45,6 +45,7 @@ from .protocol import (AccessibilityProtocolPart,
PrintProtocolPart,
DebugProtocolPart,
VirtualSensorProtocolPart,
+ DevicePostureProtocolPart,
merge_dicts)
@@ -749,6 +750,17 @@ class MarionetteVirtualSensorProtocolPart(VirtualSensorProtocolPart):
raise NotImplementedError("get_virtual_sensor_information not yet implemented")
+class MarionetteDevicePostureProtocolPart(DevicePostureProtocolPart):
+ def setup(self):
+ self.marionette = self.parent.marionette
+
+ def set_device_posture(self, posture):
+ raise NotImplementedError("set_device_posture not yet implemented")
+
+ def clear_device_posture(self):
+ raise NotImplementedError("clear_device_posture not yet implemented")
+
+
class MarionetteProtocol(Protocol):
implements = [MarionetteBaseProtocolPart,
MarionetteTestharnessProtocolPart,
@@ -769,7 +781,8 @@ class MarionetteProtocol(Protocol):
MarionettePrintProtocolPart,
MarionetteDebugProtocolPart,
MarionetteAccessibilityProtocolPart,
- MarionetteVirtualSensorProtocolPart]
+ MarionetteVirtualSensorProtocolPart,
+ MarionetteDevicePostureProtocolPart]
def __init__(self, executor, browser, capabilities=None, timeout_multiplier=1, e10s=True, ccov=False):
do_delayed_imports()
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py
index 6df2d96461..69013e5e79 100644
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py
@@ -35,6 +35,7 @@ from .protocol import (BaseProtocolPart,
RPHRegistrationsProtocolPart,
FedCMProtocolPart,
VirtualSensorProtocolPart,
+ DevicePostureProtocolPart,
merge_dicts)
from webdriver.client import Session
@@ -431,6 +432,16 @@ class WebDriverVirtualSensorPart(VirtualSensorProtocolPart):
def get_virtual_sensor_information(self, sensor_type):
return self.webdriver.send_session_command("GET", "sensor/%s" % sensor_type)
+class WebDriverDevicePostureProtocolPart(DevicePostureProtocolPart):
+ def setup(self):
+ self.webdriver = self.parent.webdriver
+
+ def set_device_posture(self, posture):
+ body = {"posture": posture}
+ return self.webdriver.send_session_command("POST", "deviceposture", body)
+
+ def clear_device_posture(self):
+ return self.webdriver.send_session_command("DELETE", "deviceposture")
class WebDriverProtocol(Protocol):
implements = [WebDriverBaseProtocolPart,
@@ -450,7 +461,8 @@ class WebDriverProtocol(Protocol):
WebDriverRPHRegistrationsProtocolPart,
WebDriverFedCMProtocolPart,
WebDriverDebugProtocolPart,
- WebDriverVirtualSensorPart]
+ WebDriverVirtualSensorPart,
+ WebDriverDevicePostureProtocolPart]
def __init__(self, executor, browser, capabilities, **kwargs):
super().__init__(executor, browser)
@@ -527,7 +539,9 @@ class WebDriverRun(TimedRunner):
self.result = True, self.func(self.protocol, self.url, self.timeout)
except (error.TimeoutException, error.ScriptTimeoutException):
self.result = False, ("EXTERNAL-TIMEOUT", None)
- except (socket.timeout, error.UnknownErrorException):
+ except socket.timeout:
+ # Checking if the browser is alive below is likely to hang, so mark
+ # this case as a CRASH unconditionally.
self.result = False, ("CRASH", None)
except Exception as e:
if (isinstance(e, error.WebDriverException) and
@@ -536,11 +550,12 @@ class WebDriverRun(TimedRunner):
# workaround for https://bugs.chromium.org/p/chromedriver/issues/detail?id=2001
self.result = False, ("EXTERNAL-TIMEOUT", None)
else:
+ status = "INTERNAL-ERROR" if self.protocol.is_alive() else "CRASH"
message = str(getattr(e, "message", ""))
if message:
message += "\n"
message += traceback.format_exc()
- self.result = False, ("INTERNAL-ERROR", message)
+ self.result = False, (status, message)
finally:
self.result_flag.set()
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/protocol.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/protocol.py
index e44d1a7666..3d588738b6 100644
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/protocol.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/protocol.py
@@ -802,3 +802,17 @@ class VirtualSensorProtocolPart(ProtocolPart):
@abstractmethod
def get_virtual_sensor_information(self, sensor_type):
pass
+
+class DevicePostureProtocolPart(ProtocolPart):
+ """Protocol part for Device Posture"""
+ __metaclass__ = ABCMeta
+
+ name = "device_posture"
+
+ @abstractmethod
+ def set_device_posture(self, posture):
+ pass
+
+ @abstractmethod
+ def clear_device_posture(self):
+ pass
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/testdriver-extra.js b/testing/web-platform/tests/tools/wptrunner/wptrunner/testdriver-extra.js
index af25bf4111..87d3826bfc 100644
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/testdriver-extra.js
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/testdriver-extra.js
@@ -327,4 +327,12 @@
window.test_driver_internal.get_virtual_sensor_information = function(sensor_type, context=null) {
return create_action("get_virtual_sensor_information", {sensor_type, context});
};
+
+ window.test_driver_internal.set_device_posture = function(posture, context=null) {
+ return create_action("set_device_posture", {posture, context});
+ };
+
+ window.test_driver_internal.clear_device_posture = function(context=null) {
+ return create_action("clear_device_posture", {context});
+ };
})();
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/testrunner.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/testrunner.py
index 70da22f5b7..93e19fa47b 100644
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/testrunner.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/testrunner.py
@@ -1,7 +1,6 @@
# mypy: allow-untyped-defs
import threading
-import time
import traceback
from queue import Empty
from collections import namedtuple, defaultdict
@@ -31,6 +30,30 @@ TestImplementation = namedtuple('TestImplementation',
'browser_cls', 'browser_kwargs'])
+class StopFlag:
+ """Synchronization for coordinating a graceful exit."""
+
+ def __init__(self, size: int):
+ # Flag that is polled by threads so that they can gracefully exit in the
+ # face of SIGINT.
+ self._should_stop = threading.Event()
+ # A barrier that each `TestRunnerManager` thread waits on when exiting
+ # its run loop. This provides a reliable way for the `ManagerGroup` to
+ # tell when all threads have cleaned up their resources.
+ #
+ # The barrier's extra waiter is the main thread (`ManagerGroup`).
+ self._all_managers_done = threading.Barrier(1 + size)
+
+ def stop(self) -> None:
+ self._should_stop.set()
+
+ def should_stop(self) -> bool:
+ return self._should_stop.is_set()
+
+ def wait_for_all_managers_done(self, timeout: Optional[float] = None) -> None:
+ self._all_managers_done.wait(timeout)
+
+
class LogMessageHandler:
def __init__(self, send_message):
self.send_message = send_message
@@ -443,7 +466,8 @@ class TestRunnerManager(threading.Thread):
if self.browser is not None:
assert self.browser.browser is not None
self.browser.browser.cleanup()
- self.logger.debug("TestRunnerManager main loop terminated")
+ self.logger.debug("TestRunnerManager main loop terminated")
+ self.parent_stop_flag.wait_for_all_managers_done()
def wait_event(self):
dispatch = {
@@ -517,7 +541,7 @@ class TestRunnerManager(threading.Thread):
return f(*data)
def should_stop(self):
- return self.child_stop_flag.is_set() or self.parent_stop_flag.is_set()
+ return self.child_stop_flag.is_set() or self.parent_stop_flag.should_stop()
def start_init(self):
subsuite, test_type, test, test_group, group_metadata = self.get_next_test()
@@ -977,9 +1001,7 @@ class ManagerGroup:
self.max_restarts = max_restarts
self.pool = set()
- # Event that is polled by threads so that they can gracefully exit in the face
- # of sigint
- self.stop_flag = threading.Event()
+ self.stop_flag = None
self.logger = structuredlog.StructuredLogger(suite_name)
def __enter__(self):
@@ -992,6 +1014,7 @@ class ManagerGroup:
"""Start all managers in the group"""
test_queue, size = self.test_queue_builder.make_queue(tests)
self.logger.info("Using %i child processes" % size)
+ self.stop_flag = StopFlag(size)
for idx in range(size):
manager = TestRunnerManager(self.suite_name,
@@ -1020,18 +1043,31 @@ class ManagerGroup:
timeout: Overall timeout (in seconds) for all threads to join. The
default value indicates an indefinite timeout.
"""
- deadline = None if timeout is None else time.time() + timeout
- for manager in self.pool:
- manager_timeout = None
- if deadline is not None:
- manager_timeout = max(0, deadline - time.time())
- manager.join(manager_timeout)
+ # Here, the main thread cannot simply `join()` the threads in
+ # `self.pool` sequentially because a keyboard interrupt raised during a
+ # `Thread.join()` may incorrectly mark that thread as "stopped" when it
+ # is not [0, 1]. Subsequent `join()`s for the affected thread won't
+ # block anymore, so a subsequent `ManagerGroup.wait()` may return with
+ # that thread still alive.
+ #
+ # To the extent the timeout allows, it's important that
+ # `ManagerGroup.wait()` returns with all `TestRunnerManager` threads
+ # actually stopped. Otherwise, a live thread may log after `mozlog`
+ # shutdown (not allowed) or worse, leak browser processes that the
+ # thread should have stopped when exiting its run loop [2].
+ #
+ # [0]: https://github.com/python/cpython/issues/90882
+ # [1]: https://github.com/python/cpython/blob/558b517b/Lib/threading.py#L1146-L1178
+ # [2]: https://crbug.com/330236796
+ assert self.stop_flag, "ManagerGroup hasn't been started yet"
+ self.stop_flag.wait_for_all_managers_done(timeout)
def stop(self):
"""Set the stop flag so that all managers in the group stop as soon
as possible"""
- self.stop_flag.set()
- self.logger.debug("Stop flag set in ManagerGroup")
+ if self.stop_flag:
+ self.stop_flag.stop()
+ self.logger.debug("Stop flag set in ManagerGroup")
def test_count(self):
return sum(manager.test_count for manager in self.pool)
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py
index d65369b380..d9d85de6a4 100644
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py
@@ -396,11 +396,16 @@ def run_tests(config, product, test_paths, **kwargs):
product.check_args(**kwargs)
+ kwargs["allow_list_paths"] = []
if kwargs["install_fonts"]:
+ # Add test font to allow list for sandbox to ensure that the content
+ # processes will have read access.
+ ahem_path = os.path.join(test_paths["/"].tests_path, "fonts/Ahem.ttf")
+ kwargs["allow_list_paths"].append(ahem_path)
env_extras.append(FontInstaller(
logger,
font_dir=kwargs["font_dir"],
- ahem=os.path.join(test_paths["/"].tests_path, "fonts/Ahem.ttf")
+ ahem=ahem_path
))
recording.set(["startup", "load_tests"])
diff --git a/testing/web-platform/tests/tools/wptserve/setup.py b/testing/web-platform/tests/tools/wptserve/setup.py
index 1496fa7e17..36aa98f1d8 100644
--- a/testing/web-platform/tests/tools/wptserve/setup.py
+++ b/testing/web-platform/tests/tools/wptserve/setup.py
@@ -1,7 +1,10 @@
from setuptools import setup
-PACKAGE_VERSION = '4.0'
-deps = ["h2>=4.1.0"]
+PACKAGE_VERSION = '4.0.2'
+deps = [
+ "h2>=4.1.0",
+ "pywebsocket3>=4.0.2",
+]
setup(name='wptserve',
version=PACKAGE_VERSION,
diff --git a/testing/web-platform/tests/tools/wptserve/wptserve/response.py b/testing/web-platform/tests/tools/wptserve/wptserve/response.py
index a6ece62dab..8d0e01bbdd 100644
--- a/testing/web-platform/tests/tools/wptserve/wptserve/response.py
+++ b/testing/web-platform/tests/tools/wptserve/wptserve/response.py
@@ -4,7 +4,7 @@ import json
import uuid
import traceback
from collections import OrderedDict
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
from io import BytesIO
from hpack.struct import HeaderTuple
@@ -135,7 +135,7 @@ class Response:
"oct", "nov", "dec"])}
if isinstance(expires, timedelta):
- expires = datetime.utcnow() + expires
+ expires = datetime.now(timezone.utc) + expires
if expires is not None:
expires_str = expires.strftime("%d %%s %Y %H:%M:%S GMT")
diff --git a/testing/web-platform/tests/tools/wptserve/wptserve/server.py b/testing/web-platform/tests/tools/wptserve/wptserve/server.py
index 8ce36201ee..1863861542 100644
--- a/testing/web-platform/tests/tools/wptserve/wptserve/server.py
+++ b/testing/web-platform/tests/tools/wptserve/wptserve/server.py
@@ -23,8 +23,8 @@ from h2.utilities import extract_method_header
from urllib.parse import urlsplit, urlunsplit
-from mod_pywebsocket import dispatch
-from mod_pywebsocket.handshake import HandshakeException, AbortedByUserException
+from pywebsocket3 import dispatch
+from pywebsocket3.handshake import HandshakeException, AbortedByUserException
from . import routes as default_routes
from .config import ConfigBuilder
diff --git a/testing/web-platform/tests/tools/wptserve/wptserve/sslutils/openssl.py b/testing/web-platform/tests/tools/wptserve/wptserve/sslutils/openssl.py
index 5a16097e37..25f86e019e 100644
--- a/testing/web-platform/tests/tools/wptserve/wptserve/sslutils/openssl.py
+++ b/testing/web-platform/tests/tools/wptserve/wptserve/sslutils/openssl.py
@@ -6,7 +6,7 @@ import random
import shutil
import subprocess
import tempfile
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
# Amount of time beyond the present to consider certificates "expired." This
# allows certificates to be proactively re-generated in the "buffer" period
@@ -316,7 +316,7 @@ class OpenSSLEnvironment:
# Because `strptime` does not account for time zone offsets, it is
# always in terms of UTC, so the current time should be calculated
# accordingly.
- if end_date < datetime.utcnow() + time_buffer:
+ if end_date < datetime.now(timezone.utc) + time_buffer:
return False
#TODO: check the key actually signed the cert.
diff --git a/testing/web-platform/tests/tools/wptserve/wptserve/ws_h2_handshake.py b/testing/web-platform/tests/tools/wptserve/wptserve/ws_h2_handshake.py
index af668dd558..ab1ab958a0 100644
--- a/testing/web-platform/tests/tools/wptserve/wptserve/ws_h2_handshake.py
+++ b/testing/web-platform/tests/tools/wptserve/wptserve/ws_h2_handshake.py
@@ -7,12 +7,12 @@ Specification:
https://tools.ietf.org/html/rfc8441
"""
-from mod_pywebsocket import common
+from pywebsocket3 import common
-from mod_pywebsocket.handshake.base import get_mandatory_header
-from mod_pywebsocket.handshake.base import HandshakeException
-from mod_pywebsocket.handshake.base import validate_mandatory_header
-from mod_pywebsocket.handshake.base import HandshakerBase
+from pywebsocket3.handshake.base import get_mandatory_header
+from pywebsocket3.handshake.base import HandshakeException
+from pywebsocket3.handshake.base import validate_mandatory_header
+from pywebsocket3.handshake.base import HandshakerBase
def check_connect_method(request):
diff --git a/testing/web-platform/tests/trust-tokens/end-to-end/has-trust-token-with-no-top-frame.tentative.https.html b/testing/web-platform/tests/trust-tokens/end-to-end/has-trust-token-with-no-top-frame.tentative.https.html
index a384bec3c2..bc92422e36 100644
--- a/testing/web-platform/tests/trust-tokens/end-to-end/has-trust-token-with-no-top-frame.tentative.https.html
+++ b/testing/web-platform/tests/trust-tokens/end-to-end/has-trust-token-with-no-top-frame.tentative.https.html
@@ -12,11 +12,13 @@
const frame = document.createElement('iframe');
document.body.appendChild(frame);
const cachedDocument = window[0].document;
+ const IFrameDOMException = frame.contentWindow.DOMException;
frame.remove();
- test(() => {
- assert_equals(cachedDocument.hasPrivateToken("https://issuer.example"), undefined,
- "Can't construct a Promise in a destroyed execution context.");
+ promise_test(async t => {
+ await promise_rejects_dom(
+ t, 'InvalidStateError', IFrameDOMException,
+ cachedDocument.hasPrivateToken("https://issuer.example"));
}, 'hasPrivateToken in a destroyed document.');
</script>
</body>
diff --git a/testing/web-platform/tests/trusted-types/Element-setAttribute-respects-Elements-node-documents-globals-CSP.html b/testing/web-platform/tests/trusted-types/Element-setAttribute-respects-Elements-node-documents-globals-CSP.html
new file mode 100644
index 0000000000..c0f72bb36a
--- /dev/null
+++ b/testing/web-platform/tests/trusted-types/Element-setAttribute-respects-Elements-node-documents-globals-CSP.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="require-trusted-types-for 'script';"
+ />
+ <title>
+ trusted-types (TT): `setAttribute`/`setAttributeNode` for an element
+ adopted from a non-TT realm respects TT's Content-Security-Policy (CSP)
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="nonSVGTestElements">
+ <iframe srcdoc="v"></iframe>
+ <embed src="v" />
+ <script src="v"></script>
+ <object data="v"></object>
+ <object codebase="v"></object>
+ </div>
+ <svg id="svgTestElements">
+ <script href="v"></script>
+ <script xlink:href="v"></script>
+ </svg>
+ <script>
+ const passThroughPolicy = trustedTypes.createPolicy("passThrough", {
+ createHTML: (s) => s,
+ });
+
+ function runTest(aTestElement) {
+ const testAttr = aTestElement.attributes[0];
+
+ async_test(
+ (t) => {
+ const sourceFrame = document.createElement("iframe");
+
+ // The markup requires the parent element to ensure the attribute is associated with the
+ // correct namespace.
+ sourceFrame.srcdoc = passThroughPolicy.createHTML(
+ `<!DOCTYPE html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <` +
+ aTestElement.parentElement.localName +
+ `>
+ <` +
+ aTestElement.localName +
+ ` ` +
+ testAttr.name +
+ `="` +
+ testAttr.value +
+ `">
+ </` +
+ aTestElement.localName +
+ `>
+ </` +
+ aTestElement.parentElement.localName +
+ `>
+ doc without TT CSP.
+ </body>`
+ );
+
+ t.add_cleanup(() => {
+ sourceFrame.remove();
+ });
+
+ sourceFrame.addEventListener(
+ "load",
+ t.step_func_done(() => {
+ // A window is a global object which has 1-to-1 mapping to a realm, see the first
+ // note of <https://html.spec.whatwg.org/#realms-settings-objects-global-objects>
+ // and its following paragraph. Here, `sourceElement`'s node document's global
+ // belongs to a non-TT realm.
+
+ const sourceElement =
+ sourceFrame.contentDocument.body.querySelector(
+ aTestElement.localName
+ );
+ const sourceAttr = sourceElement.getAttributeNode(
+ testAttr.name
+ );
+ sourceElement.removeAttributeNode(sourceAttr);
+
+ document.body.append(sourceElement);
+ // Now `sourceElement`'s node document's global belongs to a TT-realm.
+
+ assert_throws_js(sourceFrame.contentWindow.TypeError, () => {
+ sourceElement.setAttributeNode(sourceAttr);
+ });
+ assert_throws_js(sourceFrame.contentWindow.TypeError, () => {
+ sourceElement.setAttributeNS(
+ sourceAttr.namespaceURI,
+ sourceAttr.name,
+ sourceAttr.value
+ );
+ });
+ })
+ );
+
+ document.body.append(sourceFrame);
+ },
+ `setAttribute and setAttributeNode respect the element's node document's global's CSP;
+ Element=${aTestElement.localName}; Parent=${aTestElement.parentElement.localName}; Attribute=${testAttr.name}`
+ );
+ }
+
+ for (const testElement of document.querySelectorAll(
+ "#nonSVGTestElements *"
+ )) {
+ runTest(testElement);
+ }
+
+ for (const testElement of document.querySelectorAll(
+ "#svgTestElements *"
+ )) {
+ runTest(testElement);
+ }
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/trusted-types/Element-setAttribute.html b/testing/web-platform/tests/trusted-types/Element-setAttribute.html
index cd6617915b..9f6eec7da2 100644
--- a/testing/web-platform/tests/trusted-types/Element-setAttribute.html
+++ b/testing/web-platform/tests/trusted-types/Element-setAttribute.html
@@ -8,7 +8,6 @@
<script>
// TrustedScriptURL Assignments
let scriptTestCases = [
- [ 'embed', 'src' ],
[ 'script', 'src' ]
];
diff --git a/testing/web-platform/tests/trusted-types/HTMLElement-generic.html b/testing/web-platform/tests/trusted-types/HTMLElement-generic.html
index 8e54fa9c57..832411b5f5 100644
--- a/testing/web-platform/tests/trusted-types/HTMLElement-generic.html
+++ b/testing/web-platform/tests/trusted-types/HTMLElement-generic.html
@@ -41,7 +41,6 @@ function getTrusted(element, attr) {
// Types is currently enabled (& enforced) or not.
function runTests(is_tt_enabled) {
for (const [element, attr] of [
- [ 'embed', 'src' ],
[ 'script', 'src' ],
[ 'div', 'innerHTML' ],
[ 'iframe', 'srcdoc' ],
diff --git a/testing/web-platform/tests/trusted-types/TrustedType-AttributeNodes.html b/testing/web-platform/tests/trusted-types/TrustedType-AttributeNodes.html
index 6b00665700..f4269a5d40 100644
--- a/testing/web-platform/tests/trusted-types/TrustedType-AttributeNodes.html
+++ b/testing/web-platform/tests/trusted-types/TrustedType-AttributeNodes.html
@@ -10,7 +10,7 @@
// identically. For background, se::
// https://github.com/w3c/webappsec-trusted-types/issues/47
- const elems = ["div", "script", "embed", "iframe"];
+ const elems = ["div", "script", "iframe"];
const attrs = ["src", "srcdoc", "onclick", "bla"];
const policy_callback = s => ("" + s + " [policy]");
diff --git a/testing/web-platform/tests/trusted-types/TrustedTypePolicyFactory-getPropertyType.html b/testing/web-platform/tests/trusted-types/TrustedTypePolicyFactory-getPropertyType.tentative.html
index 84bcb8d839..e7218e9333 100644
--- a/testing/web-platform/tests/trusted-types/TrustedTypePolicyFactory-getPropertyType.html
+++ b/testing/web-platform/tests/trusted-types/TrustedTypePolicyFactory-getPropertyType.tentative.html
@@ -19,11 +19,6 @@
assert_equals(trustedTypes.getAttributeType("img", "madeup"), null);
}, "sanity check trustedTypes.getAttributeType.");
- test(t => {
- assert_true(!!trustedTypes.getTypeMapping());
- }, "sanity check trustedTypes.getTypeMapping");
-
-
// getPropertyType tests adapted from w3c/trusted-types polyfill:
test(t => {
// returns the proper type for attribute-related properties
@@ -71,27 +66,9 @@
assert_equals(trustedTypes.getAttributeType('img', 'bar'), null);
}, "getAttributeType tests adapted from w3c/trusted-types polyfill");
-
- test(t=> {
- const map = trustedTypes.getTypeMapping();
-
- // Spot testing some values.
- assert_equals(map["script"].attributes.src, "TrustedScriptURL");
- assert_equals(map["*"].properties.innerHTML, "TrustedHTML");
- assert_equals(map["foo"], undefined);
-
- // getTypeMapping returns a 'clean' object, in case the return value has
- // been modified.
- map["*"].attributes["foo"] = "bar";
- assert_equals(trustedTypes.getTypeMapping()["*"].attributes["foo"], undefined);
-;
- // Unknown namespaces:
- assert_equals(trustedTypes.getTypeMapping("http://foo/bar"), null);
- }, "getTypeMapping tests adapted from w3c/trusted-types polyfill");
-
// Test case handling for both attributes and properties.
- for (let attr of ["codeBase", "CODEBASE", "codebase"]) {
- for (let elem of ["object", "OBJECT", "oBjEcT"]) {
+ for (let attr of ["srcDoc", "SRCDOC", "srcdoc"]) {
+ for (let elem of ["iframe", "IFRAME", "iFrAmE"]) {
test(t => {
// attributes are case-insensitive, so all variants should be defined.
assert_true(trustedTypes.getAttributeType(elem, attr) != undefined);
@@ -100,7 +77,7 @@
// properties are case-sensitive, so only the "correct" spelling
// should be defined.
assert_equals(trustedTypes.getPropertyType(elem, attr) != undefined,
- attr == "codeBase");
+ attr == "srcdoc");
}, `${elem}.${attr} is maybe defined`);
}
}
diff --git a/testing/web-platform/tests/trusted-types/TrustedTypePolicyFactory-metadata.html b/testing/web-platform/tests/trusted-types/TrustedTypePolicyFactory-metadata.tentative.html
index 70a5b44666..e7772bf0d1 100644
--- a/testing/web-platform/tests/trusted-types/TrustedTypePolicyFactory-metadata.html
+++ b/testing/web-platform/tests/trusted-types/TrustedTypePolicyFactory-metadata.tentative.html
@@ -33,22 +33,6 @@
let properties = ['madeup', 'id', "onerror", "onclick"];
const types = [null, "TrustedHTML", "TrustedScript", "TrustedScriptURL"];
- // We'll wrap construction of the elements/properties list in a test, mainly
- // so we'll get decent error messages when it might fail.
- test(t => {
- // Collect all element and property names from getTypeMapping().
- const map = trustedTypes.getTypeMapping();
- for (let elem in map) {
- elements.push(elem);
- properties = properties.concat(Object.keys(map[elem].properties));
- }
-
- // Remove "*" entries and de-duplicate properties.
- elements = elements.filter(s => !s.includes("*"));
- properties = properties.filter(s => !s.includes("*"));
- properties = Array.from(new Set(properties));
- }, "Prepare parameters for generic test series below.");
-
// Iterate one test for each combination of element, property, and sink type.
const target = document.getElementById("target");
for (elem of elements) {
diff --git a/testing/web-platform/tests/trusted-types/block-string-assignment-to-Element-setAttribute.html b/testing/web-platform/tests/trusted-types/block-string-assignment-to-Element-setAttribute.html
index 295890f319..4cc877efb2 100644
--- a/testing/web-platform/tests/trusted-types/block-string-assignment-to-Element-setAttribute.html
+++ b/testing/web-platform/tests/trusted-types/block-string-assignment-to-Element-setAttribute.html
@@ -13,9 +13,6 @@
// TrustedScriptURL Assignments
const scriptURLTestCases = [
- [ 'embed', 'src', INPUTS.SCRIPTURL, RESULTS.SCRIPTURL],
- [ 'object', 'data', INPUTS.SCRIPTURL, RESULTS.SCRIPTURL ],
- [ 'object', 'codeBase', INPUTS.SCRIPTURL, RESULTS.SCRIPTURL ],
[ 'script', 'src', INPUTS.SCRIPTURL, RESULTS.SCRIPTURL ]
];
diff --git a/testing/web-platform/tests/trusted-types/block-string-assignment-to-Element-setAttributeNS.html b/testing/web-platform/tests/trusted-types/block-string-assignment-to-Element-setAttributeNS.html
index b7f74be6b7..78c8c0db98 100644
--- a/testing/web-platform/tests/trusted-types/block-string-assignment-to-Element-setAttributeNS.html
+++ b/testing/web-platform/tests/trusted-types/block-string-assignment-to-Element-setAttributeNS.html
@@ -63,10 +63,7 @@
// <https://w3c.github.io/trusted-types/dist/spec/#validate-attribute-mutation>.
const nonLowerCaseTests = [
{ element: "iframe", attribute: "SRCDOC", elementNamespace: htmlNamespace },
- { element: "embed", attribute: "SRC", elementNamespace: htmlNamespace },
{ element: "script", attribute: "SRC", elementNamespace: htmlNamespace },
- { element: "object", attribute: "DATA", elementNamespace: htmlNamespace },
- { element: "object", attribute: "CODEBASE", elementNamespace: htmlNamespace },
{ element: "script", attribute: "HREF", elementNamespace: svgNamespace },
{ element: "script", attribute: "HREF", elementNamespace: svgNamespace,
attributeNamespace: xlinkNamespace },
diff --git a/testing/web-platform/tests/trusted-types/block-string-assignment-to-HTMLElement-generic.html b/testing/web-platform/tests/trusted-types/block-string-assignment-to-HTMLElement-generic.html
index 9e780c1ed2..a9764c0258 100644
--- a/testing/web-platform/tests/trusted-types/block-string-assignment-to-HTMLElement-generic.html
+++ b/testing/web-platform/tests/trusted-types/block-string-assignment-to-HTMLElement-generic.html
@@ -12,9 +12,6 @@
var testnb = 0;
// TrustedScriptURL Assignments
const scriptURLTestCases = [
- [ 'embed', 'src' ],
- [ 'object', 'codeBase' ],
- [ 'object', 'data' ],
[ 'script', 'src' ]
];
diff --git a/testing/web-platform/tests/trusted-types/block-string-assignment-to-attribute-via-attribute-node.html b/testing/web-platform/tests/trusted-types/block-string-assignment-to-attribute-via-attribute-node.html
index b881e8cb37..91ac9b6485 100644
--- a/testing/web-platform/tests/trusted-types/block-string-assignment-to-attribute-via-attribute-node.html
+++ b/testing/web-platform/tests/trusted-types/block-string-assignment-to-attribute-via-attribute-node.html
@@ -7,9 +7,6 @@
</head>
<body>
<div id="testcases">
- <embed src="x"></embed>
- <object data="x"></object>
- <object codeBase="x"></object>
<script src="x"></script>
<iframe srcdoc="x"></iframe>
<div onclick="x"></div>
diff --git a/testing/web-platform/tests/trusted-types/default-policy-callback-arguments.html b/testing/web-platform/tests/trusted-types/default-policy-callback-arguments.html
index a4a9c9e351..4801b55387 100644
--- a/testing/web-platform/tests/trusted-types/default-policy-callback-arguments.html
+++ b/testing/web-platform/tests/trusted-types/default-policy-callback-arguments.html
@@ -31,7 +31,7 @@
const cases = [
[ "abc", "TrustedHTML", "Element innerHTML",
_ => div.innerHTML = "abc" ],
- [ "2+2", "TrustedScript", "Node textContent",
+ [ "2+2", "TrustedScript", "HTMLScriptElement textContent",
_ => script.textContent = "2+2" ],
[ "about:blank", "TrustedScriptURL", "HTMLScriptElement src",
_ => script.src = "about:blank" ],
diff --git a/testing/web-platform/tests/trusted-types/modify-attributes-in-callback.html b/testing/web-platform/tests/trusted-types/modify-attributes-in-callback.html
new file mode 100644
index 0000000000..96b4501128
--- /dev/null
+++ b/testing/web-platform/tests/trusted-types/modify-attributes-in-callback.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <meta http-equiv="Content-Security-Policy"
+ content="require-trusted-types-for 'script'; trusted-types *">
+</head>
+<body>
+<iframe id="iframe" data-x="" srcdoc="content" onmouseover=""></iframe>
+<script>
+ // This is a regression test for https://g-issues.chromium.org/issues/333739948
+ // The test should hold true for any browser that supports Trusted Types.
+
+ let target = "data-x";
+ trustedTypes.createPolicy("default", {
+ createHTML: (s) => {
+ iframe.removeAttribute(target);
+ return s;
+ }
+ });
+
+ test(t => {
+ // Original bug report: Delete an attribute *before* the current one.
+ assert_equals(iframe.srcdoc, "content");
+ assert_equals(iframe.getAttribute("onmouseover"), "");
+ iframe.setAttribute("srcdoc", "alert(1)");
+ assert_equals(iframe.srcdoc, "alert(1)");
+ assert_equals(iframe.getAttribute("onmouseover"), "");
+ }, "Ensure the right attributes are modified.");
+
+ test(t => {
+ // Second case: Delete the exact attribute. It still gets set.
+ target = "srcdoc";
+ assert_equals(iframe.srcdoc, "alert(1)");
+ iframe.setAttribute("srcdoc", "new srcdoc value");
+ assert_equals(iframe.srcdoc, "new srcdoc value");
+ }, "Ensure the deleted attributes is modified.");
+
+</script>
diff --git a/testing/web-platform/tests/trusted-types/support/resolve-spv.js b/testing/web-platform/tests/trusted-types/support/resolve-spv.js
new file mode 100644
index 0000000000..89e58b2a8b
--- /dev/null
+++ b/testing/web-platform/tests/trusted-types/support/resolve-spv.js
@@ -0,0 +1,9 @@
+// Returns a promise that resolves with a Security Policy Violation (spv)
+ // even when it is received.
+function promise_spv() {
+ return new Promise((resolve, reject) => {
+ window.addEventListener("securitypolicyviolation", e => {
+ resolve(e);
+ }, { once: true });
+ });
+}
diff --git a/testing/web-platform/tests/trusted-types/trusted-types-svg-script-set-href.html b/testing/web-platform/tests/trusted-types/trusted-types-svg-script-set-href.html
new file mode 100644
index 0000000000..ab3174c192
--- /dev/null
+++ b/testing/web-platform/tests/trusted-types/trusted-types-svg-script-set-href.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="./support/resolve-spv.js"></script>
+ <meta http-equiv="Content-Security-Policy"
+ content="require-trusted-types-for 'script'">
+</head>
+<body>
+ <div id="log"></div>
+ <svg id="svg"><script id="script">"some script text";</script></svg>
+ <script>
+ const policy = trustedTypes.createPolicy("policy", {
+ createScript: x => x, createHTML: x => x, createScriptURL: x => x });
+
+ promise_test(t => {
+ const elem = document.createElementNS(
+ "http://www.w3.org/2000/svg", "script");
+ assert_throws_js(TypeError, _ => {
+ elem.href.baseVal = "about:blank";
+ });
+ document.getElementById("svg").appendChild(elem);
+ return promise_spv();
+ }, "Assign string to SVGScriptElement.href.baseVal.");
+
+ promise_test(t => {
+ const elem = document.createElementNS(
+ "http://www.w3.org/2000/svg", "script");
+ elem.href.baseVal = policy.createScriptURL("about:blank");
+ document.getElementById("svg").appendChild(elem);
+ return Promise.resolve();
+ }, "Assign TrustedScriptURL to SVGScriptElement.href.baseVal.");
+
+ promise_test(t => {
+ const elem = document.createElementNS(
+ "http://www.w3.org/2000/svg", "script");
+ assert_throws_js(TypeError, _ => {
+ elem.setAttribute("href", "about:blank");
+ });
+ document.getElementById("svg").appendChild(elem);
+ return promise_spv();
+ }, "Assign string to non-attached SVGScriptElement.href via setAttribute.");
+
+ promise_test(t => {
+ const elem = document.createElementNS(
+ "http://www.w3.org/2000/svg", "script");
+ elem.setAttribute("href", policy.createScriptURL("about:blank"));
+ document.getElementById("svg").appendChild(elem);
+ return Promise.resolve();
+ }, "Assign TrustedScriptURL to non-attached SVGScriptElement.href via setAttribute.");
+
+ promise_test(t => {
+ const elem = document.createElementNS(
+ "http://www.w3.org/2000/svg", "script");
+ document.getElementById("svg").appendChild(elem);
+ assert_throws_js(TypeError, _ => {
+ elem.setAttribute("href", "about:blank");
+ });
+ return promise_spv();
+ }, "Assign string to attached SVGScriptElement.href via setAttribute.");
+
+ promise_test(t => {
+ const elem = document.createElementNS(
+ "http://www.w3.org/2000/svg", "script");
+ document.getElementById("svg").appendChild(elem);
+ elem.setAttribute("href", policy.createScriptURL("about:blank"));
+ return Promise.resolve();
+ }, "Assign TrustedScriptURL to attached SVGScriptElement.href via setAttribute.");
+
+ // Default policy test: We repate the string assignment tests above,
+ // but now expect all of them to pass.
+ promise_test(t => {
+ trustedTypes.createPolicy("default", {
+ createScript: x => x, createHTML: x => x, createScriptURL: x => x });
+ return Promise.resolve();
+ }, "Setup default policy");
+
+ promise_test(t => {
+ document.getElementById("script").innerHTML = "'modified via innerHTML';";
+ return Promise.resolve();
+ }, "Assign String to SVGScriptElement.innerHTML w/ default policy.");
+
+ promise_test(t => {
+ const elem = document.createElementNS(
+ "http://www.w3.org/2000/svg", "script");
+ elem.href.baseVal = "about:blank";
+ document.getElementById("svg").appendChild(elem);
+ return Promise.resolve();
+ }, "Assign string to SVGScriptElement.href.baseVal w/ default policy.");
+
+ promise_test(t => {
+ const elem = document.createElementNS(
+ "http://www.w3.org/2000/svg", "script");
+ elem.setAttribute("href", "about:blank");
+ document.getElementById("svg").appendChild(elem);
+ return Promise.resolve();
+ }, "Assign string to non-attached SVGScriptElement.href via setAttribute w/ default policy.");
+
+ promise_test(t => {
+ const elem = document.createElementNS(
+ "http://www.w3.org/2000/svg", "script");
+ document.getElementById("svg").appendChild(elem);
+ elem.setAttribute("href", "about:blank");
+ return Promise.resolve();
+ }, "Assign string to attached SVGScriptElement.href via setAttribute w/ default policy.");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/trusted-types/trusted-types-svg-script.html b/testing/web-platform/tests/trusted-types/trusted-types-svg-script.html
index 946f825fa3..4d604f353c 100644
--- a/testing/web-platform/tests/trusted-types/trusted-types-svg-script.html
+++ b/testing/web-platform/tests/trusted-types/trusted-types-svg-script.html
@@ -2,6 +2,7 @@
<head>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
+ <script src="./support/resolve-spv.js"></script>
<meta http-equiv="Content-Security-Policy"
content="require-trusted-types-for 'script'">
</head>
@@ -9,16 +10,6 @@
<div id="log"></div>
<svg id="svg"><script id="script">"some script text";</script></svg>
<script>
- // Returns a promise that resolves with a Security Policy Violation (spv)
- // even when it is received.
- function promise_spv() {
- return new Promise((resolve, reject) => {
- window.addEventListener("securitypolicyviolation", e => {
- resolve(e);
- }, { once: true });
- });
- }
-
const policy = trustedTypes.createPolicy("policy", {
createScript: x => x, createHTML: x => x, createScriptURL: x => x });
@@ -49,96 +40,5 @@
document.getElementById("svg").appendChild(elem);
return promise_spv();
}, "Modify SVGScriptElement via DOM manipulation.");
-
- promise_test(t => {
- const elem = document.createElementNS(
- "http://www.w3.org/2000/svg", "script");
- assert_throws_js(TypeError, _ => {
- elem.href.baseVal = "about:blank";
- });
- document.getElementById("svg").appendChild(elem);
- return promise_spv();
- }, "Assign string to SVGScriptElement.href.baseVal.");
-
- promise_test(t => {
- const elem = document.createElementNS(
- "http://www.w3.org/2000/svg", "script");
- elem.href.baseVal = policy.createScriptURL("about:blank");
- document.getElementById("svg").appendChild(elem);
- return Promise.resolve();
- }, "Assign TrustedScriptURL to SVGScriptElement.href.baseVal.");
-
- promise_test(t => {
- const elem = document.createElementNS(
- "http://www.w3.org/2000/svg", "script");
- assert_throws_js(TypeError, _ => {
- elem.setAttribute("href", "about:blank");
- });
- document.getElementById("svg").appendChild(elem);
- return promise_spv();
- }, "Assign string to non-attached SVGScriptElement.href via setAttribute.");
-
- promise_test(t => {
- const elem = document.createElementNS(
- "http://www.w3.org/2000/svg", "script");
- elem.setAttribute("href", policy.createScriptURL("about:blank"));
- document.getElementById("svg").appendChild(elem);
- return Promise.resolve();
- }, "Assign TrustedScriptURL to non-attached SVGScriptElement.href via setAttribute.");
-
- promise_test(t => {
- const elem = document.createElementNS(
- "http://www.w3.org/2000/svg", "script");
- document.getElementById("svg").appendChild(elem);
- assert_throws_js(TypeError, _ => {
- elem.setAttribute("href", "about:blank");
- });
- return promise_spv();
- }, "Assign string to attached SVGScriptElement.href via setAttribute.");
-
- promise_test(t => {
- const elem = document.createElementNS(
- "http://www.w3.org/2000/svg", "script");
- document.getElementById("svg").appendChild(elem);
- elem.setAttribute("href", policy.createScriptURL("about:blank"));
- return Promise.resolve();
- }, "Assign TrustedScriptURL to attached SVGScriptElement.href via setAttribute.");
-
- // Default policy test: We repate the string assignment tests above,
- // but now expect all of them to pass.
- promise_test(t => {
- trustedTypes.createPolicy("default", {
- createScript: x => x, createHTML: x => x, createScriptURL: x => x });
- return Promise.resolve();
- }, "Setup default policy");
-
- promise_test(t => {
- document.getElementById("script").innerHTML = "'modified via innerHTML';";
- return Promise.resolve();
- }, "Assign String to SVGScriptElement.innerHTML w/ default policy.");
-
- promise_test(t => {
- const elem = document.createElementNS(
- "http://www.w3.org/2000/svg", "script");
- elem.href.baseVal = "about:blank";
- document.getElementById("svg").appendChild(elem);
- return Promise.resolve();
- }, "Assign string to SVGScriptElement.href.baseVal w/ default policy.");
-
- promise_test(t => {
- const elem = document.createElementNS(
- "http://www.w3.org/2000/svg", "script");
- elem.setAttribute("href", "about:blank");
- document.getElementById("svg").appendChild(elem);
- return Promise.resolve();
- }, "Assign string to non-attached SVGScriptElement.href via setAttribute w/ default policy.");
-
- promise_test(t => {
- const elem = document.createElementNS(
- "http://www.w3.org/2000/svg", "script");
- document.getElementById("svg").appendChild(elem);
- elem.setAttribute("href", "about:blank");
- return Promise.resolve();
- }, "Assign string to attached SVGScriptElement.href via setAttribute w/ default policy.");
</script>
</body>
diff --git a/testing/web-platform/tests/url/WEB_FEATURES.yml b/testing/web-platform/tests/url/WEB_FEATURES.yml
new file mode 100644
index 0000000000..4711efc1fa
--- /dev/null
+++ b/testing/web-platform/tests/url/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: url-canparse
+ files:
+ - url-statics-canparse.*
diff --git a/testing/web-platform/tests/url/resources/setters_tests.json b/testing/web-platform/tests/url/resources/setters_tests.json
index 82adf4cdce..3850606d66 100644
--- a/testing/web-platform/tests/url/resources/setters_tests.json
+++ b/testing/web-platform/tests/url/resources/setters_tests.json
@@ -826,6 +826,17 @@
}
},
{
+ "comment": "Stuff after a ? delimiter is ignored, trailing 'port'",
+ "href": "http://example.net/path",
+ "new_value": "example.com?stuff:8080",
+ "expected": {
+ "href": "http://example.com/path",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": ""
+ }
+ },
+ {
"comment": "Stuff after a ? delimiter is ignored",
"href": "http://example.net/path",
"new_value": "example.com:8080?stuff",
@@ -925,6 +936,39 @@
}
},
{
+ "comment": "Anything other than ASCII digit stops the port parser in a setter but is not an error",
+ "href": "http://example.net:8080",
+ "new_value": "example.com:invalid",
+ "expected": {
+ "href": "http://example.com:8080/",
+ "host": "example.com:8080",
+ "hostname": "example.com",
+ "port": "8080"
+ }
+ },
+ {
+ "comment": "Anything other than ASCII digit stops the port parser in a setter but is not an error",
+ "href": "http://example.net:8080/test",
+ "new_value": "[::1]:invalid",
+ "expected": {
+ "href": "http://[::1]:8080/test",
+ "host": "[::1]:8080",
+ "hostname": "[::1]",
+ "port": "8080"
+ }
+ },
+ {
+ "comment": "IPv6 without port",
+ "href": "http://example.net:8080/test",
+ "new_value": "[::1]",
+ "expected": {
+ "href": "http://[::1]:8080/test",
+ "host": "[::1]:8080",
+ "hostname": "[::1]",
+ "port": "8080"
+ }
+ },
+ {
"comment": "Port numbers are 16 bit integers",
"href": "http://example.net/path",
"new_value": "example.com:65535",
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/background-shorthand.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/background-shorthand.html
new file mode 100644
index 0000000000..f186643331
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/background-shorthand.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Animations: Expansion of shorthand properties</title>
+<link rel="help" href="https://www.w3.org/TR/web-animations-1/#calculating-computed-keyframes">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #block {
+ background: green;
+ background-position-x: 10px;
+ height: 100px;
+ width: 100px;
+ }
+</style>
+<body>
+ <div id="block"></div>
+</body>
+<script>
+ function assert_background_position_at(time, x, y) {
+ document.getAnimations()[0].currentTime = time;
+ const target = document.getElementById('block');
+ const style = getComputedStyle(target);
+ assert_equals(style.backgroundPositionX, x,
+ `background-position-x @${time/10}% progress`);
+ assert_equals(style.backgroundPositionY, y,
+ `background-position-y @${time/10}% progress`);
+ }
+
+ test(() => {
+ const target = document.getElementById('block');
+ target.animate([
+ { background: 'red' },
+ { backgroundPositionY: '10px', background: 'blue' }
+ ], { duration: 1000 });
+ // Animation is active in the semi-closed interval [0, 1000).
+ // The background shorthand expands to its longhand counterparts with
+ // background-position-(x|y) picking up the default value of 0%.
+ // The explicit background-property-y in the second keyframe takes priority
+ // over the value from expansion of the shorthand.
+ const test_cases = [
+ { time: -100, x: "10px", y: "0%" },
+ { time: 0, x: "0%", y: "0%" },
+ { time: 200, x: "0%", y: "calc(0% + 2px)" },
+ { time: 500, x: "0%", y: "calc(0% + 5px)" },
+ { time: 800, x: "0%", y: "calc(0% + 8px)" },
+ { time: 1100, x: "10px", y: "0%" }
+ ];
+ test_cases.forEach(test => {
+ assert_background_position_at(test.time, test.x, test.y);
+ });
+ }, 'Shorthand properties expand to longhand counterparts in computed ' +
+ 'keyframes.');
+</script>
diff --git a/testing/web-platform/tests/web-locks/WEB_FEATURES.yml b/testing/web-platform/tests/web-locks/WEB_FEATURES.yml
new file mode 100644
index 0000000000..dac5b1ba75
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: web-locks
+ files: "**"
diff --git a/testing/web-platform/tests/webauthn/WEB_FEATURES.yml b/testing/web-platform/tests/webauthn/WEB_FEATURES.yml
new file mode 100644
index 0000000000..8b273de80b
--- /dev/null
+++ b/testing/web-platform/tests/webauthn/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: webauthn-public-key-easy
+ files:
+ - createcredential-getpublickey.https.html
diff --git a/testing/web-platform/tests/webauthn/createcredential-passing.https.html b/testing/web-platform/tests/webauthn/createcredential-passing.https.html
index f64a4ff039..4124c2247e 100644
--- a/testing/web-platform/tests/webauthn/createcredential-passing.https.html
+++ b/testing/web-platform/tests/webauthn/createcredential-passing.https.html
@@ -58,6 +58,10 @@ standardSetup(function() {
new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [pkParamEC512, pkParamEC256])
.runTest("SelectEC256 pubKeyCredParams from a list");
+ // pubKeyCredParams with unknown value
+ new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [{ "type": "unknown", alg: -7, }, { "type": "public-key", alg: -7, }])
+ .runTest("pubKeyCredParams with unknown value");
+
// timeout
new CreateCredentialsTest({path: "options.publicKey.timeout", value: undefined}).runTest("passing credentials.create() with no timeout");
diff --git a/testing/web-platform/tests/webauthn/createcredential-pubkeycredparams.https.html b/testing/web-platform/tests/webauthn/createcredential-pubkeycredparams.https.html
index d1df7952d6..cb830bfe92 100644
--- a/testing/web-platform/tests/webauthn/createcredential-pubkeycredparams.https.html
+++ b/testing/web-platform/tests/webauthn/createcredential-pubkeycredparams.https.html
@@ -36,10 +36,10 @@ standardSetup(function() {
new CreateCredentialsTest({path: "options.publicKey.pubKeyCredParams", value: undefined}).runTest("Bad pubKeyCredParams: pubKeyCredParams is undefined", TypeError);
new CreateCredentialsTest("options.publicKey.pubKeyCredParams", "hi mom").runTest("Bad pubKeyCredParams: pubKeyCredParams is string", TypeError);
new CreateCredentialsTest("options.publicKey.pubKeyCredParams", null).runTest("Bad pubKeyCredParams: pubKeyCredParams is null", TypeError);
- new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badType]).runTest("Bad pubKeyCredParams: first param has bad type (\"something-else\")", TypeError);
- new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badTypeEmptyString]).runTest("Bad pubKeyCredParams: first param has bad type (\"\")", TypeError);
- new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badTypeNull]).runTest("Bad pubKeyCredParams: first param has bad type (null)", TypeError);
- new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badTypeEmptyObj]).runTest("Bad pubKeyCredParams: first param has bad type (empty object)", TypeError);
+ new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badType]).runTest("Bad pubKeyCredParams: first param has bad type (\"something-else\")", "NotSupportedError");
+ new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badTypeEmptyString]).runTest("Bad pubKeyCredParams: first param has bad type (\"\")", "NotSupportedError");
+ new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badTypeNull]).runTest("Bad pubKeyCredParams: first param has bad type (null)", "NotSupportedError");
+ new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badTypeEmptyObj]).runTest("Bad pubKeyCredParams: first param has bad type (empty object)", "NotSupportedError");
new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badAlg])
.modify("options.publicKey.timeout", 300)
.runTest("Bad pubKeyCredParams: first param has bad alg (42)", "NotAllowedError");
diff --git a/testing/web-platform/tests/webauthn/getcredential-allowcredentials.https.html b/testing/web-platform/tests/webauthn/getcredential-allowcredentials.https.html
new file mode 100644
index 0000000000..0263774142
--- /dev/null
+++ b/testing/web-platform/tests/webauthn/getcredential-allowcredentials.https.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>navigator.credentials.get() tests with allowCredentials</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src=helpers.js></script>
+<body></body>
+<script>
+standardSetup(async function() {
+ "use strict";
+
+ promise_test(async t => {
+ await navigator.credentials.get({publicKey: {
+ challenge: new Uint8Array(),
+ allowCredentials: [{
+ id: (await createCredential()).rawId,
+ type: "public-key",
+ }],
+ }});
+ }, "navigator.credentials.get() with public-key allowCredentials.");
+
+ promise_test(async t => {
+ return promise_rejects_dom(t, "NotAllowedError",
+ navigator.credentials.get({publicKey: {
+ challenge: new Uint8Array(),
+ allowCredentials: [],
+ }}));
+ }, "navigator.credentials.get() with empty allowCredentials.");
+
+ promise_test(async t => {
+ return promise_rejects_dom(t, "NotAllowedError",
+ navigator.credentials.get({publicKey: {
+ challenge: new Uint8Array(),
+ allowCredentials: [{
+ id: (await createCredential()).rawId,
+ type: "not-yet-supported-by-browser",
+ }],
+ }}));
+ }, "navigator.credentials.get() with unknown allowCredentials.");
+
+ promise_test(async t => {
+ await navigator.credentials.get({publicKey: {
+ challenge: new Uint8Array(),
+ allowCredentials: [{
+ id: (await createCredential()).rawId,
+ type: "not-yet-supported-by-browser",
+ },
+ {
+ id: (await createCredential()).rawId,
+ type: "public-key",
+ }],
+ }});
+ }, "navigator.credentials.get() with mixing allowCredentials with first unknown type.");
+
+ promise_test(async t => {
+ await navigator.credentials.get({publicKey: {
+ challenge: new Uint8Array(),
+ allowCredentials: [{
+ id: (await createCredential()).rawId,
+ type: "public-key",
+ },
+ {
+ id: (await createCredential()).rawId,
+ type: "not-yet-supported-by-browser",
+ }],
+ }});
+ }, "navigator.credentials.get() with mixing allowCredentials with last unknown type.");
+}, {
+ protocol: "ctap2_1",
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+});
+</script>
diff --git a/testing/web-platform/tests/webcodecs/audio-encoder-config.https.any.js b/testing/web-platform/tests/webcodecs/audio-encoder-config.https.any.js
index 559ff3203b..a70bf28f80 100644
--- a/testing/web-platform/tests/webcodecs/audio-encoder-config.https.any.js
+++ b/testing/web-platform/tests/webcodecs/audio-encoder-config.https.any.js
@@ -272,6 +272,8 @@ const validConfigs = [
numberOfChannels: 2,
opus: {
complexity: 5,
+ signal: 'music',
+ application: 'audio',
frameDuration: 20000,
packetlossperc: 10,
useinbandfec: true,
@@ -283,6 +285,8 @@ const validConfigs = [
numberOfChannels: 2,
opus: {
format: 'opus',
+ signal: 'voice',
+ application: 'lowdelay',
complexity: 10,
frameDuration: 60000,
packetlossperc: 20, // Irrelevant without useinbandfec, but still valid.
diff --git a/testing/web-platform/tests/webcodecs/video-encoder-flush.https.any.js b/testing/web-platform/tests/webcodecs/video-encoder-flush.https.any.js
index 8f1724bc85..c1ebafc4a3 100644
--- a/testing/web-platform/tests/webcodecs/video-encoder-flush.https.any.js
+++ b/testing/web-platform/tests/webcodecs/video-encoder-flush.https.any.js
@@ -2,16 +2,36 @@
// META: script=/common/media.js
// META: script=/webcodecs/utils.js
// META: script=/webcodecs/video-encoder-utils.js
+// META: variant=?vp8
+// META: variant=?h264_avc
+
+const VP8_CONFIG = {
+ codec: 'vp8',
+ width: 640,
+ height: 480,
+ displayWidth: 800,
+ displayHeight: 600,
+};
+
+const H264_AVC_CONFIG = {
+ codec: 'avc1.42001e', // Baseline
+ width: 640,
+ height: 480,
+ displayWidth: 800,
+ displayHeight: 600,
+ avc: {format: 'avc'},
+};
+
+let CONFIG = null;
+promise_setup(async () => {
+ CONFIG = {
+ '?vp8': VP8_CONFIG,
+ '?h264_avc': H264_AVC_CONFIG,
+ }[location.search];
+});
promise_test(async t => {
let codecInit = getDefaultCodecInit(t);
- let encoderConfig = {
- codec: 'vp8',
- width: 640,
- height: 480,
- displayWidth: 800,
- displayHeight: 600,
- };
let outputs = 0;
let firstOutput = new Promise(resolve => {
@@ -24,7 +44,7 @@ promise_test(async t => {
});
let encoder = new VideoEncoder(codecInit);
- encoder.configure(encoderConfig);
+ encoder.configure(CONFIG);
let frame1 = createFrame(640, 480, 0);
let frame2 = createFrame(640, 480, 33333);
@@ -45,3 +65,48 @@ promise_test(async t => {
assert_equals(outputs, 1, 'outputs');
}, 'Test reset during flush');
+
+promise_test(async t => {
+ let frame1 = createFrame(640, 480, 0);
+ let frame2 = createFrame(640, 480, 33333);
+ t.add_cleanup(() => {
+ frame1.close();
+ frame2.close();
+ });
+
+ const callbacks = {};
+ const encoder = createVideoEncoder(t, callbacks);
+
+ let flushInCallbackDone;
+ let outputs = 0;
+ let firstOutput = new Promise(resolve => {
+ callbacks.output = (chunk, metadata) => {
+ encoder.reset();
+
+ callbacks.output = (chunk, metadata) => {
+ outputs++;
+ };
+
+ encoder.configure(CONFIG);
+ encoder.encode(frame2);
+ flushInCallbackDone = encoder.flush();
+
+ resolve();
+ };
+ });
+
+ encoder.configure(CONFIG);
+ encoder.encode(frame1);
+ const flushDone = encoder.flush();
+
+ // Wait for the first output, then reset.
+ await firstOutput;
+
+ // Flush should have been synchronously rejected.
+ await promise_rejects_dom(t, 'AbortError', flushDone);
+
+ // Wait for the second flush and check the output count.
+ await flushInCallbackDone;
+
+ assert_equals(outputs, 1, 'outputs');
+}, 'Test new flush after reset in a flush callback');
diff --git a/testing/web-platform/tests/webcodecs/video-encoder-utils.js b/testing/web-platform/tests/webcodecs/video-encoder-utils.js
index 0838260d31..916f995156 100644
--- a/testing/web-platform/tests/webcodecs/video-encoder-utils.js
+++ b/testing/web-platform/tests/webcodecs/video-encoder-utils.js
@@ -101,3 +101,22 @@ function createDottedFrame(width, height, dots, ts) {
putBlackDots(ctx, width, height, dots);
return new VideoFrame(cnv, { timestamp: ts, duration });
}
+
+function createVideoEncoder(t, callbacks) {
+ return new VideoEncoder({
+ output(chunk, metadata) {
+ if (callbacks && callbacks.output) {
+ t.step(() => callbacks.output(chunk, metadata));
+ } else {
+ t.unreached_func('unexpected output()');
+ }
+ },
+ error(e) {
+ if (callbacks && callbacks.error) {
+ t.step(() => callbacks.error(e));
+ } else {
+ t.unreached_func('unexpected error()');
+ }
+ }
+ });
+}
diff --git a/testing/web-platform/tests/webcodecs/videoDecoder-codec-specific.https.any.js b/testing/web-platform/tests/webcodecs/videoDecoder-codec-specific.https.any.js
index a3acb82ab2..1c3b8f120d 100644
--- a/testing/web-platform/tests/webcodecs/videoDecoder-codec-specific.https.any.js
+++ b/testing/web-platform/tests/webcodecs/videoDecoder-codec-specific.https.any.js
@@ -556,6 +556,75 @@ promise_test(async t => {
const callbacks = {};
const decoder = createVideoDecoder(t, callbacks);
+ decoder.configure(CONFIG);
+ decoder.decode(CHUNKS[0]);
+ const flushDone = decoder.flush();
+
+ let flushDoneInCallback;
+ let outputs = 0;
+ await new Promise(resolve => {
+ callbacks.output = frame => {
+ decoder.reset();
+ frame.close();
+
+ callbacks.output = frame => {
+ outputs++;
+ frame.close();
+ };
+ callbacks.error = e => {
+ t.unreached_func('unexpected error()');
+ };
+ decoder.configure(CONFIG);
+ decoder.decode(CHUNKS[0]);
+ flushDoneInCallback = decoder.flush();
+
+ resolve();
+ };
+ });
+
+ // First flush should have been synchronously rejected.
+ await promise_rejects_dom(t, 'AbortError', flushDone);
+ // Wait for the second flush and check the output count.
+ await flushDoneInCallback;
+ assert_equals(outputs, 1, 'outputs');
+}, 'Test new flush after reset in a flush callback');
+
+promise_test(async t => {
+ await checkImplements();
+ const callbacks = {};
+ const decoder = createVideoDecoder(t, callbacks);
+
+ decoder.configure(CONFIG);
+ decoder.decode(CHUNKS[0]);
+ const flushDone = decoder.flush();
+ let flushDoneInCallback;
+
+ await new Promise(resolve => {
+ callbacks.output = frame => {
+ decoder.reset();
+ frame.close();
+
+ callbacks.output = frame => { frame.close(); };
+ decoder.configure(CONFIG);
+ decoder.decode(CHUNKS[0]);
+ decoder.decode(createCorruptChunk(1));
+ flushDoneInCallback = decoder.flush();
+
+ resolve();
+ };
+ });
+
+ // First flush should have been synchronously rejected.
+ await promise_rejects_dom(t, 'AbortError', flushDone);
+ // Wait for the second flush and check the error in the rejected promise.
+ await promise_rejects_dom(t, 'EncodingError', flushDoneInCallback);
+}, 'Test decoding a corrupt frame after reset in a flush callback');
+
+promise_test(async t => {
+ await checkImplements();
+ const callbacks = {};
+ const decoder = createVideoDecoder(t, callbacks);
+
decoder.configure({...CONFIG, optimizeForLatency: true});
decoder.decode(CHUNKS[0]);
diff --git a/testing/web-platform/tests/webcodecs/videoFrame-copyTo-rgb.any.js b/testing/web-platform/tests/webcodecs/videoFrame-copyTo-rgb.any.js
new file mode 100644
index 0000000000..442efc4b0f
--- /dev/null
+++ b/testing/web-platform/tests/webcodecs/videoFrame-copyTo-rgb.any.js
@@ -0,0 +1,252 @@
+// META: global=window,dedicatedworker
+// META: script=/webcodecs/videoFrame-utils.js
+// META: script=/webcodecs/video-encoder-utils.js
+
+function compareColors(actual, expected, tolerance, msg) {
+ let channel = ['R', 'G', 'B', 'A'];
+ for (let i = 0; i < 4; i++) {
+ assert_approx_equals(
+ actual[i], expected[i], tolerance,
+ `${msg} ${channel[i]}: actual: ${actual[i]} expected: ${expected[i]}`);
+ }
+}
+
+function rgb2yuv(r, g, b) {
+ let y = r * .299000 + g * .587000 + b * .114000
+ let u = r * -.168736 + g * -.331264 + b * .500000 + 128
+ let v = r * .500000 + g * -.418688 + b * -.081312 + 128
+
+ y = Math.round(y);
+ u = Math.round(u);
+ v = Math.round(v);
+ return {
+ y, u, v
+ }
+}
+
+function makeI420Frames() {
+ const kYellow = {r: 0xFF, g: 0xFF, b: 0x00};
+ const kRed = {r: 0xFF, g: 0x00, b: 0x00};
+ const kBlue = {r: 0x00, g: 0x00, b: 0xFF};
+ const kGreen = {r: 0x00, g: 0xFF, b: 0x00};
+ const kPink = {r: 0xFF, g: 0x78, b: 0xFF};
+ const kMagenta = {r: 0xFF, g: 0x00, b: 0xFF};
+ const kBlack = {r: 0x00, g: 0x00, b: 0x00};
+ const kWhite = {r: 0xFF, g: 0xFF, b: 0xFF};
+ const smpte170m = {
+ matrix: 'smpte170m',
+ primaries: 'smpte170m',
+ transfer: 'smpte170m',
+ fullRange: false
+ };
+ const bt709 = {
+ matrix: 'bt709',
+ primaries: 'bt709',
+ transfer: 'bt709',
+ fullRange: false
+ };
+
+ const result = [];
+ const init = {format: 'I420', timestamp: 0, codedWidth: 4, codedHeight: 4};
+ const colors =
+ [kYellow, kRed, kBlue, kGreen, kMagenta, kBlack, kWhite, kPink];
+ const data = new Uint8Array(24);
+ for (let colorSpace of [null, smpte170m, bt709]) {
+ init.colorSpace = colorSpace;
+ result.push(new VideoFrame(data, init));
+ for (let color of colors) {
+ color = rgb2yuv(color.r, color.g, color.b);
+ data.fill(color.y, 0, 16);
+ data.fill(color.u, 16, 20);
+ data.fill(color.v, 20, 24);
+ result.push(new VideoFrame(data, init));
+ }
+ }
+ return result;
+}
+
+function makeRGBXFrames() {
+ const kYellow = 0xFFFF00;
+ const kRed = 0xFF0000;
+ const kBlue = 0x0000FF;
+ const kGreen = 0x00FF00;
+ const kBlack = 0x000000;
+ const kWhite = 0xFFFFFF;
+ const smpte170m = {
+ matrix: 'smpte170m',
+ primaries: 'smpte170m',
+ transfer: 'smpte170m',
+ fullRange: false
+ };
+ const bt709 = {
+ matrix: 'bt709',
+ primaries: 'bt709',
+ transfer: 'bt709',
+ fullRange: false
+ };
+
+ const result = [];
+ const init = {format: 'RGBX', timestamp: 0, codedWidth: 4, codedHeight: 4};
+ const colors = [kYellow, kRed, kBlue, kGreen, kBlack, kWhite];
+ const data = new Uint32Array(16);
+ for (let colorSpace of [null, smpte170m, bt709]) {
+ init.colorSpace = colorSpace;
+ for (let color of colors) {
+ data.fill(color, 0, 16);
+ result.push(new VideoFrame(data, init));
+ }
+ }
+ return result;
+}
+
+async function testFrame(frame, colorSpace, pixelFormat) {
+ const width = frame.visibleRect.width;
+ const height = frame.visibleRect.height;
+ let frame_message = 'Frame: ' + JSON.stringify({
+ format: frame.format,
+ width: width,
+ height: height,
+ matrix: frame.colorSpace?.matrix,
+ primaries: frame.colorSpace?.primaries,
+ transfer: frame.colorSpace?.transfer,
+ });
+ const cnv = new OffscreenCanvas(width, height);
+ const ctx =
+ cnv.getContext('2d', {colorSpace: colorSpace, willReadFrequently: true});
+
+ // Read VideoFrame pixels via copyTo()
+ let imageData = ctx.createImageData(width, height);
+ let copy_to_buf = imageData.data.buffer;
+ let layout = null;
+ try {
+ const options = {
+ rect: {x: 0, y: 0, width: width, height: height},
+ format: pixelFormat,
+ colorSpace: colorSpace
+ };
+ layout = await frame.copyTo(copy_to_buf, options);
+ } catch (e) {
+ assert_unreached(`copyTo() failure: ${e}`);
+ return;
+ }
+ if (layout.length != 1) {
+ assert_unreached('Conversion to RGB is not supported by the browser');
+ return;
+ }
+
+ // Read VideoFrame pixels via drawImage()
+ ctx.drawImage(frame, 0, 0, width, height, 0, 0, width, height);
+ imageData = ctx.getImageData(0, 0, width, height, {colorSpace: colorSpace});
+ let get_image_buf = imageData.data.buffer;
+
+ // Compare!
+ const tolerance = 1;
+ for (let i = 0; i < copy_to_buf.byteLength; i += 4) {
+ if (pixelFormat.startsWith('BGR')) {
+ // getImageData() always gives us RGB, we need to swap bytes before
+ // comparing them with BGR.
+ new Uint8Array(get_image_buf, i, 3).reverse();
+ }
+ compareColors(
+ new Uint8Array(copy_to_buf, i, 4), new Uint8Array(get_image_buf, i, 4),
+ tolerance, frame_message + ` Mismatch at offset ${i}`);
+ }
+}
+
+function test_4x4_I420_frames() {
+ for (let colorSpace of ['srgb', 'display-p3']) {
+ for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) {
+ promise_test(async t => {
+ for (let frame of makeI420Frames()) {
+ await testFrame(frame, colorSpace, pixelFormat);
+ frame.close();
+ }
+ }, `Convert 4x4 I420 frames to ${pixelFormat} / ${colorSpace}`);
+ }
+ }
+}
+test_4x4_I420_frames();
+
+function test_4x4_RGB_frames() {
+ for (let colorSpace of ['srgb', 'display-p3']) {
+ for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) {
+ promise_test(async t => {
+ for (let frame of makeRGBXFrames()) {
+ await testFrame(frame, colorSpace, pixelFormat);
+ frame.close();
+ }
+ }, `Convert 4x4 RGBX frames to ${pixelFormat} / ${colorSpace}`);
+ }
+ }
+}
+test_4x4_RGB_frames();
+
+
+function test_4color_canvas_frames() {
+ for (let colorSpace of ['srgb', 'display-p3']) {
+ for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) {
+ promise_test(async t => {
+ const frame = createFrame(32, 16);
+ await testFrame(frame, colorSpace, pixelFormat);
+ frame.close();
+ }, `Convert 4-color canvas frame to ${pixelFormat} / ${colorSpace}`);
+ }
+ }
+}
+test_4color_canvas_frames();
+
+promise_test(async t => {
+ let pixelFormat = 'RGBA'
+ const init = {format: 'RGBA', timestamp: 0, codedWidth: 4, codedHeight: 4};
+ const src_data = new Uint32Array(init.codedWidth * init.codedHeight);
+ src_data.fill(0xFFFFFFFF);
+ const offset = 5;
+ const stride = 40;
+ const dst_data = new Uint8Array(offset + stride * init.codedHeight);
+ const options = {
+ format: pixelFormat,
+ layout: [
+ {offset: offset, stride: stride},
+ ]
+ };
+ const frame = new VideoFrame(src_data, init);
+ await frame.copyTo(dst_data, options)
+ assert_false(dst_data.slice(0, offset).some(e => e != 0), 'offset');
+ for (let row = 0; row < init.codedHeight; ++row) {
+ let width = init.codedWidth * 4;
+ const row_data =
+ dst_data.slice(offset + stride * row, offset + stride * row + width);
+ const margin_data = dst_data.slice(
+ offset + stride * row + width, offset + stride * (row + 1));
+
+ assert_false(
+ row_data.some(e => e != 0xFF),
+ `unexpected data in row ${row} [${row_data}]`);
+ assert_false(
+ margin_data.some(e => e != 0),
+ `unexpected margin in row ${row} [${margin_data}]`);
+ }
+
+ frame.close();
+}, `copyTo() with layout`);
+
+function test_unsupported_pixel_formats() {
+ const kUnsupportedFormats = [
+ 'I420', 'I420P10', 'I420P12', 'I420A', 'I422', 'I422A', 'I444', 'I444A',
+ 'NV12'
+ ];
+
+ for (let pixelFormat of kUnsupportedFormats) {
+ promise_test(async t => {
+ const init =
+ {format: 'RGBX', timestamp: 0, codedWidth: 4, codedHeight: 4};
+ const data = new Uint32Array(16);
+ const options = {format: pixelFormat};
+ const frame = new VideoFrame(data, init);
+ await promise_rejects_dom(
+ t, 'NotSupportedError', frame.copyTo(data, options))
+ frame.close();
+ }, `Unsupported format ${pixelFormat}`);
+ }
+}
+test_unsupported_pixel_formats();
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/capture_screenshot.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/capture_screenshot.py
index 40497ce6ac..414f5ae2d0 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/capture_screenshot.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/capture_screenshot.py
@@ -1,6 +1,6 @@
import pytest
-from math import floor
+from math import ceil, floor
from tests.support.image import png_dimensions
from . import get_physical_viewport_dimensions
@@ -79,3 +79,29 @@ async def test_capture_with_viewport(bidi_session, new_tab, delta_width, delta_h
result = await bidi_session.browsing_context.capture_screenshot(
context=new_tab["context"])
assert png_dimensions(result) == (expected_size["width"], expected_size["height"])
+
+
+@pytest.mark.parametrize("dpr", [0.5, 2])
+@pytest.mark.asyncio
+async def test_capture_with_different_dpr(bidi_session, new_tab, inline, dpr):
+ page = inline("<div style='background-color: black; width: 100px; height: 100px;'></div>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page, wait="complete"
+ )
+
+ original_viewport = await get_viewport_dimensions(bidi_session, new_tab)
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ device_pixel_ratio=dpr)
+
+ expected_width = original_viewport["width"] * dpr
+ expected_height = original_viewport["height"] * dpr
+
+ data = await bidi_session.browsing_context.capture_screenshot(context=new_tab["context"])
+ (actual_width, actual_height) = png_dimensions(data)
+ # The rounding is implementation-specific and can be either floor, ceil or round depending on the browser
+ # implementation. Tolerate any value between floor and ceil.
+ assert floor(expected_width) <= actual_width <= ceil(expected_width)
+ assert floor(expected_height) <= actual_height <= ceil(expected_height)
+
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/clip.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/clip.py
index 8300e962b9..67d4b0d06c 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/clip.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/clip.py
@@ -373,3 +373,42 @@ async def test_clip_element_outside_of_window_viewport(
comparison = await compare_png_bidi(reference_data, data)
assert comparison.equal()
+
+
+@pytest.mark.parametrize("dpr", [0.5, 2])
+async def test_clip_with_different_dpr(bidi_session, new_tab, inline, compare_png_bidi, dpr):
+ div_size = {"width": 100, "height": 100}
+
+ reference_page = inline(f"""<div style='background-color: black; width: {div_size["width"]*dpr}px; height: {div_size["height"]*dpr}px;'></div>""")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=reference_page, wait="complete"
+ )
+ element = await bidi_session.script.evaluate(
+ await_promise=False,
+ expression="document.querySelector('div')",
+ target=ContextTarget(new_tab["context"]),
+ )
+ reference_data = await bidi_session.browsing_context.capture_screenshot(
+ context=new_tab["context"], clip=ElementOptions(element=element)
+ )
+
+ page = inline(f"""<div style='background-color: black; width: {div_size["width"]}px; height: {div_size["height"]}px;'></div>""")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page, wait="complete"
+ )
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ device_pixel_ratio=dpr)
+
+ element = await bidi_session.script.evaluate(
+ await_promise=False,
+ expression="document.querySelector('div')",
+ target=ContextTarget(new_tab["context"]),
+ )
+ data = await bidi_session.browsing_context.capture_screenshot(
+ context=new_tab["context"], clip=ElementOptions(element=element)
+ )
+
+ comparison = await compare_png_bidi(data, reference_data)
+ assert comparison.equal()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/invalid.py
index ecd3173e87..52aabca267 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/invalid.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/invalid.py
@@ -46,10 +46,36 @@ async def test_params_locator_type_invalid_value(bidi_session, inline, top_conte
)
+@pytest.mark.parametrize("type", ["css", "xpath", "innerText"])
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_locator_value_invalid_type(
+ bidi_session, inline, top_context, type, value
+):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"], locator={"type": type, "value": value}
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_locator_accessability_value_invalid_type(
+ bidi_session, inline, top_context, value
+):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"], locator={"type": "accessability", "value": value}
+ )
+
+
@pytest.mark.parametrize("type,value", [
("css", "a*b"),
("xpath", ""),
- ("innerText", "")
+ ("innerText", ""),
+ ("accessibility", {})
])
async def test_params_locator_value_invalid_value(bidi_session, inline, top_context, type, value):
await navigate_to_page(bidi_session, inline, top_context)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/locator.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/locator.py
index e560fa9239..66c512d792 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/locator.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/locator.py
@@ -6,11 +6,17 @@ from ... import any_string, recursive_compare
@pytest.mark.parametrize("type,value", [
("css", "div"),
("xpath", "//div"),
- ("innerText", "foobarBARbaz")
+ ("innerText", "foobarBARbaz"),
+ ("accessibility", {"role": "banner"}),
+ ("accessibility", {"name": "foo"}),
+ ("accessibility", {"role": "banner", "name": "foo"}),
])
@pytest.mark.asyncio
async def test_find_by_locator(bidi_session, inline, top_context, type, value):
- url = inline("""<div data-class="one">foobarBARbaz</div><div data-class="two">foobarBARbaz</div>""")
+ url = inline("""
+ <div data-class="one" role="banner" aria-label="foo">foobarBARbaz</div>
+ <div data-class="two" role="banner" aria-label="foo">foobarBARbaz</div>
+ """)
await bidi_session.browsing_context.navigate(
context=top_context["context"], url=url, wait="complete"
)
@@ -165,3 +171,69 @@ async def test_find_by_inner_text(bidi_session, inline, top_context, locator, ex
)
recursive_compare(expected, result["nodes"])
+
+
+@pytest.mark.parametrize(
+ "html,locator_value,expected_node_local_name",
+ [
+ (
+ "<article data-class='one'>foo</article><div data-class='two'>bar</div>",
+ {"role": "article"},
+ "article",
+ ),
+ (
+ "<input role='searchbox' data-class='one' /><input data-class='two' type='text'/>",
+ {"role": "searchbox"},
+ "input",
+ ),
+ (
+ "<button data-class='one'>Ok</button><button data-class='two'>Cancel</button>",
+ {"name": "Ok"},
+ "button",
+ ),
+ (
+ "<button data-class='one' aria-labelledby='one two'></button><div id='one'>ok</div><div id='two'>go</div><button data-class='two'>Cancel</button>",
+ {"name": "ok go"},
+ "button",
+ ),
+ (
+ "<button data-class='one' aria-label='foo'>bar</button><button data-class='two' aria-label='bar'>foo</button>",
+ {"name": "foo"},
+ "button",
+ ),
+ (
+ "<div role='banner' aria-label='foo' data-class='one'></div><div role='banner' data-class='two'></div><div aria-label='foo' data-class='three'></div>",
+ {"role": "banner", "name": "foo"},
+ "div",
+ ),
+ ],
+)
+@pytest.mark.asyncio
+async def test_locate_by_accessibility_attributes(
+ bidi_session,
+ inline,
+ top_context,
+ html,
+ locator_value,
+ expected_node_local_name,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=inline(html), wait="complete"
+ )
+
+ expected = [
+ {
+ "type": "node",
+ "value": {
+ "attributes": {"data-class": "one"},
+ "localName": expected_node_local_name,
+ },
+ }
+ ]
+
+ result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={"type": "accessibility", "value": locator_value},
+ )
+
+ recursive_compare(expected, result["nodes"])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/max_node_count.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/max_node_count.py
index 4652026e96..9d9c05260b 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/max_node_count.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/max_node_count.py
@@ -43,6 +43,45 @@ from ... import any_string, recursive_compare
},
}]
),
+ ("accessibility", {"role": "banner"}, 1, [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }]
+ ),
+ ("accessibility", {"name": "bar"}, 1, [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }]
+ ),
+ ("accessibility", {"role": "banner", "name": "bar"}, 1, [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }]
+ ),
("css", "div", 10, [
{
"type": "node",
@@ -114,18 +153,96 @@ from ... import any_string, recursive_compare
"nodeType": 1,
},
}]
- )
+ ),
+ ("accessibility", {"role": "banner"}, 10, [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }, {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"two"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }]
+ ),
+ ("accessibility", {"name": "bar"}, 10, [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }, {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"two"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }]
+ ),
+ ("accessibility", {"role": "banner", "name": "bar"}, 10, [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }, {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"two"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }]
+ ),
], ids=[
"css_single",
"xpath_single",
"inner_text_single",
+ "accessibility_role_single",
+ "accessibility_name_single",
+ "accessibility_role_name_single",
"css_multiple",
"xpath_multiple",
- "inner_text_multiple"
+ "inner_text_multiple",
+ "accessibility_role_multiple",
+ "accessibility_name_multiple",
+ "accessibility_role_name_multiple",
])
@pytest.mark.asyncio
async def test_find_by_locator_limit_return_count(bidi_session, inline, top_context, type, value, max_count, expected):
- url = inline("""<div data-class="one">foo</div><div data-class="two">foo</div>""")
+ url = inline("""
+ <div data-class="one" role="banner" aria-label="bar">foo</div>
+ <div data-class="two" role="banner" aria-label="bar">foo</div>
+ """)
await bidi_session.browsing_context.navigate(
context=top_context["context"], url=url, wait="complete"
)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/start_nodes.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/start_nodes.py
index 707d83a337..f44a6d4857 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/start_nodes.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/start_nodes.py
@@ -92,13 +92,59 @@ from ... import any_string, recursive_compare
"namespaceURI": "http://www.w3.org/1999/xhtml",
"nodeType": 1,
}
- }])
+ }]),
+ ("accessibility", {"role": "banner"}, [{
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ },
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"two"},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }]),
+ ("accessibility", {"name": "bar"}, [{
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }
+ ]),
+ ("accessibility", {"role": "banner", "name": "bar"}, [{
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }
+ ])
])
@pytest.mark.asyncio
async def test_locate_with_context_nodes(bidi_session, inline, top_context, type, value, expected):
url = inline("""<div id="parent">
- <p data-class="one">foo</p>
- <p data-class="two">foo</p>
+ <p data-class="one" role="banner" aria-label="bar">foo</p>
+ <p data-class="two" role="banner">foo</p>
<a data-class="three">
<span id="text">bar</span>
</a>
@@ -125,14 +171,23 @@ async def test_locate_with_context_nodes(bidi_session, inline, top_context, type
@pytest.mark.parametrize("type,value", [
("css", "p[data-class='one']"),
("xpath", ".//p[@data-class='one']"),
- ("innerText", "foo")
+ ("innerText", "foo"),
+ ("accessibility", {"role": "banner"}),
+ ("accessibility", {"name": "bar"}),
+ ("accessibility", {"role": "banner", "name": "bar"}),
])
@pytest.mark.asyncio
async def test_locate_with_multiple_context_nodes(bidi_session, inline, top_context, type, value):
url = inline("""
- <div id="parent-one"><p data-class="one">foo</p><p data-class="two">bar</p></div>
- <div id="parent-two"><p data-class="one">foo</p><p data-class="two">bar</p></div>
- """)
+ <div id="parent-one">
+ <p data-class="one" role="banner" aria-label="bar">foo</p>
+ <p data-class="two">bar</p>
+ </div>
+ <div id="parent-two">
+ <p data-class="one" role="banner" aria-label="bar">foo</p>
+ <p data-class="two">bar</p>
+ </div>
+ """)
await bidi_session.browsing_context.navigate(
context=top_context["context"], url=url, wait="complete"
)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/device_pixel_ratio.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/device_pixel_ratio.py
index e4db779bd5..88de2de334 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/device_pixel_ratio.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/device_pixel_ratio.py
@@ -1,4 +1,5 @@
import pytest
+from webdriver.bidi.modules.script import ContextTarget
from ... import get_device_pixel_ratio, get_viewport_dimensions
@@ -45,6 +46,14 @@ async def test_device_pixel_ratio_with_viewport(
assert await get_viewport_dimensions(bidi_session, new_tab) == test_viewport
assert await get_device_pixel_ratio(bidi_session, new_tab) == device_pixel_ratio
+ result = await bidi_session.script.evaluate(
+ await_promise=False,
+ expression="window.devicePixelRatio",
+ target=ContextTarget(new_tab["context"]),
+ )
+
+ assert result == {"type": "number", "value": device_pixel_ratio}
+
@pytest.mark.asyncio
async def test_reset_device_pixel_ratio(bidi_session, inline, new_tab):
@@ -68,3 +77,47 @@ async def test_reset_device_pixel_ratio(bidi_session, inline, new_tab):
device_pixel_ratio=None)
assert await get_device_pixel_ratio(bidi_session, new_tab) == original_dpr
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("device_pixel_ratio", [0.5, 2])
+@pytest.mark.parametrize(
+ "use_horizontal_scrollbar, use_vertical_scrollbar",
+ [
+ (True, False),
+ (False, True),
+ (True, True),
+ ],
+ ids=["horizontal", "vertical", "both"],
+)
+async def test_device_pixel_ratio_with_scrollbar(
+ bidi_session,
+ inline,
+ new_tab,
+ device_pixel_ratio,
+ use_horizontal_scrollbar,
+ use_vertical_scrollbar,
+):
+ viewport_dimensions = await get_viewport_dimensions(bidi_session, new_tab)
+
+ width = 100
+ if use_horizontal_scrollbar:
+ width = viewport_dimensions["width"] + 100
+
+ height = 100
+ if use_vertical_scrollbar:
+ height = viewport_dimensions["height"] + 100
+
+ html = f"""<div style="width: {width}px; height: {height}px;">foo</div>"""
+ page_url = inline(html)
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page_url, wait="complete"
+ )
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"], device_pixel_ratio=device_pixel_ratio
+ )
+
+ assert await get_device_pixel_ratio(bidi_session, new_tab) == device_pixel_ratio
+ assert await get_viewport_dimensions(bidi_session, new_tab) == viewport_dimensions
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/viewport.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/viewport.py
index 2e8126b1f8..e9ff8517f2 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/viewport.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/viewport.py
@@ -243,6 +243,6 @@ async def test_with_scrollbars(
# The side which has scrollbar takes up space on the other side
# (e.g. if we have a horizontal scroll height is going to be smaller than viewport height)
if use_horizontal_scrollbar:
- assert viewport_without_scrollbar["height"] < test_viewport["height"]
+ assert viewport_without_scrollbar["height"] <= test_viewport["height"]
if use_vertical_scrollbar:
- assert viewport_without_scrollbar["width"] < test_viewport["width"]
+ assert viewport_without_scrollbar["width"] <= test_viewport["width"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/invalid.py
index 5397dc7b62..d717216072 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/invalid.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/invalid.py
@@ -4,6 +4,7 @@ from webdriver.bidi.undefined import UNDEFINED
pytestmark = pytest.mark.asyncio
+
@pytest.mark.parametrize("descriptor", [False, "SOME_STRING", 42, {}, [], {"name": 23}, None, UNDEFINED])
async def test_params_descriptor_invalid_type(bidi_session, descriptor):
with pytest.raises(error.InvalidArgumentException):
@@ -54,8 +55,8 @@ async def test_params_origin_invalid_type(bidi_session, origin):
)
-@pytest.mark.parametrize("user_context", [False, 42, {}, [], None])
-async def test_params_origin_invalid_type(bidi_session, user_context):
+@pytest.mark.parametrize("user_context", [False, 42, {}, []])
+async def test_params_user_context_invalid_type(bidi_session, user_context):
with pytest.raises(error.InvalidArgumentException):
await bidi_session.permissions.set_permission(
descriptor={"name": "geolocation"},
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/set_permission.py b/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/set_permission.py
index 45c50dbf88..18f8e6fed0 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/set_permission.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/set_permission.py
@@ -1,10 +1,10 @@
import pytest
-import webdriver.bidi.error as error
from . import get_context_origin, get_permission_state
pytestmark = pytest.mark.asyncio
+
@pytest.mark.asyncio
async def test_set_permission(bidi_session, new_tab, url):
test_url = url("/common/blank.html", protocol="https")
@@ -44,24 +44,6 @@ async def test_set_permission(bidi_session, new_tab, url):
@pytest.mark.asyncio
-async def test_set_permission_insecure_context(bidi_session, new_tab, url):
- test_url = url("/common/blank.html", protocol="http")
- await bidi_session.browsing_context.navigate(
- context=new_tab["context"],
- url=test_url,
- wait="complete",
- )
-
- origin = await get_context_origin(bidi_session, new_tab)
-
- with pytest.raises(error.InvalidArgumentException):
- await bidi_session.permissions.set_permission(
- descriptor={"name": "push"},
- state="granted",
- origin=origin,
- )
-
-@pytest.mark.asyncio
async def test_set_permission_new_context(bidi_session, new_tab, url):
test_url = url("/common/blank.html", protocol="https")
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key_events.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key_events.py
index e93c132e0a..275b542b11 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key_events.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key_events.py
@@ -23,7 +23,7 @@ pytestmark = pytest.mark.asyncio
)
async def test_non_printable_key_sends_events(
- bidi_session, top_context, key, event
+ bidi_session, top_context, setup_key_test, key, event
):
code = ALL_EVENTS[event]["code"]
value = ALL_EVENTS[event]["key"]
@@ -142,7 +142,7 @@ async def test_key_printable_key(
@pytest.mark.parametrize("use_keyup", [True, False])
-async def test_key_printable_sequence(bidi_session, top_context, use_keyup):
+async def test_key_printable_sequence(bidi_session, top_context, setup_key_test, use_keyup):
actions = Actions()
actions.add_key()
if use_keyup:
@@ -229,7 +229,7 @@ async def test_key_special_key_sends_keydown(
assert len(keys_value) == 0
-async def test_key_space(bidi_session, top_context):
+async def test_key_space(bidi_session, top_context, setup_key_test):
actions = Actions()
(
actions.add_key()
@@ -254,7 +254,7 @@ async def test_key_space(bidi_session, top_context):
assert events[0] == events[1]
-async def test_keyup_only_sends_no_events(bidi_session, top_context):
+async def test_keyup_only_sends_no_events(bidi_session, top_context, setup_key_test):
actions = Actions()
actions.add_key().key_up("a")
await bidi_session.input.perform_actions(
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/wheel.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/wheel.py
index 4f897479e2..ee0d4d4600 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/wheel.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/wheel.py
@@ -4,6 +4,7 @@ from webdriver.bidi.error import NoSuchFrameException
from webdriver.bidi.modules.input import Actions, get_element_origin
from webdriver.bidi.modules.script import ContextTarget
+from tests.support.keys import Keys
from .. import get_events, get_object_from_context
from . import get_shadow_root_from_test_page
@@ -159,3 +160,28 @@ async def test_scroll_shadow_tree(
assert events[0]["deltaX"] >= 5
assert events[0]["deltaY"] >= 10
assert events[0]["target"] == "scrollableShadowTreeContent"
+
+
+async def test_scroll_with_key_pressed(
+ bidi_session, setup_wheel_test, top_context, get_element
+):
+ scrollable = await get_element("#scrollable")
+
+ actions = Actions()
+ actions.add_key().key_down(Keys.R_SHIFT)
+ actions.add_wheel().scroll(
+ x=0,
+ y=0,
+ delta_x=5,
+ delta_y=10,
+ origin=get_element_origin(scrollable),
+ )
+ actions.add_key().key_up(Keys.R_SHIFT)
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+ events = await get_events(bidi_session, top_context["context"])
+ assert len(events) == 1
+ assert events[0]["type"] == "wheel"
+ assert events[0]["shiftKey"] == True
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed.py b/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed.py
index 56b9461642..f17a522766 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed.py
@@ -159,7 +159,7 @@ async def test_load_page_twice(
@pytest.mark.parametrize(
"status, status_text",
- [(status, text) for (status, text) in HTTP_STATUS_AND_STATUS_TEXT if status not in [101, 407]],
+ HTTP_STATUS_AND_STATUS_TEXT,
)
@pytest.mark.asyncio
async def test_response_status(
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed_status.py b/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed_status.py
deleted file mode 100644
index 36e3da667e..0000000000
--- a/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed_status.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# TODO(#42482): Merge this file with response_completed.py
-#
-# The status codes in this file are currently problematic in some implementations.
-#
-# The only mechanism currently provided by WPT to disable subtests with
-# expectations is to disable the entire file. As such, this file is a copy of
-# response_completed.py with the problematic status codes extracted.
-#
-# Once it is possible to disable subtests, this file should be merged with
-# response_completed.py.
-
-import pytest
-
-from .. import (
- assert_response_event,
- HTTP_STATUS_AND_STATUS_TEXT,
- RESPONSE_COMPLETED_EVENT,
-)
-
-
-@pytest.mark.parametrize(
- "status, status_text",
- [(status, text) for (status, text) in HTTP_STATUS_AND_STATUS_TEXT if status in [101, 407]],
-)
-@pytest.mark.asyncio
-async def test_response_status(
- wait_for_event, wait_for_future_safe, url, fetch, setup_network_test, status, status_text
-):
- status_url = url(
- f"/webdriver/tests/support/http_handlers/status.py?status={status}&nocache={RESPONSE_COMPLETED_EVENT}"
- )
-
- network_events = await setup_network_test(events=[RESPONSE_COMPLETED_EVENT])
- events = network_events[RESPONSE_COMPLETED_EVENT]
-
- on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
- await fetch(status_url)
- await wait_for_future_safe(on_response_completed)
-
- assert len(events) == 1
- expected_request = {"method": "GET", "url": status_url}
- expected_response = {
- "url": status_url,
- "fromCache": False,
- "mimeType": "text/plain",
- "status": status,
- "statusText": status_text,
- "protocol": "http/1.1",
- }
- assert_response_event(
- events[0],
- expected_request=expected_request,
- expected_response=expected_response,
- redirect_count=0,
- )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/response_started/response_started.py b/testing/web-platform/tests/webdriver/tests/bidi/network/response_started/response_started.py
index 6c10714ca8..9aa77739ab 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/network/response_started/response_started.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/response_started/response_started.py
@@ -135,6 +135,32 @@ async def test_load_page_twice(
)
+@pytest.mark.asyncio
+async def test_request_bodysize(
+ wait_for_event, wait_for_future_safe, url, fetch, setup_network_test
+):
+ html_url = url(PAGE_EMPTY_HTML)
+
+ network_events = await setup_network_test(events=[RESPONSE_STARTED_EVENT])
+ events = network_events[RESPONSE_STARTED_EVENT]
+
+ on_before_request_sent = wait_for_event(RESPONSE_STARTED_EVENT)
+ await fetch(html_url, method="POST", post_data="{'a': 1}")
+ await wait_for_future_safe(on_before_request_sent)
+
+ assert len(events) == 1
+ expected_request = {
+ "method": "POST",
+ "url": html_url,
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ redirect_count=0,
+ )
+ assert events[0]["request"]["bodySize"] > 0
+
+
@pytest.mark.parametrize(
"status, status_text",
HTTP_STATUS_AND_STATUS_TEXT,
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/__init__.py
index 4ca0f7bdd7..56ea539914 100644
--- a/testing/web-platform/tests/webdriver/tests/bidi/storage/__init__.py
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/__init__.py
@@ -78,7 +78,7 @@ def create_cookie(
def generate_expiry_date(day_diff=1):
return (
- (datetime.utcnow() + timedelta(days=day_diff))
+ (datetime.now(timezone.utc) + timedelta(days=day_diff))
.replace(microsecond=0)
.replace(tzinfo=timezone.utc)
)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/add_cookie/add.py b/testing/web-platform/tests/webdriver/tests/classic/add_cookie/add.py
index 24b71c52fd..60b67d051b 100644
--- a/testing/web-platform/tests/webdriver/tests/classic/add_cookie/add.py
+++ b/testing/web-platform/tests/webdriver/tests/classic/add_cookie/add.py
@@ -1,6 +1,6 @@
import pytest
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
from webdriver.transport import Response
@@ -154,7 +154,7 @@ def test_add_cookie_for_ip(session, server_config):
def test_add_non_session_cookie(session, url):
a_day_from_now = int(
- (datetime.utcnow() + timedelta(days=1) - datetime.utcfromtimestamp(0)).total_seconds())
+ (datetime.now(timezone.utc) + timedelta(days=1) - datetime.fromtimestamp(0, timezone.utc)).total_seconds())
new_cookie = {
"name": "hello",
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_clear/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/element_clear/__init__.py
index e69de29bb2..c84215a636 100644
--- a/testing/web-platform/tests/webdriver/tests/classic/element_clear/__init__.py
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_clear/__init__.py
@@ -0,0 +1,5 @@
+def element_clear(session, element):
+ return session.transport.send(
+ "POST", "/session/{session_id}/element/{element_id}/clear".format(
+ session_id=session.session_id,
+ element_id=element.id))
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_clear/clear.py b/testing/web-platform/tests/webdriver/tests/classic/element_clear/clear.py
index 9a0549ce4f..0977f60cb6 100644
--- a/testing/web-platform/tests/webdriver/tests/classic/element_clear/clear.py
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_clear/clear.py
@@ -8,6 +8,8 @@ from tests.support.asserts import (
assert_in_events,
assert_success,
)
+from tests.support.dom import BUTTON_TYPES
+from . import element_clear
@pytest.fixture
@@ -19,13 +21,6 @@ def tracked_events():
]
-def element_clear(session, element):
- return session.transport.send(
- "POST", "/session/{session_id}/element/{element_id}/clear".format(
- session_id=session.session_id,
- element_id=element.id))
-
-
@pytest.fixture(scope="session")
def text_file(tmpdir_factory):
fh = tmpdir_factory.mktemp("tmp").join("hello.txt")
@@ -196,31 +191,6 @@ def test_input(session, inline, add_event_listeners, tracked_events, type, value
"month",
"week",
"file"])
-def test_input_disabled(session, inline, type):
- session.url = inline("<input type=%s disabled>" % type)
- element = session.find.css("input", all=False)
-
- response = element_clear(session, element)
- assert_error(response, "invalid element state")
-
-
-@pytest.mark.parametrize("type",
- ["number",
- "range",
- "email",
- "password",
- "search",
- "tel",
- "text",
- "url",
- "color",
- "date",
- "datetime",
- "datetime-local",
- "time",
- "month",
- "week",
- "file"])
def test_input_readonly(session, inline, type):
session.url = inline("<input type=%s readonly>" % type)
element = session.find.css("input", all=False)
@@ -241,14 +211,6 @@ def test_textarea(session, inline, add_event_listeners, tracked_events):
assert_in_events(session, ["focus", "change", "blur"])
-def test_textarea_disabled(session, inline):
- session.url = inline("<textarea disabled></textarea>")
- element = session.find.css("textarea", all=False)
-
- response = element_clear(session, element)
- assert_error(response, "invalid element state")
-
-
def test_textarea_readonly(session, inline):
session.url = inline("<textarea readonly></textarea>")
element = session.find.css("textarea", all=False)
@@ -278,26 +240,12 @@ def test_input_file_multiple(session, text_file, inline):
assert element.property("value") == ""
-def test_select(session, inline):
- session.url = inline("""
- <select>
- <option>foo
- </select>
- """)
- select = session.find.css("select", all=False)
- option = session.find.css("option", all=False)
+@pytest.mark.parametrize("type", BUTTON_TYPES)
+def test_button(session, inline, type):
+ session.url = inline(f"""<button type="{type}">""")
+ element = session.find.css("button", all=False)
- response = element_clear(session, select)
- assert_error(response, "invalid element state")
- response = element_clear(session, option)
- assert_error(response, "invalid element state")
-
-
-def test_button(session, inline):
- session.url = inline("<button></button>")
- button = session.find.css("button", all=False)
-
- response = element_clear(session, button)
+ response = element_clear(session, element)
assert_error(response, "invalid element state")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_clear/disabled.py b/testing/web-platform/tests/webdriver/tests/classic/element_clear/disabled.py
new file mode 100644
index 0000000000..f0d0fcd9aa
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_clear/disabled.py
@@ -0,0 +1,113 @@
+import pytest
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.dom import BUTTON_TYPES, INPUT_TYPES
+from . import element_clear
+
+
+@pytest.mark.parametrize("type", BUTTON_TYPES)
+def test_button(session, inline, type):
+ session.url = inline(f"""<button type="{type}" disabled>""")
+ element = session.find.css("button", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+@pytest.mark.parametrize("type", INPUT_TYPES)
+def test_input(session, inline, type):
+ session.url = inline(f"""<input type="{type}" disabled>""")
+ element = session.find.css("input", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+def test_textarea(session, inline):
+ session.url = inline("<textarea disabled></textarea>")
+ element = session.find.css("textarea", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+def test_fieldset_descendant(session, inline):
+ session.url = inline("<fieldset disabled><input>foo")
+ element = session.find.css("input", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+def test_fieldset_descendant_first_legend(session, inline):
+ session.url = inline("<fieldset disabled><legend><input>foo")
+ element = session.find.css("input", all=False)
+
+ response = element_clear(session, element)
+ assert_success(response)
+
+
+def test_fieldset_descendant_not_first_legend(session, inline):
+ session.url = inline("<fieldset disabled><legend></legend><legend><input>foo")
+ element = session.find.css("input", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+def test_option(session, inline):
+ session.url = inline("<select><option disabled>foo")
+ element = session.find.css("option", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+def test_option_optgroup(session, inline):
+ session.url = inline("<select><optgroup disabled><option>foo")
+ element = session.find.css("option", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+def test_option_select(session, inline):
+ session.url = inline("<select disabled><option>foo")
+ element = session.find.css("option", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+def test_optgroup_select(session, inline):
+ session.url = inline("<select disabled><optgroup>foo")
+ element = session.find.css("optgroup", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+def test_select(session, inline):
+ session.url = inline("<select disabled>")
+ element = session.find.css("select", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+@pytest.mark.parametrize("tagname", ["button", "input", "select", "textarea"])
+def test_xhtml(session, inline, tagname):
+ session.url = inline(
+ f"""<{tagname} disabled="disabled"></{tagname}>""", doctype="xhtml")
+ element = session.find.css(tagname, all=False)
+
+ result = element_clear(session, element)
+ assert_error(result, "invalid element state")
+
+
+def test_xml(session, inline):
+ session.url = inline("""<note></note>""", doctype="xml")
+ element = session.find.css("note", all=False)
+
+ result = element_clear(session, element)
+ assert_error(result, "invalid element state")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_clear/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/element_clear/user_prompts.py
index 7a8564a684..ccf048bc0a 100644
--- a/testing/web-platform/tests/webdriver/tests/classic/element_clear/user_prompts.py
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_clear/user_prompts.py
@@ -3,13 +3,7 @@
import pytest
from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
-
-
-def element_clear(session, element):
- return session.transport.send(
- "POST", "/session/{session_id}/element/{element_id}/clear".format(
- session_id=session.session_id,
- element_id=element.id))
+from . import element_clear
@pytest.fixture
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/navigate.py b/testing/web-platform/tests/webdriver/tests/classic/element_click/navigate.py
index 6fadee9869..7fbb812f02 100644
--- a/testing/web-platform/tests/webdriver/tests/classic/element_click/navigate.py
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/navigate.py
@@ -36,14 +36,14 @@ def test_multi_line_link(session, inline, url):
assert session.url == url(link)
-def test_link_unload_event(session, url, server_config, inline):
+def test_navigation_retains_input_state(session, url, server_config, inline):
link = "/webdriver/tests/classic/element_click/support/input.html"
session.url = inline(f"""
- <body onunload="checkUnload()">
+ <body onpagehide="checkPageHide()">
<a href="{link}">click here</a>
<input type="checkbox">
<script>
- function checkUnload() {{
+ function checkPageHide() {{
document.getElementsByTagName("input")[0].checked = true;
}}
</script>
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_text/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_text/get.py
index 924a4e8d79..8f832077e1 100644
--- a/testing/web-platform/tests/webdriver/tests/classic/get_element_text/get.py
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_text/get.py
@@ -87,13 +87,6 @@ def test_stale_element_reference(session, stale_element, as_frame):
assert_error(response, "stale element reference")
-def test_getting_text_of_a_non_existant_element_is_an_error(session, inline):
- session.url = inline("""<body>Hello world</body>""")
-
- result = get_element_text(session, "foo")
- assert_error(result, "no such element")
-
-
def test_read_element_text(session, inline):
session.url = inline("Before f<span id='id'>oo</span> after")
element = session.find.css("#id", all=False)
@@ -102,6 +95,20 @@ def test_read_element_text(session, inline):
assert_success(result, "oo")
+@pytest.mark.parametrize("text, expected", [
+ ("foo bar", "Foo Bar"),
+ ("foo-bar", "Foo-Bar"),
+ ("foo_bar", "Foo_bar"),
+], ids=["space", "dash", "underscore"])
+def test_transform_capitalize(session, inline, text, expected):
+ session.url = inline(
+ f"""<div style="text-transform: capitalize;">{text}""")
+ element = session.find.css("div", all=False)
+
+ result = get_element_text(session, element.id)
+ assert_success(result, expected)
+
+
@pytest.mark.parametrize("text, inner_html, expected", [
("cheese", "<slot><span>foo</span>bar</slot>", "cheese"),
("cheese", "<slot><span>foo</span></slot>bar", "cheesebar"),
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/get.py
index 41426532ef..78418dae93 100644
--- a/testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/get.py
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/get.py
@@ -1,6 +1,6 @@
import pytest
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
from tests.support.asserts import assert_error, assert_success
@@ -62,7 +62,7 @@ def test_get_named_cookie(session, url):
# same formatting as Date.toUTCString() in javascript
utc_string_format = "%a, %d %b %Y %H:%M:%S"
- a_day_from_now = (datetime.utcnow() + timedelta(days=1)).strftime(utc_string_format)
+ a_day_from_now = (datetime.now(timezone.utc) + timedelta(days=1)).strftime(utc_string_format)
session.execute_script("document.cookie = 'foo=bar;expires=%s'" % a_day_from_now)
result = get_named_cookie(session, "foo")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/__init__.py
index e69de29bb2..9a6d884000 100644
--- a/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/__init__.py
+++ b/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/__init__.py
@@ -0,0 +1,8 @@
+def is_element_enabled(session, element_id):
+ return session.transport.send(
+ "GET",
+ "session/{session_id}/element/{element_id}/enabled".format(
+ session_id=session.session_id,
+ element_id=element_id
+ )
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/enabled.py b/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/enabled.py
index 24fc85fdad..9588486dde 100644
--- a/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/enabled.py
+++ b/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/enabled.py
@@ -3,16 +3,8 @@ import pytest
from webdriver import WebElement
from tests.support.asserts import assert_error, assert_success
-
-
-def is_element_enabled(session, element_id):
- return session.transport.send(
- "GET",
- "session/{session_id}/element/{element_id}/enabled".format(
- session_id=session.session_id,
- element_id=element_id
- )
- )
+from tests.support.dom import BUTTON_TYPES, INPUT_TYPES
+from . import is_element_enabled
def test_no_top_browsing_context(session, closed_window):
@@ -90,80 +82,171 @@ def test_stale_element_reference(session, stale_element, as_frame):
assert_error(result, "stale element reference")
-@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
-def test_form_control_disabled(session, inline, element):
- session.url = inline("<{} disabled/>".format(element))
- element = session.find.css(element, all=False)
+@pytest.mark.parametrize("status, expected", [
+ ("", True),
+ ("disabled", False),
+], ids=["enabled", "disabled"])
+@pytest.mark.parametrize("type", BUTTON_TYPES)
+def test_button(session, inline, status, expected, type):
+ session.url = inline(f"""<button type="{type}" {status}>""")
+ element = session.find.css("button", all=False)
- result = is_element_enabled(session, element.id)
- assert_success(result, False)
+ response = is_element_enabled(session, element.id)
+ assert_success(response, expected)
-@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
-def test_form_control_enabled(session, inline, element):
- session.url = inline("<{}/>".format(element))
- element = session.find.css(element, all=False)
+@pytest.mark.parametrize("status, expected", [
+ ("", True),
+ ("disabled", False),
+], ids=["enabled", "disabled"])
+@pytest.mark.parametrize("type", INPUT_TYPES)
+def test_input(session, inline, status, expected, type):
+ session.url = inline(f"""<input type="{type}" {status}>""")
+ element = session.find.css("input", all=False)
result = is_element_enabled(session, element.id)
- assert_success(result, True)
+ assert_success(result, expected)
-@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
-def test_fieldset_disabled_descendant(session, inline, element):
- session.url = inline("<fieldset disabled><{}/></fieldset>".format(element))
- element = session.find.css(element, all=False)
+@pytest.mark.parametrize("status, expected", [
+ ("", True),
+ ("disabled", False),
+], ids=["enabled", "disabled"])
+def test_textarea(session, inline, status, expected):
+ session.url = inline(f"<textarea {status}></textarea>")
+ element = session.find.css("textarea", all=False)
- result = is_element_enabled(session, element.id)
- assert_success(result, False)
+ response = is_element_enabled(session, element.id)
+ assert_success(response, expected)
-@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
-def test_fieldset_enabled_descendant(session, inline, element):
- session.url = inline("<fieldset><{}/></fieldset>".format(element))
- element = session.find.css(element, all=False)
+@pytest.mark.parametrize("status, expected", [
+ ("", True),
+ ("disabled", False),
+], ids=["enabled", "disabled"])
+def test_fieldset(session, inline, status, expected):
+ session.url = inline(f"<fieldset {status}><input>foo")
+ element = session.find.css("input", all=False)
- result = is_element_enabled(session, element.id)
- assert_success(result, True)
+ response = is_element_enabled(session, element.id)
+ assert_success(response, expected)
-@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
-def test_fieldset_disabled_descendant_legend(session, inline, element):
- session.url = inline("<fieldset disabled><legend><{}/></legend></fieldset>".format(element))
- element = session.find.css(element, all=False)
+@pytest.mark.parametrize("status, expected", [
+ ("", True),
+ ("disabled", False),
+], ids=["enabled", "disabled"])
+def test_fieldset_descendant(session, inline, status, expected):
+ session.url = inline(f"<fieldset {status}><input>foo")
+ element = session.find.css("input", all=False)
- result = is_element_enabled(session, element.id)
- assert_success(result, True)
+ response = is_element_enabled(session, element.id)
+ assert_success(response, expected)
-@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
-def test_fieldset_enabled_descendant_legend(session, inline, element):
- session.url = inline("<fieldset><legend><{}/></legend></fieldset>".format(element))
- element = session.find.css(element, all=False)
+@pytest.mark.parametrize("status, expected", [
+ ("", True),
+ ("disabled", True),
+], ids=["enabled", "disabled"])
+def test_fieldset_descendant_first_legend(session, inline, status, expected):
+ session.url = inline(f"<fieldset {status}><legend><input>foo")
+ element = session.find.css("input", all=False)
- result = is_element_enabled(session, element.id)
- assert_success(result, True)
+ response = is_element_enabled(session, element.id)
+ assert_success(response, expected)
-@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
-def test_xhtml_form_control_disabled(session, inline, element):
- session.url = inline("""<{} disabled="disabled"/>""".format(element),
- doctype="xhtml")
- element = session.find.css(element, all=False)
+@pytest.mark.parametrize("status, expected", [
+ ("", True),
+ ("disabled", False),
+], ids=["enabled", "disabled"])
+def test_fieldset_descendant_not_first_legend(session, inline, status, expected):
+ session.url = inline(f"<fieldset {status}><legend></legend><legend><input>foo")
+ element = session.find.css("input", all=False)
- result = is_element_enabled(session, element.id)
- assert_success(result, False)
+ response = is_element_enabled(session, element.id)
+ assert_success(response, expected)
+
+
+@pytest.mark.parametrize("status, expected", [
+ ("", True),
+ ("disabled", False),
+], ids=["enabled", "disabled"])
+def test_option(session, inline, status, expected):
+ session.url = inline(f"<select><option {status}>foo")
+ element = session.find.css("option", all=False)
+
+ response = is_element_enabled(session, element.id)
+ assert_success(response, expected)
+
+
+@pytest.mark.parametrize("status, expected", [
+ ("", True),
+ ("disabled", False),
+], ids=["enabled", "disabled"])
+def test_option_with_optgroup(session, inline, status, expected):
+ session.url = inline(f"<select><optgroup {status}><option>foo")
+ element = session.find.css("optgroup", all=False)
+
+ response = is_element_enabled(session, element.id)
+ assert_success(response, expected)
+
+ option = session.find.css("option", all=False)
+ response = is_element_enabled(session, option.id)
+ assert_success(response, expected)
+
+
+@pytest.mark.parametrize("status, expected", [
+ ("", True),
+ ("disabled", False),
+], ids=["enabled", "disabled"])
+def test_option_with_select(session, inline, status, expected):
+ session.url = inline(f"<select {status}><option>foo")
+
+ option = session.find.css("option", all=False)
+ response = is_element_enabled(session, option.id)
+ assert_success(response, expected)
+
+
+@pytest.mark.parametrize("status, expected", [
+ ("", True),
+ ("disabled", False),
+], ids=["enabled", "disabled"])
+def test_optgroup_with_select(session, inline, status, expected):
+ session.url = inline(f"<select {status}><optgroup>foo")
+
+ option = session.find.css("optgroup", all=False)
+ response = is_element_enabled(session, option.id)
+ assert_success(response, expected)
+
+
+@pytest.mark.parametrize("status, expected", [
+ ("", True),
+ ("disabled", False),
+], ids=["enabled", "disabled"])
+def test_select(session, inline, status, expected):
+ session.url = inline(f"<select {status}>")
+ element = session.find.css("select", all=False)
+
+ response = is_element_enabled(session, element.id)
+ assert_success(response, expected)
-@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
-def test_xhtml_form_control_enabled(session, inline, element):
- session.url = inline("""<{}/>""".format(element), doctype="xhtml")
- element = session.find.css(element, all=False)
+@pytest.mark.parametrize("status, expected", [
+ ("", True),
+ ("disabled=\"disabled\"", False),
+], ids=["enabled", "disabled"])
+@pytest.mark.parametrize("tagname", ["button", "input", "select", "textarea"])
+def test_xhtml(session, inline, status, expected, tagname):
+ session.url = inline(
+ f"""<{tagname} {status}></{tagname}>""", doctype="xhtml")
+ element = session.find.css(tagname, all=False)
result = is_element_enabled(session, element.id)
- assert_success(result, True)
+ assert_success(result, expected)
-def test_xml_always_not_enabled(session, inline):
+def test_xml(session, inline):
session.url = inline("""<note></note>""", doctype="xml")
element = session.find.css("note", all=False)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/user_prompts.py
index 5dd7d582bd..a3af947d51 100644
--- a/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/user_prompts.py
+++ b/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/user_prompts.py
@@ -3,16 +3,7 @@
import pytest
from tests.support.asserts import assert_error, assert_dialog_handled, assert_success
-
-
-def is_element_enabled(session, element_id):
- return session.transport.send(
- "GET",
- "session/{session_id}/element/{element_id}/enabled".format(
- session_id=session.session_id,
- element_id=element_id
- )
- )
+from . import is_element_enabled
@pytest.fixture
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/wheel.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/wheel.py
index a75a84378a..6d0f9ddb11 100644
--- a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/wheel.py
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/wheel.py
@@ -4,6 +4,7 @@ from webdriver.error import NoSuchWindowException
from tests.classic.perform_actions.support.refine import get_events
+from tests.support.keys import Keys
def test_null_response_value(session, wheel_chain):
@@ -111,3 +112,18 @@ def test_scroll_shadow_tree(session, get_test_page, wheel_chain, mode, nested):
assert events[0]["deltaX"] == 5
assert events[0]["deltaY"] == 10
assert events[0]["target"] == "scrollableShadowTreeContent"
+
+
+def test_scroll_with_key_pressed(
+ session, test_actions_scroll_page, key_chain, wheel_chain
+):
+ scrollable = session.find.css("#scrollable", all=False)
+
+ key_chain.key_down(Keys.R_SHIFT).perform()
+ wheel_chain.scroll(0, 0, 5, 10, origin=scrollable).perform()
+ key_chain.key_up(Keys.R_SHIFT).perform()
+
+ events = get_events(session)
+ assert len(events) == 1
+ assert events[0]["type"] == "wheel"
+ assert events[0]["shiftKey"] == True
diff --git a/testing/web-platform/tests/webdriver/tests/support/dom.py b/testing/web-platform/tests/webdriver/tests/support/dom.py
new file mode 100644
index 0000000000..f77fb49387
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/dom.py
@@ -0,0 +1,29 @@
+BUTTON_TYPES = [
+ "button",
+ "reset",
+ "submit",
+]
+
+INPUT_TYPES = [
+ "button",
+ "checkbox",
+ "color",
+ "date",
+ "datetime-local",
+ "email",
+ "file",
+ "image",
+ "month",
+ "number",
+ "password",
+ "radio",
+ "range",
+ "reset",
+ "search",
+ "submit",
+ "tel",
+ "text",
+ "time",
+ "url",
+ "week",
+];
diff --git a/testing/web-platform/tests/webdriver/tests/support/fixtures_bidi.py b/testing/web-platform/tests/webdriver/tests/support/fixtures_bidi.py
index 32919210bf..c76b369f21 100644
--- a/testing/web-platform/tests/webdriver/tests/support/fixtures_bidi.py
+++ b/testing/web-platform/tests/webdriver/tests/support/fixtures_bidi.py
@@ -538,7 +538,12 @@ def fetch(bidi_session, top_context, configuration):
"""
async def fetch(
- url, method="GET", headers=None, context=top_context, timeout_in_seconds=3
+ url,
+ method="GET",
+ headers=None,
+ post_data=None,
+ context=top_context,
+ timeout_in_seconds=3,
):
method_arg = f"method: '{method}',"
@@ -546,6 +551,10 @@ def fetch(bidi_session, top_context, configuration):
if headers is not None:
headers_arg = f"headers: {json.dumps(headers)},"
+ body_arg = ""
+ if post_data is not None:
+ body_arg = f"body: {post_data},"
+
timeout_in_seconds = timeout_in_seconds * configuration["timeout_multiplier"]
# Wait for fetch() to resolve a response and for response.text() to
# resolve as well to make sure the request/response is completed when
@@ -558,6 +567,7 @@ def fetch(bidi_session, top_context, configuration):
fetch("{url}", {{
{method_arg}
{headers_arg}
+ {body_arg}
signal: controller.signal,
}}).then(response => response.text());
}}""",
diff --git a/testing/web-platform/tests/webdriver/tests/support/html/test_actions_scroll.html b/testing/web-platform/tests/webdriver/tests/support/html/test_actions_scroll.html
index db5952ed74..edeff342fe 100644
--- a/testing/web-platform/tests/webdriver/tests/support/html/test_actions_scroll.html
+++ b/testing/web-platform/tests/webdriver/tests/support/html/test_actions_scroll.html
@@ -63,6 +63,10 @@
"deltaZ": event.deltaZ,
"deltaMode": event.deltaMode,
"target": event.target.id,
+ "altKey": event.altKey,
+ "ctrlKey": event.ctrlKey,
+ "metaKey": event.metaKey,
+ "shiftKey": event.shiftKey,
});
addMessage(
@@ -75,7 +79,11 @@
"deltaY: " + event.deltaY + ", " +
"deltaZ: " + event.deltaZ + ", " +
"deltaMode: " + event.deltaMode + ", " +
- "target id: " + event.target.id
+ "target id: " + event.target.id + ", " +
+ "altKey: " + event.altKey + ", " +
+ "ctrlKey: " + event.ctrlKey + ", " +
+ "metaKey: " + event.metaKey + ", " +
+ "shiftKey: " + event.shiftKey
);
}
@@ -121,7 +129,11 @@
"deltaY": event.deltaY,
"deltaZ": event.deltaZ,
"deltaMode": event.deltaMode,
- "target": event.target
+ "target": event.target,
+ "altKey": event.altKey,
+ "ctrlKey": event.ctrlKey,
+ "metaKey": event.metaKey,
+ "shiftKey": event.shiftKey,
});
});
</script>
diff --git a/testing/web-platform/tests/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js b/testing/web-platform/tests/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js
new file mode 100644
index 0000000000..fc5c48aca3
--- /dev/null
+++ b/testing/web-platform/tests/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js
@@ -0,0 +1,6 @@
+"use strict";
+
+test(() => {
+ const ownPropKeys = Reflect.ownKeys(Image).slice(0, 3);
+ assert_array_equals(ownPropKeys, ["length", "name", "prototype"]);
+}, 'Legacy factory function property enumeration order of "length", "name", and "prototype"');
diff --git a/testing/web-platform/tests/webnn/conformance_tests/arg_min_max.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/arg_min_max.https.any.js
index 123c8b1048..0f9e590fc8 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/arg_min_max.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/arg_min_max.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API argMin/Max operations
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-argminmax
-testWebNNOperation(['argMin', 'argMax'], buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests(['argMin', 'argMax'], buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/batch_normalization.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/batch_normalization.https.any.js
index 9a1c85db19..d3107820db 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/batch_normalization.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/batch_normalization.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API batchNormalization operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-batchnorm
-testWebNNOperation('batchNormalization', buildBatchNorm); \ No newline at end of file
+runWebNNConformanceTests('batchNormalization', buildBatchNorm);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/buffer.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/buffer.https.any.js
index 9391be8dbf..5a09b05c7d 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/buffer.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/buffer.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API buffer operations
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,10 +9,11 @@
// https://webmachinelearning.github.io/webnn/#api-mlbuffer
-testCreateWebNNBuffer("create", 4);
-
-testDestroyWebNNBuffer('destroyTwice');
-
-testReadWebNNBuffer('read');
-
-testWriteWebNNBuffer('write');
+if (navigator.ml) {
+ testCreateWebNNBuffer('create', 4);
+ testDestroyWebNNBuffer('destroyTwice');
+ testReadWebNNBuffer('read');
+ testWriteWebNNBuffer('write');
+} else {
+ test(() => assert_implements(navigator.ml, 'missing navigator.ml'));
+}
diff --git a/testing/web-platform/tests/webnn/conformance_tests/cast.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/cast.https.any.js
index bde2b9a4ce..086428dd96 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/cast.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/cast.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API cast operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-cast
-testWebNNOperation('cast', buildCast); \ No newline at end of file
+runWebNNConformanceTests('cast', buildCast);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/clamp.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/clamp.https.any.js
index 7b60c41f2c..ab47ac9c5c 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/clamp.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/clamp.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API clamp operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-clamp
-testWebNNOperation('clamp', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('clamp', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/compute-arraybufferview-with-bigger-arraybuffer.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/compute-arraybufferview-with-bigger-arraybuffer.https.any.js
new file mode 100644
index 0000000000..62ce16c93e
--- /dev/null
+++ b/testing/web-platform/tests/webnn/conformance_tests/compute-arraybufferview-with-bigger-arraybuffer.https.any.js
@@ -0,0 +1,61 @@
+// META: title=test WebNN MLContext.compute() for ArrayBufferView created from bigger ArrayBuffer
+// META: global=window,dedicatedworker
+// META: variant=?gpu
+// META: script=../resources/utils.js
+
+'use strict';
+
+// These tests are used to reproduce the Chromium issue:
+// https://issues.chromium.org/issues/332151809
+
+if (navigator.ml) {
+ const variant = location.search.substring(1);
+ const contextOptions = kContextOptionsForVariant[variant];
+
+ let context;
+ let builder;
+
+ promise_setup(async () => {
+ let supported = false;
+ try {
+ context = await navigator.ml.createContext(contextOptions);
+ supported = true;
+ } catch (e) {
+ }
+ assert_implements(
+ supported, `Unable to create context for ${variant} variant`);
+ builder = new MLGraphBuilder(context);
+ });
+
+ promise_test(async t => {
+ const a = builder.input('a', {dataType: 'float32', dimensions: [2]});
+ const b = builder.relu(a);
+ const graph = await builder.build({b});
+ const arraybuffer = new ArrayBuffer(100);
+ const aBuffer =
+ new Float32Array(arraybuffer, /*byteOffset*/ 4, /*length*/ 2)
+ aBuffer.set([1, -1]);
+ const bBuffer = new Float32Array(2);
+ const results =
+ await context.compute(graph, {'a': aBuffer}, {'b': bBuffer});
+ assert_array_approx_equals_ulp(
+ results.outputs.b, [1, 0], /*nulp*/ 0, 'float32');
+ }, 'Test compute() working for input ArrayBufferView created from bigger ArrayBuffer');
+
+ promise_test(async t => {
+ const a = builder.input('a', {dataType: 'float32', dimensions: [2]});
+ const b = builder.relu(a);
+ const graph = await builder.build({b});
+ const aBuffer = new Float32Array(2);
+ aBuffer.set([1, -1]);
+ const arraybuffer = new ArrayBuffer(100);
+ const bBuffer =
+ new Float32Array(arraybuffer, /*byteOffset*/ 8, /*length*/ 2);
+ const results =
+ await context.compute(graph, {'a': aBuffer}, {'b': bBuffer});
+ assert_array_approx_equals_ulp(
+ results.outputs.b, [1, 0], /*nulp*/ 0, 'float32');
+ }, 'Test compute() working for output ArrayBufferView created from bigger ArrayBuffer');
+} else {
+ test(() => assert_implements(navigator.ml, 'missing navigator.ml'));
+}
diff --git a/testing/web-platform/tests/webnn/conformance_tests/concat.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/concat.https.any.js
index 254e0b657b..619f20fe1c 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/concat.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/concat.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API concat operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-concat
-testWebNNOperation('concat', buildConcat); \ No newline at end of file
+runWebNNConformanceTests('concat', buildConcat);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/constant.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/constant.https.any.js
index 4814734886..79362947f1 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/constant.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/constant.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API constant
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-constant-range
-testWebNNOperation('constant', buildConstantRange); \ No newline at end of file
+runWebNNConformanceTests('constant', buildConstantRange);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/conv2d.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/conv2d.https.any.js
index 0d9a621356..34af583162 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/conv2d.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/conv2d.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API conv2d operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-conv2d
-testWebNNOperation('conv2d', buildConv2d); \ No newline at end of file
+runWebNNConformanceTests('conv2d', buildConv2d);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/conv_transpose2d.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/conv_transpose2d.https.any.js
index ee5d28c72a..2943e67851 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/conv_transpose2d.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/conv_transpose2d.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API convTranspose2d operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-convtranspose2d
-testWebNNOperation('convTranspose2d', buildConvTranspose2d); \ No newline at end of file
+runWebNNConformanceTests('convTranspose2d', buildConvTranspose2d);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/elementwise_binary.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/elementwise_binary.https.any.js
index 5db14a43a1..a85a06e1d2 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/elementwise_binary.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/elementwise_binary.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API element-wise binary operations
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,6 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-binary
-testWebNNOperation(['add', 'sub', 'mul', 'div', 'max', 'min', 'pow'], buildOperationWithTwoInputs); \ No newline at end of file
+runWebNNConformanceTests(
+ ['add', 'sub', 'mul', 'div', 'max', 'min', 'pow'],
+ buildOperationWithTwoInputs);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/elementwise_logical.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/elementwise_logical.https.any.js
index a60c199447..3d3a825f9c 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/elementwise_logical.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/elementwise_logical.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API element-wise logical operations
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,14 +9,17 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-logical
-testWebNNOperation(
- [
- 'equal',
- 'greater',
- 'greaterOrEqual',
- 'lesser',
- 'lesserOrEqual',
- ],
- buildOperationWithTwoInputs
-);
-testWebNNOperation('logicalNot', buildOperationWithSingleInput); \ No newline at end of file
+if (navigator.ml) {
+ testWebNNOperation(
+ [
+ 'equal',
+ 'greater',
+ 'greaterOrEqual',
+ 'lesser',
+ 'lesserOrEqual',
+ ],
+ buildOperationWithTwoInputs);
+ testWebNNOperation('logicalNot', buildOperationWithSingleInput);
+} else {
+ test(() => assert_implements(navigator.ml, 'missing navigator.ml'));
+}
diff --git a/testing/web-platform/tests/webnn/conformance_tests/elementwise_unary.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/elementwise_unary.https.any.js
index 8029539eda..f202af01e5 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/elementwise_unary.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/elementwise_unary.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API element-wise unary operations
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,7 +9,9 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-unary
-testWebNNOperation(
- ['abs', 'ceil', 'cos', 'erf', 'exp', 'floor', 'identity', 'log', 'neg', 'reciprocal', 'sin', 'sqrt', 'tan'],
- buildOperationWithSingleInput
-); \ No newline at end of file
+runWebNNConformanceTests(
+ [
+ 'abs', 'ceil', 'cos', 'erf', 'exp', 'floor', 'identity', 'log', 'neg',
+ 'reciprocal', 'sin', 'sqrt', 'tan'
+ ],
+ buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/elu.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/elu.https.any.js
index 382faa97fd..ac1c19a80b 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/elu.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/elu.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API elu operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-elu
-testWebNNOperation('elu', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('elu', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/expand.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/expand.https.any.js
index b1be129eac..e7bf106f96 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/expand.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/expand.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API expand operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,5 +9,5 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-expand
-// reuse buildReshape method
-testWebNNOperation('expand', buildReshape); \ No newline at end of file
+// Reuse buildReshape method
+runWebNNConformanceTests('expand', buildReshape);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gather.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gather.https.any.js
index 39b1970563..504f2dd792 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/gather.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/gather.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API gather operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-gather
-testWebNNOperation('gather', buildOperationWithTwoInputs); \ No newline at end of file
+runWebNNConformanceTests('gather', buildOperationWithTwoInputs);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gemm.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gemm.https.any.js
index 61fd7c9b39..03a836a44a 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/gemm.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/gemm.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API gemm operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-gemm
-testWebNNOperation('gemm', buildGemm); \ No newline at end of file
+runWebNNConformanceTests('gemm', buildGemm);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/arg_min_max.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/arg_min_max.https.any.js
deleted file mode 100644
index c700ee5cad..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/arg_min_max.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API argMin/Max operations
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-argminmax
-
-testWebNNOperation(['argMin', 'argMax'], buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/batch_normalization.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/batch_normalization.https.any.js
deleted file mode 100644
index 534cdf6365..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/batch_normalization.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API batchNormalization operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-batchnorm
-
-testWebNNOperation('batchNormalization', buildBatchNorm, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/buffer.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/buffer.https.any.js
deleted file mode 100644
index 225bc40185..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/buffer.https.any.js
+++ /dev/null
@@ -1,16 +0,0 @@
-// META: title=test WebNN API buffer operations
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlbuffer
-
-testCreateWebNNBuffer("create", 4, 'gpu');
-
-testDestroyWebNNBuffer('destroyTwice', 'gpu');
-
-testReadWebNNBuffer('read', 'gpu');
-
-testWriteWebNNBuffer('write', 'gpu');
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/cast.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/cast.https.any.js
deleted file mode 100644
index e4309ffd8e..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/cast.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API cast operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-cast
-
-testWebNNOperation('cast', buildCast, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/clamp.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/clamp.https.any.js
deleted file mode 100644
index 9b3f93ecc7..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/clamp.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API clamp operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-clamp
-
-testWebNNOperation('clamp', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/concat.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/concat.https.any.js
deleted file mode 100644
index c0cfb8626b..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/concat.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API concat operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-concat
-
-testWebNNOperation('concat', buildConcat, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/constant.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/constant.https.any.js
deleted file mode 100644
index 77b4d889a2..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/constant.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API constant
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-constant-range
-
-testWebNNOperation('constant', buildConstantRange, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/conv2d.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/conv2d.https.any.js
deleted file mode 100644
index 770540abd8..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/conv2d.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API conv2d operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-conv2d
-
-testWebNNOperation('conv2d', buildConv2d, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/conv_transpose2d.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/conv_transpose2d.https.any.js
deleted file mode 100644
index 08c441b5b4..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/conv_transpose2d.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API convTranspose2d operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-convtranspose2d
-
-testWebNNOperation('convTranspose2d', buildConvTranspose2d, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/elementwise_binary.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/elementwise_binary.https.any.js
deleted file mode 100644
index 8b9fa486f8..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/elementwise_binary.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API element-wise binary operations
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-binary
-
-testWebNNOperation(['add', 'sub', 'mul', 'div', 'max', 'min', 'pow'], buildOperationWithTwoInputs, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/elementwise_logical.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/elementwise_logical.https.any.js
deleted file mode 100644
index 70a887a147..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/elementwise_logical.https.any.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// META: title=test WebNN API element-wise logical operations
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-logical
-
-testWebNNOperation(
- [
- 'equal',
- 'greater',
- 'greaterOrEqual',
- 'lesser',
- 'lesserOrEqual',
- ],
- buildOperationWithTwoInputs, 'gpu'
-);
-testWebNNOperation('logicalNot', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/elementwise_unary.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/elementwise_unary.https.any.js
deleted file mode 100644
index 8871129311..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/elementwise_unary.https.any.js
+++ /dev/null
@@ -1,13 +0,0 @@
-// META: title=test WebNN API element-wise unary operations
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-unary
-
-testWebNNOperation(
- ['abs', 'ceil', 'cos', 'erf', 'exp', 'floor', 'identity', 'log', 'neg', 'reciprocal', 'sin', 'sqrt', 'tan'],
- buildOperationWithSingleInput, 'gpu'
-); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/elu.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/elu.https.any.js
deleted file mode 100644
index db14442641..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/elu.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API elu operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-elu
-
-testWebNNOperation('elu', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/expand.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/expand.https.any.js
deleted file mode 100644
index f46f463781..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/expand.https.any.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// META: title=test WebNN API expand operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-expand
-
-// reuse buildReshape method
-testWebNNOperation('expand', buildReshape, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/gather.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/gather.https.any.js
deleted file mode 100644
index 8e457192d8..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/gather.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API gather operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-gather
-
-testWebNNOperation('gather', buildOperationWithTwoInputs, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/gemm.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/gemm.https.any.js
deleted file mode 100644
index f288c31bed..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/gemm.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API gemm operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-gemm
-
-testWebNNOperation('gemm', buildGemm, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/hard_sigmoid.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/hard_sigmoid.https.any.js
deleted file mode 100644
index d40e42a211..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/hard_sigmoid.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API hardSigmoid operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-hard-sigmoid
-
-testWebNNOperation('hardSigmoid', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/hard_swish.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/hard_swish.https.any.js
deleted file mode 100644
index 031e65ee16..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/hard_swish.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API tanh operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-hard-swish
-
-testWebNNOperation('hardSwish', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/instance_normalization.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/instance_normalization.https.any.js
deleted file mode 100644
index ecfaac71ee..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/instance_normalization.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API instanceNormalization operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-instancenorm
-
-testWebNNOperation('instanceNormalization', buildLayerNorm, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/layer_normalization.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/layer_normalization.https.any.js
deleted file mode 100644
index 0e4f6caebf..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/layer_normalization.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API layerNormalization operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-layernorm
-
-testWebNNOperation('layerNormalization', buildLayerNorm, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/leaky_relu.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/leaky_relu.https.any.js
deleted file mode 100644
index 9fab2353b9..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/leaky_relu.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API leakyRelu operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-leakyrelu
-
-testWebNNOperation('leakyRelu', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/linear.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/linear.https.any.js
deleted file mode 100644
index ccec2c3eac..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/linear.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API linear operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-linear
-
-testWebNNOperation('linear', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/matmul.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/matmul.https.any.js
deleted file mode 100644
index 635ce84ac6..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/matmul.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API matmul operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-matmul
-
-testWebNNOperation('matmul', buildOperationWithTwoInputs, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/pad.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/pad.https.any.js
deleted file mode 100644
index f313e2c9f9..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/pad.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API pad operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-pad
-
-testWebNNOperation('pad', buildPad, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/pooling.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/pooling.https.any.js
deleted file mode 100644
index 837bca2c71..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/pooling.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API pooling operations
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-pool2d
-
-testWebNNOperation(['averagePool2d', 'l2Pool2d', 'maxPool2d'], buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/prelu.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/prelu.https.any.js
deleted file mode 100644
index 475cd9e5ce..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/prelu.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API prelu operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-prelu
-
-testWebNNOperation('prelu', buildOperationWithTwoInputs, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/reduction.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/reduction.https.any.js
deleted file mode 100644
index 0f3cefa02e..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/reduction.https.any.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// META: title=test WebNN API reduction operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-reduce
-
-testWebNNOperation(
- [
- 'reduceL1',
- 'reduceL2',
- 'reduceLogSum',
- 'reduceLogSumExp',
- 'reduceMax',
- 'reduceMean',
- 'reduceMin',
- 'reduceProduct',
- 'reduceSum',
- 'reduceSumSquare',
- ],
- buildOperationWithSingleInput, 'gpu'
-); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/relu.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/relu.https.any.js
deleted file mode 100644
index d1a35367df..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/relu.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API relu operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-relu
-
-testWebNNOperation('relu', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/resample2d.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/resample2d.https.any.js
deleted file mode 100644
index dd8e441946..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/resample2d.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API resample2d operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-resample2d-method
-
-testWebNNOperation('resample2d', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/reshape.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/reshape.https.any.js
deleted file mode 100644
index b0217d2e67..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/reshape.https.any.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// META: title=test WebNN API reshape operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-reshape
-
-testWebNNOperation('reshape', buildReshape, 'gpu');
-
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/sigmoid.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/sigmoid.https.any.js
deleted file mode 100644
index 26116c0ff9..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/sigmoid.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API sigmoid operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-sigmoid
-
-testWebNNOperation('sigmoid', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/slice.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/slice.https.any.js
deleted file mode 100644
index 1710c79a9c..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/slice.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API slice operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-slice
-
-testWebNNOperation('slice', buildSlice, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/softmax.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/softmax.https.any.js
deleted file mode 100644
index 9eaffe2beb..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/softmax.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API softmax operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-softmax
-
-testWebNNOperation('softmax', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/softplus.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/softplus.https.any.js
deleted file mode 100644
index 5f06846113..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/softplus.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API softplus operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-softplus
-
-testWebNNOperation('softplus', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/softsign.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/softsign.https.any.js
deleted file mode 100644
index eac0b7ec40..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/softsign.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API softsign operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-softsign
-
-testWebNNOperation('softsign', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/split.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/split.https.any.js
deleted file mode 100644
index 3b0aafd787..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/split.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API split operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-split
-
-testWebNNOperation('split', buildSplit, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/tanh.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/tanh.https.any.js
deleted file mode 100644
index 3029f4865a..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/tanh.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API tanh operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-tanh
-
-testWebNNOperation('tanh', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/transpose.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/transpose.https.any.js
deleted file mode 100644
index 295ef43ec1..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/transpose.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API transpose operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-transpose
-
-testWebNNOperation('transpose', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/triangular.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/triangular.https.any.js
deleted file mode 100644
index 3e1b0d5ab1..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/triangular.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API triangular operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-triangular
-
-testWebNNOperation('triangular', buildOperationWithSingleInput, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/gpu/where.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/gpu/where.https.any.js
deleted file mode 100644
index 49c6cbd4e3..0000000000
--- a/testing/web-platform/tests/webnn/conformance_tests/gpu/where.https.any.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// META: title=test WebNN API where operation
-// META: global=window,dedicatedworker
-// META: script=../../resources/utils.js
-// META: timeout=long
-
-'use strict';
-
-// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-where
-
-testWebNNOperation('where', buildWhere, 'gpu'); \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/conformance_tests/hard_sigmoid.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/hard_sigmoid.https.any.js
index 8161a24538..55391e7f1c 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/hard_sigmoid.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/hard_sigmoid.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API hardSigmoid operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-hard-sigmoid
-testWebNNOperation('hardSigmoid', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('hardSigmoid', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/hard_swish.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/hard_swish.https.any.js
index b4a7c53d8d..24b8c413bb 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/hard_swish.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/hard_swish.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API tanh operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-hard-swish
-testWebNNOperation('hardSwish', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('hardSwish', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/instance_normalization.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/instance_normalization.https.any.js
index fce879172e..fc339e5bab 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/instance_normalization.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/instance_normalization.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API instanceNormalization operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-instancenorm
-testWebNNOperation('instanceNormalization', buildLayerNorm); \ No newline at end of file
+runWebNNConformanceTests('instanceNormalization', buildLayerNorm);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/layer_normalization.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/layer_normalization.https.any.js
index ab8a50cc03..ea3cd04240 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/layer_normalization.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/layer_normalization.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API layerNormalization operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-layernorm
-testWebNNOperation('layerNormalization', buildLayerNorm); \ No newline at end of file
+runWebNNConformanceTests('layerNormalization', buildLayerNorm);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/leaky_relu.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/leaky_relu.https.any.js
index 2b6f17e95d..b2a4055bde 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/leaky_relu.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/leaky_relu.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API leakyRelu operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-leakyrelu
-testWebNNOperation('leakyRelu', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('leakyRelu', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/linear.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/linear.https.any.js
index 465b697f29..0e22f7a036 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/linear.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/linear.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API linear operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-linear
-testWebNNOperation('linear', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('linear', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/matmul.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/matmul.https.any.js
index 64eeb37f08..da78230579 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/matmul.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/matmul.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API matmul operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-matmul
-testWebNNOperation('matmul', buildOperationWithTwoInputs); \ No newline at end of file
+runWebNNConformanceTests('matmul', buildOperationWithTwoInputs);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/pad.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/pad.https.any.js
index f1a2400d1c..d733cbb6ed 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/pad.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/pad.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API pad operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-pad
-testWebNNOperation('pad', buildPad); \ No newline at end of file
+runWebNNConformanceTests('pad', buildPad);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/parallel-compute.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/parallel-compute.https.any.js
new file mode 100644
index 0000000000..642fec9f73
--- /dev/null
+++ b/testing/web-platform/tests/webnn/conformance_tests/parallel-compute.https.any.js
@@ -0,0 +1,19 @@
+// META: title=test parallel WebNN API compute operations
+// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
+// META: script=../resources/utils.js
+// META: timeout=long
+
+'use strict';
+
+// https://webmachinelearning.github.io/webnn/#api-mlcontext-compute
+
+if (navigator.ml) {
+ testParallelCompute();
+} else {
+ // Show indication to users why the test failed
+ test(
+ () => assert_not_equals(
+ navigator.ml, undefined, 'ml property is defined on navigator'));
+}
diff --git a/testing/web-platform/tests/webnn/conformance_tests/pooling.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/pooling.https.any.js
index 400d5ed37d..de2ae35a9c 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/pooling.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/pooling.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API pooling operations
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,5 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-pool2d
-testWebNNOperation(['averagePool2d', 'l2Pool2d', 'maxPool2d'], buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests(
+ ['averagePool2d', 'l2Pool2d', 'maxPool2d'], buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/prelu.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/prelu.https.any.js
index 83cc9db4b4..9337211e54 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/prelu.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/prelu.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API prelu operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-prelu
-testWebNNOperation('prelu', buildOperationWithTwoInputs); \ No newline at end of file
+runWebNNConformanceTests('prelu', buildOperationWithTwoInputs);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/reduction.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/reduction.https.any.js
index 30bfb4ba7a..c291906ba1 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/reduction.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/reduction.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API reduction operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,18 +9,17 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-reduce
-testWebNNOperation(
- [
- 'reduceL1',
- 'reduceL2',
- 'reduceLogSum',
- 'reduceLogSumExp',
- 'reduceMax',
- 'reduceMean',
- 'reduceMin',
- 'reduceProduct',
- 'reduceSum',
- 'reduceSumSquare',
- ],
- buildOperationWithSingleInput
-); \ No newline at end of file
+runWebNNConformanceTests(
+ [
+ 'reduceL1',
+ 'reduceL2',
+ 'reduceLogSum',
+ 'reduceLogSumExp',
+ 'reduceMax',
+ 'reduceMean',
+ 'reduceMin',
+ 'reduceProduct',
+ 'reduceSum',
+ 'reduceSumSquare',
+ ],
+ buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/relu.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/relu.https.any.js
index 51e427898f..7cb23eea1b 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/relu.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/relu.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API relu operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-relu
-testWebNNOperation('relu', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('relu', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/resample2d.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/resample2d.https.any.js
index 0b5b3e0032..b5bdda7197 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/resample2d.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/resample2d.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API resample2d operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-resample2d-method
-testWebNNOperation('resample2d', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('resample2d', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/reshape.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/reshape.https.any.js
index c0dafb176c..a7d03b2a0c 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/reshape.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/reshape.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API reshape operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,5 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-reshape
-testWebNNOperation('reshape', buildReshape);
-
+runWebNNConformanceTests('reshape', buildReshape);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/sigmoid.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/sigmoid.https.any.js
index 186f468918..9730b548b5 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/sigmoid.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/sigmoid.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API sigmoid operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-sigmoid
-testWebNNOperation('sigmoid', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('sigmoid', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/slice.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/slice.https.any.js
index 6441204517..b316ea58c4 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/slice.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/slice.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API slice operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-slice
-testWebNNOperation('slice', buildSlice); \ No newline at end of file
+runWebNNConformanceTests('slice', buildSlice);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/softmax.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/softmax.https.any.js
index 143b7d969f..a68a32c45f 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/softmax.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/softmax.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API softmax operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-softmax
-testWebNNOperation('softmax', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('softmax', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/softplus.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/softplus.https.any.js
index fcd6410bdb..7d89b117eb 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/softplus.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/softplus.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API softplus operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-softplus
-testWebNNOperation('softplus', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('softplus', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/softsign.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/softsign.https.any.js
index 6e26afdade..e175e0de56 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/softsign.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/softsign.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API softsign operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-softsign
-testWebNNOperation('softsign', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('softsign', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/split.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/split.https.any.js
index 0de6cb4d8d..78d707687f 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/split.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/split.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API split operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-split
-testWebNNOperation('split', buildSplit); \ No newline at end of file
+runWebNNConformanceTests('split', buildSplit);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/tanh.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/tanh.https.any.js
index c5d1f86ab1..e3ab5e9192 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/tanh.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/tanh.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API tanh operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-tanh
-testWebNNOperation('tanh', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('tanh', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/transpose.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/transpose.https.any.js
index 746e53d512..83bd7a45c1 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/transpose.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/transpose.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API transpose operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-transpose
-testWebNNOperation('transpose', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('transpose', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/triangular.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/triangular.https.any.js
index 503f310620..499f60ed36 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/triangular.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/triangular.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API triangular operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-triangular
-testWebNNOperation('triangular', buildOperationWithSingleInput); \ No newline at end of file
+runWebNNConformanceTests('triangular', buildOperationWithSingleInput);
diff --git a/testing/web-platform/tests/webnn/conformance_tests/where.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/where.https.any.js
index 7926221d3a..4ab453ab24 100644
--- a/testing/web-platform/tests/webnn/conformance_tests/where.https.any.js
+++ b/testing/web-platform/tests/webnn/conformance_tests/where.https.any.js
@@ -1,5 +1,7 @@
// META: title=test WebNN API where operation
// META: global=window,dedicatedworker
+// META: variant=?cpu
+// META: variant=?gpu
// META: script=../resources/utils.js
// META: timeout=long
@@ -7,4 +9,4 @@
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-where
-testWebNNOperation('where', buildWhere); \ No newline at end of file
+runWebNNConformanceTests('where', buildWhere);
diff --git a/testing/web-platform/tests/webnn/resources/test_data/arg_max.json b/testing/web-platform/tests/webnn/resources/test_data/arg_max.json
index d2fe9e62ca..348a54dc24 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/arg_max.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/arg_max.json
@@ -460,7 +460,7 @@
}
},
"options": {
- "keepDimensions": true
+ "keepDimensions": false
},
"expected": {
"name": "output",
diff --git a/testing/web-platform/tests/webnn/resources/test_data/arg_min.json b/testing/web-platform/tests/webnn/resources/test_data/arg_min.json
index 132a2dc3e8..330afbc710 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/arg_min.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/arg_min.json
@@ -460,7 +460,7 @@
}
},
"options": {
- "keepDimensions": true
+ "keepDimensions": false
},
"expected": {
"name": "output",
diff --git a/testing/web-platform/tests/webnn/resources/test_data/matmul.json b/testing/web-platform/tests/webnn/resources/test_data/matmul.json
index f3a03e442e..cc1789ee25 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/matmul.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/matmul.json
@@ -1,332 +1,6 @@
{
"tests": [
{
- "name": "matmul float32 constant 1D and 1D tensors all positive produces a scalar",
- "inputs": {
- "a": {
- "shape": [4],
- "data": [
- 50.10142534731317,
- 22.2193058046253,
- 34.65448469299386,
- 36.35148095671881
- ],
- "type": "float32",
- "constant": true
- },
- "b": {
- "shape": [4],
- "data": [
- 27.829805134194842,
- 83.14548502311283,
- 34.4128942110155,
- 83.20379675185079
- ],
- "type": "float32",
- "constant": true
- }
- },
- "expected": {
- "name": "output",
- "data": 7458.89013671875,
- "type": "float32"
- }
- },
- {
- "name": "matmul float32 1D and 1D tensors all positive produces a scalar",
- "inputs": {
- "a": {
- "shape": [4],
- "data": [
- 50.10142534731317,
- 22.2193058046253,
- 34.65448469299386,
- 36.35148095671881
- ],
- "type": "float32"
- },
- "b": {
- "shape": [4],
- "data": [
- 27.829805134194842,
- 83.14548502311283,
- 34.4128942110155,
- 83.20379675185079
- ],
- "type": "float32"
- }
- },
- "expected": {
- "name": "output",
- "data": 7458.89013671875,
- "type": "float32"
- }
- },
- {
- "name": "matmul float32 1D and 1D tensors all negative produces a scalar",
- "inputs": {
- "a": {
- "shape": [4],
- "data": [
- -86.60583871803968,
- -94.74202421330796,
- -86.16720380573273,
- -76.0215630990851
- ],
- "type": "float32"
- },
- "b": {
- "shape": [4],
- "data": [
- -21.22328469397374,
- -56.66441199254357,
- -77.66753889428908,
- -56.55797454862353
- ],
- "type": "float32"
- }
- },
- "expected": {
- "name": "output",
- "data": 18198.58203125,
- "type": "float32"
- }
- },
- {
- "name": "matmul float32 positive 1D and negative 1D tensors produces a scalar",
- "inputs": {
- "a": {
- "shape": [4],
- "data": [
- 50.10142534731317,
- 22.2193058046253,
- 34.65448469299386,
- 36.35148095671881
- ],
- "type": "float32"
- },
- "b": {
- "shape": [4],
- "data": [
- -21.22328469397374,
- -56.66441199254357,
- -77.66753889428908,
- -56.55797454862353
- ],
- "type": "float32"
- }
- },
- "expected": {
- "name": "output",
- "data": -7069.85546875,
- "type": "float32"
- }
- },
- {
- "name": "matmul float32 negative 1D and positive 1D tensors produces a scalar",
- "inputs": {
- "a": {
- "shape": [4],
- "data": [
- -86.60583871803968,
- -94.74202421330796,
- -86.16720380573273,
- -76.0215630990851
- ],
- "type": "float32"
- },
- "b": {
- "shape": [4],
- "data": [
- 27.829805134194842,
- 83.14548502311283,
- 34.4128942110155,
- 83.20379675185079
- ],
- "type": "float32"
- }
- },
- "expected": {
- "name": "output",
- "data": -19578.140625,
- "type": "float32"
- }
- },
- {
- "name": "matmul float32 1D and 2D tensors",
- "inputs": {
- "a": {
- "shape": [4],
- "data": [
- 50.10142534731317,
- 22.2193058046253,
- 34.65448469299386,
- 36.35148095671881
- ],
- "type": "float32"
- },
- "b": {
- "shape": [4, 5],
- "data": [
- 88.17004408170853,
- 78.40126706348094,
- 14.819002753540623,
- 3.692303791736573,
- 45.9064286713635,
- 43.08391896733015,
- 47.19946845924572,
- 60.925216107016425,
- 8.162760351602216,
- 20.33326305093228,
- 20.438397895943282,
- 27.01940859922867,
- 15.601424432184263,
- 87.46969388883927,
- 65.79554455585657,
- 69.31696864490797,
- 31.984439910782992,
- 12.291812891860388,
- 13.304834654547172,
- 85.26705387930089
- ],
- "type": "float32"
- }
- },
- "expected": {
- "name": "output",
- "shape": [1, 5],
- "data": [
- 8602.796875,
- 7075.7802734375,
- 3083.654052734375,
- 3881.228271484375,
- 8131.462890625
- ],
- "type": "float32"
- }
- },
- {
- "name": "matmul float32 1D and 4D tensors",
- "inputs": {
- "a": {
- "shape": [4],
- "data": [
- 50.10142534731317,
- 22.2193058046253,
- 34.65448469299386,
- 36.35148095671881
- ],
- "type": "float32"
- },
- "b": {
- "shape": [2, 2, 4, 2],
- "data": [
- 71.54684436671175,
- 78.61926127874348,
- 2.246814764541316,
- 12.044773359659434,
- 1.8342867333124069,
- 82.09732511549477,
- 43.884761946067094,
- 5.616231825100204,
- 34.67424097413332,
- 49.152710076333506,
- 75.34904358690912,
- 84.74523302920429,
- 36.56497325521975,
- 22.89479672718755,
- 15.02636975800511,
- 66.49530785246675,
- 65.81345056776044,
- 26.749681209347376,
- 19.415639234175774,
- 98.60692665127114,
- 65.39448996549784,
- 56.47023672202065,
- 80.64523905250766,
- 82.20401464839868,
- 70.84606482516416,
- 50.27994995977012,
- 67.39406108056262,
- 75.35806805146241,
- 2.7487208784167327,
- 68.0645872775828,
- 70.73791158057968,
- 46.26436742398676
- ],
- "type": "float32"
- }
- },
- "expected": {
- "name": "output",
- "shape": [2, 2, 1, 2],
- "data": [
- 5293.36376953125,
- 7255.7626953125,
- 5224.80322265625,
- 7556.21142578125,
- 8926.5361328125,
- 8476.359375,
- 7713.62158203125,
- 8234.0224609375
- ],
- "type": "float32"
- }
- },
- {
- "name": "matmul float32 2D and 1D tensors",
- "inputs": {
- "a": {
- "shape": [5, 4],
- "data": [
- 88.17004408170853,
- 43.08391896733015,
- 20.438397895943282,
- 69.31696864490797,
- 78.40126706348094,
- 47.19946845924572,
- 27.01940859922867,
- 31.984439910782992,
- 14.819002753540623,
- 60.925216107016425,
- 15.601424432184263,
- 12.291812891860388,
- 3.692303791736573,
- 8.162760351602216,
- 87.46969388883927,
- 13.304834654547172,
- 45.9064286713635,
- 20.33326305093228,
- 65.79554455585657,
- 85.26705387930089
- ],
- "type": "float32"
- },
- "b": {
- "shape": [4],
- "data": [
- 50.10142534731317,
- 22.2193058046253,
- 34.65448469299386,
- 36.35148095671881
- ],
- "type": "float32"
- }
- },
- "expected": {
- "name": "output",
- "shape": [5, 1],
- "data": [
- 8602.796875,
- 7075.7802734375,
- 3083.654052734375,
- 3881.228271484375,
- 8131.462890625
- ],
- "type": "float32"
- }
- },
- {
"name": "matmul float32 2D and 2D tensors",
"inputs": {
"a": {
@@ -750,64 +424,6 @@
}
},
{
- "name": "matmul float32 3D and 1D tensors",
- "inputs": {
- "a": {
- "shape": [2, 3, 4],
- "data": [
- 56.46701250066562,
- 99.86045478237251,
- 71.05493372292567,
- 32.45438455331333,
- 17.310747999630017,
- 2.586275053048559,
- 92.31499166302054,
- 96.9758519231732,
- 26.4721315276526,
- 77.67031776320978,
- 29.278788710989147,
- 82.12142428847062,
- 89.89308471484885,
- 82.49795321217854,
- 64.36866008901963,
- 23.75928513568486,
- 6.67026681065197,
- 81.55583129445503,
- 16.142963270263433,
- 57.45134849716054,
- 26.82641739603182,
- 85.0296980735713,
- 36.198863464757956,
- 89.60960360138286
- ],
- "type": "float32"
- },
- "b": {
- "shape": [4],
- "data": [
- 27.829805134194842,
- 83.14548502311283,
- 34.4128942110155,
- 83.20379675185079
- ],
- "type": "float32"
- }
- },
- "expected": {
- "name": "output",
- "shape": [2, 3, 1],
- "data": [
- 15019.9462890625,
- 11942.376953125,
- 15035.0322265625,
- 13553.013671875,
- 12302.328125,
- 16517.9765625
- ],
- "type": "float32"
- }
- },
- {
"name": "matmul float32 4D and 4D (broadcast) tensors",
"inputs": {
"a": {
diff --git a/testing/web-platform/tests/webnn/resources/test_data/prelu.json b/testing/web-platform/tests/webnn/resources/test_data/prelu.json
index cf79bee7a9..14a7c412dd 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/prelu.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/prelu.json
@@ -1,6 +1,34 @@
{
"tests": [
{
+ "name": "prelu float32 0D scalar",
+ "inputs": {
+ "x": {
+ "shape": [],
+ "data": [
+ -4.794857500523286
+ ],
+ "type": "float32"
+ },
+ "slope": {
+ "shape": [],
+ "data": [
+ 1.1202747481570352
+ ],
+ "type": "float32",
+ "constant": true
+ }
+ },
+ "expected": {
+ "name": "output",
+ "shape": [],
+ "data": [
+ -5.371557712554932
+ ],
+ "type": "float32"
+ }
+ },
+ {
"name": "prelu float32 1D constant tensors",
"inputs": {
"x": {
diff --git a/testing/web-platform/tests/webnn/resources/test_data/reduce_l1.json b/testing/web-platform/tests/webnn/resources/test_data/reduce_l1.json
index 7cbc442511..c3f2b618e9 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/reduce_l1.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/reduce_l1.json
@@ -676,7 +676,8 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": false
},
"expected": {
"name": "output",
@@ -725,11 +726,12 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": true
},
"expected": {
"name": "output",
- "shape": [1, 2, 2, 1],
+ "shape": [2, 1, 2, 1],
"data": [
108.43173217773438,
315.6007995605469,
diff --git a/testing/web-platform/tests/webnn/resources/test_data/reduce_l2.json b/testing/web-platform/tests/webnn/resources/test_data/reduce_l2.json
index 7e59a45d5e..d83eea9dfb 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/reduce_l2.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/reduce_l2.json
@@ -676,7 +676,8 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": false
},
"expected": {
"name": "output",
@@ -725,11 +726,12 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": true
},
"expected": {
"name": "output",
- "shape": [1, 2, 2, 1],
+ "shape": [2, 1, 2, 1],
"data": [
138.580078125,
166.67791748046875,
diff --git a/testing/web-platform/tests/webnn/resources/test_data/reduce_log_sum.json b/testing/web-platform/tests/webnn/resources/test_data/reduce_log_sum.json
index 250398d227..061e12ad51 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/reduce_log_sum.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/reduce_log_sum.json
@@ -596,7 +596,8 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": false
},
"expected": {
"name": "output",
@@ -645,11 +646,12 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": true
},
"expected": {
"name": "output",
- "shape": [1, 2, 2, 1],
+ "shape": [2, 1, 2, 1],
"data": [
5.7273993492126465,
5.64375114440918,
diff --git a/testing/web-platform/tests/webnn/resources/test_data/reduce_log_sum_exp.json b/testing/web-platform/tests/webnn/resources/test_data/reduce_log_sum_exp.json
index b7f39abd52..3577d6aa9e 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/reduce_log_sum_exp.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/reduce_log_sum_exp.json
@@ -676,7 +676,8 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": false
},
"expected": {
"name": "output",
@@ -725,11 +726,12 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": true
},
"expected": {
"name": "output",
- "shape": [1, 2, 2, 1],
+ "shape": [2, 1, 2, 1],
"data": [
8.563796997070312,
5.500619411468506,
diff --git a/testing/web-platform/tests/webnn/resources/test_data/reduce_max.json b/testing/web-platform/tests/webnn/resources/test_data/reduce_max.json
index 967aea8bf4..11ed0ed919 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/reduce_max.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/reduce_max.json
@@ -556,7 +556,8 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": false
},
"expected": {
"name": "output",
@@ -605,11 +606,12 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": true
},
"expected": {
"name": "output",
- "shape": [1, 2, 2, 1],
+ "shape": [2, 1, 2, 1],
"data": [
90.42288208007812,
94.99645233154297,
diff --git a/testing/web-platform/tests/webnn/resources/test_data/reduce_mean.json b/testing/web-platform/tests/webnn/resources/test_data/reduce_mean.json
index 5a48952c06..8c26d0a562 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/reduce_mean.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/reduce_mean.json
@@ -670,7 +670,8 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": false
},
"expected": {
"shape": [2, 2],
@@ -718,10 +719,11 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": true
},
"expected": {
- "shape": [1, 2, 2, 1],
+ "shape": [2, 1, 2, 1],
"data": [
52.287559509277344,
45.10261917114258,
diff --git a/testing/web-platform/tests/webnn/resources/test_data/reduce_min.json b/testing/web-platform/tests/webnn/resources/test_data/reduce_min.json
index 92de75e92a..6c26df5db1 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/reduce_min.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/reduce_min.json
@@ -556,7 +556,8 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": false
},
"expected": {
"name": "output",
@@ -605,11 +606,12 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": true
},
"expected": {
"name": "output",
- "shape": [1, 2, 2, 1],
+ "shape": [2, 1, 2, 1],
"data": [
-87.9623031616211,
-53.747413635253906,
diff --git a/testing/web-platform/tests/webnn/resources/test_data/reduce_product.json b/testing/web-platform/tests/webnn/resources/test_data/reduce_product.json
index 691bf4da9b..d58af30ec1 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/reduce_product.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/reduce_product.json
@@ -556,7 +556,8 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": false
},
"expected": {
"name": "output",
@@ -605,11 +606,12 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": true
},
"expected": {
"name": "output",
- "shape": [1, 2, 2, 1],
+ "shape": [2, 1, 2, 1],
"data": [
-3638925568,
6523364352,
diff --git a/testing/web-platform/tests/webnn/resources/test_data/reduce_sum.json b/testing/web-platform/tests/webnn/resources/test_data/reduce_sum.json
index df47a1a2b2..7027d38b67 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/reduce_sum.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/reduce_sum.json
@@ -670,7 +670,8 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": false
},
"expected": {
"shape": [2, 2],
@@ -718,10 +719,11 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": true
},
"expected": {
- "shape": [1, 2, 2, 1],
+ "shape": [2, 1, 2, 1],
"data": [
355.21942138671875,
185.98255920410156,
diff --git a/testing/web-platform/tests/webnn/resources/test_data/reduce_sum_square.json b/testing/web-platform/tests/webnn/resources/test_data/reduce_sum_square.json
index 8ac373e4b3..bd2ebb341a 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/reduce_sum_square.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/reduce_sum_square.json
@@ -676,7 +676,8 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": false
},
"expected": {
"name": "output",
@@ -725,11 +726,12 @@
}
},
"options": {
- "axes": [1, 3]
+ "axes": [1, 3],
+ "keepDimensions": true
},
"expected": {
"name": "output",
- "shape": [1, 2, 2, 1],
+ "shape": [2, 1, 2, 1],
"data": [
12302.474609375,
22772.77734375,
diff --git a/testing/web-platform/tests/webnn/resources/test_data/softplus.json b/testing/web-platform/tests/webnn/resources/test_data/softplus.json
index eb05b7b281..373612d5ca 100644
--- a/testing/web-platform/tests/webnn/resources/test_data/softplus.json
+++ b/testing/web-platform/tests/webnn/resources/test_data/softplus.json
@@ -1,7 +1,7 @@
-{ // softplus: The calculation follows the expression ln(1 + exp(steepness * x)) / steepness.
+{ // softplus: The calculation follows the expression ln(1 + exp(x)).
"tests": [
{
- "name": "softplus float32 1D constant tensor default options", // default options: {steepness: 1}
+ "name": "softplus float32 1D constant tensor",
"inputs": {
"x": {
"shape": [24],
@@ -68,7 +68,7 @@
}
},
{
- "name": "softplus float32 1D tensor default options", // default options: {steepness: 1}
+ "name": "softplus float32 1D tensor",
"inputs": {
"x": {
"shape": [24],
@@ -134,7 +134,7 @@
}
},
{
- "name": "softplus float32 2D tensor default options",
+ "name": "softplus float32 2D tensor",
"inputs": {
"x": {
"shape": [4, 6],
@@ -200,7 +200,7 @@
}
},
{
- "name": "softplus float32 3D tensor default options",
+ "name": "softplus float32 3D tensor",
"inputs": {
"x": {
"shape": [2, 3, 4],
@@ -266,7 +266,7 @@
}
},
{
- "name": "softplus float32 4D tensor default options",
+ "name": "softplus float32 4D tensor",
"inputs": {
"x": {
"shape": [1, 2, 3, 4],
@@ -332,7 +332,7 @@
}
},
{
- "name": "softplus float32 5D tensor default options",
+ "name": "softplus float32 5D tensor",
"inputs": {
"x": {
"shape": [1, 2, 1, 3, 4],
@@ -396,144 +396,6 @@
],
"type": "float32"
}
- },
- {
- "name": "softplus both positive float32 4D tensor and options.steepness",
- "inputs": {
- "x": {
- "shape": [1, 2, 3, 4],
- "data": [
- 5.626614582460632,
- 5.167487045486892,
- 4.0146356193402655,
- 9.48003299650489,
- 9.989938045769978,
- 7.0654412821434125,
- 2.132681001794825,
- 8.187151346059956,
- 5.169976220175496,
- 2.1044997879382077,
- 3.523329401138895,
- 4.136340646976668,
- 1.7418719794295656,
- 5.145224066290767,
- 5.015515309165462,
- 0.045903935074711466,
- 2.9570898924917377,
- 3.959244712098706,
- 5.517926978255181,
- 7.192322388417094,
- 8.76492480390928,
- 1.3734704039113388,
- 8.930669016709397,
- 8.660283210871246
- ],
- "type": "float32"
- }
- },
- "options": {
- "steepness": 1.5104469060897827
- },
- "expected": {
- "name": "output",
- "shape": [1, 2, 3, 4],
- "data": [
- 5.626749515533447,
- 5.167757034301758,
- 4.016173362731934,
- 9.480032920837402,
- 9.989937782287598,
- 7.065456390380859,
- 2.158585548400879,
- 8.187153816223145,
- 5.170245170593262,
- 2.1315081119537354,
- 3.526555061340332,
- 4.137620449066162,
- 1.7879058122634888,
- 5.145503044128418,
- 5.015854835510254,
- 0.4822517931461334,
- 2.964651584625244,
- 3.960916519165039,
- 5.518085956573486,
- 7.19233512878418,
- 8.764925956726074,
- 1.4518096446990967,
- 8.930669784545898,
- 8.660284042358398
- ],
- "type": "float32"
- }
- },
- {
- "name": "softplus both negative float32 4D tensor and options.steepness",
- "inputs": {
- "x": {
- "shape": [1, 2, 3, 4],
- "data": [
- -5.584833476104802,
- -8.188738740810354,
- -8.981280004134987,
- -1.7315531899284586,
- -0.7266543578958906,
- -0.0034800119290885334,
- -7.378389455552106,
- -8.907525953796949,
- -6.0483786568116304,
- -6.328561142365743,
- -2.6006513567654626,
- -5.02005264196455,
- -2.0647716093484414,
- -1.5499896740695434,
- -2.221591675966657,
- -1.1088025713211636,
- -2.7854626064634385,
- -2.105037489961294,
- -5.144277741727352,
- -5.081219916574497,
- -7.499426297617635,
- -2.4305558382286545,
- -8.390520024268328,
- -0.07117499202643174
- ],
- "type": "float32"
- }
- },
- "options": {
- "steepness": -1.2985155767552126
- },
- "expected": {
- "name": "output",
- "shape": [1, 2, 3, 4],
- "data": [
- -5.585379123687744,
- -8.188756942749023,
- -8.981287002563477,
- -1.8088372945785522,
- -0.9798305630683899,
- -0.5355416536331177,
- -7.378442287445068,
- -8.907533645629883,
- -6.048677444458008,
- -6.328769207000732,
- -2.626511573791504,
- -5.021188259124756,
- -2.1157851219177246,
- -1.6465802192687988,
- -2.2634570598602295,
- -1.2725814580917358,
- -2.805877923965454,
- -2.1535322666168213,
- -5.145244121551514,
- -5.082269191741943,
- -7.499471664428711,
- -2.4626762866973877,
- -8.390534400939941,
- -0.5702091455459595
- ],
- "type": "float32"
- }
}
]
} \ No newline at end of file
diff --git a/testing/web-platform/tests/webnn/resources/utils.js b/testing/web-platform/tests/webnn/resources/utils.js
index d1dc0675a7..e5b80ae9f7 100644
--- a/testing/web-platform/tests/webnn/resources/utils.js
+++ b/testing/web-platform/tests/webnn/resources/utils.js
@@ -13,6 +13,15 @@ const TypedArrayDict = {
int64: BigInt64Array,
};
+const kContextOptionsForVariant = {
+ cpu: {
+ deviceType: 'cpu',
+ },
+ gpu: {
+ deviceType: 'gpu',
+ }
+};
+
// The maximum index to validate for the output's expected value.
const kMaximumIndexToValidate = 1000;
@@ -867,17 +876,15 @@ const run = async (operationName, context, builder, resources, buildFunc) => {
checkResults(operationName, namedOutputOperands, result.outputs, resources);
};
+const variant = location.search.substring(1);
+const contextOptions = kContextOptionsForVariant[variant];
+
/**
* Run WebNN operation tests.
* @param {(String[]|String)} operationName - An operation name array or an operation name
* @param {Function} buildFunc - A build function for an operation
- * @param {String} deviceType - The execution device type for this test
*/
-const testWebNNOperation = (operationName, buildFunc, deviceType = 'cpu') => {
- test(() => assert_not_equals(navigator.ml, undefined, "ml property is defined on navigator"));
- if (navigator.ml === undefined) {
- return;
- }
+const testWebNNOperation = (operationName, buildFunc) => {
let operationNameArray;
if (typeof operationName === 'string') {
operationNameArray = [operationName];
@@ -890,7 +897,14 @@ const testWebNNOperation = (operationName, buildFunc, deviceType = 'cpu') => {
operationNameArray.forEach((subOperationName) => {
const tests = loadTests(subOperationName);
promise_setup(async () => {
- context = await navigator.ml.createContext({deviceType});
+ let supported = false;
+ try {
+ context = await navigator.ml.createContext(contextOptions);
+ supported = true;
+ } catch (e) {
+ }
+ assert_implements(
+ supported, `Unable to create context for ${variant} variant`);
builder = new MLGraphBuilder(context);
});
for (const subTest of tests) {
@@ -901,6 +915,65 @@ const testWebNNOperation = (operationName, buildFunc, deviceType = 'cpu') => {
});
};
+/**
+ * WebNN parallel compute operation test.
+ */
+const testParallelCompute = () => {
+ let ml_context;
+ let ml_graph;
+
+ promise_setup(async () => {
+ let supported = false;
+ try {
+ ml_context = await navigator.ml.createContext(contextOptions);
+ supported = true;
+ } catch (e) {
+ }
+ assert_implements(
+ supported, `Unable to create context for ${variant} variant`);
+ // Construct a simple graph: A = B * 2.
+ const builder = new MLGraphBuilder(ml_context);
+ const operandType = {dataType: 'float32', dimensions: [1]};
+ const input_operand = builder.input('input', operandType);
+ const const_operand = builder.constant(operandType, Float32Array.from([2]));
+ const output_operand = builder.mul(input_operand, const_operand);
+ ml_graph = await builder.build({'output': output_operand});
+ });
+
+ promise_test(async () => {
+ const test_inputs = [1, 2, 3, 4];
+
+ const actual_outputs = await Promise.all(test_inputs.map(async (input) => {
+ let inputs = {'input': Float32Array.from([input])};
+ let outputs = {'output': new Float32Array(1)};
+ ({inputs, outputs} = await ml_context.compute(ml_graph, inputs, outputs));
+ return outputs.output[0];
+ }));
+
+ const expected_outputs = [2, 4, 6, 8];
+ assert_array_equals(actual_outputs, expected_outputs);
+ });
+};
+
+/**
+ * Run WebNN conformance tests by specified operation.
+ * @param {(String[]|String)} operationName - An operation name array or an
+ * operation name
+ * @param {Function} buildFunc - A build function for an operation
+ */
+const runWebNNConformanceTests = (operationName, buildFunc) => {
+ // Link to https://github.com/web-platform-tests/wpt/pull/44883
+ // Check navigator.ml is defined before trying to run WebNN tests
+ if (navigator.ml) {
+ testWebNNOperation(operationName, buildFunc);
+ } else {
+ // Show indication to users why the test failed
+ test(
+ () => assert_not_equals(
+ navigator.ml, undefined, 'ml property is defined on navigator'));
+ }
+};
+
// ref: http://stackoverflow.com/questions/32633585/how-do-you-convert-to-half-floats-in-javascript
const toHalf = (value) => {
let floatView = new Float32Array(1);
@@ -970,13 +1043,19 @@ const createBuffer = (context, bufferSize) => {
/**
* WebNN destroy buffer twice test.
* @param {String} testName - The name of the test operation.
- * @param {String} deviceType - The execution device type for this test.
*/
-const testDestroyWebNNBuffer = (testName, deviceType = 'cpu') => {
+const testDestroyWebNNBuffer = (testName) => {
let context;
let buffer;
promise_setup(async () => {
- context = await navigator.ml.createContext({deviceType});
+ let supported = false;
+ try {
+ context = await navigator.ml.createContext(contextOptions);
+ supported = true;
+ } catch (e) {
+ }
+ assert_implements(
+ supported, `Unable to create context for ${variant} variant`);
buffer = createBuffer(context, 4);
});
promise_test(async () => {
@@ -993,12 +1072,19 @@ const testDestroyWebNNBuffer = (testName, deviceType = 'cpu') => {
* WebNN create buffer test.
* @param {String} testName - The name of the test operation.
* @param {Number} bufferSize - Size of the buffer to create, in bytes.
- * @param {String} deviceType - The execution device type for this test.
*/
-const testCreateWebNNBuffer = (testName, bufferSize, deviceType = 'cpu') => {
+const testCreateWebNNBuffer = (testName, bufferSize) => {
let context;
+
promise_setup(async () => {
- context = await navigator.ml.createContext({deviceType});
+ let supported = false;
+ try {
+ context = await navigator.ml.createContext(contextOptions);
+ supported = true;
+ } catch (e) {
+ }
+ assert_implements(
+ supported, `Unable to create context for ${variant} variant`);
});
promise_test(async () => {
createBuffer(context, bufferSize);
@@ -1021,12 +1107,18 @@ const assert_buffer_data_equals = async (ml_context, ml_buffer, expected) => {
/**
* WebNN write buffer operation test.
* @param {String} testName - The name of the test operation.
- * @param {String} deviceType - The execution device type for this test.
*/
-const testWriteWebNNBuffer = (testName, deviceType = 'cpu') => {
+const testWriteWebNNBuffer = (testName) => {
let ml_context;
promise_setup(async () => {
- ml_context = await navigator.ml.createContext({deviceType});
+ let supported = false;
+ try {
+ ml_context = await navigator.ml.createContext(contextOptions);
+ supported = true;
+ } catch (e) {
+ }
+ assert_implements(
+ supported, `Unable to create context for ${variant} variant`);
});
promise_test(async () => {
@@ -1117,7 +1209,7 @@ const testWriteWebNNBuffer = (testName, deviceType = 'cpu') => {
return;
}
- let another_ml_context = await navigator.ml.createContext({deviceType});
+ let another_ml_context = await navigator.ml.createContext(contextOptions);
let another_ml_buffer = createBuffer(another_ml_context, ml_buffer.size);
let input_data = new Uint8Array(ml_buffer.size).fill(0xAA);
@@ -1131,12 +1223,18 @@ const testWriteWebNNBuffer = (testName, deviceType = 'cpu') => {
/**
* WebNN read buffer operation test.
* @param {String} testName - The name of the test operation.
- * @param {String} deviceType - The execution device type for this test.
*/
-const testReadWebNNBuffer = (testName, deviceType = 'cpu') => {
+const testReadWebNNBuffer = (testName) => {
let ml_context;
promise_setup(async () => {
- ml_context = await navigator.ml.createContext({deviceType});
+ let supported = false;
+ try {
+ ml_context = await navigator.ml.createContext(contextOptions);
+ supported = true;
+ } catch (e) {
+ }
+ assert_implements(
+ supported, `Unable to create context for ${variant} variant`);
});
promise_test(async t => {
@@ -1277,7 +1375,7 @@ const testReadWebNNBuffer = (testName, deviceType = 'cpu') => {
return;
}
- let another_ml_context = await navigator.ml.createContext({deviceType});
+ let another_ml_context = await navigator.ml.createContext(contextOptions);
let another_ml_buffer = createBuffer(another_ml_context, ml_buffer.size);
await promise_rejects_js(
diff --git a/testing/web-platform/tests/webnn/validation_tests/clamp.https.any.js b/testing/web-platform/tests/webnn/validation_tests/clamp.https.any.js
index 85cd19a566..126fa90e16 100644
--- a/testing/web-platform/tests/webnn/validation_tests/clamp.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/clamp.https.any.js
@@ -5,3 +5,56 @@
'use strict';
validateInputFromAnotherBuilder('clamp');
+
+validateUnaryOperation(
+ 'clamp', allWebNNOperandDataTypes, /*alsoBuildActivation=*/ true);
+
+promise_test(async t => {
+ const options = {minValue: 1.0, maxValue: 3.0};
+ const input =
+ builder.input('input', {dataType: 'uint32', dimensions: [1, 2, 3]});
+ const output = builder.clamp(input, options);
+ assert_equals(output.dataType(), 'uint32');
+ assert_array_equals(output.shape(), [1, 2, 3]);
+}, '[clamp] Test building an operator with options');
+
+promise_test(async t => {
+ const options = {minValue: 0, maxValue: 0};
+ const input =
+ builder.input('input', {dataType: 'int32', dimensions: [1, 2, 3, 4]});
+ const output = builder.clamp(input, options);
+ assert_equals(output.dataType(), 'int32');
+ assert_array_equals(output.shape(), [1, 2, 3, 4]);
+}, '[clamp] Test building an operator with options.minValue == options.maxValue');
+
+promise_test(async t => {
+ const options = {minValue: 2.0};
+ builder.clamp(options);
+}, '[clamp] Test building an activation with options');
+
+promise_test(async t => {
+ const options = {minValue: 3.0, maxValue: 1.0};
+ const input =
+ builder.input('input', {dataType: 'uint8', dimensions: [1, 2, 3]});
+ assert_throws_js(TypeError, () => builder.clamp(input, options));
+}, '[clamp] Throw if options.minValue > options.maxValue when building an operator');
+
+// To be removed once infinite `minValue` is allowed. Tracked in
+// https://github.com/webmachinelearning/webnn/pull/647.
+promise_test(async t => {
+ const options = {minValue: -Infinity};
+ const input = builder.input('input', {dataType: 'float16', dimensions: []});
+ assert_throws_js(TypeError, () => builder.clamp(input, options));
+}, '[clamp] Throw if options.minValue is -Infinity when building an operator');
+
+promise_test(async t => {
+ const options = {minValue: 2.0, maxValue: -1.0};
+ assert_throws_js(TypeError, () => builder.clamp(options));
+}, '[clamp] Throw if options.minValue > options.maxValue when building an activation');
+
+// To be removed once NaN `maxValue` is allowed. Tracked in
+// https://github.com/webmachinelearning/webnn/pull/647.
+promise_test(async t => {
+ const options = {maxValue: NaN};
+ assert_throws_js(TypeError, () => builder.clamp(options));
+}, '[clamp] Throw if options.maxValue is NaN when building an activation');
diff --git a/testing/web-platform/tests/webnn/validation_tests/conv2d.https.any.js b/testing/web-platform/tests/webnn/validation_tests/conv2d.https.any.js
index ffc9c2c65d..7dac654951 100644
--- a/testing/web-platform/tests/webnn/validation_tests/conv2d.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/conv2d.https.any.js
@@ -55,3 +55,478 @@ multi_builder_test(async (t, builder, otherBuilder) => {
const filter = builder.input('filter', kExampleFilterDescriptor);
assert_throws_js(TypeError, () => builder.conv2d(input, filter, options));
}, '[conv2d] throw if activation option is from another builder');
+
+const tests = [
+ {
+ name: '[conv2d] Test with default options.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ output: {dataType: 'float32', dimensions: [1, 1, 3, 3]}
+ },
+ {
+ name: '[conv2d] Test with padding.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ options: {
+ padding: [1, 1, 1, 1],
+ },
+ output: {dataType: 'float32', dimensions: [1, 1, 5, 5]}
+ },
+ {
+ name: '[conv2d] Test with strides and padding.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ options: {
+ padding: [1, 1, 1, 1],
+ strides: [2, 2],
+ },
+ output: {dataType: 'float32', dimensions: [1, 1, 3, 3]}
+ },
+ {
+ name: '[conv2d] Test with strides and asymmetric padding.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 4, 2]},
+ options: {
+ padding: [1, 2, 0, 1],
+ strides: [2, 2],
+ },
+ output: {dataType: 'float32', dimensions: [1, 1, 3, 3]}
+ },
+ {
+ name: '[conv2d] Test depthwise conv2d by setting groups to input channels.',
+ input: {dataType: 'float32', dimensions: [1, 4, 2, 2]},
+ filter: {dataType: 'float32', dimensions: [4, 1, 2, 2]},
+ options: {
+ groups: 4,
+ },
+ output: {dataType: 'float32', dimensions: [1, 4, 1, 1]}
+ },
+ {
+ name:
+ '[conv2d] Test depthwise conv2d with groups, inputLayout="nhwc" and filterLayout="ihwo".',
+ input: {dataType: 'float32', dimensions: [1, 2, 2, 4]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 2, 4]},
+ options: {
+ groups: 4,
+ inputLayout: 'nhwc',
+ filterLayout: 'ihwo',
+ },
+ output: {dataType: 'float32', dimensions: [1, 1, 1, 4]}
+ },
+ {
+ name:
+ '[conv2d] Test with dilations, inputLayout="nhwc" and filterLayout="ihwo".',
+ input: {dataType: 'float32', dimensions: [1, 65, 65, 1]},
+ filter: {dataType: 'float32', dimensions: [1, 3, 3, 1]},
+ options: {
+ inputLayout: 'nhwc',
+ filterLayout: 'ihwo',
+ dilations: [4, 4],
+ },
+ output: {dataType: 'float32', dimensions: [1, 57, 57, 1]}
+ },
+ {
+ name: '[conv2d] Test with inputLayout="nchw" and filterLayout="oihw".',
+ input: {dataType: 'float32', dimensions: [1, 2, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]},
+ options: {
+ inputLayout: 'nchw',
+ filterLayout: 'oihw',
+ },
+ output: {dataType: 'float32', dimensions: [1, 1, 3, 3]}
+ },
+ {
+ name: '[conv2d] Test with inputLayout="nchw" and filterLayout="hwio".',
+ input: {dataType: 'float32', dimensions: [1, 2, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [3, 3, 2, 1]},
+ options: {
+ inputLayout: 'nchw',
+ filterLayout: 'hwio',
+ },
+ output: {dataType: 'float32', dimensions: [1, 1, 3, 3]}
+ },
+ {
+ name: '[conv2d] Test with inputLayout="nchw" and filterLayout="ohwi".',
+ input: {dataType: 'float32', dimensions: [1, 2, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 3, 3, 2]},
+ options: {
+ inputLayout: 'nchw',
+ filterLayout: 'ohwi',
+ },
+ output: {dataType: 'float32', dimensions: [1, 1, 3, 3]}
+ },
+ {
+ name: '[conv2d] Test with inputLayout="nchw" and filterLayout="ihwo".',
+ input: {dataType: 'float32', dimensions: [1, 2, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [2, 3, 3, 1]},
+ options: {
+ inputLayout: 'nchw',
+ filterLayout: 'ihwo',
+ },
+ output: {dataType: 'float32', dimensions: [1, 1, 3, 3]}
+ },
+ {
+ name: '[conv2d] Test with inputLayout="nhwc" and filterLayout="oihw".',
+ input: {dataType: 'float32', dimensions: [1, 5, 5, 2]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]},
+ options: {
+ inputLayout: 'nhwc',
+ filterLayout: 'oihw',
+ },
+ output: {dataType: 'float32', dimensions: [1, 3, 3, 1]}
+ },
+ {
+ name: '[conv2d] Test with inputLayout="nhwc" and filterLayout="hwio".',
+ input: {dataType: 'float32', dimensions: [1, 5, 5, 2]},
+ filter: {dataType: 'float32', dimensions: [3, 3, 2, 1]},
+ options: {
+ inputLayout: 'nhwc',
+ filterLayout: 'hwio',
+ },
+ output: {dataType: 'float32', dimensions: [1, 3, 3, 1]}
+ },
+ {
+ name: '[conv2d] Test with inputLayout="nhwc" and filterLayout="ohwi".',
+ input: {dataType: 'float32', dimensions: [1, 5, 5, 2]},
+ filter: {dataType: 'float32', dimensions: [1, 3, 3, 2]},
+ options: {
+ inputLayout: 'nhwc',
+ filterLayout: 'ohwi',
+ },
+ output: {dataType: 'float32', dimensions: [1, 3, 3, 1]}
+ },
+ {
+ name: '[conv2d] Test with inputLayout="nhwc" and filterLayout="ihwo".',
+ input: {dataType: 'float32', dimensions: [1, 5, 5, 2]},
+ filter: {dataType: 'float32', dimensions: [2, 3, 3, 1]},
+ options: {
+ inputLayout: 'nhwc',
+ filterLayout: 'ihwo',
+ },
+ output: {dataType: 'float32', dimensions: [1, 3, 3, 1]}
+ },
+ {
+ name: '[conv2d] Throw if the input is not a 4-D tensor.',
+ input: {dataType: 'float32', dimensions: [1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 2, 1]},
+ },
+ {
+ name: '[conv2d] Throw if the input data type is not floating point.',
+ input: {dataType: 'int32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'int32', dimensions: [1, 1, 2, 2]},
+ },
+ {
+ name: '[conv2d] Throw if the filter is not a 4-D tensor.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [2, 2]},
+ },
+ {
+ name:
+ '[conv2d] Throw if the filter data type doesn\'t match the input data type.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'int32', dimensions: [1, 1, 2, 2]},
+ },
+ {
+ name: '[conv2d] Throw if the length of padding is not 4.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ padding: [2, 2],
+ },
+ },
+ {
+ name: '[conv2d] Throw if the length of strides is not 2.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ strides: [2],
+ },
+ },
+ {
+ name: '[conv2d] Throw if strideHeight is smaller than 1.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ strides: [0, 1],
+ },
+ },
+ {
+ name: '[conv2d] Throw if strideWidth is smaller than 1.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ strides: [1, 0],
+ },
+ },
+ {
+ name: '[conv2d] Throw if the length of dilations is not 2.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ dilations: [1],
+ },
+ },
+ {
+ name: '[conv2d] Throw if dilationHeight is smaller than 1.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ dilations: [0, 1],
+ },
+ },
+ {
+ name: '[conv2d] Throw if dilationWidth is smaller than 1.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ dilations: [1, 0],
+ },
+ },
+ {
+ name: '[conv2d] Throw if inputChannels % groups is not 0.',
+ input: {dataType: 'float32', dimensions: [1, 4, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ groups: 3,
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels.',
+ input: {dataType: 'float32', dimensions: [1, 4, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ groups: 2,
+ },
+ },
+ {
+ name: '[conv2d] Throw if the groups is smaller than 1.',
+ input: {dataType: 'float32', dimensions: [1, 4, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ groups: 0,
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw due to overflow when calculating the effective filter height.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 434983, 2]},
+ options: {
+ dilations: [328442, 1],
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw due to overflow when calculating the effective filter width.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 234545]},
+ options: {
+ dilations: [2, 843452],
+ },
+ },
+ {
+ name: '[conv2d] Throw due to overflow when dilation height is too large.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ options: {
+ dilations: [kMaxUnsignedLong, 1],
+ },
+ },
+ {
+ name: '[conv2d] Throw due to overflow when dilation width is too large.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ options: {
+ dilations: [1, kMaxUnsignedLong],
+ },
+ },
+ {
+ name: '[conv2d] Throw due to underflow when calculating the output height.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 4, 2]},
+ options: {
+ dilations: [4, 1],
+ padding: [1, 1, 1, 1],
+ strides: [2, 2],
+ },
+ },
+ {
+ name: '[conv2d] Throw due to underflow when calculating the output width.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 8]},
+ options: {
+ dilations: [1, 4],
+ padding: [1, 1, 1, 1],
+ strides: [2, 2],
+ },
+ },
+ {
+ name: '[conv2d] Throw if the bias is not a 1-D tensor.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ bias: {dataType: 'float32', dimensions: [1, 2]},
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="oihw".',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ bias: {dataType: 'float32', dimensions: [2]},
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="hwio".',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [2, 2, 1, 1]},
+ options: {
+ bias: {dataType: 'float32', dimensions: [2]},
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="ohwi".',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 2, 1]},
+ options: {
+ bias: {dataType: 'float32', dimensions: [2]},
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="ihwo".',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 2, 1]},
+ options: {
+ bias: {dataType: 'float32', dimensions: [2]},
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw if the bias data type doesn\'t match input data type.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ bias: {dataType: 'int32', dimensions: [1]},
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nchw" and filterLayout="oihw".',
+ input: {dataType: 'float32', dimensions: [1, 2, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]},
+ options: {
+ inputLayout: 'nchw',
+ filterLayout: 'oihw',
+ groups: 2,
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nchw" and filterLayout="hwio".',
+ input: {dataType: 'float32', dimensions: [1, 2, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [3, 3, 2, 1]},
+ options: {
+ inputLayout: 'nchw',
+ filterLayout: 'hwio',
+ groups: 2,
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nchw" and filterLayout="ohwi".',
+ input: {dataType: 'float32', dimensions: [1, 2, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 3, 3, 2]},
+ options: {
+ inputLayout: 'nchw',
+ filterLayout: 'ohwi',
+ groups: 2,
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nchw" and filterLayout="ihwo".',
+ input: {dataType: 'float32', dimensions: [1, 2, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [2, 3, 3, 1]},
+ options: {
+ inputLayout: 'nchw',
+ filterLayout: 'ihwo',
+ groups: 2,
+ },
+
+ },
+ {
+ name:
+ '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nhwc" and filterLayout="oihw".',
+ input: {dataType: 'float32', dimensions: [1, 5, 5, 2]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]},
+ options: {
+ inputLayout: 'nhwc',
+ filterLayout: 'oihw',
+ groups: 2,
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nhwc" and filterLayout="hwio".',
+ input: {dataType: 'float32', dimensions: [1, 5, 5, 2]},
+ filter: {dataType: 'float32', dimensions: [3, 3, 2, 1]},
+ options: {
+ inputLayout: 'nhwc',
+ filterLayout: 'hwio',
+ groups: 2,
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nhwc" and filterLayout="ohwi".',
+ input: {dataType: 'float32', dimensions: [1, 5, 5, 2]},
+ filter: {dataType: 'float32', dimensions: [1, 3, 3, 2]},
+ options: {
+ inputLayout: 'nhwc',
+ filterLayout: 'ohwi',
+ groups: 2,
+ },
+ },
+ {
+ name:
+ '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nhwc" and filterLayout="ihwo".',
+ input: {dataType: 'float32', dimensions: [1, 5, 5, 2]},
+ filter: {dataType: 'float32', dimensions: [2, 3, 3, 1]},
+ options: {
+ inputLayout: 'nhwc',
+ filterLayout: 'ihwo',
+ groups: 2,
+ },
+ },
+];
+
+tests.forEach(
+ test => promise_test(async t => {
+ const input = builder.input(
+ 'input',
+ {dataType: test.input.dataType, dimensions: test.input.dimensions});
+ const filter = builder.input(
+ 'filter',
+ {dataType: test.filter.dataType, dimensions: test.filter.dimensions});
+
+ if (test.options && test.options.bias) {
+ test.options.bias = builder.input('bias', {
+ dataType: test.options.bias.dataType,
+ dimensions: test.options.bias.dimensions
+ });
+ }
+
+ if (test.output) {
+ const output = builder.conv2d(input, filter, test.options);
+ assert_equals(output.dataType(), test.output.dataType);
+ assert_array_equals(output.shape(), test.output.dimensions);
+ } else {
+ assert_throws_js(
+ TypeError, () => builder.conv2d(input, filter, test.options));
+ }
+ }, test.name));
diff --git a/testing/web-platform/tests/webnn/validation_tests/convTranspose2d.https.any.js b/testing/web-platform/tests/webnn/validation_tests/convTranspose2d.https.any.js
index c14f445bf3..02822c5274 100644
--- a/testing/web-platform/tests/webnn/validation_tests/convTranspose2d.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/convTranspose2d.https.any.js
@@ -57,3 +57,473 @@ multi_builder_test(async (t, builder, otherBuilder) => {
assert_throws_js(
TypeError, () => builder.convTranspose2d(input, filter, options));
}, '[convTranspose2d] throw if activation option is from another builder');
+
+const tests = [
+ {
+ name: '[convTranspose2d] Test with default options.',
+ input: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ output: {dataType: 'float32', dimensions: [1, 1, 5, 5]}
+ },
+ {
+ name:
+ '[convTranspose2d] Test with inputLayout="nchw" and filterLayout="hwoi".',
+ input: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [3, 3, 2, 1]},
+ options: {
+ filterLayout: 'hwoi',
+ inputLayout: 'nchw',
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 5, 5]}
+ },
+ {
+ name:
+ '[convTranspose2d] Test with inputLayout="nchw" and filterLayout="ohwi".',
+ input: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [2, 3, 3, 1]},
+ options: {
+ filterLayout: 'ohwi',
+ inputLayout: 'nchw',
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 5, 5]}
+ },
+ {
+ name:
+ '[convTranspose2d] Test with inputLayout="nhwc" and filterLayout="iohw".',
+ input: {dataType: 'float32', dimensions: [1, 3, 3, 1]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]},
+ options: {
+ filterLayout: 'iohw',
+ inputLayout: 'nhwc',
+ },
+ output: {dataType: 'float32', dimensions: [1, 5, 5, 2]}
+ },
+ {
+ name:
+ '[convTranspose2d] Test with inputLayout="nhwc" and filterLayout="hwoi".',
+ input: {dataType: 'float32', dimensions: [1, 3, 3, 1]},
+ filter: {dataType: 'float32', dimensions: [3, 3, 2, 1]},
+ options: {
+ filterLayout: 'hwoi',
+ inputLayout: 'nhwc',
+ },
+ output: {dataType: 'float32', dimensions: [1, 5, 5, 2]}
+ },
+ {
+ name:
+ '[convTranspose2d] Test with inputLayout="nhwc" and filterLayout="ohwi".',
+ input: {dataType: 'float32', dimensions: [1, 3, 3, 1]},
+ filter: {dataType: 'float32', dimensions: [2, 3, 3, 1]},
+ options: {
+ filterLayout: 'ohwi',
+ inputLayout: 'nhwc',
+ },
+ output: {dataType: 'float32', dimensions: [1, 5, 5, 2]}
+ },
+ {
+ name: '[convTranspose2d] Test with strides=[3, 2], outputSizes=[10, 8].',
+ input: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]},
+ options: {
+ strides: [3, 2],
+ outputSizes: [10, 8],
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 10, 8]}
+ },
+ {
+ name: '[convTranspose2d] Test with strides=[3, 2], outputPadding=[1, 1].',
+ input: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]},
+ options: {
+ strides: [3, 2],
+ outputPadding: [1, 1],
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 10, 8]}
+ },
+ {
+ name: '[convTranspose2d] Test with padding=1.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ options: {
+ padding: [1, 1, 1, 1],
+ },
+ output: {dataType: 'float32', dimensions: [1, 1, 5, 5]}
+ },
+ {
+ name: '[convTranspose2d] Test with padding=1, groups=3.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ options: {
+ padding: [1, 1, 1, 1],
+ groups: 3,
+ },
+ output: {dataType: 'float32', dimensions: [1, 3, 5, 5]}
+ },
+ {
+ name: '[convTranspose2d] Test with strides=2.',
+ input: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]},
+ options: {
+ strides: [2, 2],
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 7, 7]}
+ },
+ {
+ name: '[convTranspose2d] Test with strides=2 and padding=1.',
+ input: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ options: {
+ padding: [1, 1, 1, 1],
+ strides: [2, 2],
+ },
+ output: {dataType: 'float32', dimensions: [1, 1, 5, 5]}
+ },
+ {
+ name:
+ '[convTranspose2d] Test when the output sizes are explicitly specified, the output padding values are ignored though padding value is not smaller than stride along the same axis.',
+ input: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]},
+ options: {
+ outputPadding: [3, 3],
+ strides: [3, 2],
+ outputSizes: [10, 8],
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 10, 8]}
+ },
+ {
+ name: '[convTranspose2d] Throw if the input is not a 4-D tensor.',
+ input: {dataType: 'float32', dimensions: [1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the input data type is not floating point.',
+ input: {dataType: 'int32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'int32', dimensions: [1, 1, 2, 2]},
+ },
+ {
+ name: '[convTranspose2d] Throw if the filter is not a 4-D tensor.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [2, 2]},
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the filter data type doesn\'t match the input data type.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'int32', dimensions: [1, 1, 2, 2]},
+ },
+ {
+ name: '[convTranspose2d] Throw if the length of padding is not 4.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ padding: [2, 2],
+ },
+ },
+ {
+ name: '[convTranspose2d] Throw if the length of strides is not 2.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ strides: [2],
+ },
+ },
+ {
+ name: '[convTranspose2d] Throw if one stride value is smaller than 1.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ strides: [1, 0],
+ },
+ },
+ {
+ name: '[convTranspose2d] Throw if the length of dilations is not 2.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ dilations: [1],
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the one dilation value is smaller than 1.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ dilations: [1, 0],
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the input channels is not equal to the filter input channels with inputLayout="nchw" and filterLayout="iohw".',
+ input: {dataType: 'float32', dimensions: [1, 3, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ options: {
+ filterLayout: 'iohw',
+ inputLayout: 'nchw',
+ groups: 1,
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the input channels is not equal to the filter input channels with inputLayout="nchw" and filterLayout="hwoi".',
+ input: {dataType: 'float32', dimensions: [1, 3, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [3, 1, 2, 1]},
+ options: {
+ filterLayout: 'hwoi',
+ inputLayout: 'nchw',
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the input channels is not equal to the filter input channels with inputLayout="nchw" and filterLayout="ohwi".',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [2, 3, 3, 1]},
+ options: {
+ filterLayout: 'ohwi',
+ inputLayout: 'nchw',
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the input channels is not equal to the filter input channels with inputLayout="nhwc" and filterLayout="iohw".',
+ input: {dataType: 'float32', dimensions: [1, 3, 3, 2]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]},
+ options: {
+ filterLayout: 'iohw',
+ inputLayout: 'nhwc',
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the input channels is not equal to the filter input channels inputLayout="nhwc" and filterLayout="hwoi".',
+ input: {dataType: 'float32', dimensions: [1, 3, 3, 2]},
+ filter: {dataType: 'float32', dimensions: [3, 3, 2, 1]},
+ options: {
+ filterLayout: 'hwoi',
+ inputLayout: 'nhwc',
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the input channels is not equal to the filter input channels with inputLayout="nhwc" and filterLayout="ohwi".',
+ input: {dataType: 'float32', dimensions: [1, 3, 3, 2]},
+ filter: {dataType: 'float32', dimensions: [2, 3, 3, 1]},
+ options: {
+ filterLayout: 'ohwi',
+ inputLayout: 'nhwc',
+ },
+ },
+ {
+ name: '[convTranspose2d] Throw if output channels is too large.',
+ input: {dataType: 'float32', dimensions: [1, 4, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [4, 2, 2, 2]},
+ options: {
+ groups: kMaxUnsignedLong,
+ },
+ },
+ {
+ name: '[convTranspose2d] Throw if the groups is smaller than 1.',
+ input: {dataType: 'float32', dimensions: [1, 4, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ groups: 0,
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw due to overflow when calculating the effective filter height.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 434983, 2]},
+ options: {
+ dilations: [328443, 1],
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw due to overflow when calculating the effective filter width.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 234545]},
+ options: {
+ dilations: [2, 843452],
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw due to overflow when dilation height is too large.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 2]},
+ options: {
+ dilations: [kMaxUnsignedLong, 1],
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw due to overflow when dilation width is too large.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 2]},
+ options: {
+ dilations: [1, kMaxUnsignedLong],
+ },
+ },
+ {
+ name: '[convTranspose2d] Throw if the bias is not a 1-D tensor.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ bias: {dataType: 'float32', dimensions: [1, 2]},
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="iohw".',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ filterLayout: 'iohw',
+ bias: {dataType: 'float32', dimensions: [2]},
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="hwoi".',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [2, 2, 1, 1]},
+ options: {
+ filterLayout: 'hwoi',
+ bias: {dataType: 'float32', dimensions: [2]},
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="ohwi".',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 2, 1]},
+ options: {
+ filterLayout: 'ohwi',
+ bias: {dataType: 'float32', dimensions: [2]},
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the bias data type doesn\'t match input data type.',
+ input: {dataType: 'float32', dimensions: [1, 1, 5, 5]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ options: {
+ bias: {dataType: 'int32', dimensions: [1]},
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the outputPadding is not a sequence of length 2.',
+ input: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]},
+ options: {
+ strides: [3, 2],
+ outputPadding: [1, 1, 1, 1],
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the outputPadding is not smaller than stride along the width dimension.',
+ input: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ options: {
+ padding: [0, 0, 3, 3],
+ strides: [2, 2],
+ outputPadding: [0, 2],
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the outputPadding is not smaller than stride along the height dimension.',
+ input: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ options: {
+ padding: [0, 0, 3, 3],
+ strides: [2, 2],
+ outputPadding: [2, 0],
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw if the outputSizes is not a sequence of length 2.',
+ input: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]},
+ options: {
+ strides: [3, 2],
+ outputSizes: [1, 2, 10, 8],
+ },
+ },
+ {
+ name: '[convTranspose2d] Throw if the padding height is too large.',
+ input: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ options: {
+ padding: [4, 4, 0, 0],
+ strides: [2, 2],
+ outputPadding: [1, 0],
+ },
+ },
+ {
+ name: '[convTranspose2d] Throw if the padding width is too large.',
+ input: {dataType: 'float32', dimensions: [1, 1, 2, 2]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ options: {
+ padding: [0, 0, 4, 4],
+ strides: [2, 2],
+ outputPadding: [0, 1],
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw due to outputSizes values are smaller than the output sizes calculated by not using outputPadding.',
+ input: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ options: {
+ padding: [1, 1, 1, 1],
+ strides: [2, 2],
+ outputSizes: [4, 4],
+ outputPadding: [1, 1],
+ },
+ },
+ {
+ name:
+ '[convTranspose2d] Throw due to outputSizes values are greater than the output sizes calculated by not using outputPadding.',
+ input: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]},
+ options: {
+ padding: [1, 1, 1, 1],
+ strides: [2, 2],
+ outputSizes: [6, 8],
+ outputPadding: [1, 1],
+ },
+ },
+];
+
+tests.forEach(
+ test => promise_test(async t => {
+ const input = builder.input(
+ 'input',
+ {dataType: test.input.dataType, dimensions: test.input.dimensions});
+ const filter = builder.input(
+ 'filter',
+ {dataType: test.filter.dataType, dimensions: test.filter.dimensions});
+
+ if (test.options && test.options.bias) {
+ test.options.bias = builder.input('bias', {
+ dataType: test.options.bias.dataType,
+ dimensions: test.options.bias.dimensions
+ });
+ }
+
+ if (test.output) {
+ const output = builder.convTranspose2d(input, filter, test.options);
+ assert_equals(output.dataType(), test.output.dataType);
+ assert_array_equals(output.shape(), test.output.dimensions);
+ } else {
+ assert_throws_js(
+ TypeError,
+ () => builder.convTranspose2d(input, filter, test.options));
+ }
+ }, test.name));
diff --git a/testing/web-platform/tests/webnn/validation_tests/elu.https.any.js b/testing/web-platform/tests/webnn/validation_tests/elu.https.any.js
index 6e842cb691..53ec5e54ae 100644
--- a/testing/web-platform/tests/webnn/validation_tests/elu.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/elu.https.any.js
@@ -5,3 +5,43 @@
'use strict';
validateInputFromAnotherBuilder('elu');
+
+validateUnaryOperation(
+ 'elu', floatingPointTypes, /*alsoBuildActivation=*/ true);
+
+promise_test(async t => {
+ const options = {alpha: 1.0};
+ const input =
+ builder.input('input', {dataType: 'float32', dimensions: [1, 2, 3]});
+ const output = builder.elu(input, options);
+ assert_equals(output.dataType(), 'float32');
+ assert_array_equals(output.shape(), [1, 2, 3]);
+}, '[elu] Test building an operator with options');
+
+promise_test(async t => {
+ const options = {alpha: 1.5};
+ builder.elu(options);
+}, '[elu] Test building an activation with options');
+
+promise_test(async t => {
+ const options = {alpha: -1.0};
+ const input =
+ builder.input('input', {dataType: 'float32', dimensions: [1, 2, 3]});
+ assert_throws_js(TypeError, () => builder.elu(input, options));
+}, '[elu] Throw if options.alpha <= 0 when building an operator');
+
+promise_test(async t => {
+ const options = {alpha: NaN};
+ const input = builder.input('input', {dataType: 'float16', dimensions: []});
+ assert_throws_js(TypeError, () => builder.elu(input, options));
+}, '[elu] Throw if options.alpha is NaN when building an operator');
+
+promise_test(async t => {
+ const options = {alpha: 0};
+ assert_throws_js(TypeError, () => builder.elu(options));
+}, '[elu] Throw if options.alpha <= 0 when building an activation');
+
+promise_test(async t => {
+ const options = {alpha: Infinity};
+ assert_throws_js(TypeError, () => builder.elu(options));
+}, '[elu] Throw if options.alpha is Infinity when building an activation');
diff --git a/testing/web-platform/tests/webnn/validation_tests/expand.https.any.js b/testing/web-platform/tests/webnn/validation_tests/expand.https.any.js
index d90ab89468..088d826df7 100644
--- a/testing/web-platform/tests/webnn/validation_tests/expand.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/expand.https.any.js
@@ -12,3 +12,66 @@ multi_builder_test(async (t, builder, otherBuilder) => {
assert_throws_js(
TypeError, () => builder.expand(inputFromOtherBuilder, newShape));
}, '[expand] throw if input is from another builder');
+
+const tests = [
+ {
+ name: '[expand] Test with 0-D scalar to 3-D tensor.',
+ input: {dataType: 'float32', dimensions: []},
+ newShape: [3, 4, 5],
+ output: {dataType: 'float32', dimensions: [3, 4, 5]}
+ },
+ {
+ name: '[expand] Test with the new shapes that are the same as input.',
+ input: {dataType: 'float32', dimensions: [4]},
+ newShape: [4],
+ output: {dataType: 'float32', dimensions: [4]}
+ },
+ {
+ name: '[expand] Test with the new shapes that are broadcastable.',
+ input: {dataType: 'int32', dimensions: [3, 1, 5]},
+ newShape: [3, 4, 5],
+ output: {dataType: 'int32', dimensions: [3, 4, 5]}
+ },
+ {
+ name:
+ '[expand] Test with the new shapes that are broadcastable and the rank of new shapes is larger than input.',
+ input: {dataType: 'int32', dimensions: [2, 5]},
+ newShape: [3, 2, 5],
+ output: {dataType: 'int32', dimensions: [3, 2, 5]}
+ },
+ {
+ name:
+ '[expand] Throw if the input shapes are the same rank but not broadcastable.',
+ input: {dataType: 'uint32', dimensions: [3, 6, 2]},
+ newShape: [4, 3, 5],
+ },
+ {
+ name: '[expand] Throw if the input shapes are not broadcastable.',
+ input: {dataType: 'uint32', dimensions: [5, 4]},
+ newShape: [5],
+ },
+ {
+ name: '[expand] Throw if the number of new shapes is too large.',
+ input: {dataType: 'float32', dimensions: [1, 2, 1, 1]},
+ newShape: [1, 2, kMaxUnsignedLong, kMaxUnsignedLong],
+ },
+];
+
+tests.forEach(
+ test => promise_test(async t => {
+ const input = builder.input(
+ 'input',
+ {dataType: test.input.dataType, dimensions: test.input.dimensions});
+ const options = {};
+ if (test.axis) {
+ options.axis = test.axis;
+ }
+
+ if (test.output) {
+ const output = builder.expand(input, test.newShape);
+ assert_equals(output.dataType(), test.output.dataType);
+ assert_array_equals(output.shape(), test.output.dimensions);
+ } else {
+ assert_throws_js(TypeError, () => builder.expand(input, test.newShape));
+ }
+ }, test.name));
diff --git a/testing/web-platform/tests/webnn/validation_tests/gelu.https.any.js b/testing/web-platform/tests/webnn/validation_tests/gelu.https.any.js
new file mode 100644
index 0000000000..c758c61f4c
--- /dev/null
+++ b/testing/web-platform/tests/webnn/validation_tests/gelu.https.any.js
@@ -0,0 +1,10 @@
+// META: title=validation tests for WebNN API gelu operation
+// META: global=window,dedicatedworker
+// META: script=../resources/utils_validation.js
+
+'use strict';
+
+validateInputFromAnotherBuilder('gelu');
+
+validateUnaryOperation(
+ 'gelu', floatingPointTypes, /*alsoBuildActivation=*/ true);
diff --git a/testing/web-platform/tests/webnn/validation_tests/gemm.https.any.js b/testing/web-platform/tests/webnn/validation_tests/gemm.https.any.js
index 77ce6383cc..abe0ba6193 100644
--- a/testing/web-platform/tests/webnn/validation_tests/gemm.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/gemm.https.any.js
@@ -19,3 +19,143 @@ multi_builder_test(async (t, builder, otherBuilder) => {
const b = builder.input('b', kExampleInputDescriptor);
assert_throws_js(TypeError, () => builder.gemm(a, b, options));
}, '[gemm] throw if c option is from another builder');
+
+const tests = [
+ {
+ name: '[gemm] Test building gemm with default option.',
+ a: {dataType: 'float32', dimensions: [2, 3]},
+ b: {dataType: 'float32', dimensions: [3, 4]},
+ output: {dataType: 'float32', dimensions: [2, 4]}
+ },
+ {
+ name:
+ '[gemm] Throw if inputShapeA[1] is not equal to inputShapeB[0] default options.',
+ a: {dataType: 'float32', dimensions: [2, 3]},
+ b: {dataType: 'float32', dimensions: [2, 4]},
+ },
+ {
+ name: '[gemm] Test building gemm with aTranspose=true.',
+ a: {dataType: 'float32', dimensions: [2, 3]},
+ b: {dataType: 'float32', dimensions: [2, 4]},
+ options: {
+ aTranspose: true,
+ },
+ output: {dataType: 'float32', dimensions: [3, 4]}
+ },
+ {
+ name:
+ '[gemm] Throw if inputShapeA[0] is not equal to inputShapeB[0] with aTranspose=true.',
+ a: {dataType: 'float32', dimensions: [2, 3]},
+ b: {dataType: 'float32', dimensions: [3, 4]},
+ options: {
+ aTranspose: true,
+ },
+ },
+ {
+ name: '[gemm] Test building gemm with bTranspose=true.',
+ a: {dataType: 'float32', dimensions: [2, 3]},
+ b: {dataType: 'float32', dimensions: [4, 3]},
+ options: {
+ bTranspose: true,
+ },
+ output: {dataType: 'float32', dimensions: [2, 4]}
+ },
+ {
+ name:
+ '[gemm] Throw if inputShapeA[0] is not equal to inputShapeB[0] with bTranspose=true.',
+ a: {dataType: 'float32', dimensions: [2, 3]},
+ b: {dataType: 'float32', dimensions: [3, 4]},
+ options: {
+ bTranspose: true,
+ },
+ },
+ {
+ name: '[gemm] Throw if the rank of inputA is not 2.',
+ a: {dataType: 'float32', dimensions: [2, 3, 1]},
+ b: {dataType: 'float32', dimensions: [2, 4]},
+ },
+ {
+ name: '[gemm] Throw if the rank of inputB is not 2.',
+ a: {dataType: 'float32', dimensions: [2, 4]},
+ b: {dataType: 'float32', dimensions: [2, 3, 1]},
+ },
+ {
+ name: '[gemm] Throw if data types of two inputs do not match.',
+ a: {dataType: 'float32', dimensions: [2, 3]},
+ b: {dataType: 'float16', dimensions: [3, 4]},
+ },
+ {
+ name: '[gemm] Test building gemm with inputC.',
+ a: {dataType: 'float32', dimensions: [2, 3]},
+ b: {dataType: 'float32', dimensions: [3, 4]},
+ options: {
+ c: {dataType: 'float32', dimensions: [4]},
+ },
+ output: {dataType: 'float32', dimensions: [2, 4]}
+ },
+ {
+ name: '[gemm] Test building gemm with scalar inputC.',
+ a: {dataType: 'float32', dimensions: [2, 3]},
+ b: {dataType: 'float32', dimensions: [3, 4]},
+ options: {
+ c: {dataType: 'float32', dimensions: []},
+ },
+ output: {dataType: 'float32', dimensions: [2, 4]}
+ },
+ {
+ name:
+ '[gemm] Throw if inputShapeC is not unidirectionally broadcastable to the output shape [inputShapeA[0], inputShapeB[1]].',
+ a: {dataType: 'float32', dimensions: [2, 3]},
+ b: {dataType: 'float32', dimensions: [3, 4]},
+ options: {
+ c: {dataType: 'float32', dimensions: [2, 3]},
+ },
+ },
+ {
+ name: '[gemm] Throw if the input data type is not floating point.',
+ a: {dataType: 'int32', dimensions: [2, 3]},
+ b: {dataType: 'int32', dimensions: [3, 4]}
+ },
+ {
+ name:
+ '[gemm] Throw if data type of inputC does not match ones of inputA and inputB.',
+ a: {dataType: 'float32', dimensions: [3, 2]},
+ b: {dataType: 'float32', dimensions: [4, 3]},
+ options: {
+ c: {dataType: 'float16', dimensions: [2, 4]},
+ aTranspose: true,
+ bTranspose: true,
+ },
+ },
+ {
+ name: '[gemm] Throw if the rank of inputC is 3.',
+ a: {dataType: 'float32', dimensions: [3, 2]},
+ b: {dataType: 'float32', dimensions: [4, 3]},
+ options: {
+ c: {dataType: 'float32', dimensions: [2, 3, 4]},
+ aTranspose: true,
+ bTranspose: true,
+ },
+ },
+];
+
+tests.forEach(
+ test => promise_test(async t => {
+ const a = builder.input(
+ 'a', {dataType: test.a.dataType, dimensions: test.a.dimensions});
+ const b = builder.input(
+ 'b', {dataType: test.b.dataType, dimensions: test.b.dimensions});
+ if (test.options && test.options.c) {
+ test.options.c = builder.input('c', {
+ dataType: test.options.c.dataType,
+ dimensions: test.options.c.dimensions
+ });
+ }
+ if (test.output) {
+ const output = builder.gemm(a, b, test.options);
+ assert_equals(output.dataType(), test.output.dataType);
+ assert_array_equals(output.shape(), test.output.dimensions);
+ } else {
+ assert_throws_js(TypeError, () => builder.gemm(a, b, test.options));
+ }
+ }, test.name));
diff --git a/testing/web-platform/tests/webnn/validation_tests/hardSigmoid.https.any.js b/testing/web-platform/tests/webnn/validation_tests/hardSigmoid.https.any.js
index 01b24dbc7c..2c55d0eb9d 100644
--- a/testing/web-platform/tests/webnn/validation_tests/hardSigmoid.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/hardSigmoid.https.any.js
@@ -5,3 +5,31 @@
'use strict';
validateInputFromAnotherBuilder('hardSigmoid');
+
+validateUnaryOperation(
+ 'hardSigmoid', floatingPointTypes, /*alsoBuildActivation=*/ true);
+
+promise_test(async t => {
+ const options = {alpha: 0.5, beta: 1.0};
+ const input =
+ builder.input('input', {dataType: 'float16', dimensions: [1, 2, 3]});
+ const output = builder.hardSigmoid(input, options);
+ assert_equals(output.dataType(), 'float16');
+ assert_array_equals(output.shape(), [1, 2, 3]);
+}, '[hardSigmoid] Test building an operator with options');
+
+promise_test(async t => {
+ const options = {alpha: 0.2};
+ builder.hardSigmoid(options);
+}, '[hardSigmoid] Test building an activation with options');
+
+promise_test(async t => {
+ const options = {beta: NaN};
+ const input = builder.input('input', {dataType: 'float32', dimensions: []});
+ assert_throws_js(TypeError, () => builder.hardSigmoid(input, options));
+}, '[hardSigmoid] Throw if options.beta is NaN when building an operator');
+
+promise_test(async t => {
+ const options = {alpha: Infinity};
+ assert_throws_js(TypeError, () => builder.hardSigmoid(options));
+}, '[hardSigmoid] Throw if options.alpha is Infinity when building an activation');
diff --git a/testing/web-platform/tests/webnn/validation_tests/instanceNormalization.https.any.js b/testing/web-platform/tests/webnn/validation_tests/instanceNormalization.https.any.js
index bdd338588f..4fc26ec5ae 100644
--- a/testing/web-platform/tests/webnn/validation_tests/instanceNormalization.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/instanceNormalization.https.any.js
@@ -41,3 +41,152 @@ multi_builder_test(async (t, builder, otherBuilder) => {
assert_throws_js(
TypeError, () => builder.instanceNormalization(input, options));
}, '[instanceNormalization] throw if bias option is from another builder');
+
+const tests = [
+ {
+ name: '[instanceNormalization] Test with default options for 4-D input.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ output: {dataType: 'float32', dimensions: [1, 2, 3, 4]}
+ },
+ {
+ name:
+ '[instanceNormalization] Test with scale, bias and default epsilon value.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {
+ scale: {dataType: 'float32', dimensions: [2]},
+ bias: {dataType: 'float32', dimensions: [2]},
+ epsilon: 1e-5,
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 3, 4]}
+ },
+ {
+ name: '[instanceNormalization] Test with a non-default epsilon value.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {
+ epsilon: 1e-4,
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 3, 4]}
+ },
+ {
+ name: '[instanceNormalization] Test with layout=nhwc.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {
+ layout: 'nhwc',
+ scale: {dataType: 'float32', dimensions: [4]},
+ bias: {dataType: 'float32', dimensions: [4]},
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 3, 4]}
+ },
+ {
+ name: '[instanceNormalization] Test when the input data type is float16.',
+ input: {dataType: 'float16', dimensions: [1, 2, 3, 4]},
+ output: {dataType: 'float16', dimensions: [1, 2, 3, 4]}
+ },
+ {
+ name: '[instanceNormalization] Throw if the input is not a 4-D tensor.',
+ input: {dataType: 'float32', dimensions: [1, 2, 5, 5, 2]},
+ },
+ {
+ name:
+ '[instanceNormalization] Throw if the input data type is not one of floating point types.',
+ input: {dataType: 'int32', dimensions: [1, 2, 5, 5]},
+ },
+ {
+ name:
+ '[instanceNormalization] Throw if the scale data type is not the same as the input data type.',
+ input: {dataType: 'float16', dimensions: [1, 2, 5, 5]},
+ options: {
+ scale: {dataType: 'float32', dimensions: [2]},
+ },
+ },
+ {
+ name:
+ '[instanceNormalization] Throw if the scale operand is not a 1-D tensor.',
+ input: {dataType: 'float32', dimensions: [1, 2, 5, 5]},
+ options: {
+ scale: {dataType: 'float32', dimensions: [2, 1]},
+ },
+ },
+ {
+ name:
+ '[instanceNormalization] Throw if the size of scale operand is not equal to the size of the feature dimension of the input with layout=nhwc.',
+ input: {dataType: 'float32', dimensions: [1, 2, 5, 5]},
+ options: {
+ layout: 'nhwc',
+ scale: {dataType: 'float32', dimensions: [2]},
+ },
+ },
+ {
+ name:
+ '[instanceNormalization] Throw if the size of scale operand is not equal to the size of the feature dimension of the input with layout=nchw.',
+ input: {dataType: 'float32', dimensions: [1, 5, 5, 2]},
+ options: {
+ layout: 'nchw',
+ scale: {dataType: 'float32', dimensions: [2]},
+ },
+ },
+ {
+ name:
+ '[instanceNormalization] Throw if the bias data type is not the same as the input data type.',
+ input: {dataType: 'float16', dimensions: [1, 2, 5, 5]},
+ options: {
+ bias: {dataType: 'float32', dimensions: [2]},
+ },
+ },
+ {
+ name:
+ '[instanceNormalization] Throw if the bias operand is not a 1-D tensor.',
+ input: {dataType: 'float32', dimensions: [1, 2, 5, 5]},
+ options: {
+ scale: {dataType: 'float32', dimensions: [2, 1]},
+ },
+ },
+ {
+ name:
+ '[instanceNormalization] Throw if the size of bias operand is not equal to the size of the feature dimension of the input with layout=nhwc.',
+ input: {dataType: 'float32', dimensions: [1, 2, 5, 5]},
+ options: {
+ layout: 'nhwc',
+ bias: {dataType: 'float32', dimensions: [2]},
+ },
+ },
+ {
+ name:
+ '[instanceNormalization] Throw if the size of bias operand is not equal to the size of the feature dimension of the input with layout=nchw.',
+ input: {dataType: 'float32', dimensions: [1, 5, 5, 2]},
+ options: {
+ layout: 'nchw',
+ bias: {dataType: 'float32', dimensions: [2]},
+ },
+ },
+];
+
+tests.forEach(
+ test => promise_test(async t => {
+ const input = builder.input(
+ 'input',
+ {dataType: test.input.dataType, dimensions: test.input.dimensions});
+
+ if (test.options && test.options.bias) {
+ test.options.bias = builder.input('bias', {
+ dataType: test.options.bias.dataType,
+ dimensions: test.options.bias.dimensions
+ });
+ }
+ if (test.options && test.options.scale) {
+ test.options.scale = builder.input('scale', {
+ dataType: test.options.scale.dataType,
+ dimensions: test.options.scale.dimensions
+ });
+ }
+
+ if (test.output) {
+ const output = builder.instanceNormalization(input, test.options);
+ assert_equals(output.dataType(), test.output.dataType);
+ assert_array_equals(output.shape(), test.output.dimensions);
+ } else {
+ assert_throws_js(
+ TypeError,
+ () => builder.instanceNormalization(input, test.options));
+ }
+ }, test.name));
diff --git a/testing/web-platform/tests/webnn/validation_tests/layerNormalization.https.any.js b/testing/web-platform/tests/webnn/validation_tests/layerNormalization.https.any.js
index e9e9141aa6..63f9c0dbc5 100644
--- a/testing/web-platform/tests/webnn/validation_tests/layerNormalization.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/layerNormalization.https.any.js
@@ -9,8 +9,6 @@ const kExampleInputDescriptor = {
dimensions: [2, 2]
};
-validateOptionsAxes('layerNormalization', 4);
-
validateInputFromAnotherBuilder('layerNormalization');
multi_builder_test(async (t, builder, otherBuilder) => {
@@ -30,3 +28,181 @@ multi_builder_test(async (t, builder, otherBuilder) => {
const input = builder.input('input', kExampleInputDescriptor);
assert_throws_js(TypeError, () => builder.layerNormalization(input, options));
}, '[layerNormalization] throw if bias option is from another builder');
+
+const tests = [
+ {
+ name: '[layerNormalization] Test with default options for scalar input.',
+ input: {dataType: 'float32', dimensions: []},
+ output: {dataType: 'float32', dimensions: []},
+ },
+ {
+ name: '[layerNormalization] Test when the input data type is float16.',
+ input: {dataType: 'float16', dimensions: []},
+ output: {dataType: 'float16', dimensions: []},
+ },
+ {
+ name: '[layerNormalization] Test with given axes.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {
+ axes: [3],
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ },
+ {
+ name: '[layerNormalization] Test with given scale.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {
+ scale: {dataType: 'float32', dimensions: [2, 3, 4]},
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ },
+ {
+ name: '[layerNormalization] Test with a non-default epsilon value.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {
+ epsilon: 1e-4, // default epsilon=1e-5
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ },
+ {
+ name: '[layerNormalization] Test with given axes, scale and bias.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {
+ scale: {dataType: 'float32', dimensions: [3, 4]},
+ bias: {dataType: 'float32', dimensions: [3, 4]},
+ axes: [2, 3],
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ },
+ {
+ name: '[layerNormalization] Test with nonconsecutive axes.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4, 5, 6]},
+ options: {
+ scale: {dataType: 'float32', dimensions: [2, 4, 6]},
+ bias: {dataType: 'float32', dimensions: [2, 4, 6]},
+ axes: [1, 3, 5],
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 3, 4, 5, 6]},
+ },
+ {
+ name: '[layerNormalization] Test with axes in descending order.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4, 5, 6]},
+ options: {
+ scale: {dataType: 'float32', dimensions: [6, 5, 4, 3, 2]},
+ bias: {dataType: 'float32', dimensions: [6, 5, 4, 3, 2]},
+ axes: [5, 4, 3, 2, 1]
+ },
+ output: {dataType: 'float32', dimensions: [1, 2, 3, 4, 5, 6]},
+ },
+ {
+ name:
+ '[layerNormalization] Throw if the input data type is not one of the floating point types.',
+ input: {dataType: 'uint32', dimensions: [1, 2, 3, 4]},
+ },
+ {
+ name:
+ '[layerNormalization] Throw if the axis is greater than the input rank.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {
+ axes: [1, 2, 4],
+ },
+ },
+ {
+ name: '[layerNormalization] Throw if the axes have duplications.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {axes: [3, 3]},
+ },
+ {
+ name:
+ '[layerNormalization] Throw if the bias data type doesn\'t match input data type',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {
+ scale: {dataType: 'float32', dimensions: [3, 4]},
+ bias: {dataType: 'float16', dimensions: [3, 4]},
+ axes: [2, 3],
+ },
+ },
+ {
+ name:
+ '[layerNormalization] Throw if the scale data type doesn\'t match input data type',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {
+ scale: {dataType: 'float16', dimensions: [3, 4]},
+ bias: {dataType: 'float32', dimensions: [3, 4]},
+ axes: [2, 3],
+ },
+ },
+ {
+ name:
+ '[layerNormalization] Throw if the bias dimensions doesn\'t match axis dimensions.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {
+ bias: {
+ dataType: 'float32',
+ dimensions: [3, 3, 4]
+ }, // for 4D input, default axes = [1,2,3]
+ },
+ },
+ {
+ name:
+ '[layerNormalization] Throw if the scale dimensions doesn\'t match axis dimensions.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {
+ scale: {
+ dataType: 'float32',
+ dimensions: [3, 3, 4]
+ }, // for 4D input, default axes = [1,2,3]
+ },
+ },
+ {
+ name:
+ '[layerNormalization] Throw if the bias rank doesn\'t match axis rank.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {
+ bias: {
+ dataType: 'float32',
+ dimensions: [1, 2, 3, 4]
+ }, // for 4D input, default axes = [1,2,3]
+ },
+ },
+ {
+ name:
+ '[layerNormalization] Throw if the scale rank doesn\'t match axis rank.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {
+ scale: {
+ dataType: 'float32',
+ dimensions: [1, 2, 3, 4]
+ }, // for 4D input, default axes = [1,2,3]
+ },
+ },
+];
+
+tests.forEach(
+ test => promise_test(async t => {
+ const input = builder.input(
+ 'input',
+ {dataType: test.input.dataType, dimensions: test.input.dimensions});
+
+ if (test.options && test.options.bias) {
+ test.options.bias = builder.input('bias', {
+ dataType: test.options.bias.dataType,
+ dimensions: test.options.bias.dimensions
+ });
+ }
+ if (test.options && test.options.scale) {
+ test.options.scale = builder.input('scale', {
+ dataType: test.options.scale.dataType,
+ dimensions: test.options.scale.dimensions
+ });
+ }
+
+ if (test.output) {
+ const output = builder.layerNormalization(input, test.options);
+ assert_equals(output.dataType(), test.output.dataType);
+ assert_array_equals(output.shape(), test.output.dimensions);
+ } else {
+ assert_throws_js(
+ TypeError, () => builder.layerNormalization(input, test.options));
+ }
+ }, test.name));
diff --git a/testing/web-platform/tests/webnn/validation_tests/leakyRelu.https.any.js b/testing/web-platform/tests/webnn/validation_tests/leakyRelu.https.any.js
index 6fc19b1f0d..f250b0eda6 100644
--- a/testing/web-platform/tests/webnn/validation_tests/leakyRelu.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/leakyRelu.https.any.js
@@ -5,3 +5,31 @@
'use strict';
validateInputFromAnotherBuilder('leakyRelu');
+
+validateUnaryOperation(
+ 'leakyRelu', floatingPointTypes, /*alsoBuildActivation=*/ true);
+
+promise_test(async t => {
+ const options = {alpha: 0.02};
+ const input =
+ builder.input('input', {dataType: 'float32', dimensions: [1, 2, 3]});
+ const output = builder.leakyRelu(input, options);
+ assert_equals(output.dataType(), 'float32');
+ assert_array_equals(output.shape(), [1, 2, 3]);
+}, '[leakyRelu] Test building an operator with options');
+
+promise_test(async t => {
+ const options = {alpha: 0.03};
+ builder.leakyRelu(options);
+}, '[leakyRelu] Test building an activation with options');
+
+promise_test(async t => {
+ const options = {alpha: Infinity};
+ const input = builder.input('input', {dataType: 'float16', dimensions: []});
+ assert_throws_js(TypeError, () => builder.leakyRelu(input, options));
+}, '[leakyRelu] Throw if options.alpha is Infinity when building an operator');
+
+promise_test(async t => {
+ const options = {alpha: -NaN};
+ assert_throws_js(TypeError, () => builder.leakyRelu(options));
+}, '[leakyRelu] Throw if options.alpha is -NaN when building an activation');
diff --git a/testing/web-platform/tests/webnn/validation_tests/linear.https.any.js b/testing/web-platform/tests/webnn/validation_tests/linear.https.any.js
index 99c1daad3f..6ec0389fc3 100644
--- a/testing/web-platform/tests/webnn/validation_tests/linear.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/linear.https.any.js
@@ -5,3 +5,31 @@
'use strict';
validateInputFromAnotherBuilder('linear');
+
+validateUnaryOperation(
+ 'linear', floatingPointTypes, /*alsoBuildActivation=*/ true);
+
+promise_test(async t => {
+ const options = {alpha: 1.5, beta: 0.3};
+ const input =
+ builder.input('input', {dataType: 'float32', dimensions: [1, 2, 3]});
+ const output = builder.linear(input, options);
+ assert_equals(output.dataType(), 'float32');
+ assert_array_equals(output.shape(), [1, 2, 3]);
+}, '[linear] Test building an operator with options');
+
+promise_test(async t => {
+ const options = {beta: 1.5};
+ builder.linear(options);
+}, '[linear] Test building an activation with options');
+
+promise_test(async t => {
+ const options = {beta: -Infinity};
+ const input = builder.input('input', {dataType: 'float16', dimensions: []});
+ assert_throws_js(TypeError, () => builder.linear(input, options));
+}, '[linear] Throw if options.beta is -Infinity when building an operator');
+
+promise_test(async t => {
+ const options = {alpha: NaN};
+ assert_throws_js(TypeError, () => builder.linear(options));
+}, '[linear] Throw if options.alpha is NaN when building an activation');
diff --git a/testing/web-platform/tests/webnn/validation_tests/matmul.https.any.js b/testing/web-platform/tests/webnn/validation_tests/matmul.https.any.js
index 03616ddb01..8db16242c9 100644
--- a/testing/web-platform/tests/webnn/validation_tests/matmul.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/matmul.https.any.js
@@ -5,3 +5,116 @@
'use strict';
validateTwoInputsFromMultipleBuilders('matmul');
+
+const tests = [
+ {
+ name: '[matmul] Throw if first input\'s rank is less than 2',
+ inputs: {
+ a: {dataType: 'float32', dimensions: [2]},
+ b: {dataType: 'float32', dimensions: [2, 2]}
+ }
+ },
+ {
+ name: '[matmul] Throw if second input\'s rank is less than 2',
+ inputs: {
+ a: {dataType: 'float32', dimensions: [2, 2]},
+ b: {dataType: 'float32', dimensions: [2]}
+ }
+ },
+ {
+ name: '[matmul] Test with 2-D input and 4-D input',
+ inputs: {
+ a: {dataType: 'float32', dimensions: [1, 4]},
+ b: {dataType: 'float32', dimensions: [2, 2, 4, 2]}
+ },
+ output: {dataType: 'float32', dimensions: [2, 2, 1, 2]}
+ },
+ {
+ name: '[matmul] Test with 2-D input and 2-D input',
+ inputs: {
+ a: {dataType: 'float32', dimensions: [4, 2]},
+ b: {dataType: 'float32', dimensions: [2, 3]}
+ },
+ output: {dataType: 'float32', dimensions: [4, 3]}
+ },
+ {
+ // batchShape is a clone of inputShape with the spatial dimensions
+ // (last 2 items) removed.
+ name:
+ '[matmul] Test with 3-D input and 3-D input of broadcastable batchShape',
+ inputs: {
+ a: {dataType: 'float32', dimensions: [2, 3, 4]},
+ b: {dataType: 'float32', dimensions: [1, 4, 1]}
+ },
+ output: {dataType: 'float32', dimensions: [2, 3, 1]}
+ },
+ {
+ // batchShape is a clone of inputShape with the spatial dimensions
+ // (last 2 items) removed.
+ name:
+ '[matmul] Test with 4-D input and 3-D input of broadcastable batchShape',
+ inputs: {
+ a: {dataType: 'float32', dimensions: [2, 2, 3, 4]},
+ b: {dataType: 'float32', dimensions: [1, 4, 5]}
+ },
+ output: {dataType: 'float32', dimensions: [2, 2, 3, 5]}
+ },
+ {
+ name: '[matmul] Test with 3-D input and 3-D input',
+ inputs: {
+ a: {dataType: 'float32', dimensions: [2, 3, 4]},
+ b: {dataType: 'float32', dimensions: [2, 4, 5]}
+ },
+ output: {dataType: 'float32', dimensions: [2, 3, 5]}
+ },
+ {
+ name: '[matmul] Throw if the input data type is not floating point',
+ inputs: {
+ a: {dataType: 'uint32', dimensions: [2, 3, 4]},
+ b: {dataType: 'uint32', dimensions: [2, 4, 5]}
+ }
+ },
+ {
+ name: '[matmul] Throw if data type of two inputs don\'t match',
+ inputs: {
+ a: {dataType: 'float32', dimensions: [2, 3, 4]},
+ b: {dataType: 'float16', dimensions: [2, 4, 5]}
+ }
+ },
+ {
+ name:
+ '[matmul] Throw if columns of first input\'s shape doesn\'t match the rows of second input\'s shape',
+ inputs: {
+ a: {dataType: 'float32', dimensions: /* [rows, columns] */[2, 3]},
+ b: {dataType: 'float32', dimensions: /* [rows, columns] */[2, 4]}
+ },
+ },
+ {
+ // batchShape is a clone of inputShape with the spatial dimensions
+ // (last 2 items) removed.
+ name: '[matmul] Throw if batchShapes aren\'t bidirectionally broadcastable',
+ inputs: {
+ a: {dataType: 'float32', dimensions: [3, 3, 4]},
+ b: {dataType: 'float32', dimensions: [2, 4, 1]}
+ },
+ },
+];
+
+tests.forEach(test => promise_test(async t => {
+ const inputA = builder.input('a', {
+ dataType: test.inputs.a.dataType,
+ dimensions: test.inputs.a.dimensions
+ });
+ const inputB = builder.input('b', {
+ dataType: test.inputs.b.dataType,
+ dimensions: test.inputs.b.dimensions
+ });
+ if (test.output) {
+ const output = builder.matmul(inputA, inputB);
+ assert_equals(output.dataType(), test.output.dataType);
+ assert_array_equals(output.shape(), test.output.dimensions);
+ } else {
+ assert_throws_js(
+ TypeError, () => builder.matmul(inputA, inputB));
+ }
+ }, test.name));
diff --git a/testing/web-platform/tests/webnn/validation_tests/pad.https.any.js b/testing/web-platform/tests/webnn/validation_tests/pad.https.any.js
index 11c6a8f7ef..cc39bee4c0 100644
--- a/testing/web-platform/tests/webnn/validation_tests/pad.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/pad.https.any.js
@@ -15,3 +15,73 @@ multi_builder_test(async (t, builder, otherBuilder) => {
() =>
builder.pad(inputFromOtherBuilder, beginningPadding, endingPadding));
}, '[pad] throw if input is from another builder');
+
+const tests = [
+ {
+ name:
+ '[pad] Test with default options, beginningPadding=[1, 2] and endingPadding=[1, 2].',
+ input: {dataType: 'float32', dimensions: [2, 3]},
+ beginningPadding: [1, 2],
+ endingPadding: [1, 2],
+ options: {
+ mode: 'constant',
+ value: 0,
+ },
+ output: {dataType: 'float32', dimensions: [4, 7]}
+ },
+ {
+ name: '[pad] Throw if building pad for scalar input.',
+ input: {dataType: 'float32', dimensions: []},
+ beginningPadding: [],
+ endingPadding: [],
+ },
+ {
+ name:
+ '[pad] Throw if the length of beginningPadding is not equal to the input rank.',
+ input: {dataType: 'float32', dimensions: [2, 3]},
+ beginningPadding: [1],
+ endingPadding: [1, 2],
+ options: {
+ mode: 'edge',
+ value: 0,
+ },
+ },
+ {
+ name:
+ '[pad] Throw if the length of endingPadding is not equal to the input rank.',
+ input: {dataType: 'float32', dimensions: [2, 3]},
+ beginningPadding: [1, 0],
+ endingPadding: [1, 2, 0],
+ options: {
+ mode: 'reflection',
+ },
+ },
+ {
+ name: '[pad] Throw if the padding of one dimension is too large.',
+ input: {dataType: 'float32', dimensions: [2, 3]},
+ beginningPadding: [2294967295, 0],
+ endingPadding: [3294967295, 2],
+ options: {
+ mode: 'reflection',
+ },
+ },
+];
+
+tests.forEach(
+ test => promise_test(async t => {
+ const input = builder.input(
+ 'input',
+ {dataType: test.input.dataType, dimensions: test.input.dimensions});
+ if (test.output) {
+ const output = builder.pad(
+ input, test.beginningPadding, test.endingPadding, test.options);
+ assert_equals(output.dataType(), test.output.dataType);
+ assert_array_equals(output.shape(), test.output.dimensions);
+ } else {
+ assert_throws_js(
+ TypeError,
+ () => builder.pad(
+ input, test.beginningPadding, test.endingPadding,
+ test.options));
+ }
+ }, test.name));
diff --git a/testing/web-platform/tests/webnn/validation_tests/pooling-and-reduction-keep-dims.https.any.js b/testing/web-platform/tests/webnn/validation_tests/pooling-and-reduction-keep-dims.https.any.js
new file mode 100644
index 0000000000..9f6b9fb338
--- /dev/null
+++ b/testing/web-platform/tests/webnn/validation_tests/pooling-and-reduction-keep-dims.https.any.js
@@ -0,0 +1,94 @@
+// META: title=validation tests for pooling and reduction operators keep dimensions
+// META: global=window,dedicatedworker
+// META: script=../resources/utils.js
+// META: script=../resources/utils_validation.js
+// META: timeout=long
+
+'use strict';
+
+// This is used to reproduce an issue(crbug.com/331841268) of averagePool2d in
+// ResNetV2 50 model.
+// [input]
+// |
+// [globalAveragePool]
+// |
+// [conv2d]
+// |
+// [reshape]
+// |
+// [output]
+promise_test(async t => {
+ const avgPool2dInputShape = [1, 7, 7, 2048];
+ const avgPool2dInput = builder.input(
+ `avgPool2dInput`, {dataType: 'float32', dimensions: avgPool2dInputShape});
+ const avgPool2dOutput =
+ builder.averagePool2d(avgPool2dInput, {layout: 'nhwc'});
+ const conv2dFilterShape = [1001, 1, 1, 2048];
+ const conv2dFilter = builder.constant(
+ {dataType: 'float32', dimensions: conv2dFilterShape},
+ new Float32Array(sizeOfShape(conv2dFilterShape)).fill(1));
+ const conv2dBias = builder.constant(
+ {dataType: 'float32', dimensions: [1001]},
+ new Float32Array(1001).fill(0.01));
+ const conv2dOutput = builder.conv2d(avgPool2dOutput, conv2dFilter, {
+ inputLayout: 'nhwc',
+ filterLayout: 'ohwi',
+ padding: [0, 0, 0, 0],
+ bias: conv2dBias
+ });
+ const newShape = [1, 1001];
+ const reshapeOutput = builder.reshape(conv2dOutput, newShape);
+ assert_equals(reshapeOutput.dataType(), avgPool2dInput.dataType());
+ assert_array_equals(reshapeOutput.shape(), newShape);
+ const graph = await builder.build({reshapeOutput});
+ const result = await context.compute(
+ graph, {
+ 'avgPool2dInput':
+ new Float32Array(sizeOfShape(avgPool2dInputShape)).fill(0.1)
+ },
+ {'reshapeOutput': new Float32Array(1001)});
+}, 'Test global average pool operator\'s output shape for ResNetV2 50 model.');
+
+// This is used to reproduce an issue(crbug.com/331841268) of reduceMean in
+// ResNetV2 50 model.
+// [input]
+// |
+// [reduceMean]
+// |
+// [conv2d]
+// |
+// [reshape]
+// |
+// [output]
+promise_test(async t => {
+ const reduceMeanInputShape = [1, 7, 7, 2048];
+ const reduceMeanInput = builder.input(
+ `reduceMeanInput`,
+ {dataType: 'float32', dimensions: reduceMeanInputShape});
+ const reduceMeanOutput =
+ builder.reduceMean(reduceMeanInput, {axes: [1, 2], keepDimensions: true});
+ const conv2dFilterShape = [1001, 1, 1, 2048];
+ const conv2dFilter = builder.constant(
+ {dataType: 'float32', dimensions: conv2dFilterShape},
+ new Float32Array(sizeOfShape(conv2dFilterShape)).fill(1));
+ const conv2dBias = builder.constant(
+ {dataType: 'float32', dimensions: [1001]},
+ new Float32Array(1001).fill(0.01));
+ const conv2dOutput = builder.conv2d(reduceMeanOutput, conv2dFilter, {
+ inputLayout: 'nhwc',
+ filterLayout: 'ohwi',
+ padding: [0, 0, 0, 0],
+ bias: conv2dBias
+ });
+ const newShape = [1, 1001];
+ const reshapeOutput = builder.reshape(conv2dOutput, newShape);
+ assert_equals(reshapeOutput.dataType(), reduceMeanInput.dataType());
+ assert_array_equals(reshapeOutput.shape(), newShape);
+ const graph = await builder.build({reshapeOutput});
+ const result = await context.compute(
+ graph, {
+ 'reduceMeanInput':
+ new Float32Array(sizeOfShape(reduceMeanInputShape)).fill(0.1)
+ },
+ {'reshapeOutput': new Float32Array(1001)});
+}, 'Test reduceMean operator\'s output shape for ResNetV2 50 model.');
diff --git a/testing/web-platform/tests/webnn/validation_tests/reshape.https.any.js b/testing/web-platform/tests/webnn/validation_tests/reshape.https.any.js
index 435551b716..67491fbc16 100644
--- a/testing/web-platform/tests/webnn/validation_tests/reshape.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/reshape.https.any.js
@@ -12,3 +12,68 @@ multi_builder_test(async (t, builder, otherBuilder) => {
assert_throws_js(
TypeError, () => builder.reshape(inputFromOtherBuilder, newShape));
}, '[reshape] throw if input is from another builder');
+
+const tests = [
+ {
+ name: '[reshape] Test with new shape=[3, 8].',
+ input: {dataType: 'float32', dimensions: [2, 3, 4]},
+ newShape: [3, 8],
+ output: {dataType: 'float32', dimensions: [3, 8]}
+ },
+ {
+ name: '[reshape] Test with new shape=[24], src shape=[2, 3, 4].',
+ input: {dataType: 'float32', dimensions: [2, 3, 4]},
+ newShape: [24],
+ output: {dataType: 'float32', dimensions: [24]}
+ },
+ {
+ name: '[reshape] Test with new shape=[1], src shape=[1].',
+ input: {dataType: 'float32', dimensions: [1]},
+ newShape: [1],
+ output: {dataType: 'float32', dimensions: [1]}
+ },
+ {
+ name: '[reshape] Test reshaping a 1-D 1-element tensor to scalar.',
+ input: {dataType: 'float32', dimensions: [1]},
+ newShape: [],
+ output: {dataType: 'float32', dimensions: []}
+ },
+ {
+ name: '[reshape] Test reshaping a scalar to 1-D 1-element tensor.',
+ input: {dataType: 'float32', dimensions: []},
+ newShape: [1],
+ output: {dataType: 'float32', dimensions: [1]}
+ },
+ {
+ name: '[reshape] Throw if one value of new shape is 0.',
+ input: {dataType: 'float32', dimensions: [2, 4]},
+ newShape: [2, 4, 0],
+ },
+ {
+ name:
+ '[reshape] Throw if the number of elements implied by new shape is not equal to the number of elements in the input tensor when new shape=[].',
+ input: {dataType: 'float32', dimensions: [2, 3, 4]},
+ newShape: [],
+ },
+ {
+ name:
+ '[reshape] Throw if the number of elements implied by new shape is not equal to the number of elements in the input tensor.',
+ input: {dataType: 'float32', dimensions: [2, 3, 4]},
+ newShape: [3, 9],
+ },
+];
+
+tests.forEach(
+ test => promise_test(async t => {
+ const input = builder.input(
+ 'input',
+ {dataType: test.input.dataType, dimensions: test.input.dimensions});
+ if (test.output) {
+ const output = builder.reshape(input, test.newShape);
+ assert_equals(output.dataType(), test.output.dataType);
+ assert_array_equals(output.shape(), test.output.dimensions);
+ } else {
+ assert_throws_js(
+ TypeError, () => builder.reshape(input, test.newShape));
+ }
+ }, test.name));
diff --git a/testing/web-platform/tests/webnn/validation_tests/slice.https.any.js b/testing/web-platform/tests/webnn/validation_tests/slice.https.any.js
index a45ecd3fcb..de42621610 100644
--- a/testing/web-platform/tests/webnn/validation_tests/slice.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/slice.https.any.js
@@ -13,3 +13,69 @@ multi_builder_test(async (t, builder, otherBuilder) => {
assert_throws_js(
TypeError, () => builder.slice(inputFromOtherBuilder, starts, sizes));
}, '[slice] throw if input is from another builder');
+
+const tests = [
+ {
+ name: '[slice] Test with starts=[0, 1, 2] and sizes=[1, 2, 3].',
+ input: {dataType: 'float32', dimensions: [3, 4, 5]},
+ starts: [0, 1, 2],
+ sizes: [1, 2, 3],
+ output: {dataType: 'float32', dimensions: [1, 2, 3]}
+ },
+ {
+ name: '[slice] Throw if input is a scalar.',
+ input: {dataType: 'float32', dimensions: []},
+ starts: [0],
+ sizes: [1]
+ },
+ {
+ name:
+ '[slice] Throw if the length of sizes is not equal to the rank of the input tensor.',
+ input: {dataType: 'float32', dimensions: [3, 4, 5]},
+ starts: [1, 2, 3],
+ sizes: [1, 1]
+ },
+ {
+ name:
+ '[slice] Throw if the length of starts is not equal to the rank of the input tensor.',
+ input: {dataType: 'float32', dimensions: [3, 4, 5]},
+ starts: [1, 2, 1, 3],
+ sizes: [1, 1, 1]
+ },
+ {
+ name:
+ '[slice] Throw if the starting index is equal to or greater than input size in the same dimension.',
+ input: {dataType: 'float32', dimensions: [3, 4, 5]},
+ starts: [0, 4, 4],
+ sizes: [1, 1, 1]
+ },
+ {
+ name: '[slice] Throw if the number of elements to slice is equal to 0.',
+ input: {dataType: 'float32', dimensions: [3, 4, 5]},
+ starts: [1, 2, 3],
+ sizes: [1, 0, 1]
+ },
+ {
+ name:
+ '[slice] Throw if the ending index to slice is greater than input size in the same dimension.',
+ input: {dataType: 'float32', dimensions: [3, 4, 5]},
+ starts: [0, 1, 2],
+ sizes: [3, 4, 1]
+ },
+];
+
+tests.forEach(
+ test => promise_test(async t => {
+ const input = builder.input(
+ 'input',
+ {dataType: test.input.dataType, dimensions: test.input.dimensions});
+
+ if (test.output) {
+ const output = builder.slice(input, test.starts, test.sizes);
+ assert_equals(output.dataType(), test.output.dataType);
+ assert_array_equals(output.shape(), test.output.dimensions);
+ } else {
+ assert_throws_js(
+ TypeError, () => builder.slice(input, test.starts, test.sizes));
+ }
+ }, test.name));
diff --git a/testing/web-platform/tests/webnn/validation_tests/softplus.https.any.js b/testing/web-platform/tests/webnn/validation_tests/softplus.https.any.js
index 347dfcd938..3cf91d26ec 100644
--- a/testing/web-platform/tests/webnn/validation_tests/softplus.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/softplus.https.any.js
@@ -5,3 +5,6 @@
'use strict';
validateInputFromAnotherBuilder('softplus');
+
+validateUnaryOperation(
+ 'softplus', floatingPointTypes, /*alsoBuildActivation=*/ true);
diff --git a/testing/web-platform/tests/webnn/validation_tests/split.https.any.js b/testing/web-platform/tests/webnn/validation_tests/split.https.any.js
index 38f3126603..6f7809744a 100644
--- a/testing/web-platform/tests/webnn/validation_tests/split.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/split.https.any.js
@@ -12,3 +12,83 @@ multi_builder_test(async (t, builder, otherBuilder) => {
assert_throws_js(
TypeError, () => builder.split(inputFromOtherBuilder, splits));
}, '[split] throw if input is from another builder');
+
+const tests = [
+ {
+ name: '[split] Test with default options.',
+ input: {dataType: 'float32', dimensions: [2, 6]},
+ splits: [2],
+ outputs: [
+ {dataType: 'float32', dimensions: [2, 6]},
+ ]
+ },
+ {
+ name:
+ '[split] Test with a sequence of unsigned long splits and with options.axis = 1.',
+ input: {dataType: 'float32', dimensions: [2, 6]},
+ splits: [1, 2, 3],
+ options: {axis: 1},
+ outputs: [
+ {dataType: 'float32', dimensions: [2, 1]},
+ {dataType: 'float32', dimensions: [2, 2]},
+ {dataType: 'float32', dimensions: [2, 3]},
+ ]
+ },
+ {
+ name: '[split] Throw if splitting a scalar.',
+ input: {dataType: 'float32', dimensions: []},
+ splits: [2],
+ },
+ {
+ name: '[split] Throw if axis is larger than input rank.',
+ input: {dataType: 'float32', dimensions: [2, 6]},
+ splits: [2],
+ options: {
+ axis: 2,
+ }
+ },
+ {
+ name: '[split] Throw if splits is equal to 0.',
+ input: {dataType: 'float32', dimensions: [2, 6]},
+ splits: [0],
+ options: {
+ axis: 2,
+ }
+ },
+ {
+ name:
+ '[split] Throw if the splits can not evenly divide the dimension size of input along options.axis.',
+ input: {dataType: 'float32', dimensions: [2, 5]},
+ splits: [2],
+ options: {
+ axis: 1,
+ }
+ },
+ {
+ name:
+ '[split] Throw if the sum of splits sizes not equal to the dimension size of input along options.axis.',
+ input: {dataType: 'float32', dimensions: [2, 6]},
+ splits: [2, 2, 3],
+ options: {
+ axis: 1,
+ }
+ },
+];
+
+tests.forEach(
+ test => promise_test(async t => {
+ const input = builder.input(
+ 'input',
+ {dataType: test.input.dataType, dimensions: test.input.dimensions});
+ if (test.outputs) {
+ const outputs = builder.split(input, test.splits, test.options);
+ assert_equals(outputs.length, test.outputs.length);
+ for (let i = 0; i < outputs.length; ++i) {
+ assert_equals(outputs[i].dataType(), test.outputs[i].dataType);
+ assert_array_equals(outputs[i].shape(), test.outputs[i].dimensions);
+ }
+ } else {
+ assert_throws_js(
+ TypeError, () => builder.split(input, test.splits, test.options));
+ }
+ }, test.name));
diff --git a/testing/web-platform/tests/webnn/validation_tests/transpose.https.any.js b/testing/web-platform/tests/webnn/validation_tests/transpose.https.any.js
index 9ea5a5dcf8..3475a427d7 100644
--- a/testing/web-platform/tests/webnn/validation_tests/transpose.https.any.js
+++ b/testing/web-platform/tests/webnn/validation_tests/transpose.https.any.js
@@ -5,3 +5,54 @@
'use strict';
validateInputFromAnotherBuilder('transpose');
+
+const tests = [
+ {
+ name: '[transpose] Test building transpose with default options.',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ output: {dataType: 'float32', dimensions: [4, 3, 2, 1]}
+ },
+ {
+ name: '[transpose] Test building transpose with permutation=[0, 2, 3, 1].',
+ input: {dataType: 'float32', dimensions: [1, 2, 3, 4]},
+ options: {permutation: [0, 2, 3, 1]},
+ output: {dataType: 'float32', dimensions: [1, 3, 4, 2]}
+ },
+ {
+ name:
+ '[transpose] Throw if permutation\'s size is not the same as input\'s rank.',
+ input: {dataType: 'int32', dimensions: [1, 2, 4]},
+ options: {permutation: [0, 2, 3, 1]},
+ },
+ {
+ name: '[transpose] Throw if two values in permutation are same.',
+ input: {dataType: 'int32', dimensions: [1, 2, 3, 4]},
+ options: {permutation: [0, 2, 3, 2]},
+ },
+ {
+ name:
+ '[transpose] Throw if any value in permutation is not in the range [0,input\'s rank).',
+ input: {dataType: 'int32', dimensions: [1, 2, 3, 4]},
+ options: {permutation: [0, 1, 2, 4]},
+ },
+ {
+ name: '[transpose] Throw if any value in permutation is negative.',
+ input: {dataType: 'int32', dimensions: [1, 2, 3, 4]},
+ options: {permutation: [0, -1, 2, 3]},
+ }
+];
+
+tests.forEach(
+ test => promise_test(async t => {
+ const input = builder.input(
+ 'input',
+ {dataType: test.input.dataType, dimensions: test.input.dimensions});
+ if (test.output) {
+ const output = builder.transpose(input, test.options);
+ assert_equals(output.dataType(), test.output.dataType);
+ assert_array_equals(output.shape(), test.output.dimensions);
+ } else {
+ assert_throws_js(
+ TypeError, () => builder.transpose(input, test.options));
+ }
+ }, test.name));
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/tentative/RTCEncodedAudioFrame-metadata.https.html b/testing/web-platform/tests/webrtc-encoded-transform/tentative/RTCEncodedAudioFrame-metadata.https.html
index 1e420e6f72..609467b5e3 100644
--- a/testing/web-platform/tests/webrtc-encoded-transform/tentative/RTCEncodedAudioFrame-metadata.https.html
+++ b/testing/web-platform/tests/webrtc-encoded-transform/tentative/RTCEncodedAudioFrame-metadata.https.html
@@ -76,7 +76,7 @@ promise_test(async t => {
const original = result.value;
let newMetadata = original.getMetadata();
newMetadata.rtpTimestamp = newMetadata.rtpTimestamp + 1;
- let newFrame = new RTCEncodedAudioFrame(original, newMetadata);
+ let newFrame = new RTCEncodedAudioFrame(original, {metadata: newMetadata});
assert_not_equals(original.getMetadata().rtpTimestamp, newFrame.getMetadata().rtpTimestamp);
assert_equals(newMetadata.rtpTimestamp, newFrame.getMetadata().rtpTimestamp);
assert_equals(original.getMetadata().absCaptureTime, newFrame.getMetadata().absCaptureTime);
@@ -117,7 +117,7 @@ promise_test(async t => {
const original = result.value;
let newMetadata = original.getMetadata();
newMetadata.synchronizationSource = newMetadata.synchronizationSource + 1;
- assert_throws_dom("InvalidModificationError", () => new RTCEncodedAudioFrame(original, newMetadata));
+ assert_throws_dom("InvalidModificationError", () => new RTCEncodedAudioFrame(original, {metadata: newMetadata}));
resolve();
}
});
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/tentative/RTCEncodedVideoFrame-metadata.https.html b/testing/web-platform/tests/webrtc-encoded-transform/tentative/RTCEncodedVideoFrame-metadata.https.html
index a2c684c1f1..77e1ed118f 100644
--- a/testing/web-platform/tests/webrtc-encoded-transform/tentative/RTCEncodedVideoFrame-metadata.https.html
+++ b/testing/web-platform/tests/webrtc-encoded-transform/tentative/RTCEncodedVideoFrame-metadata.https.html
@@ -80,29 +80,29 @@ promise_test(async t => {
promise_test(async t => {
const senderReader = await setupLoopbackWithCodecAndGetReader(t, 'VP8');
const result = await senderReader.read();
- const metadata = result.value.getMetadata();
- metadata.rtpTimestamp = 100;
- const newFrame = new RTCEncodedVideoFrame(result.value, metadata);
+ const frame_metadata = result.value.getMetadata();
+ frame_metadata.rtpTimestamp = 100;
+ const newFrame = new RTCEncodedVideoFrame(result.value, {metadata: frame_metadata});
const newMetadata = newFrame.getMetadata();
// Encoding-related metadata.
- assert_equals(newMetadata.frameId, metadata.frameId, 'frameId');
- assert_array_equals(newMetadata.dependencies, metadata.dependencies,
+ assert_equals(newMetadata.frameId, frame_metadata.frameId, 'frameId');
+ assert_array_equals(newMetadata.dependencies, frame_metadata.dependencies,
'dependencies');
- assert_equals(newMetadata.width, metadata.width, 'width');
- assert_equals(newMetadata.height, metadata.height, 'height');
- assert_equals(newMetadata.spatialIndex, metadata.spatialIndex,
+ assert_equals(newMetadata.width, frame_metadata.width, 'width');
+ assert_equals(newMetadata.height, frame_metadata.height, 'height');
+ assert_equals(newMetadata.spatialIndex, frame_metadata.spatialIndex,
'spatialIndex');
- assert_equals(newMetadata.temporalIndex, metadata.temporalIndex,
+ assert_equals(newMetadata.temporalIndex, frame_metadata.temporalIndex,
'temporalIndex');
// RTP-related metadata.
assert_equals(newMetadata.synchronizationSource,
- metadata.synchronizationSource, 'synchronizationSource');
+ frame_metadata.synchronizationSource, 'synchronizationSource');
assert_array_equals(newMetadata.contributingSources,
- metadata.contributingSources, 'contributingSources');
- assert_equals(newMetadata.payloadType, metadata.payloadType, 'payloadType');
- assert_equals(newMetadata.rtpTimestamp, metadata.rtpTimestamp, 'rtpTimestamp');
+ frame_metadata.contributingSources, 'contributingSources');
+ assert_equals(newMetadata.payloadType, frame_metadata.payloadType, 'payloadType');
+ assert_equals(newMetadata.rtpTimestamp, frame_metadata.rtpTimestamp, 'rtpTimestamp');
assert_not_equals(newMetadata.rtpTimestamp, result.value.getMetadata().rtpTimestamp, 'rtpTimestamp');
}, "[VP8] constructor with metadata carries over codec-specific properties");
diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-audio-jitterBufferTarget-stats.https.html b/testing/web-platform/tests/webrtc/RTCRtpReceiver-audio-jitterBufferTarget-stats.https.html
index d728ec5a9c..d728ec5a9c 100644
--- a/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-audio-jitterBufferTarget-stats.https.html
+++ b/testing/web-platform/tests/webrtc/RTCRtpReceiver-audio-jitterBufferTarget-stats.https.html
diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget-stats-helper.js b/testing/web-platform/tests/webrtc/RTCRtpReceiver-jitterBufferTarget-stats-helper.js
index 31d80926d3..31d80926d3 100644
--- a/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget-stats-helper.js
+++ b/testing/web-platform/tests/webrtc/RTCRtpReceiver-jitterBufferTarget-stats-helper.js
diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget.html b/testing/web-platform/tests/webrtc/RTCRtpReceiver-jitterBufferTarget.html
index 448162d3a2..448162d3a2 100644
--- a/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget.html
+++ b/testing/web-platform/tests/webrtc/RTCRtpReceiver-jitterBufferTarget.html
diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-video-jitterBufferTarget-stats.html b/testing/web-platform/tests/webrtc/RTCRtpReceiver-video-jitterBufferTarget-stats.html
index 022dbe70c5..022dbe70c5 100644
--- a/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-video-jitterBufferTarget-stats.html
+++ b/testing/web-platform/tests/webrtc/RTCRtpReceiver-video-jitterBufferTarget-stats.html
diff --git a/testing/web-platform/tests/webrtc/WEB_FEATURES.yml b/testing/web-platform/tests/webrtc/WEB_FEATURES.yml
new file mode 100644
index 0000000000..117b04f81f
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/WEB_FEATURES.yml
@@ -0,0 +1,4 @@
+features:
+- name: webrtc-sctp
+ files:
+ - RTCSctpTransport-*
diff --git a/testing/web-platform/tests/webrtc/back-forward-cache-with-open-webrtc-connection.https.window.js b/testing/web-platform/tests/webrtc/back-forward-cache-with-open-webrtc-connection.https.window.js
index fe41a9cfd5..de797b3f2c 100644
--- a/testing/web-platform/tests/webrtc/back-forward-cache-with-open-webrtc-connection.https.window.js
+++ b/testing/web-platform/tests/webrtc/back-forward-cache-with-open-webrtc-connection.https.window.js
@@ -4,6 +4,7 @@
// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js
// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
// META: script=resources/webrtc-test-helpers.sub.js
+// META: timeout=long
'use strict';
diff --git a/testing/web-platform/tests/websockets/Send-binary-arraybufferview-float16.any.js b/testing/web-platform/tests/websockets/Send-binary-arraybufferview-float16.any.js
new file mode 100644
index 0000000000..7251ebfed2
--- /dev/null
+++ b/testing/web-platform/tests/websockets/Send-binary-arraybufferview-float16.any.js
@@ -0,0 +1,40 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Float16Array - Connection should be closed");
+
+var data = "";
+var datasize = 4;
+var view;
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.binaryType = "arraybuffer";
+ data = new ArrayBuffer(datasize);
+ view = new Float16Array(data);
+ for (var i = 0; i < 2; i++) {
+ view[i] = i;
+ }
+ wsocket.send(view);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ var resultView = new Float16Array(evt.data);
+ for (var i = 0; i < resultView.length; i++) {
+ assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+ }
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received")
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/testing/web-platform/tests/websockets/handlers/basic_auth_wsh.py b/testing/web-platform/tests/websockets/handlers/basic_auth_wsh.py
index 72e920a1d8..84f42711b2 100755
--- a/testing/web-platform/tests/websockets/handlers/basic_auth_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/basic_auth_wsh.py
@@ -4,7 +4,7 @@
'foo' and password is 'bar'."""
-from mod_pywebsocket.handshake import AbortedByUserException
+from pywebsocket3.handshake import AbortedByUserException
def web_socket_do_extra_handshake(request):
diff --git a/testing/web-platform/tests/websockets/handlers/delayed-passive-close_wsh.py b/testing/web-platform/tests/websockets/handlers/delayed-passive-close_wsh.py
index 7d55b88ecc..5da09fd059 100755
--- a/testing/web-platform/tests/websockets/handlers/delayed-passive-close_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/delayed-passive-close_wsh.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-from mod_pywebsocket import common
+from pywebsocket3 import common
import time
def web_socket_do_extra_handshake(request):
diff --git a/testing/web-platform/tests/websockets/handlers/echo-cookie_wsh.py b/testing/web-platform/tests/websockets/handlers/echo-cookie_wsh.py
index 98620b6552..746eafd2cd 100755
--- a/testing/web-platform/tests/websockets/handlers/echo-cookie_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/echo-cookie_wsh.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-from mod_pywebsocket import msgutil
+from pywebsocket3 import msgutil
def web_socket_do_extra_handshake(request):
request.ws_cookie = request.headers_in.get('cookie')
diff --git a/testing/web-platform/tests/websockets/handlers/echo-query_v13_wsh.py b/testing/web-platform/tests/websockets/handlers/echo-query_v13_wsh.py
index d670e6e660..7fb4cfe593 100755
--- a/testing/web-platform/tests/websockets/handlers/echo-query_v13_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/echo-query_v13_wsh.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-from mod_pywebsocket import msgutil
+from pywebsocket3 import msgutil
def web_socket_do_extra_handshake(request):
pass
diff --git a/testing/web-platform/tests/websockets/handlers/echo-query_wsh.py b/testing/web-platform/tests/websockets/handlers/echo-query_wsh.py
index 3921913495..88696175d2 100755
--- a/testing/web-platform/tests/websockets/handlers/echo-query_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/echo-query_wsh.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-from mod_pywebsocket import msgutil
+from pywebsocket3 import msgutil
def web_socket_do_extra_handshake(request):
pass # Always accept.
diff --git a/testing/web-platform/tests/websockets/handlers/echo_raw_wsh.py b/testing/web-platform/tests/websockets/handlers/echo_raw_wsh.py
index e1fc26608f..5b434cf266 100755
--- a/testing/web-platform/tests/websockets/handlers/echo_raw_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/echo_raw_wsh.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-from mod_pywebsocket import msgutil
+from pywebsocket3 import msgutil
def web_socket_do_extra_handshake(request):
diff --git a/testing/web-platform/tests/websockets/handlers/echo_wsh.py b/testing/web-platform/tests/websockets/handlers/echo_wsh.py
index 7367b70af1..35a0a6f6ea 100755
--- a/testing/web-platform/tests/websockets/handlers/echo_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/echo_wsh.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-from mod_pywebsocket import common
+from pywebsocket3 import common
_GOODBYE_MESSAGE = u'Goodbye'
diff --git a/testing/web-platform/tests/websockets/handlers/empty-message_wsh.py b/testing/web-platform/tests/websockets/handlers/empty-message_wsh.py
index 0eb107f0b1..f791ee1842 100755
--- a/testing/web-platform/tests/websockets/handlers/empty-message_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/empty-message_wsh.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-from mod_pywebsocket import msgutil
+from pywebsocket3 import msgutil
def web_socket_do_extra_handshake(request):
pass # Always accept.
diff --git a/testing/web-platform/tests/websockets/handlers/msg_channel_wsh.py b/testing/web-platform/tests/websockets/handlers/msg_channel_wsh.py
index 7a66646f2b..a45dd99164 100644
--- a/testing/web-platform/tests/websockets/handlers/msg_channel_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/msg_channel_wsh.py
@@ -6,7 +6,7 @@ import threading
import traceback
from queue import Empty
-from mod_pywebsocket import stream, msgutil
+from pywebsocket3 import stream, msgutil
from wptserve import stash as stashmod
logger = logging.getLogger()
diff --git a/testing/web-platform/tests/websockets/handlers/origin_wsh.py b/testing/web-platform/tests/websockets/handlers/origin_wsh.py
index ce5f3a7f6a..b833db6381 100755
--- a/testing/web-platform/tests/websockets/handlers/origin_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/origin_wsh.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-from mod_pywebsocket import msgutil
+from pywebsocket3 import msgutil
def web_socket_do_extra_handshake(request):
diff --git a/testing/web-platform/tests/websockets/handlers/passive-close-abort_wsh.py b/testing/web-platform/tests/websockets/handlers/passive-close-abort_wsh.py
index ac3f67c8db..6f8a3d1bf1 100644
--- a/testing/web-platform/tests/websockets/handlers/passive-close-abort_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/passive-close-abort_wsh.py
@@ -7,7 +7,7 @@ Wait for a Close frame from the client and then close the connection without
sending a Close frame in return.
"""
-from mod_pywebsocket.handshake import AbortedByUserException
+from pywebsocket3.handshake import AbortedByUserException
def web_socket_do_extra_handshake(request):
diff --git a/testing/web-platform/tests/websockets/handlers/protocol_array_wsh.py b/testing/web-platform/tests/websockets/handlers/protocol_array_wsh.py
index be24ee01fd..d8e1229cdb 100755
--- a/testing/web-platform/tests/websockets/handlers/protocol_array_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/protocol_array_wsh.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-from mod_pywebsocket import msgutil
+from pywebsocket3 import msgutil
def web_socket_do_extra_handshake(request):
line = request.headers_in.get('sec-websocket-protocol')
diff --git a/testing/web-platform/tests/websockets/handlers/protocol_wsh.py b/testing/web-platform/tests/websockets/handlers/protocol_wsh.py
index 10bdf33205..3a6aeb400b 100755
--- a/testing/web-platform/tests/websockets/handlers/protocol_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/protocol_wsh.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-from mod_pywebsocket import msgutil
+from pywebsocket3 import msgutil
def web_socket_do_extra_handshake(request):
request.ws_protocol = request.headers_in.get('sec-websocket-protocol')
diff --git a/testing/web-platform/tests/websockets/handlers/referrer_wsh.py b/testing/web-platform/tests/websockets/handlers/referrer_wsh.py
index 9df652dc3c..2d183d5586 100755
--- a/testing/web-platform/tests/websockets/handlers/referrer_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/referrer_wsh.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-from mod_pywebsocket import msgutil
+from pywebsocket3 import msgutil
def web_socket_do_extra_handshake(request):
pass
diff --git a/testing/web-platform/tests/websockets/handlers/remote-close_wsh.py b/testing/web-platform/tests/websockets/handlers/remote-close_wsh.py
index aadd99ea95..c94870ad87 100644
--- a/testing/web-platform/tests/websockets/handlers/remote-close_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/remote-close_wsh.py
@@ -20,7 +20,7 @@ Example: /remote-close?code=1000&reason=Done
import urllib
-from mod_pywebsocket.handshake import AbortedByUserException
+from pywebsocket3.handshake import AbortedByUserException
def web_socket_do_extra_handshake(request):
diff --git a/testing/web-platform/tests/websockets/handlers/simple_handshake_wsh.py b/testing/web-platform/tests/websockets/handlers/simple_handshake_wsh.py
index ad466877fe..23893c95f1 100755
--- a/testing/web-platform/tests/websockets/handlers/simple_handshake_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/simple_handshake_wsh.py
@@ -1,7 +1,7 @@
#!/usr/bin/python
-from mod_pywebsocket import common, stream
-from mod_pywebsocket.handshake import AbortedByUserException, hybi
+from pywebsocket3 import common, stream
+from pywebsocket3.handshake import AbortedByUserException, hybi
def web_socket_do_extra_handshake(request):
diff --git a/testing/web-platform/tests/websockets/handlers/sleep_10_v13_wsh.py b/testing/web-platform/tests/websockets/handlers/sleep_10_v13_wsh.py
index bdef2f2afd..4faa42aa45 100755
--- a/testing/web-platform/tests/websockets/handlers/sleep_10_v13_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/sleep_10_v13_wsh.py
@@ -1,7 +1,7 @@
#!/usr/bin/python
import sys, urllib, time
-from mod_pywebsocket import msgutil
+from pywebsocket3 import msgutil
def web_socket_do_extra_handshake(request):
time.sleep(10)
diff --git a/testing/web-platform/tests/websockets/handlers/stash_responder_blocking_wsh.py b/testing/web-platform/tests/websockets/handlers/stash_responder_blocking_wsh.py
index 10ecdfe0da..968156868a 100755
--- a/testing/web-platform/tests/websockets/handlers/stash_responder_blocking_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/stash_responder_blocking_wsh.py
@@ -2,7 +2,7 @@
import json
import threading
import wptserve.stash
-from mod_pywebsocket import msgutil
+from pywebsocket3 import msgutil
address, authkey = wptserve.stash.load_env_config()
path = "/stash_responder_blocking"
diff --git a/testing/web-platform/tests/websockets/handlers/stash_responder_wsh.py b/testing/web-platform/tests/websockets/handlers/stash_responder_wsh.py
index d18ad3bc96..b401997480 100755
--- a/testing/web-platform/tests/websockets/handlers/stash_responder_wsh.py
+++ b/testing/web-platform/tests/websockets/handlers/stash_responder_wsh.py
@@ -1,7 +1,7 @@
#!/usr/bin/python
import json
import urllib
-from mod_pywebsocket import msgutil
+from pywebsocket3 import msgutil
from wptserve import stash
address, authkey = stash.load_env_config()
diff --git a/testing/web-platform/tests/webvtt/parsing/cue-text-parsing/tests/tree-building.html b/testing/web-platform/tests/webvtt/parsing/cue-text-parsing/tests/tree-building.html
index 6cd617dece..06bc8b9d62 100644
--- a/testing/web-platform/tests/webvtt/parsing/cue-text-parsing/tests/tree-building.html
+++ b/testing/web-platform/tests/webvtt/parsing/cue-text-parsing/tests/tree-building.html
@@ -18,6 +18,7 @@ runTests([
{name:'325c1e590e74f1ff33ca5b4838c04cf6b6dd71ba', input:'%3Cruby%3Etest%3Crt%3E%3Cb%3Etest%3C/rt%3E%3C/ruby%3Etest', expected:'%23document-fragment%0A%7C%20%3Cruby%3E%0A%7C%20%20%20%22test%22%0A%7C%20%20%20%3Crt%3E%0A%7C%20%20%20%20%20%3Cb%3E%0A%7C%20%20%20%20%20%20%20%22test%22%0A%7C%20%20%20%20%20%20%20%22test%22'},
{name:'92847ed2694c9639ba96f4cc61e2215362a74904', input:'%3Cruby%3Etest%3Crt%3E%3Cb%3Etest%3C/ruby%3Etest', expected:'%23document-fragment%0A%7C%20%3Cruby%3E%0A%7C%20%20%20%22test%22%0A%7C%20%20%20%3Crt%3E%0A%7C%20%20%20%20%20%3Cb%3E%0A%7C%20%20%20%20%20%20%20%22test%22%0A%7C%20%20%20%20%20%20%20%22test%22'},
{name:'c0da62d1c8716ca544c96799f06ac7e4664500fb', input:'%3Cruby%3Etest%3Crt%3E%3Cb%3Etest%3C/rt%3E%3C/ruby%3E%3C/b%3Etest', expected:'%23document-fragment%0A%7C%20%3Cruby%3E%0A%7C%20%20%20%22test%22%0A%7C%20%20%20%3Crt%3E%0A%7C%20%20%20%20%20%3Cb%3E%0A%7C%20%20%20%20%20%20%20%22test%22%0A%7C%20%20%20%20%20%22test%22'},
-{name:'b85bd616672eba0591718182ef32e3307d223bb0', input:'%3Cruby%3Etest%3Crt%3E%3Cb%3Etest%3C/rt%3E%3C/b%3E%3C/ruby%3Etest', expected:'%23document-fragment%0A%7C%20%3Cruby%3E%0A%7C%20%20%20%22test%22%0A%7C%20%20%20%3Crt%3E%0A%7C%20%20%20%20%20%3Cb%3E%0A%7C%20%20%20%20%20%20%20%22test%22%0A%7C%20%22test%22'}
+{name:'b85bd616672eba0591718182ef32e3307d223bb0', input:'%3Cruby%3Etest%3Crt%3E%3Cb%3Etest%3C/rt%3E%3C/b%3E%3C/ruby%3Etest', expected:'%23document-fragment%0A%7C%20%3Cruby%3E%0A%7C%20%20%20%22test%22%0A%7C%20%20%20%3Crt%3E%0A%7C%20%20%20%20%20%3Cb%3E%0A%7C%20%20%20%20%20%20%20%22test%22%0A%7C%20%22test%22'},
+{name:'421c76d77563afa1914846b010bd164f395bd34c', input:'%3Cruby%3Etest%3Crt%3E%3Cb%3Etest%3C%2Frt%3E%3C%2Fb%3Etest%3C%2Fruby%3Etest', expected:'%23document-fragment%0A%7C%20%3Cruby%3E%0A%7C%20%20%20%22test%22%0A%7C%20%20%20%3Crt%3E%0A%7C%20%20%20%20%20%3Cb%3E%0A%7C%20%20%20%20%20%20%20%22test%22%0A%7C%20%20%20%20%20%22test%22%0A%7C%20%22test%22'}
]);
</script>
diff --git a/testing/web-platform/tests/workers/Worker-creation-happens-in-parallel.https.html b/testing/web-platform/tests/workers/Worker-creation-happens-in-parallel.https.html
new file mode 100644
index 0000000000..887d95f3df
--- /dev/null
+++ b/testing/web-platform/tests/workers/Worker-creation-happens-in-parallel.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>Test that creation of a "new Worker()" will occur in parallel to the main JS thread performing other computation, and can be joined with.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(t => {
+ return new Promise(resolve => {
+ let worker = new Worker("support/Worker-creation-happens-in-parallel.js");
+ let sab = new Uint8Array(new SharedArrayBuffer(16));
+ window.sab = sab;
+ worker.postMessage(sab);
+ let end = performance.now() + 10*1000;
+ while(sab[0] != 1 && performance.now() < end) /*wait to join with the result*/;
+ assert_true(sab[0] == 1);
+ resolve();
+ });
+}, 'Tests that creation of a "new Worker()" will occur in parallel');
+</script>
diff --git a/testing/web-platform/tests/workers/Worker-creation-happens-in-parallel.https.html.headers b/testing/web-platform/tests/workers/Worker-creation-happens-in-parallel.https.html.headers
new file mode 100644
index 0000000000..4b06ac7cc6
--- /dev/null
+++ b/testing/web-platform/tests/workers/Worker-creation-happens-in-parallel.https.html.headers
@@ -0,0 +1,2 @@
+Cross-Origin-Opener-Policy:same-origin
+Cross-Origin-Embedder-Policy:require-corp
diff --git a/testing/web-platform/tests/workers/Worker-postMessage-happens-in-parallel.https.html b/testing/web-platform/tests/workers/Worker-postMessage-happens-in-parallel.https.html
new file mode 100644
index 0000000000..3b8683f79d
--- /dev/null
+++ b/testing/web-platform/tests/workers/Worker-postMessage-happens-in-parallel.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>Test that calling "worker.postMessage()" will occur truly in parallel to the main JS thread performing other computation.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(t => {
+ return new Promise(resolve => {
+ let worker = new Worker("support/Worker-postMessage-happens-in-parallel.js");
+ worker.postMessage('init');
+ worker.onmessage = () => {
+ let sab = new Uint8Array(new SharedArrayBuffer(16));
+ worker.postMessage(sab);
+ let end = performance.now() + 30*1000;
+ while(sab[0] != 1 && performance.now() < end) /*wait to join with the result*/;
+ assert_true(sab[0] == 1);
+ resolve();
+ };
+ });
+}, 'Tests that calling "worker.postMessage()" will occur truly in parallel to the main JS thread');
+</script>
diff --git a/testing/web-platform/tests/workers/Worker-postMessage-happens-in-parallel.https.html.headers b/testing/web-platform/tests/workers/Worker-postMessage-happens-in-parallel.https.html.headers
new file mode 100644
index 0000000000..4b06ac7cc6
--- /dev/null
+++ b/testing/web-platform/tests/workers/Worker-postMessage-happens-in-parallel.https.html.headers
@@ -0,0 +1,2 @@
+Cross-Origin-Opener-Policy:same-origin
+Cross-Origin-Embedder-Policy:require-corp
diff --git a/testing/web-platform/tests/workers/modules/WEB_FEATURES.yml b/testing/web-platform/tests/workers/modules/WEB_FEATURES.yml
new file mode 100644
index 0000000000..ab73efc0d0
--- /dev/null
+++ b/testing/web-platform/tests/workers/modules/WEB_FEATURES.yml
@@ -0,0 +1,7 @@
+features:
+- name: js-modules-workers
+ files:
+ - dedicated-worker-*
+- name: js-modules-shared-workers
+ files:
+ - shared-worker-*
diff --git a/testing/web-platform/tests/workers/semantics/interface-objects/001.worker.js b/testing/web-platform/tests/workers/semantics/interface-objects/001.worker.js
index 873e8701bb..a95da5638f 100644
--- a/testing/web-platform/tests/workers/semantics/interface-objects/001.worker.js
+++ b/testing/web-platform/tests/workers/semantics/interface-objects/001.worker.js
@@ -30,6 +30,7 @@ var expected = [
"Uint16Array",
"Int32Array",
"Uint32Array",
+ "Float16Array",
"Float32Array",
"Float64Array",
"DataView",
diff --git a/testing/web-platform/tests/workers/semantics/interface-objects/003.any.js b/testing/web-platform/tests/workers/semantics/interface-objects/003.any.js
index 974756c508..9dcc024ab7 100644
--- a/testing/web-platform/tests/workers/semantics/interface-objects/003.any.js
+++ b/testing/web-platform/tests/workers/semantics/interface-objects/003.any.js
@@ -30,6 +30,7 @@ var expected = [
"Uint16Array",
"Int32Array",
"Uint32Array",
+ "Float16Array",
"Float32Array",
"Float64Array",
"DataView",
diff --git a/testing/web-platform/tests/workers/support/Worker-creation-happens-in-parallel.js b/testing/web-platform/tests/workers/support/Worker-creation-happens-in-parallel.js
new file mode 100644
index 0000000000..1e68302a0f
--- /dev/null
+++ b/testing/web-platform/tests/workers/support/Worker-creation-happens-in-parallel.js
@@ -0,0 +1,3 @@
+onmessage = (e) => {
+ e.data[0] = 1;
+}
diff --git a/testing/web-platform/tests/workers/support/Worker-creation-happens-in-parallel.js.headers b/testing/web-platform/tests/workers/support/Worker-creation-happens-in-parallel.js.headers
new file mode 100644
index 0000000000..8249c49c34
--- /dev/null
+++ b/testing/web-platform/tests/workers/support/Worker-creation-happens-in-parallel.js.headers
@@ -0,0 +1 @@
+Cross-Origin-Embedder-Policy:require-corp
diff --git a/testing/web-platform/tests/workers/support/Worker-postMessage-happens-in-parallel.js b/testing/web-platform/tests/workers/support/Worker-postMessage-happens-in-parallel.js
new file mode 100644
index 0000000000..48b4c34fe3
--- /dev/null
+++ b/testing/web-platform/tests/workers/support/Worker-postMessage-happens-in-parallel.js
@@ -0,0 +1,7 @@
+onmessage = (e) => {
+ if (e.data == 'init') {
+ postMessage(0);
+ } else {
+ e.data[0] = 1;
+ }
+}
diff --git a/testing/web-platform/tests/workers/support/Worker-postMessage-happens-in-parallel.js.headers b/testing/web-platform/tests/workers/support/Worker-postMessage-happens-in-parallel.js.headers
new file mode 100644
index 0000000000..8249c49c34
--- /dev/null
+++ b/testing/web-platform/tests/workers/support/Worker-postMessage-happens-in-parallel.js.headers
@@ -0,0 +1 @@
+Cross-Origin-Embedder-Policy:require-corp
diff --git a/testing/web-platform/tests/wpt b/testing/web-platform/tests/wpt
index e0abacd85d..5712156129 100755
--- a/testing/web-platform/tests/wpt
+++ b/testing/web-platform/tests/wpt
@@ -2,8 +2,8 @@
if __name__ == "__main__":
import sys
- if sys.version_info < (3, 7):
- sys.stderr.write("wpt requires Python 3.7 or higher\n")
+ if sys.version_info < (3, 8):
+ sys.stderr.write("wpt requires Python 3.8 or higher\n")
sys.exit(1)
from tools.wpt import wpt
diff --git a/testing/web-platform/tests/x-frame-options/invalid.html b/testing/web-platform/tests/x-frame-options/invalid.html
index 26b2905e4d..6c0ed90f73 100644
--- a/testing/web-platform/tests/x-frame-options/invalid.html
+++ b/testing/web-platform/tests/x-frame-options/invalid.html
@@ -40,6 +40,12 @@ xfo_simple_tests({
});
xfo_simple_tests({
+ headerValue: `DE NY`,
+ sameOriginAllowed: true,
+ crossOriginAllowed: true
+});
+
+xfo_simple_tests({
headerValue: `"SAMEORIGIN"`,
sameOriginAllowed: true,
crossOriginAllowed: true
diff --git a/testing/web-platform/tests/xhr/send-data-sharedarraybuffer.any.js b/testing/web-platform/tests/xhr/send-data-sharedarraybuffer.any.js
index 79774c3d30..a87160c56b 100644
--- a/testing/web-platform/tests/xhr/send-data-sharedarraybuffer.any.js
+++ b/testing/web-platform/tests/xhr/send-data-sharedarraybuffer.any.js
@@ -13,7 +13,7 @@ test(() => {
["Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array",
"Int32Array", "Uint32Array", "BigInt64Array", "BigUint64Array",
- "Float32Array", "Float64Array", "DataView"].forEach((type) => {
+ "Float16Array", "Float32Array", "Float64Array", "DataView"].forEach((type) => {
test(() => {
const xhr = new XMLHttpRequest();
// See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()`