summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/bluetooth
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/bluetooth
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/bluetooth')
-rw-r--r--testing/web-platform/tests/bluetooth/META.yml5
-rw-r--r--testing/web-platform/tests/bluetooth/README.md120
-rw-r--r--testing/web-platform/tests/bluetooth/adapter/adapter-absent-getAvailability.https.window.js15
-rw-r--r--testing/web-platform/tests/bluetooth/adapter/adapter-added-getAvailability.https.window.js23
-rw-r--r--testing/web-platform/tests/bluetooth/adapter/adapter-powered-off-getAvailability.https.window.js16
-rw-r--r--testing/web-platform/tests/bluetooth/adapter/adapter-powered-on-getAvailability.https.window.js16
-rw-r--r--testing/web-platform/tests/bluetooth/adapter/adapter-powered-on-off-on-getAvailability.https.window.js37
-rw-r--r--testing/web-platform/tests/bluetooth/adapter/adapter-removed-getAvailability.https.window.js24
-rw-r--r--testing/web-platform/tests/bluetooth/adapter/cross-origin-iframe-getAvailability.sub.https.window.js31
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/characteristicProperties.https.window.js22
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/getDescriptor/detachedIframe.https.window.js33
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/getDescriptor/gen-characteristic-is-removed.https.window.js22
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/getDescriptor/gen-descriptor-get-same-object.https.window.js36
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/getDescriptor/gen-service-is-removed.https.window.js24
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/getDescriptors/detachedIframe.https.window.js33
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-characteristic-is-removed-with-uuid.https.window.js22
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-characteristic-is-removed.https.window.js22
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-descriptor-get-same-object.https.window.js36
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-service-is-removed-with-uuid.https.window.js24
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-service-is-removed.https.window.js24
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/notifications/characteristic-is-removed.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/notifications/service-is-removed.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/readValue/add-multiple-event-listeners.https.window.js25
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/readValue/characteristic-is-removed.https.window.js16
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/readValue/detachedIframe.https.window.js33
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/readValue/event-is-fired.https.window.js23
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/readValue/gen-characteristic-is-removed.https.window.js22
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/readValue/read-succeeds.https.window.js16
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/readValue/read-updates-value.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/readValue/service-is-removed.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/service-same-from-2-characteristics.https.window.js15
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/service-same-object.https.window.js12
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/startNotifications/detachedIframe.https.window.js33
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/startNotifications/gen-characteristic-is-removed.https.window.js22
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/stopNotifications/detachedIframe.https.window.js33
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValue/buffer-is-detached.https.window.js24
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValue/characteristic-is-removed.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValue/detachedIframe.https.window.js33
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValue/gen-characteristic-is-removed.https.window.js22
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValue/service-is-removed.https.window.js18
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValue/write-succeeds.https.window.js47
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/buffer-is-detached.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/characteristic-is-removed.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/gen-characteristic-is-removed.https.window.js22
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/service-is-removed.https.window.js18
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/write-succeeds.https.window.js47
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/buffer-is-detached.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/characteristic-is-removed.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/gen-characteristic-is-removed.https.window.js22
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/service-is-removed.https.window.js18
-rw-r--r--testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/write-succeeds.https.window.js41
-rw-r--r--testing/web-platform/tests/bluetooth/descriptor/readValue/detachedIframe.https.window.js34
-rw-r--r--testing/web-platform/tests/bluetooth/descriptor/readValue/gen-service-is-removed.https.window.js22
-rw-r--r--testing/web-platform/tests/bluetooth/descriptor/readValue/read-succeeds.https.window.js16
-rw-r--r--testing/web-platform/tests/bluetooth/descriptor/writeValue/buffer-is-detached.https.window.js24
-rw-r--r--testing/web-platform/tests/bluetooth/descriptor/writeValue/detachedIframe.https.window.js34
-rw-r--r--testing/web-platform/tests/bluetooth/descriptor/writeValue/gen-service-is-removed.https.window.js22
-rw-r--r--testing/web-platform/tests/bluetooth/device/forget/connect-after-forget.https.window.js11
-rw-r--r--testing/web-platform/tests/bluetooth/device/forget/detachedIframe.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/device/forget/getDevices.https.window.js18
-rw-r--r--testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/disconnected.https.window.js16
-rw-r--r--testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/disconnected_gc.https.window.js23
-rw-r--r--testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/one-event-per-disconnection.https.window.js30
-rw-r--r--testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/reconnect-during-disconnected-event.https.window.js31
-rw-r--r--testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-before-watchAdvertisements.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-pending-operation.https.window.js22
-rw-r--r--testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-signal-stops-events.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-subsequent-watchAdvertisements-call-stops-events.https.window.js26
-rw-r--r--testing/web-platform/tests/bluetooth/device/watchAdvertisements/advertisementreceived-event-fired.https.window.js25
-rw-r--r--testing/web-platform/tests/bluetooth/device/watchAdvertisements/blocklisted-manufacturer-data-filtered-from-event.https.window.js50
-rw-r--r--testing/web-platform/tests/bluetooth/device/watchAdvertisements/concurrent-watchAdvertisements-calls.https.window.js29
-rw-r--r--testing/web-platform/tests/bluetooth/device/watchAdvertisements/detachedIframe.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/device/watchAdvertisements/service-and-manufacturer-data-filtered-from-event.https.window.js41
-rw-r--r--testing/web-platform/tests/bluetooth/device/watchAdvertisements/subsequent-watchAdvertisements-call.https.window.js25
-rw-r--r--testing/web-platform/tests/bluetooth/device/watchAdvertisements/watching-two-devices-abort-one-watchAdvertisements.https.window.js47
-rw-r--r--testing/web-platform/tests/bluetooth/device/watchAdvertisements/watching-two-devices.https.window.js41
-rw-r--r--testing/web-platform/tests/bluetooth/generate.py189
-rwxr-xr-xtesting/web-platform/tests/bluetooth/generate_test.py56
-rw-r--r--testing/web-platform/tests/bluetooth/getAvailability/reject_opaque_origin.https.html13
-rw-r--r--testing/web-platform/tests/bluetooth/getAvailability/reject_opaque_origin.https.html.headers1
-rw-r--r--testing/web-platform/tests/bluetooth/getAvailability/sandboxed_iframe.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/getDevices/granted-devices-with-services.https.window.js72
-rw-r--r--testing/web-platform/tests/bluetooth/getDevices/no-granted-devices.https.window.js15
-rw-r--r--testing/web-platform/tests/bluetooth/getDevices/reject_opaque_origin.https.html13
-rw-r--r--testing/web-platform/tests/bluetooth/getDevices/reject_opaque_origin.https.html.headers1
-rw-r--r--testing/web-platform/tests/bluetooth/getDevices/returns-same-bluetooth-device-object.https.window.js23
-rw-r--r--testing/web-platform/tests/bluetooth/getDevices/sandboxed_iframe.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/idl/idl-Bluetooth.https.window.js18
-rw-r--r--testing/web-platform/tests/bluetooth/idl/idl-BluetoothDevice.https.window.js37
-rw-r--r--testing/web-platform/tests/bluetooth/idl/idl-BluetoothUUID.window.js177
-rw-r--r--testing/web-platform/tests/bluetooth/idl/idl-NavigatorBluetooth.https.window.js12
-rw-r--r--testing/web-platform/tests/bluetooth/idl/idl-NavigatorBluetooth.window.js7
-rw-r--r--testing/web-platform/tests/bluetooth/idl/idlharness.tentative.https.window.js25
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/device-with-empty-name.https.window.js19
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/device-with-name.https.window.js20
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/optional-services-missing.https.window.js21
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/optional-services-present.https.window.js25
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/blocklisted-manufacturer-data-in-filter.https.window.js29
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/blocklisted-service-in-filter.https.window.js21
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/blocklisted-service-in-optionalServices.https.window.js29
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/data-prefix-and-mask-size.https.window.js22
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/dataPrefix-buffer-is-detached.https.window.js33
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/device-name-longer-than-29-bytes.https.window.js16
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-dataPrefix.https.window.js16
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-filter.https.window.js12
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-filters-member.https.window.js16
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-manufacturerData-member.https.window.js35
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-namePrefix.https.window.js33
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-services-member.https.window.js18
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/filters-xor-acceptAllDevices.https.window.js26
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/invalid-companyIdentifier.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/mask-buffer-is-detached.https.window.js36
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-name-unicode.https.window.js20
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-name.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-namePrefix-unicode.https.window.js20
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-namePrefix.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-name-unicode.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-name.https.window.js15
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-namePrefix-unicode.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-namePrefix.https.window.js15
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/no-arguments.https.window.js12
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/same-company-identifier.https.window.js23
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/unicode-valid-length-name-name.https.window.js18
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/unicode-valid-length-name-namePrefix.https.window.js18
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/wrong-service-in-optionalServices-member.https.window.js37
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/wrong-service-in-services-member.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/cross-origin-iframe.sub.https.window.js28
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/discovery-succeeds.https.window.js31
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/doesnt-consume-user-gesture.https.window.js24
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/filter-matches.https.window.js76
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/le-not-supported.https.window.js15
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/manufacturer-data-filter-matches.https.window.js139
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/name-empty-device-from-name-empty-filter.https.window.js14
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/not-processing-user-gesture.https.window.js18
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/radio-not-present.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/reject_opaque_origin.https.html13
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/reject_opaque_origin.https.html.headers1
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/request-from-iframe.https.window.js43
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/request-from-sandboxed-iframe.https.window.js35
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/same-device.https.window.js19
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/sandboxed_iframe.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/requestDevice/single-filter-single-service.https.window.js14
-rw-r--r--testing/web-platform/tests/bluetooth/requestLEScan/reject_opaque_origin.https.html13
-rw-r--r--testing/web-platform/tests/bluetooth/requestLEScan/reject_opaque_origin.https.html.headers1
-rw-r--r--testing/web-platform/tests/bluetooth/requestLEScan/sandboxed_iframe.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/resources/bluetooth-fake-devices.js1203
-rw-r--r--testing/web-platform/tests/bluetooth/resources/bluetooth-scanning-helpers.js42
-rw-r--r--testing/web-platform/tests/bluetooth/resources/bluetooth-test.js363
-rw-r--r--testing/web-platform/tests/bluetooth/resources/health-thermometer-iframe.html92
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/base_test_js.template7
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/characteristic/characteristic-is-removed.js24
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/characteristic/descriptor-get-same-object.js32
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/characteristic/service-is-removed.js20
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/descriptor/service-is-removed.js18
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/disconnect-called-before.js22
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/disconnect-called-during-error.js22
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/disconnect-called-during-success.js23
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/disconnect-discovery-timeout.js42
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/disconnect-invalidates-objects.js39
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/disconnected-device.js20
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/discovery-complete-no-permission-absent-service.js25
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/discovery-complete-service-not-found.js16
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/garbage-collection-ran-during-error.js25
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/garbage-collection-ran-during-success.js24
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/get-different-service-after-reconnection.js35
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/get-same-object.js33
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/invalid-service-name.js22
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/no-permission-absent-service.js23
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/no-permission-for-any-service.js17
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/no-permission-present-service.js22
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/server/service-not-found.js16
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/service/blocklisted-characteristic.js19
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/service/characteristic-not-found.js15
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/service/garbage-collection-ran-during-error.js24
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/service/get-same-object.js24
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/service/invalid-characteristic-name.js23
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/service/reconnect-during.js36
-rw-r--r--testing/web-platform/tests/bluetooth/script-tests/service/service-is-removed.js20
-rw-r--r--testing/web-platform/tests/bluetooth/server/connect/connection-succeeds.https.window.js13
-rw-r--r--testing/web-platform/tests/bluetooth/server/connect/detachedIframe.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/server/connect/garbage-collection-ran-during-success.https.window.js19
-rw-r--r--testing/web-platform/tests/bluetooth/server/connect/get-same-gatt-server.https.window.js19
-rw-r--r--testing/web-platform/tests/bluetooth/server/device-same-object.https.window.js13
-rw-r--r--testing/web-platform/tests/bluetooth/server/disconnect/connect-disconnect-twice.https.window.js23
-rw-r--r--testing/web-platform/tests/bluetooth/server/disconnect/detach-gc.https.window.js34
-rw-r--r--testing/web-platform/tests/bluetooth/server/disconnect/detachedIframe.https.window.js28
-rw-r--r--testing/web-platform/tests/bluetooth/server/disconnect/disconnect-twice-in-a-row.https.window.js20
-rw-r--r--testing/web-platform/tests/bluetooth/server/disconnect/gc-detach.https.window.js36
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-called-before.https.window.js26
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-called-during-error.https.window.js25
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-called-during-success.https.window.js26
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-discovery-timeout.https.window.js46
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-invalidates-objects.https.window.js43
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnected-device.https.window.js23
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-discovery-complete-no-permission-absent-service.https.window.js29
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-discovery-complete-service-not-found.https.window.js20
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-garbage-collection-ran-during-error.https.window.js28
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-garbage-collection-ran-during-success.https.window.js28
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-get-different-service-after-reconnection.https.window.js39
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-get-same-object.https.window.js37
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-invalid-service-name.https.window.js26
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-no-permission-absent-service.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-no-permission-for-any-service.https.window.js21
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-no-permission-present-service.https.window.js26
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-service-not-found.https.window.js20
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/service-found.https.window.js28
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryService/two-iframes-from-same-origin.https.window.js88
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/blocklisted-services-with-uuid.https.window.js20
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/blocklisted-services.https.window.js22
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/correct-services.https.window.js30
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-before-with-uuid.https.window.js26
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-before.https.window.js26
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-error-with-uuid.https.window.js25
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-error.https.window.js25
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-success-with-uuid.https.window.js26
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-success.https.window.js26
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-discovery-timeout-with-uuid.https.window.js46
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-discovery-timeout.https.window.js46
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-invalidates-objects-with-uuid.https.window.js43
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-invalidates-objects.https.window.js43
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnected-device-with-uuid.https.window.js23
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnected-device.https.window.js23
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-discovery-complete-no-permission-absent-service-with-uuid.https.window.js29
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-discovery-complete-service-not-found-with-uuid.https.window.js20
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-error-with-uuid.https.window.js28
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-error.https.window.js28
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-success-with-uuid.https.window.js28
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-success.https.window.js28
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-different-service-after-reconnection-with-uuid.https.window.js39
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-different-service-after-reconnection.https.window.js39
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-same-object-with-uuid.https.window.js37
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-same-object.https.window.js37
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-invalid-service-name.https.window.js26
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-absent-service-with-uuid.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-for-any-service-with-uuid.https.window.js21
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-for-any-service.https.window.js21
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-present-service-with-uuid.https.window.js26
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-service-not-found-with-uuid.https.window.js20
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/services-found-with-uuid.https.window.js25
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/services-found.https.window.js25
-rw-r--r--testing/web-platform/tests/bluetooth/server/getPrimaryServices/services-not-found.https.window.js15
-rw-r--r--testing/web-platform/tests/bluetooth/service/detachedIframe.https.window.js26
-rw-r--r--testing/web-platform/tests/bluetooth/service/device-same-from-2-services.https.window.js14
-rw-r--r--testing/web-platform/tests/bluetooth/service/device-same-object.https.window.js13
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristic/characteristic-found.https.window.js25
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristic/detachedIframe.https.window.js31
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-blocklisted-characteristic.https.window.js23
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-characteristic-not-found.https.window.js19
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-garbage-collection-ran-during-error.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-get-same-object.https.window.js28
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-invalid-characteristic-name.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-reconnect-during.https.window.js39
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-service-is-removed.https.window.js23
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/blocklisted-characteristics.https.window.js17
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/characteristics-found-with-uuid.https.window.js39
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/characteristics-found.https.window.js41
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/characteristics-not-found.https.window.js15
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-blocklisted-characteristic-with-uuid.https.window.js23
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-characteristic-not-found-with-uuid.https.window.js19
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-garbage-collection-ran-during-error-with-uuid.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-garbage-collection-ran-during-error.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-get-same-object-with-uuid.https.window.js28
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-get-same-object.https.window.js28
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-invalid-characteristic-name.https.window.js27
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-reconnect-during-with-uuid.https.window.js39
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-reconnect-during.https.window.js39
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-service-is-removed-with-uuid.https.window.js23
-rw-r--r--testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-service-is-removed.https.window.js23
268 files changed, 8885 insertions, 0 deletions
diff --git a/testing/web-platform/tests/bluetooth/META.yml b/testing/web-platform/tests/bluetooth/META.yml
new file mode 100644
index 0000000000..607edb6365
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/META.yml
@@ -0,0 +1,5 @@
+spec: https://webbluetoothcg.github.io/web-bluetooth/
+suggested_reviewers:
+ - dougt
+ - odejesush
+ - reillyeon
diff --git a/testing/web-platform/tests/bluetooth/README.md b/testing/web-platform/tests/bluetooth/README.md
new file mode 100644
index 0000000000..027b606e85
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/README.md
@@ -0,0 +1,120 @@
+# Web Bluetooth API Tests
+
+The Web Bluetooth API enables sites to connect to and interact with Bluetooth
+Low Energy devices. Please check the [Web Bluetooth specification] for more
+details.
+
+Web Bluetooth testing relies on the [FakeBluetooth][Web Bluetooth
+Testing] test API which must be provided by browsers under test.
+
+TODO([#485]): Update the links to [FakeBluetooth][Web Bluetooth Testing] to
+point to the [Testing Web Bluetooth specification].
+
+In this test suite `resources/bluetooth-test.js` detects and triggers
+the API to be loaded as needed. This file also contains test helper methods,
+such as for asserting that Bluetooth events are fired in a specific order.
+The `resources/bluetooth-fake-devices.js` contains several helper methods that set
+up fake Bluetooth devices.
+
+[Web Bluetooth specification]: https://WebBluetoothCG.github.io/web-bluetooth
+[Web Bluetooth Testing]:
+https://docs.google.com/document/d/1Nhv_oVDCodd1pEH_jj9k8gF4rPGb_84VYaZ9IG8M_WY/
+[#485]: https://github.com/WebBluetoothCG/web-bluetooth/issues/485
+[Testing Web Bluetooth specification]:
+https://WebBluetoothCG.github.io/web-bluetooth/tests.html
+
+## Generated Tests
+
+Several Web Bluetooth tests share common test logic. For these tests, the
+`script-tests` directory contains templates that are used by the
+`generate.py` script to create several tests from these templates. The templates
+are JavaScript files that contain a `CALLS()` keyword with functions delimited by
+a `|` character. A test will be created for each function in the `CALLS()` by
+`generate.py`. Note that for each subdirectory in `script-tests` there is a
+matching directory under `bluetooth`. The generator will expand `CALLS`
+functions into the
+corresponding directory.
+
+### Example
+
+The `./script-tests/server/get-same-object.js` contains the following
+code:
+
+```js
+gattServer.CALLS([
+ getPrimaryService('heart_rate')|
+ getPrimaryServices()|
+ getPrimaryServices('heart_rate')[UUID]]),
+```
+
+The functions in `CALLS()` will be expanded to generate 3 test files prefixed
+with `gen-`:
+
+```
+bluetooth/server/getPrimaryService/gen-get-same-object.html
+bluetooth/server/getPrimaryServices/gen-get-same-object.html
+bluetooth/server/getPrimaryServices/gen-get-same-object-with-uuid.html
+```
+
+### Generate Tests
+
+To generate the tests in `script-tests`, run the following command from the
+source root:
+
+```sh
+$ python bluetooth/generate.py
+```
+
+To check that generated tests are correct and that there are no obsolete tests,
+or tests for which a template does not exist anymore, run:
+
+```sh
+$ python bluetooth/generate_test.py
+```
+
+More details can be found in `generate.py` and `generate_test.py`.
+
+## Chromium Implementation
+The Chromium implementation is provided by
+`../resources/chromium/web-bluetooth-test.js` using [MojoJS].
+
+The Chromium implementation is not included in stable Chrome builds since it
+would add too much to the binary size. On Chromium infrastructure, it is run
+using the `content_shell` executable.
+
+In the future, Chromium `src/device/bluetooth` may be refactored into a Mojo
+service. At this point, it would be possible to add the necessary testing hooks
+into stable Chrome without substantially increasing the binary size, similar to
+WebUSB.
+
+These Bluetooth tests are upstreamed here because other browsers can reuse them
+by implementing the [Web Bluetooth Testing] API, even if only on their internal
+infrastructure.
+
+For more implementation details, see the [Web Bluetooth Service README].
+
+[MojoJS]: https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/testing/web_platform_tests.md#mojojs
+[Web Bluetooth Service README]:
+https://chromium.googlesource.com/chromium/src.git/+/main/content/browser/bluetooth/README.md
+
+# Resources and Documentation
+
+For any issues pertaining to the specification, please file a [GitHub]
+issue. For issues pertaining to an implementation of Web Bluetooth, please
+file an issue with the implementor's bug tracker.
+
+* [Web Bluetooth specification]
+* [Web Bluetooth Testing]
+* [Testing Web Bluetooth specification]
+
+[GitHub]: https://github.com/WebBluetoothCG/web-bluetooth
+
+## Chromium
+
+Mailing list: web-bluetooth@chromium.org
+
+Bug tracker: [Blink>Bluetooth]
+
+* [Web Bluetooth Service README]
+
+[Blink>Bluetooth]: https://bugs.chromium.org/p/chromium/issues/list?q=component%3ABlink%3EBluetooth&can=2
diff --git a/testing/web-platform/tests/bluetooth/adapter/adapter-absent-getAvailability.https.window.js b/testing/web-platform/tests/bluetooth/adapter/adapter-absent-getAvailability.https.window.js
new file mode 100644
index 0000000000..55f4a675da
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/adapter/adapter-absent-getAvailability.https.window.js
@@ -0,0 +1,15 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'getAvailability() resolves with false if the system does ' +
+ 'not have an adapter.';
+
+bluetooth_test(async () => {
+ await navigator.bluetooth.test.simulateCentral({state: 'absent'});
+ let availability = await navigator.bluetooth.getAvailability();
+ assert_false(
+ availability,
+ 'getAvailability() resolves promise with false when adapter is absent.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/adapter/adapter-added-getAvailability.https.window.js b/testing/web-platform/tests/bluetooth/adapter/adapter-added-getAvailability.https.window.js
new file mode 100644
index 0000000000..f8e25b2ac2
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/adapter/adapter-added-getAvailability.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'getAvailability() resolves with true after adapter is ' +
+ 'inserted into a system with a platform that supports Bluetooth LE.';
+
+bluetooth_test(async () => {
+ const fake_central =
+ await navigator.bluetooth.test.simulateCentral({state: 'absent'});
+ let availability = await navigator.bluetooth.getAvailability();
+ assert_false(
+ availability,
+ 'getAvailability() resolves promise with false when adapter is absent.');
+
+ await fake_central.setState('powered-on');
+ availability = await navigator.bluetooth.getAvailability();
+ assert_true(
+ availability,
+ 'getAvailability() resolves promise with true after Bluetooth LE ' +
+ 'capable adapter has been has been added.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/adapter/adapter-powered-off-getAvailability.https.window.js b/testing/web-platform/tests/bluetooth/adapter/adapter-powered-off-getAvailability.https.window.js
new file mode 100644
index 0000000000..1ffcd3bb09
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/adapter/adapter-powered-off-getAvailability.https.window.js
@@ -0,0 +1,16 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'getAvailability() resolves with true if the Bluetooth ' +
+ 'radio is powered off, but the platform that supports Bluetooth LE.';
+
+bluetooth_test(async () => {
+ await navigator.bluetooth.test.simulateCentral({state: 'powered-off'});
+ let availability = await navigator.bluetooth.getAvailability();
+ assert_true(
+ availability,
+ 'getAvailability() resolves promise with true when adapter is powered ' +
+ 'off.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/adapter/adapter-powered-on-getAvailability.https.window.js b/testing/web-platform/tests/bluetooth/adapter/adapter-powered-on-getAvailability.https.window.js
new file mode 100644
index 0000000000..84c7982d21
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/adapter/adapter-powered-on-getAvailability.https.window.js
@@ -0,0 +1,16 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'getAvailability() resolves with true if the Bluetooth ' +
+ 'radio is powered on and the platform supports Bluetooth LE.';
+
+bluetooth_test(async () => {
+ await navigator.bluetooth.test.simulateCentral({state: 'powered-on'});
+ let availability = await navigator.bluetooth.getAvailability();
+ assert_true(
+ availability,
+ 'getAvailability() resolves promise with true when adapter is powered ' +
+ 'on and it supports Bluetooth Low-Energy.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/adapter/adapter-powered-on-off-on-getAvailability.https.window.js b/testing/web-platform/tests/bluetooth/adapter/adapter-powered-on-off-on-getAvailability.https.window.js
new file mode 100644
index 0000000000..c4ba9b5f3a
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/adapter/adapter-powered-on-off-on-getAvailability.https.window.js
@@ -0,0 +1,37 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'getAvailability() is not affected by the powered state of ' +
+ 'the adapter.';
+
+bluetooth_test(async () => {
+ const fake_central =
+ await navigator.bluetooth.test.simulateCentral({state: 'powered-on'});
+ {
+ const availability = await navigator.bluetooth.getAvailability();
+ assert_true(
+ availability,
+ 'getAvailability() resolves promise with true when adapter is ' +
+ 'powered on and it supports Bluetooth Low-Energy.');
+ }
+
+ {
+ await fake_central.setState('powered-off');
+ const availability = await navigator.bluetooth.getAvailability();
+ assert_true(
+ availability,
+ 'getAvailability() resolves promise with true after adapter powered ' +
+ 'off.');
+ }
+
+ {
+ await fake_central.setState('powered-on');
+ const availability = await navigator.bluetooth.getAvailability();
+ assert_true(
+ availability,
+ 'getAvailability() resolves promise with true when adapter is ' +
+ 'powered back on.');
+ }
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/adapter/adapter-removed-getAvailability.https.window.js b/testing/web-platform/tests/bluetooth/adapter/adapter-removed-getAvailability.https.window.js
new file mode 100644
index 0000000000..ca0b51f47d
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/adapter/adapter-removed-getAvailability.https.window.js
@@ -0,0 +1,24 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'getAvailability() resolves with false after the powered ' +
+ 'on adapter is removed.';
+
+bluetooth_test(async () => {
+ const fake_central =
+ await navigator.bluetooth.test.simulateCentral({state: 'powered-on'});
+ let availability = await navigator.bluetooth.getAvailability();
+ assert_true(
+ availability,
+ 'getAvailability() resolves promise with true when adapter is powered ' +
+ 'on and it supports Bluetooth Low-Energy.');
+
+ await fake_central.setState('absent');
+ availability = await navigator.bluetooth.getAvailability();
+ assert_false(
+ availability,
+ 'getAvailability() resolves promise with false after adapter has been ' +
+ 'has been removed.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/adapter/cross-origin-iframe-getAvailability.sub.https.window.js b/testing/web-platform/tests/bluetooth/adapter/cross-origin-iframe-getAvailability.sub.https.window.js
new file mode 100644
index 0000000000..54abfbb5ce
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/adapter/cross-origin-iframe-getAvailability.sub.https.window.js
@@ -0,0 +1,31 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'getAvailability() resolves with false if called from a ' +
+ 'unique origin';
+const cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' +
+ '/bluetooth/resources/health-thermometer-iframe.html'
+let iframe = document.createElement('iframe');
+
+bluetooth_test(async () => {
+ await navigator.bluetooth.test.simulateCentral({state: 'powered-on'});
+ await new Promise(resolve => {
+ iframe.src = cross_origin_src;
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve);
+ });
+ await new Promise(resolve => {
+ callWithTrustedClick(
+ () => iframe.contentWindow.postMessage({type: 'GetAvailability'}, '*'));
+
+ window.onmessage = messageEvent => {
+ assert_equals(
+ messageEvent.data, false,
+ 'getAvailability resolves to false when called from a unique ' +
+ 'origin.');
+ resolve();
+ };
+ });
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/characteristicProperties.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/characteristicProperties.https.window.js
new file mode 100644
index 0000000000..f7a57a9c4b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/characteristicProperties.https.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'HeartRate device properties';
+
+bluetooth_test(async () => {
+ const {service} = await getHealthThermometerService()
+ const [temperature_measurement, measurement_interval] = await Promise.all([
+ service.getCharacteristic('temperature_measurement'),
+ service.getCharacteristic('measurement_interval')
+ ]);
+ const tm_expected_properties = new TestCharacteristicProperties(['indicate']);
+ assert_properties_equal(
+ temperature_measurement.properties, tm_expected_properties);
+
+ const mi_expected_properties =
+ new TestCharacteristicProperties(['read', 'write', 'indicate']);
+ assert_properties_equal(
+ measurement_interval.properties, mi_expected_properties);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/getDescriptor/detachedIframe.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/getDescriptor/detachedIframe.https.window.js
new file mode 100644
index 0000000000..de7d0b0b7c
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/getDescriptor/detachedIframe.https.window.js
@@ -0,0 +1,33 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ let iframe = document.createElement('iframe');
+ let error;
+
+ const {device, fakes} = await getHealthThermometerDeviceFromIframe(iframe);
+ await fakes.fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_SUCCESS,
+ });
+ let service = await device.gatt.getPrimaryService(health_thermometer.name);
+ let characteristic =
+ await service.getCharacteristic(measurement_interval.name);
+
+ iframe.remove();
+ // Set iframe to null to ensure that the GC cleans up as much as possible.
+ iframe = null;
+ await garbageCollect();
+
+ try {
+ await characteristic.getDescriptor(user_description.name);
+ } catch (e) {
+ // Cannot use promise_rejects_dom() because |e| is thrown from a different
+ // global.
+ error = e;
+ }
+ assert_not_equals(error, undefined);
+ assert_equals(error.name, 'NetworkError');
+}, 'getDescriptor() rejects in a detached context');
diff --git a/testing/web-platform/tests/bluetooth/characteristic/getDescriptor/gen-characteristic-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/getDescriptor/gen-characteristic-is-removed.https.window.js
new file mode 100644
index 0000000000..9e48a7caab
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/getDescriptor/gen-characteristic-is-removed.https.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Characteristic gets removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Characteristic no longer exists.',
+ 'InvalidStateError');
+let fake_peripheral, characteristic, fake_characteristic;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({fake_peripheral, characteristic, fake_characteristic} = _))
+ .then(() => characteristic.getDescriptor(user_description.name))
+ .then(() => null, (e) => assert_unreached('Caught error unexpectedly.', e))
+ .then(() => fake_characteristic.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ characteristic.getDescriptor(user_description.name), expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/characteristic/getDescriptor/gen-descriptor-get-same-object.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/getDescriptor/gen-descriptor-get-same-object.https.window.js
new file mode 100644
index 0000000000..708f67da6a
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/getDescriptor/gen-descriptor-get-same-object.https.window.js
@@ -0,0 +1,36 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Calls to getDescriptor should return the same object.';
+let characteristic;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({characteristic} = _))
+ .then(() => Promise.all([
+ characteristic.getDescriptor(user_description.alias),
+ characteristic.getDescriptor(user_description.name),
+ characteristic.getDescriptor(user_description.uuid)
+ ]))
+ .then(descriptors_arrays => {
+ assert_true(descriptors_arrays.length > 0)
+
+ // Convert to arrays if necessary.
+ for (let i = 0; i < descriptors_arrays.length; i++) {
+ descriptors_arrays[i] = [].concat(descriptors_arrays[i]);
+ }
+
+ for (let i = 1; i < descriptors_arrays.length; i++) {
+ assert_equals(descriptors_arrays[0].length,
+ descriptors_arrays[i].length);
+ }
+
+ let base_set = new Set(descriptors_arrays[0]);
+ for (let descriptors of descriptors_arrays) {
+ descriptors.forEach(descriptor => assert_true(base_set.has(descriptor)));
+ }
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/characteristic/getDescriptor/gen-service-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/getDescriptor/gen-service-is-removed.https.window.js
new file mode 100644
index 0000000000..c256050b0f
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/getDescriptor/gen-service-is-removed.https.window.js
@@ -0,0 +1,24 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+// TODO(https://crbug.com/672127) Use this test case to test the rest of
+// characteristic functions.
+'use strict';
+const test_desc = 'Service is removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Service no longer exists.',
+ 'InvalidStateError');
+let characteristic, fake_peripheral, fake_service;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({characteristic, fake_peripheral, fake_service} = _))
+ .then(() => fake_service.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ characteristic.getDescriptor(user_description.name),
+ expected,
+ 'Service got removed.')),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/detachedIframe.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/detachedIframe.https.window.js
new file mode 100644
index 0000000000..45dd23752f
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/detachedIframe.https.window.js
@@ -0,0 +1,33 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ let iframe = document.createElement('iframe');
+ let error;
+
+ const {device, fakes} = await getHealthThermometerDeviceFromIframe(iframe);
+ await fakes.fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_SUCCESS,
+ });
+ let service = await device.gatt.getPrimaryService(health_thermometer.name);
+ let characteristic =
+ await service.getCharacteristic(measurement_interval.name);
+
+ iframe.remove();
+ // Set iframe to null to ensure that the GC cleans up as much as possible.
+ iframe = null;
+ await garbageCollect();
+
+ try {
+ await characteristic.getDescriptors(user_description.name);
+ } catch (e) {
+ // Cannot use promise_rejects_dom() because |e| is thrown from a different
+ // global.
+ error = e;
+ }
+ assert_not_equals(error, undefined);
+ assert_equals(error.name, 'NetworkError');
+}, 'getDescriptors() rejects in a detached context');
diff --git a/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-characteristic-is-removed-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-characteristic-is-removed-with-uuid.https.window.js
new file mode 100644
index 0000000000..a0424c0110
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-characteristic-is-removed-with-uuid.https.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Characteristic gets removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Characteristic no longer exists.',
+ 'InvalidStateError');
+let fake_peripheral, characteristic, fake_characteristic;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({fake_peripheral, characteristic, fake_characteristic} = _))
+ .then(() => characteristic.getDescriptor(user_description.name))
+ .then(() => null, (e) => assert_unreached('Caught error unexpectedly.', e))
+ .then(() => fake_characteristic.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ characteristic.getDescriptors(user_description.name), expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-characteristic-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-characteristic-is-removed.https.window.js
new file mode 100644
index 0000000000..29325c3bb5
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-characteristic-is-removed.https.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Characteristic gets removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Characteristic no longer exists.',
+ 'InvalidStateError');
+let fake_peripheral, characteristic, fake_characteristic;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({fake_peripheral, characteristic, fake_characteristic} = _))
+ .then(() => characteristic.getDescriptor(user_description.name))
+ .then(() => null, (e) => assert_unreached('Caught error unexpectedly.', e))
+ .then(() => fake_characteristic.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ characteristic.getDescriptors(), expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-descriptor-get-same-object.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-descriptor-get-same-object.https.window.js
new file mode 100644
index 0000000000..7f1001f3ee
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-descriptor-get-same-object.https.window.js
@@ -0,0 +1,36 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Calls to getDescriptors should return the same object.';
+let characteristic;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({characteristic} = _))
+ .then(() => Promise.all([
+ characteristic.getDescriptors(user_description.alias),
+ characteristic.getDescriptors(user_description.name),
+ characteristic.getDescriptors(user_description.uuid)
+ ]))
+ .then(descriptors_arrays => {
+ assert_true(descriptors_arrays.length > 0)
+
+ // Convert to arrays if necessary.
+ for (let i = 0; i < descriptors_arrays.length; i++) {
+ descriptors_arrays[i] = [].concat(descriptors_arrays[i]);
+ }
+
+ for (let i = 1; i < descriptors_arrays.length; i++) {
+ assert_equals(descriptors_arrays[0].length,
+ descriptors_arrays[i].length);
+ }
+
+ let base_set = new Set(descriptors_arrays[0]);
+ for (let descriptors of descriptors_arrays) {
+ descriptors.forEach(descriptor => assert_true(base_set.has(descriptor)));
+ }
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-service-is-removed-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-service-is-removed-with-uuid.https.window.js
new file mode 100644
index 0000000000..b7c4bff32d
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-service-is-removed-with-uuid.https.window.js
@@ -0,0 +1,24 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+// TODO(https://crbug.com/672127) Use this test case to test the rest of
+// characteristic functions.
+'use strict';
+const test_desc = 'Service is removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Service no longer exists.',
+ 'InvalidStateError');
+let characteristic, fake_peripheral, fake_service;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({characteristic, fake_peripheral, fake_service} = _))
+ .then(() => fake_service.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ characteristic.getDescriptors(user_description.uuid),
+ expected,
+ 'Service got removed.')),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-service-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-service-is-removed.https.window.js
new file mode 100644
index 0000000000..22dc30f6d8
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/getDescriptors/gen-service-is-removed.https.window.js
@@ -0,0 +1,24 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+// TODO(https://crbug.com/672127) Use this test case to test the rest of
+// characteristic functions.
+'use strict';
+const test_desc = 'Service is removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Service no longer exists.',
+ 'InvalidStateError');
+let characteristic, fake_peripheral, fake_service;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({characteristic, fake_peripheral, fake_service} = _))
+ .then(() => fake_service.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ characteristic.getDescriptors(user_description.name),
+ expected,
+ 'Service got removed.')),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/characteristic/notifications/characteristic-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/notifications/characteristic-is-removed.https.window.js
new file mode 100644
index 0000000000..9641ad71e9
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/notifications/characteristic-is-removed.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Characteristic is removed. Reject with InvalidStateError.';
+const expected = new DOMException(
+ 'GATT Characteristic no longer exists.', 'InvalidStateError');
+
+bluetooth_test(async () => {
+ const {characteristic, fake_characteristic} =
+ await getMeasurementIntervalCharacteristic();
+ await fake_characteristic.remove();
+ await assert_promise_rejects_with_message(
+ characteristic.startNotifications(), expected,
+ 'Characteristic got removed.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/notifications/service-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/notifications/service-is-removed.https.window.js
new file mode 100644
index 0000000000..a5851fc473
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/notifications/service-is-removed.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Service is removed. Reject with InvalidStateError.';
+const expected =
+ new DOMException('GATT Service no longer exists.', 'InvalidStateError');
+
+bluetooth_test(async () => {
+ const {characteristic, fake_peripheral, fake_service} =
+ await getMeasurementIntervalCharacteristic();
+ await fake_service.remove();
+ await fake_peripheral.simulateGATTServicesChanged();
+ await assert_promise_rejects_with_message(
+ characteristic.startNotifications(), expected, 'Service got removed.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/readValue/add-multiple-event-listeners.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/readValue/add-multiple-event-listeners.https.window.js
new file mode 100644
index 0000000000..0eeafd0b79
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/readValue/add-multiple-event-listeners.https.window.js
@@ -0,0 +1,25 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Add multiple event listeners then readValue().';
+
+bluetooth_test(async () => {
+ const {characteristic, fake_characteristic} =
+ await getMeasurementIntervalCharacteristic();
+ await fake_characteristic.setNextReadResponse(GATT_SUCCESS, [0, 1, 2]);
+
+ // Make sure that |characteristic.readValue()| resolves after
+ // |characteristicvaluechanged| is fired |3| times.
+ const results = await assert_promise_resolves_after_event(
+ characteristic /* object */, 'readValue' /* func */,
+ 'characteristicvaluechanged' /* event */, 3 /* num_listeners */);
+
+ const read_value = new Uint8Array(results[0].buffer);
+ const event_values = results.slice(1).map(v => new Uint8Array(v.buffer));
+ for (const event_value of event_values) {
+ assert_equals(event_value.buffer, read_value.buffer);
+ assert_array_equals(event_value, read_value);
+ }
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/readValue/characteristic-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/readValue/characteristic-is-removed.https.window.js
new file mode 100644
index 0000000000..e97b94f736
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/readValue/characteristic-is-removed.https.window.js
@@ -0,0 +1,16 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Characteristic gets removed. Reject with InvalidStateError.';
+const expected = new DOMException(
+ 'GATT Characteristic no longer exists.', 'InvalidStateError');
+
+bluetooth_test(async () => {
+ const {characteristic, fake_characteristic} =
+ await getMeasurementIntervalCharacteristic();
+ await fake_characteristic.remove();
+ await assert_promise_rejects_with_message(
+ characteristic.readValue(), expected, 'Characteristic got removed.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/readValue/detachedIframe.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/readValue/detachedIframe.https.window.js
new file mode 100644
index 0000000000..6e2510b58d
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/readValue/detachedIframe.https.window.js
@@ -0,0 +1,33 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ let iframe = document.createElement('iframe');
+ let error;
+
+ const {device, fakes} = await getHealthThermometerDeviceFromIframe(iframe);
+ await fakes.fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_SUCCESS,
+ });
+ let service = await device.gatt.getPrimaryService(health_thermometer.name);
+ let characteristic =
+ await service.getCharacteristic(measurement_interval.name);
+
+ iframe.remove();
+ // Set iframe to null to ensure that the GC cleans up as much as possible.
+ iframe = null;
+ await garbageCollect();
+
+ try {
+ await characteristic.readValue();
+ } catch (e) {
+ // Cannot use promise_rejects_dom() because |e| is thrown from a different
+ // global.
+ error = e;
+ }
+ assert_not_equals(error, undefined);
+ assert_equals(error.name, 'NetworkError');
+}, 'readValue() rejects in a detached context');
diff --git a/testing/web-platform/tests/bluetooth/characteristic/readValue/event-is-fired.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/readValue/event-is-fired.https.window.js
new file mode 100644
index 0000000000..52b70e7a08
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/readValue/event-is-fired.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Reading a characteristic should fire an event.';
+
+bluetooth_test(async () => {
+ const {characteristic, fake_characteristic} =
+ await getMeasurementIntervalCharacteristic();
+ await fake_characteristic.setNextReadResponse(GATT_SUCCESS, [0, 1, 2]);
+
+ // Make sure that |characteristic.readValue()| resolves after
+ // |characteristicvaluechanged| is fired.
+ const results = await assert_promise_resolves_after_event(
+ characteristic /* object */, 'readValue' /* func */,
+ 'characteristicvaluechanged' /* event */);
+
+ const read_value = new Uint8Array(results[0].buffer);
+ const event_value = new Uint8Array(results[1].buffer);
+ assert_equals(event_value.buffer, read_value.buffer);
+ assert_array_equals(event_value, read_value);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/readValue/gen-characteristic-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/readValue/gen-characteristic-is-removed.https.window.js
new file mode 100644
index 0000000000..5bee6db107
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/readValue/gen-characteristic-is-removed.https.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Characteristic gets removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Characteristic no longer exists.',
+ 'InvalidStateError');
+let fake_peripheral, characteristic, fake_characteristic;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({fake_peripheral, characteristic, fake_characteristic} = _))
+ .then(() => characteristic.getDescriptor(user_description.name))
+ .then(() => null, (e) => assert_unreached('Caught error unexpectedly.', e))
+ .then(() => fake_characteristic.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ characteristic.readValue(), expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/characteristic/readValue/read-succeeds.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/readValue/read-succeeds.https.window.js
new file mode 100644
index 0000000000..e5ddfb8169
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/readValue/read-succeeds.https.window.js
@@ -0,0 +1,16 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A read request succeeds and returns the characteristic\'s ' +
+ 'value.';
+const EXPECTED_VALUE = [0, 1, 2];
+
+bluetooth_test(async () => {
+ const {characteristic, fake_characteristic} =
+ await getMeasurementIntervalCharacteristic();
+ await fake_characteristic.setNextReadResponse(GATT_SUCCESS, EXPECTED_VALUE);
+ const value = await characteristic.readValue();
+ assert_array_equals(new Uint8Array(value.buffer), EXPECTED_VALUE)
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/readValue/read-updates-value.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/readValue/read-updates-value.https.window.js
new file mode 100644
index 0000000000..bb98aeb18f
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/readValue/read-updates-value.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+const test_desc = 'Succesful read should update characteristic.value';
+const EXPECTED_VALUE = [0, 1, 2];
+
+bluetooth_test(async () => {
+ const {characteristic, fake_characteristic} =
+ await getMeasurementIntervalCharacteristic();
+ assert_equals(characteristic.value, null);
+
+ await fake_characteristic.setNextReadResponse(GATT_SUCCESS, EXPECTED_VALUE);
+ await characteristic.readValue();
+ assert_array_equals(
+ new Uint8Array(characteristic.value.buffer), EXPECTED_VALUE)
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/readValue/service-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/readValue/service-is-removed.https.window.js
new file mode 100644
index 0000000000..1f699ca25e
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/readValue/service-is-removed.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Service gets removed. Reject with InvalidStateError.';
+const expected =
+ new DOMException('GATT Service no longer exists.', 'InvalidStateError');
+
+bluetooth_test(async () => {
+ const {characteristic, fake_peripheral, fake_service} =
+ await getMeasurementIntervalCharacteristic();
+ await fake_service.remove();
+ await fake_peripheral.simulateGATTServicesChanged();
+ await assert_promise_rejects_with_message(
+ characteristic.readValue(), expected, 'Service got removed.')
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/service-same-from-2-characteristics.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/service-same-from-2-characteristics.https.window.js
new file mode 100644
index 0000000000..dafd755fd1
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/service-same-from-2-characteristics.https.window.js
@@ -0,0 +1,15 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Same parent service returned from multiple characteristics.';
+
+bluetooth_test(async () => {
+ const {service} = await getHealthThermometerService();
+ const characteristics = await Promise.all([
+ service.getCharacteristic('measurement_interval'),
+ service.getCharacteristic('temperature_measurement')
+ ]);
+ assert_equals(characteristics[0].service, characteristics[1].service);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/service-same-object.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/service-same-object.https.window.js
new file mode 100644
index 0000000000..01b3a25e35
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/service-same-object.https.window.js
@@ -0,0 +1,12 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = '[SameObject] test for BluetoothRemoteGATTCharacteristic ' +
+ 'service.';
+
+bluetooth_test(async () => {
+ const {characteristic} = await getMeasurementIntervalCharacteristic();
+ assert_equals(characteristic.service, characteristic.service);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/startNotifications/detachedIframe.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/startNotifications/detachedIframe.https.window.js
new file mode 100644
index 0000000000..f09c52d328
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/startNotifications/detachedIframe.https.window.js
@@ -0,0 +1,33 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ let iframe = document.createElement('iframe');
+ let error;
+
+ const {device, fakes} = await getHealthThermometerDeviceFromIframe(iframe);
+ await fakes.fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_SUCCESS,
+ });
+ let service = await device.gatt.getPrimaryService(health_thermometer.name);
+ let characteristic =
+ await service.getCharacteristic(measurement_interval.name);
+
+ iframe.remove();
+ // Set iframe to null to ensure that the GC cleans up as much as possible.
+ iframe = null;
+ await garbageCollect();
+
+ try {
+ await characteristic.startNotifications();
+ } catch (e) {
+ // Cannot use promise_rejects_dom() because |e| is thrown from a different
+ // global.
+ error = e;
+ }
+ assert_not_equals(error, undefined);
+ assert_equals(error.name, 'NetworkError');
+}, 'startNotifications() rejects in a detached context');
diff --git a/testing/web-platform/tests/bluetooth/characteristic/startNotifications/gen-characteristic-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/startNotifications/gen-characteristic-is-removed.https.window.js
new file mode 100644
index 0000000000..c2a23d7f44
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/startNotifications/gen-characteristic-is-removed.https.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Characteristic gets removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Characteristic no longer exists.',
+ 'InvalidStateError');
+let fake_peripheral, characteristic, fake_characteristic;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({fake_peripheral, characteristic, fake_characteristic} = _))
+ .then(() => characteristic.getDescriptor(user_description.name))
+ .then(() => null, (e) => assert_unreached('Caught error unexpectedly.', e))
+ .then(() => fake_characteristic.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ characteristic.startNotifications(), expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/characteristic/stopNotifications/detachedIframe.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/stopNotifications/detachedIframe.https.window.js
new file mode 100644
index 0000000000..a459a5b15d
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/stopNotifications/detachedIframe.https.window.js
@@ -0,0 +1,33 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ let iframe = document.createElement('iframe');
+ let error;
+
+ const {device, fakes} = await getHealthThermometerDeviceFromIframe(iframe);
+ await fakes.fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_SUCCESS,
+ });
+ let service = await device.gatt.getPrimaryService(health_thermometer.name);
+ let characteristic =
+ await service.getCharacteristic(measurement_interval.name);
+
+ iframe.remove();
+ // Set iframe to null to ensure that the GC cleans up as much as possible.
+ iframe = null;
+ await garbageCollect();
+
+ try {
+ await characteristic.stopNotifications();
+ } catch (e) {
+ // Cannot use promise_rejects_dom() because |e| is thrown from a different
+ // global.
+ error = e;
+ }
+ assert_not_equals(error, undefined);
+ assert_equals(error.name, 'NetworkError');
+}, 'stopNotifications() rejects in a detached context');
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValue/buffer-is-detached.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValue/buffer-is-detached.https.window.js
new file mode 100644
index 0000000000..5d707775e1
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValue/buffer-is-detached.https.window.js
@@ -0,0 +1,24 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'writeValue() fails when passed a detached buffer';
+
+function detachBuffer(buffer) {
+ window.postMessage('', '*', [buffer]);
+}
+
+bluetooth_test(async (t) => {
+ const {characteristic} = await getMeasurementIntervalCharacteristic();
+
+ const typed_array = Uint8Array.of(1, 2);
+ detachBuffer(typed_array.buffer);
+ await promise_rejects_dom(
+ t, 'InvalidStateError', characteristic.writeValue(typed_array));
+
+ const array_buffer = Uint8Array.of(3, 4).buffer;
+ detachBuffer(array_buffer);
+ await promise_rejects_dom(
+ t, 'InvalidStateError', characteristic.writeValue(array_buffer));
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValue/characteristic-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValue/characteristic-is-removed.https.window.js
new file mode 100644
index 0000000000..6e9da8802c
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValue/characteristic-is-removed.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Characteristic gets removed. Reject with InvalidStateError.';
+const expected = new DOMException(
+ 'GATT Characteristic no longer exists.', 'InvalidStateError');
+
+bluetooth_test(async () => {
+ const {characteristic, fake_characteristic} =
+ await getMeasurementIntervalCharacteristic();
+ await fake_characteristic.remove();
+ await assert_promise_rejects_with_message(
+ characteristic.writeValue(new ArrayBuffer(1 /* length */)), expected,
+ 'Characteristic got removed.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValue/detachedIframe.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValue/detachedIframe.https.window.js
new file mode 100644
index 0000000000..eb243a3508
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValue/detachedIframe.https.window.js
@@ -0,0 +1,33 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ let iframe = document.createElement('iframe');
+ let error;
+
+ const {device, fakes} = await getHealthThermometerDeviceFromIframe(iframe);
+ await fakes.fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_SUCCESS,
+ });
+ let service = await device.gatt.getPrimaryService(health_thermometer.name);
+ let characteristic =
+ await service.getCharacteristic(measurement_interval.name);
+
+ iframe.remove();
+ // Set iframe to null to ensure that the GC cleans up as much as possible.
+ iframe = null;
+ await garbageCollect();
+
+ try {
+ await characteristic.writeValue(new DataView(new ArrayBuffer(2)));
+ } catch (e) {
+ // Cannot use promise_rejects_dom() because |e| is thrown from a different
+ // global.
+ error = e;
+ }
+ assert_not_equals(error, undefined);
+ assert_equals(error.name, 'NetworkError');
+}, 'writeValue() rejects in a detached context');
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValue/gen-characteristic-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValue/gen-characteristic-is-removed.https.window.js
new file mode 100644
index 0000000000..5750cb82c7
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValue/gen-characteristic-is-removed.https.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Characteristic gets removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Characteristic no longer exists.',
+ 'InvalidStateError');
+let fake_peripheral, characteristic, fake_characteristic;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({fake_peripheral, characteristic, fake_characteristic} = _))
+ .then(() => characteristic.getDescriptor(user_description.name))
+ .then(() => null, (e) => assert_unreached('Caught error unexpectedly.', e))
+ .then(() => fake_characteristic.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ characteristic.writeValue(new Uint8Array(1)), expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValue/service-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValue/service-is-removed.https.window.js
new file mode 100644
index 0000000000..89c3112475
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValue/service-is-removed.https.window.js
@@ -0,0 +1,18 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Service gets removed. Reject with InvalidStateError.';
+const expected =
+ new DOMException('GATT Service no longer exists.', 'InvalidStateError');
+
+bluetooth_test(async () => {
+ const {characteristic, fake_peripheral, fake_service} =
+ await getMeasurementIntervalCharacteristic();
+ await fake_service.remove();
+ await fake_peripheral.simulateGATTServicesChanged();
+ await assert_promise_rejects_with_message(
+ characteristic.writeValue(new ArrayBuffer(1 /* length */)), expected,
+ 'Service got removed.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValue/write-succeeds.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValue/write-succeeds.https.window.js
new file mode 100644
index 0000000000..b57fe941d0
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValue/write-succeeds.https.window.js
@@ -0,0 +1,47 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A regular write request to a writable characteristic ' +
+ 'should succeed.';
+
+bluetooth_test(async () => {
+ const {characteristic, fake_characteristic} =
+ await getMeasurementIntervalCharacteristic();
+
+ let lastValue, lastWriteType;
+ ({lastValue, lastWriteType} =
+ await fake_characteristic.getLastWrittenValue());
+ assert_equals(lastValue, null);
+ assert_equals(lastWriteType, 'none');
+
+ await fake_characteristic.setNextWriteResponse(GATT_SUCCESS);
+
+ const typed_array = Uint8Array.of(1, 2);
+ await characteristic.writeValue(typed_array);
+ ({lastValue, lastWriteType} =
+ await fake_characteristic.getLastWrittenValue());
+ assert_array_equals(lastValue, [1, 2]);
+ assert_equals(lastWriteType, 'default-deprecated');
+
+ await fake_characteristic.setNextWriteResponse(GATT_SUCCESS);
+
+ const array_buffer = Uint8Array.of(3, 4).buffer;
+ await characteristic.writeValue(array_buffer);
+ ({lastValue, lastWriteType} =
+ await fake_characteristic.getLastWrittenValue());
+ assert_array_equals(lastValue, [3, 4]);
+ assert_equals(lastWriteType, 'default-deprecated');
+
+ await fake_characteristic.setNextWriteResponse(GATT_SUCCESS);
+
+ const data_view = new DataView(new ArrayBuffer(2));
+ data_view.setUint8(0, 5);
+ data_view.setUint8(1, 6);
+ await characteristic.writeValue(data_view);
+ ({lastValue, lastWriteType} =
+ await fake_characteristic.getLastWrittenValue());
+ assert_array_equals(lastValue, [5, 6]);
+ assert_equals(lastWriteType, 'default-deprecated');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/buffer-is-detached.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/buffer-is-detached.https.window.js
new file mode 100644
index 0000000000..ebd8aefeca
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/buffer-is-detached.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc =
+ 'writeValueWithResponse() fails when passed a detached buffer';
+
+function detachBuffer(buffer) {
+ window.postMessage('', '*', [buffer]);
+}
+
+bluetooth_test(async (t) => {
+ const {characteristic} = await getMeasurementIntervalCharacteristic();
+
+ const typed_array = Uint8Array.of(1, 2);
+ detachBuffer(typed_array.buffer);
+ await promise_rejects_dom(
+ t, 'InvalidStateError',
+ characteristic.writeValueWithResponse(typed_array));
+
+ const array_buffer = Uint8Array.of(3, 4).buffer;
+ detachBuffer(array_buffer);
+ await promise_rejects_dom(
+ t, 'InvalidStateError',
+ characteristic.writeValueWithResponse(array_buffer));
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/characteristic-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/characteristic-is-removed.https.window.js
new file mode 100644
index 0000000000..9309cd5a3c
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/characteristic-is-removed.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Characteristic gets removed. Reject with InvalidStateError.';
+const expected = new DOMException(
+ 'GATT Characteristic no longer exists.', 'InvalidStateError');
+
+bluetooth_test(async () => {
+ const {characteristic, fake_characteristic} =
+ await getMeasurementIntervalCharacteristic();
+ await fake_characteristic.remove();
+ await assert_promise_rejects_with_message(
+ characteristic.writeValueWithResponse(new ArrayBuffer(1 /* length */)),
+ expected, 'Characteristic got removed.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/gen-characteristic-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/gen-characteristic-is-removed.https.window.js
new file mode 100644
index 0000000000..e202376da7
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/gen-characteristic-is-removed.https.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Characteristic gets removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Characteristic no longer exists.',
+ 'InvalidStateError');
+let fake_peripheral, characteristic, fake_characteristic;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({fake_peripheral, characteristic, fake_characteristic} = _))
+ .then(() => characteristic.getDescriptor(user_description.name))
+ .then(() => null, (e) => assert_unreached('Caught error unexpectedly.', e))
+ .then(() => fake_characteristic.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ characteristic.writeValueWithResponse(new Uint8Array(1)), expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/service-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/service-is-removed.https.window.js
new file mode 100644
index 0000000000..81b2dff44e
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/service-is-removed.https.window.js
@@ -0,0 +1,18 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Service gets removed. Reject with InvalidStateError.';
+const expected =
+ new DOMException('GATT Service no longer exists.', 'InvalidStateError');
+
+bluetooth_test(async () => {
+ const {characteristic, fake_peripheral, fake_service} =
+ await getMeasurementIntervalCharacteristic();
+ await fake_service.remove();
+ await fake_peripheral.simulateGATTServicesChanged();
+ await assert_promise_rejects_with_message(
+ characteristic.writeValueWithResponse(new ArrayBuffer(1 /* length */)),
+ expected, 'Service got removed.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/write-succeeds.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/write-succeeds.https.window.js
new file mode 100644
index 0000000000..c87e7ac6ab
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithResponse/write-succeeds.https.window.js
@@ -0,0 +1,47 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A regular write request to a writable characteristic ' +
+ 'should succeed.';
+
+bluetooth_test(async () => {
+ const {characteristic, fake_characteristic} =
+ await getMeasurementIntervalCharacteristic();
+
+ let lastValue, lastWriteType;
+ ({lastValue, lastWriteType} =
+ await fake_characteristic.getLastWrittenValue());
+ assert_equals(lastValue, null);
+ assert_equals(lastWriteType, 'none');
+
+ await fake_characteristic.setNextWriteResponse(GATT_SUCCESS);
+
+ const typed_array = Uint8Array.of(1, 2);
+ await characteristic.writeValueWithResponse(typed_array);
+ ({lastValue, lastWriteType} =
+ await fake_characteristic.getLastWrittenValue());
+ assert_array_equals(lastValue, [1, 2]);
+ assert_equals(lastWriteType, 'with-response');
+
+ await fake_characteristic.setNextWriteResponse(GATT_SUCCESS);
+
+ const array_buffer = Uint8Array.of(3, 4).buffer;
+ await characteristic.writeValueWithResponse(array_buffer);
+ ({lastValue, lastWriteType} =
+ await fake_characteristic.getLastWrittenValue());
+ assert_array_equals(lastValue, [3, 4]);
+ assert_equals(lastWriteType, 'with-response');
+
+ await fake_characteristic.setNextWriteResponse(GATT_SUCCESS);
+
+ const data_view = new DataView(new ArrayBuffer(2));
+ data_view.setUint8(0, 5);
+ data_view.setUint8(1, 6);
+ await characteristic.writeValueWithResponse(data_view);
+ ({lastValue, lastWriteType} =
+ await fake_characteristic.getLastWrittenValue());
+ assert_array_equals(lastValue, [5, 6]);
+ assert_equals(lastWriteType, 'with-response');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/buffer-is-detached.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/buffer-is-detached.https.window.js
new file mode 100644
index 0000000000..75d4cf0805
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/buffer-is-detached.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc =
+ 'writeValueWithoutResponse() fails when passed a detached buffer';
+
+function detachBuffer(buffer) {
+ window.postMessage('', '*', [buffer]);
+}
+
+bluetooth_test(async (t) => {
+ const {characteristic} = await getMeasurementIntervalCharacteristic();
+
+ const typed_array = Uint8Array.of(1, 2);
+ detachBuffer(typed_array.buffer);
+ await promise_rejects_dom(
+ t, 'InvalidStateError',
+ characteristic.writeValueWithoutResponse(typed_array));
+
+ const array_buffer = Uint8Array.of(3, 4).buffer;
+ detachBuffer(array_buffer);
+ await promise_rejects_dom(
+ t, 'InvalidStateError',
+ characteristic.writeValueWithoutResponse(array_buffer));
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/characteristic-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/characteristic-is-removed.https.window.js
new file mode 100644
index 0000000000..8d3ed1f663
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/characteristic-is-removed.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Characteristic gets removed. Reject with InvalidStateError.';
+const expected = new DOMException(
+ 'GATT Characteristic no longer exists.', 'InvalidStateError');
+
+bluetooth_test(async () => {
+ const {characteristic, fake_characteristic} =
+ await getMeasurementIntervalCharacteristic();
+ await fake_characteristic.remove();
+ await assert_promise_rejects_with_message(
+ characteristic.writeValueWithoutResponse(new ArrayBuffer(1 /* length */)),
+ expected, 'Characteristic got removed.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/gen-characteristic-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/gen-characteristic-is-removed.https.window.js
new file mode 100644
index 0000000000..b88246aae8
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/gen-characteristic-is-removed.https.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Characteristic gets removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Characteristic no longer exists.',
+ 'InvalidStateError');
+let fake_peripheral, characteristic, fake_characteristic;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({fake_peripheral, characteristic, fake_characteristic} = _))
+ .then(() => characteristic.getDescriptor(user_description.name))
+ .then(() => null, (e) => assert_unreached('Caught error unexpectedly.', e))
+ .then(() => fake_characteristic.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ characteristic.writeValueWithoutResponse(new Uint8Array(1)), expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/service-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/service-is-removed.https.window.js
new file mode 100644
index 0000000000..feb711c64e
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/service-is-removed.https.window.js
@@ -0,0 +1,18 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Service gets removed. Reject with InvalidStateError.';
+const expected =
+ new DOMException('GATT Service no longer exists.', 'InvalidStateError');
+
+bluetooth_test(async () => {
+ const {characteristic, fake_peripheral, fake_service} =
+ await getMeasurementIntervalCharacteristic();
+ await fake_service.remove();
+ await fake_peripheral.simulateGATTServicesChanged();
+ await assert_promise_rejects_with_message(
+ characteristic.writeValueWithoutResponse(new ArrayBuffer(1 /* length */)),
+ expected, 'Service got removed.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/write-succeeds.https.window.js b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/write-succeeds.https.window.js
new file mode 100644
index 0000000000..0dcf8ad0b1
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/characteristic/writeValueWithoutResponse/write-succeeds.https.window.js
@@ -0,0 +1,41 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A regular write request to a writable characteristic ' +
+ 'should succeed.';
+
+bluetooth_test(async () => {
+ const {characteristic, fake_characteristic} =
+ await getMeasurementIntervalCharacteristic();
+
+ let lastValue, lastWriteType;
+ ({lastValue, lastWriteType} =
+ await fake_characteristic.getLastWrittenValue());
+ assert_equals(lastValue, null);
+ assert_equals(lastWriteType, 'none');
+
+ const typed_array = Uint8Array.of(1, 2);
+ await characteristic.writeValueWithoutResponse(typed_array);
+ ({lastValue, lastWriteType} =
+ await fake_characteristic.getLastWrittenValue());
+ assert_array_equals(lastValue, [1, 2]);
+ assert_equals(lastWriteType, 'without-response');
+
+ const array_buffer = Uint8Array.of(3, 4).buffer;
+ await characteristic.writeValueWithoutResponse(array_buffer);
+ ({lastValue, lastWriteType} =
+ await fake_characteristic.getLastWrittenValue());
+ assert_array_equals(lastValue, [3, 4]);
+ assert_equals(lastWriteType, 'without-response');
+
+ const data_view = new DataView(new ArrayBuffer(2));
+ data_view.setUint8(0, 5);
+ data_view.setUint8(1, 6);
+ await characteristic.writeValueWithoutResponse(data_view);
+ ({lastValue, lastWriteType} =
+ await fake_characteristic.getLastWrittenValue());
+ assert_array_equals(lastValue, [5, 6]);
+ assert_equals(lastWriteType, 'without-response');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/descriptor/readValue/detachedIframe.https.window.js b/testing/web-platform/tests/bluetooth/descriptor/readValue/detachedIframe.https.window.js
new file mode 100644
index 0000000000..47765a1315
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/descriptor/readValue/detachedIframe.https.window.js
@@ -0,0 +1,34 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ let iframe = document.createElement('iframe');
+ let error;
+
+ const {device, fakes} = await getHealthThermometerDeviceFromIframe(iframe);
+ await fakes.fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_SUCCESS,
+ });
+ let service = await device.gatt.getPrimaryService(health_thermometer.name);
+ let characteristic =
+ await service.getCharacteristic(measurement_interval.name);
+ let descriptor = await characteristic.getDescriptor(user_description.name);
+
+ iframe.remove();
+ // Set iframe to null to ensure that the GC cleans up as much as possible.
+ iframe = null;
+ await garbageCollect();
+
+ try {
+ await descriptor.readValue();
+ } catch (e) {
+ // Cannot use promise_rejects_dom() because |e| is thrown from a different
+ // global.
+ error = e;
+ }
+ assert_not_equals(error, undefined);
+ assert_equals(error.name, 'NetworkError');
+}, 'readValue() rejects in a detached context');
diff --git a/testing/web-platform/tests/bluetooth/descriptor/readValue/gen-service-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/descriptor/readValue/gen-service-is-removed.https.window.js
new file mode 100644
index 0000000000..d6c73ba60e
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/descriptor/readValue/gen-service-is-removed.https.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Service gets removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Service no longer exists.',
+ 'InvalidStateError');
+let descriptor, fake_peripheral, fake_service;
+
+bluetooth_test(() => getUserDescriptionDescriptor()
+ .then(_ => ({descriptor, fake_peripheral, fake_service} = _))
+ .then(() => fake_service.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ descriptor.readValue(),
+ expected,
+ 'Service got removed.')),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/descriptor/readValue/read-succeeds.https.window.js b/testing/web-platform/tests/bluetooth/descriptor/readValue/read-succeeds.https.window.js
new file mode 100644
index 0000000000..d81db2f8c0
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/descriptor/readValue/read-succeeds.https.window.js
@@ -0,0 +1,16 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = `A read request succeeds and returns the descriptor's value.`;
+
+bluetooth_test(async () => {
+ const {descriptor, fake_descriptor} = await getUserDescriptionDescriptor();
+
+ const EXPECTED_VALUE = [0, 1, 2];
+ await fake_descriptor.setNextReadResponse(GATT_SUCCESS, EXPECTED_VALUE);
+
+ const value = await descriptor.readValue();
+ assert_array_equals(Array.from(new Uint8Array(value.buffer)), EXPECTED_VALUE);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/descriptor/writeValue/buffer-is-detached.https.window.js b/testing/web-platform/tests/bluetooth/descriptor/writeValue/buffer-is-detached.https.window.js
new file mode 100644
index 0000000000..49daf7cf86
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/descriptor/writeValue/buffer-is-detached.https.window.js
@@ -0,0 +1,24 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'writeValue() fails when passed a detached buffer';
+
+function detachBuffer(buffer) {
+ window.postMessage('', '*', [buffer]);
+}
+
+bluetooth_test(async (t) => {
+ const {descriptor, fake_descriptor} = await getUserDescriptionDescriptor();
+
+ const typed_array = Uint8Array.of(1, 2);
+ detachBuffer(typed_array.buffer);
+ await promise_rejects_dom(
+ t, 'InvalidStateError', descriptor.writeValue(typed_array));
+
+ const array_buffer = Uint8Array.of(3, 4).buffer;
+ detachBuffer(array_buffer);
+ await promise_rejects_dom(
+ t, 'InvalidStateError', descriptor.writeValue(array_buffer));
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/descriptor/writeValue/detachedIframe.https.window.js b/testing/web-platform/tests/bluetooth/descriptor/writeValue/detachedIframe.https.window.js
new file mode 100644
index 0000000000..aa143ca8e5
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/descriptor/writeValue/detachedIframe.https.window.js
@@ -0,0 +1,34 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ let iframe = document.createElement('iframe');
+ let error;
+
+ const {device, fakes} = await getHealthThermometerDeviceFromIframe(iframe);
+ await fakes.fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_SUCCESS,
+ });
+ let service = await device.gatt.getPrimaryService(health_thermometer.name);
+ let characteristic =
+ await service.getCharacteristic(measurement_interval.name);
+ let descriptor = await characteristic.getDescriptor(user_description.name);
+
+ iframe.remove();
+ // Set iframe to null to ensure that the GC cleans up as much as possible.
+ iframe = null;
+ await garbageCollect();
+
+ try {
+ await descriptor.writeValue(new ArrayBuffer(1 /* length */));
+ } catch (e) {
+ // Cannot use promise_rejects_dom() because |e| is thrown from a different
+ // global.
+ error = e;
+ }
+ assert_not_equals(error, undefined);
+ assert_equals(error.name, 'NetworkError');
+}, 'writeValue() rejects in a detached context');
diff --git a/testing/web-platform/tests/bluetooth/descriptor/writeValue/gen-service-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/descriptor/writeValue/gen-service-is-removed.https.window.js
new file mode 100644
index 0000000000..c7f6d6efe3
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/descriptor/writeValue/gen-service-is-removed.https.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Service gets removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Service no longer exists.',
+ 'InvalidStateError');
+let descriptor, fake_peripheral, fake_service;
+
+bluetooth_test(() => getUserDescriptionDescriptor()
+ .then(_ => ({descriptor, fake_peripheral, fake_service} = _))
+ .then(() => fake_service.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ descriptor.writeValue(new ArrayBuffer(1 /* length */)),
+ expected,
+ 'Service got removed.')),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/device/forget/connect-after-forget.https.window.js b/testing/web-platform/tests/bluetooth/device/forget/connect-after-forget.https.window.js
new file mode 100644
index 0000000000..0b15b4d060
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/forget/connect-after-forget.https.window.js
@@ -0,0 +1,11 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async (t) => {
+ const { device } = await getConnectedHealthThermometerDevice();
+ await device.forget();
+
+ await promise_rejects_dom(t, 'SecurityError', device.gatt.connect());
+}, 'gatt.connect() rejects after forget().');
diff --git a/testing/web-platform/tests/bluetooth/device/forget/detachedIframe.https.window.js b/testing/web-platform/tests/bluetooth/device/forget/detachedIframe.https.window.js
new file mode 100644
index 0000000000..f4803542fb
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/forget/detachedIframe.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ let iframe = document.createElement('iframe');
+ let error;
+
+ const {device} = await getHealthThermometerDeviceFromIframe(iframe);
+
+ iframe.remove();
+ // Set iframe to null to ensure that the GC cleans up as much as possible.
+ iframe = null;
+ await garbageCollect();
+
+ try {
+ await device.forget();
+ } catch (e) {
+ // Cannot use promise_rejects_dom() because |e| is thrown from a different
+ // global.
+ error = e;
+ }
+ assert_not_equals(error, undefined);
+ assert_equals(error.name, 'TypeError');
+}, 'forget() rejects in a detached context');
diff --git a/testing/web-platform/tests/bluetooth/device/forget/getDevices.https.window.js b/testing/web-platform/tests/bluetooth/device/forget/getDevices.https.window.js
new file mode 100644
index 0000000000..e9ce656319
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/forget/getDevices.https.window.js
@@ -0,0 +1,18 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ await getConnectedHealthThermometerDevice();
+ const devicesBeforeForget = await navigator.bluetooth.getDevices();
+ assert_equals(
+ devicesBeforeForget.length, 1, 'getDevices() should return the granted device.');
+
+ const device = devicesBeforeForget[0];
+ await device.forget();
+ const devicesAfterForget = await navigator.bluetooth.getDevices();
+ assert_equals(
+ devicesAfterForget.length, 0,
+ 'getDevices() is empty after device.forget().');
+}, 'forget() removes devices from getDevices().');
diff --git a/testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/disconnected.https.window.js b/testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/disconnected.https.window.js
new file mode 100644
index 0000000000..43a11a88cb
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/disconnected.https.window.js
@@ -0,0 +1,16 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A device disconnecting while connected should fire the ' +
+ 'gattserverdisconnected event.';
+
+bluetooth_test(async () => {
+ const {device, fake_peripheral} = await getConnectedHealthThermometerDevice();
+ const disconnectPromise = eventPromise(device, 'gattserverdisconnected');
+
+ await fake_peripheral.simulateGATTDisconnection();
+ let disconnectEvent = await disconnectPromise;
+ assert_true(disconnectEvent.bubbles);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/disconnected_gc.https.window.js b/testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/disconnected_gc.https.window.js
new file mode 100644
index 0000000000..0cf4973e21
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/disconnected_gc.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A device disconnecting after the BluetoothDevice object ' +
+ 'has been GC\'ed should not access freed memory.';
+
+bluetooth_test(async () => {
+ let {fake_peripheral} = await getConnectedHealthThermometerDevice();
+
+ // 1. Disconnect.
+ await fake_peripheral.simulateGATTDisconnection();
+
+ // 2. Run garbage collection.
+ fake_peripheral = undefined;
+ await garbageCollect();
+
+ // 3. Wait 50ms after the GC runs for the disconnection event to come back.
+ // There's nothing to assert other than that only valid memory is used.
+ await new Promise(resolve => step_timeout(resolve, 50));
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/one-event-per-disconnection.https.window.js b/testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/one-event-per-disconnection.https.window.js
new file mode 100644
index 0000000000..ab273adbc8
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/one-event-per-disconnection.https.window.js
@@ -0,0 +1,30 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'If a site disconnects from a device while the platform is ' +
+ 'disconnecting that device, only one gattserverdisconnected event should ' +
+ 'fire.';
+
+bluetooth_test(async () => {
+ const {device, fake_peripheral} = await getConnectedHealthThermometerDevice();
+ let num_events = 0;
+
+ // 1. Listen for disconnections.
+ device.addEventListener('gattserverdisconnected', () => num_events++);
+
+ // 2. Disconnect several times.
+ await Promise.all([
+ eventPromise(device, 'gattserverdisconnected'),
+ fake_peripheral.simulateGATTDisconnection(),
+ device.gatt.disconnect(),
+ device.gatt.disconnect(),
+ ]);
+
+ // 3. Wait to catch disconnect events.
+ await new Promise(resolve => step_timeout(resolve, 50));
+
+ // 4. Ensure there is exactly 1 disconnection recorded.
+ assert_equals(num_events, 1);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/reconnect-during-disconnected-event.https.window.js b/testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/reconnect-during-disconnected-event.https.window.js
new file mode 100644
index 0000000000..bdaf47c661
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/gattserverdisconnected-event/reconnect-during-disconnected-event.https.window.js
@@ -0,0 +1,31 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A device that reconnects during the ' +
+ 'gattserverdisconnected event should still receive ' +
+ 'gattserverdisconnected events after re-connection.';
+
+bluetooth_test(async () => {
+ const {device, fake_peripheral} = await getConnectedHealthThermometerDevice();
+
+ const reconnectPromise = new Promise(async (resolve) => {
+ device.addEventListener('gattserverdisconnected', async () => {
+ // 2. Reconnect.
+ await fake_peripheral.setNextGATTConnectionResponse({
+ code: HCI_SUCCESS,
+ });
+ await device.gatt.connect();
+
+ // 3. Disconnect after reconnecting.
+ const disconnectPromise = eventPromise(device, 'gattserverdisconnected');
+ fake_peripheral.simulateGATTDisconnection();
+ resolve(disconnectPromise);
+ }, {once: true});
+ });
+
+ // 1. Disconnect.
+ await fake_peripheral.simulateGATTDisconnection();
+ await reconnectPromise;
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-before-watchAdvertisements.https.window.js b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-before-watchAdvertisements.https.window.js
new file mode 100644
index 0000000000..e1ac1fb136
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-before-watchAdvertisements.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'watchAdvertisements() rejects if passed an aborted signal.';
+
+bluetooth_test(async (t) => {
+ let {device} = await getDiscoveredHealthThermometerDevice();
+ let abortController = new AbortController();
+ abortController.abort();
+
+ await promise_rejects_dom(
+ t, 'AbortError',
+ device.watchAdvertisements({signal: abortController.signal}));
+ assert_false(device.watchingAdvertisements);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-pending-operation.https.window.js b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-pending-operation.https.window.js
new file mode 100644
index 0000000000..c1022ff4a9
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-pending-operation.https.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'AbortController stops a pending watchAdvertisements() ' +
+ 'operation.';
+
+bluetooth_test(async (t) => {
+ let {device} = await getDiscoveredHealthThermometerDevice();
+ const watcher = new EventWatcher(t, device, ['advertisementreceived']);
+ let abortController = new AbortController();
+
+ let watchAdvertisementsPromise =
+ device.watchAdvertisements({signal: abortController.signal});
+ abortController.abort();
+ assert_false(device.watchingAdvertisements);
+ await promise_rejects_dom(t, 'AbortError', watchAdvertisementsPromise);
+
+ await fake_central.simulateAdvertisementReceived(
+ health_thermometer_ad_packet);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-signal-stops-events.https.window.js b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-signal-stops-events.https.window.js
new file mode 100644
index 0000000000..21b6883fee
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-signal-stops-events.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = `AbortController stops 'advertisementreceived' ` +
+ `events from being fired on the device object.`;
+
+bluetooth_test(async (t) => {
+ let {device} = await getDiscoveredHealthThermometerDevice();
+ const watcher = new EventWatcher(t, device, ['advertisementreceived']);
+ let abortController = new AbortController();
+
+ await device.watchAdvertisements({signal: abortController.signal});
+ assert_true(device.watchingAdvertisements);
+
+ let advertisementreceivedPromise = watcher.wait_for('advertisementreceived');
+ await fake_central.simulateAdvertisementReceived(
+ health_thermometer_ad_packet);
+ await advertisementreceivedPromise;
+
+ abortController.abort();
+ assert_false(device.watchingAdvertisements);
+
+ await fake_central.simulateAdvertisementReceived(
+ health_thermometer_ad_packet);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-subsequent-watchAdvertisements-call-stops-events.https.window.js b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-subsequent-watchAdvertisements-call-stops-events.https.window.js
new file mode 100644
index 0000000000..a5da75012b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/abort-subsequent-watchAdvertisements-call-stops-events.https.window.js
@@ -0,0 +1,26 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'AbortController on subsequent watchAdvertisements() call ' +
+ 'cancels the watch advertisements operation.';
+
+bluetooth_test(async (t) => {
+ let {device} = await getDiscoveredHealthThermometerDevice();
+ const watcher = new EventWatcher(t, device, ['advertisementreceived']);
+
+ // Start a watchAdvertisements() operation.
+ await device.watchAdvertisements();
+ assert_true(device.watchingAdvertisements);
+
+ // Start a second watchAdvertisements() operation after the first one
+ // resolves. This operation should resolve successfully.
+ let abortController = new AbortController();
+ await device.watchAdvertisements({signal: abortController.signal});
+ abortController.abort();
+ assert_false(device.watchingAdvertisements);
+
+ // This advertisement packet should not cause the event to be fired.
+ await fake_central.simulateAdvertisementReceived(heart_rate_ad_packet);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/device/watchAdvertisements/advertisementreceived-event-fired.https.window.js b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/advertisementreceived-event-fired.https.window.js
new file mode 100644
index 0000000000..fff18bc47e
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/advertisementreceived-event-fired.https.window.js
@@ -0,0 +1,25 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = `watchAdvertisements() enables 'advertisementreceived' ` +
+ `events to be fired on the device object.`;
+
+bluetooth_test(async (t) => {
+ let {device} = await getDiscoveredHealthThermometerDevice();
+ const watcher = new EventWatcher(t, device, ['advertisementreceived']);
+
+ await device.watchAdvertisements();
+ assert_true(device.watchingAdvertisements);
+
+ // This advertisement packet represents a different device and should not
+ // cause an event to be fired on |device|.
+ await fake_central.simulateAdvertisementReceived(heart_rate_ad_packet);
+
+ let advertisementreceivedPromise = watcher.wait_for('advertisementreceived');
+ await fake_central.simulateAdvertisementReceived(
+ health_thermometer_ad_packet);
+ let evt = await advertisementreceivedPromise;
+ assert_equals(evt.device, device);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/device/watchAdvertisements/blocklisted-manufacturer-data-filtered-from-event.https.window.js b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/blocklisted-manufacturer-data-filtered-from-event.https.window.js
new file mode 100644
index 0000000000..c73e3dbad1
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/blocklisted-manufacturer-data-filtered-from-event.https.window.js
@@ -0,0 +1,50 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = `Blocked manufacturer data is filtered from the ` +
+ `advertisement event.`;
+
+const advertisement_packet_with_blocked_manufacturer_data = {
+ deviceAddress: '07:07:07:07:07:07',
+ rssi: -10,
+ scanRecord: {
+ name: 'LE Device',
+ uuids: [uuid1234],
+ manufacturerData: {
+ [nonBlocklistedManufacturerId]: nonBlocklistedManufacturerData,
+ [blocklistedManufacturerId]: blocklistedManufacturerData,
+ },
+ }
+};
+
+bluetooth_test(async (t) => {
+ let {device} = await setUpPreconnectedFakeDevice({
+ fakeDeviceOptions: {
+ address: '07:07:07:07:07:07',
+ knownServiceUUIDs: [uuid1234],
+ },
+ requestDeviceOptions: {
+ filters: [{services: [uuid1234]}],
+ optionalManufacturerData: [nonBlocklistedManufacturerId, blocklistedManufacturerId]
+ }
+ });
+ const watcher = new EventWatcher(t, device, ['advertisementreceived']);
+
+ await device.watchAdvertisements();
+ assert_true(device.watchingAdvertisements);
+
+ let advertisementreceivedPromise = watcher.wait_for('advertisementreceived');
+ await fake_central.simulateAdvertisementReceived(
+ advertisement_packet_with_blocked_manufacturer_data);
+ let evt = await advertisementreceivedPromise;
+ assert_equals(evt.device, device);
+
+ // Check if block-listed manufacturer data is filtered out properly.
+ assert_false(evt.manufacturerData.has(blocklistedManufacturerId));
+
+ // Check if non blocked-listed manufacturer still exists.
+ assert_data_maps_equal(
+ evt.manufacturerData, /*expected_key=*/ nonBlocklistedManufacturerId, nonBlocklistedManufacturerData);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/device/watchAdvertisements/concurrent-watchAdvertisements-calls.https.window.js b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/concurrent-watchAdvertisements-calls.https.window.js
new file mode 100644
index 0000000000..cb6532be68
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/concurrent-watchAdvertisements-calls.https.window.js
@@ -0,0 +1,29 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'concurrent watchAdvertisements() calls results in the ' +
+ `second call rejecting with 'InvalidStateError'`;
+
+bluetooth_test(async (t) => {
+ let {device} = await getDiscoveredHealthThermometerDevice();
+ const watcher = new EventWatcher(t, device, ['advertisementreceived']);
+
+ // Start a watchAdvertisements() operation.
+ let firstWatchAdvertisementsPromise = device.watchAdvertisements();
+
+ // Start a second watchAdvertisements() operation. This operation should
+ // reject with 'InvalidStateError'.
+ await promise_rejects_dom(
+ t, 'InvalidStateError', device.watchAdvertisements());
+
+ // The first watchAdvertisements() operation should resolve successfully.
+ await firstWatchAdvertisementsPromise;
+
+ let advertisementreceivedPromise = watcher.wait_for('advertisementreceived');
+ await fake_central.simulateAdvertisementReceived(
+ health_thermometer_ad_packet);
+ let evt = await advertisementreceivedPromise;
+ assert_equals(evt.device, device);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/device/watchAdvertisements/detachedIframe.https.window.js b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/detachedIframe.https.window.js
new file mode 100644
index 0000000000..202a8dab7d
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/detachedIframe.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ let iframe = document.createElement('iframe');
+ let error;
+
+ const {device} = await getHealthThermometerDeviceFromIframe(iframe);
+
+ iframe.remove();
+ // Set iframe to null to ensure that the GC cleans up as much as possible.
+ iframe = null;
+ await garbageCollect();
+
+ try {
+ await device.watchAdvertisements();
+ } catch (e) {
+ // Cannot use promise_rejects_dom() because |e| is thrown from a different
+ // global.
+ error = e;
+ }
+ assert_not_equals(error, undefined);
+ assert_equals(error.name, 'TypeError');
+}, 'watchAdvertisements() rejects in a detached context');
diff --git a/testing/web-platform/tests/bluetooth/device/watchAdvertisements/service-and-manufacturer-data-filtered-from-event.https.window.js b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/service-and-manufacturer-data-filtered-from-event.https.window.js
new file mode 100644
index 0000000000..f6b93ffb4b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/service-and-manufacturer-data-filtered-from-event.https.window.js
@@ -0,0 +1,41 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = `Service and Manufacturer that were not granted with ` +
+ `requestDevice() are filtered from the advertisement event.`;
+
+bluetooth_test(async (t) => {
+ let {device} = await setUpPreconnectedFakeDevice({
+ fakeDeviceOptions: {
+ address: '07:07:07:07:07:07',
+ knownServiceUUIDs: [uuid1234, uuid5678, uuidABCD],
+ },
+ requestDeviceOptions: {
+ filters: [{services: [uuid1234]}],
+ optionalServices: [uuid5678],
+ optionalManufacturerData: [0x0001]
+ }
+ });
+ const watcher = new EventWatcher(t, device, ['advertisementreceived']);
+
+ await device.watchAdvertisements();
+ assert_true(device.watchingAdvertisements);
+
+ let advertisementreceivedPromise = watcher.wait_for('advertisementreceived');
+ await fake_central.simulateAdvertisementReceived(
+ service_and_manufacturer_data_ad_packet);
+ let evt = await advertisementreceivedPromise;
+ assert_equals(evt.device, device);
+
+ // Check that service data is filtered out properly.
+ assert_data_maps_equal(evt.serviceData, uuid1234, uuid1234Data);
+ assert_data_maps_equal(evt.serviceData, uuid5678, uuid5678Data);
+ assert_false(evt.serviceData.has(uuidABCD));
+
+ // Check that manufacturer data is filtered out properly.
+ assert_data_maps_equal(
+ evt.manufacturerData, /*expected_key=*/ 0x0001, manufacturer1Data);
+ assert_false(evt.manufacturerData.has(0x0002));
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/device/watchAdvertisements/subsequent-watchAdvertisements-call.https.window.js b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/subsequent-watchAdvertisements-call.https.window.js
new file mode 100644
index 0000000000..797bfd1fa0
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/subsequent-watchAdvertisements-call.https.window.js
@@ -0,0 +1,25 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'subsequent watchAdvertisements() calls result in the ' +
+ 'second call resolving successfully.';
+
+bluetooth_test(async (t) => {
+ let {device} = await getDiscoveredHealthThermometerDevice();
+ const watcher = new EventWatcher(t, device, ['advertisementreceived']);
+
+ // Start a watchAdvertisements() operation.
+ await device.watchAdvertisements();
+
+ // Start a second watchAdvertisements() operation after the first one
+ // resolves. This operation should resolve successfully.
+ await device.watchAdvertisements();
+
+ let advertisementreceivedPromise = watcher.wait_for('advertisementreceived');
+ await fake_central.simulateAdvertisementReceived(
+ health_thermometer_ad_packet);
+ let evt = await advertisementreceivedPromise;
+ assert_equals(evt.device, device);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/device/watchAdvertisements/watching-two-devices-abort-one-watchAdvertisements.https.window.js b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/watching-two-devices-abort-one-watchAdvertisements.https.window.js
new file mode 100644
index 0000000000..8be02adb34
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/watching-two-devices-abort-one-watchAdvertisements.https.window.js
@@ -0,0 +1,47 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'AbortController while watching advertisements for two ' +
+ 'devices stops the correct watchAdvertisements() operation.';
+
+bluetooth_test(async (t) => {
+ let health_thermometer_device;
+ let heart_rate_device;
+ {
+ let {device} = await getDiscoveredHealthThermometerDevice();
+ health_thermometer_device = device;
+ }
+ {
+ let {device} = await getHeartRateDevice(
+ {requestDeviceOptions: heartRateRequestDeviceOptionsDefault});
+ heart_rate_device = device;
+ }
+ const healthThermometerWatcher =
+ new EventWatcher(t, health_thermometer_device, ['advertisementreceived']);
+ const heartRateWatcher =
+ new EventWatcher(t, heart_rate_device, ['advertisementreceived']);
+
+ await health_thermometer_device.watchAdvertisements();
+
+ let abortController = new AbortController();
+ await heart_rate_device.watchAdvertisements({signal: abortController.signal});
+
+ assert_true(health_thermometer_device.watchingAdvertisements);
+ assert_true(heart_rate_device.watchingAdvertisements);
+
+ abortController.abort();
+ assert_true(health_thermometer_device.watchingAdvertisements);
+ assert_false(heart_rate_device.watchingAdvertisements);
+
+ // This should not cause |heart_rate_device| to receive an Event.
+ await fake_central.simulateAdvertisementReceived(heart_rate_ad_packet);
+
+ let advertisementreceivedPromise =
+ healthThermometerWatcher.wait_for('advertisementreceived');
+ await fake_central.simulateAdvertisementReceived(
+ health_thermometer_ad_packet);
+ let evt = await advertisementreceivedPromise;
+ assert_equals(evt.device, health_thermometer_device);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/device/watchAdvertisements/watching-two-devices.https.window.js b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/watching-two-devices.https.window.js
new file mode 100644
index 0000000000..32ec89a1eb
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/device/watchAdvertisements/watching-two-devices.https.window.js
@@ -0,0 +1,41 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = `Events are fired on correct device with multiple ` +
+ `watchAdvertisements() calls.`;
+
+bluetooth_test(async (t) => {
+ let health_thermometer_device;
+ let heart_rate_device;
+ {
+ let {device} = await getDiscoveredHealthThermometerDevice();
+ health_thermometer_device = device;
+ }
+ {
+ let {device} = await getHeartRateDevice(
+ {requestDeviceOptions: heartRateRequestDeviceOptionsDefault});
+ heart_rate_device = device;
+ }
+ const healthThermometerWatcher =
+ new EventWatcher(t, health_thermometer_device, ['advertisementreceived']);
+ const heartRateWatcher =
+ new EventWatcher(t, heart_rate_device, ['advertisementreceived']);
+
+ await health_thermometer_device.watchAdvertisements();
+ await heart_rate_device.watchAdvertisements();
+
+ let advertisementreceivedPromise =
+ heartRateWatcher.wait_for('advertisementreceived');
+ await fake_central.simulateAdvertisementReceived(heart_rate_ad_packet);
+ let heartEvt = await advertisementreceivedPromise;
+ assert_equals(heartEvt.device, heart_rate_device);
+
+ advertisementreceivedPromise =
+ healthThermometerWatcher.wait_for('advertisementreceived');
+ await fake_central.simulateAdvertisementReceived(
+ health_thermometer_ad_packet);
+ let healthEvt = await advertisementreceivedPromise;
+ assert_equals(healthEvt.device, health_thermometer_device);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/generate.py b/testing/web-platform/tests/bluetooth/generate.py
new file mode 100644
index 0000000000..505375d55f
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/generate.py
@@ -0,0 +1,189 @@
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+#
+# TODO(509038): Delete the file in LayoutTests/bluetooth after all the script
+# tests have been migrated to this directory.
+"""Generator script for Web Bluetooth LayoutTests.
+
+For each script-tests/X.js creates the following test files depending on the
+contents of X.js
+- getPrimaryService/X.https.window.js
+- getPrimaryServices/X.https.window.js
+- getPrimaryServices/X-with-uuid.https.window.js
+
+script-tests/X.js files should contain "CALLS([variation1 | variation2 | ...])"
+tokens that indicate what files to generate. Each variation in CALLS([...])
+should corresponds to a js function call and its arguments. Additionally a
+variation can end in [UUID] to indicate that the generated file's name should
+have the -with-uuid suffix.
+
+The PREVIOUS_CALL token will be replaced with the function that replaced CALLS.
+
+The FUNCTION_NAME token will be replaced with the name of the function that
+replaced CALLS.
+
+For example, for the following template file:
+
+// script-tests/example.js
+promise_test(() => {
+ return navigator.bluetooth.requestDevice(...)
+ .then(device => device.gatt.CALLS([
+ getPrimaryService('heart_rate')|
+ getPrimaryServices('heart_rate')[UUID]]))
+ .then(device => device.gatt.PREVIOUS_CALL);
+}, 'example test for FUNCTION_NAME');
+
+this script will generate:
+
+// getPrimaryService/example.https.window.js
+promise_test(() => {
+ return navigator.bluetooth.requestDevice(...)
+ .then(device => device.gatt.getPrimaryService('heart_rate'))
+ .then(device => device.gatt.getPrimaryService('heart_rate'));
+}, 'example test for getPrimaryService');
+
+// getPrimaryServices/example-with-uuid.https.window.js
+promise_test(() => {
+ return navigator.bluetooth.requestDevice(...)
+ .then(device => device.gatt.getPrimaryServices('heart_rate'))
+ .then(device => device.gatt.getPrimaryServices('heart_rate'));
+}, 'example test for getPrimaryServices');
+
+Run
+$ python //third_party/WebKit/LayoutTests/bluetooth/generate.py
+and commit the generated files.
+"""
+
+import fnmatch
+import os
+import re
+import sys
+import logging
+
+TEMPLATES_DIR = 'script-tests'
+
+
+class GeneratedTest:
+
+ def __init__(self, data, path, template):
+ self.data = data
+ self.path = path
+ self.template = template
+
+
+def GetGeneratedTests():
+ """Yields a GeneratedTest for each call in templates in script-tests."""
+ bluetooth_tests_dir = os.path.dirname(os.path.realpath(__file__))
+
+ # Read Base Test Template.
+ base_template_file_handle = open(
+ os.path.join(
+ bluetooth_tests_dir,
+ TEMPLATES_DIR,
+ 'base_test_js.template'
+ ), 'r')
+ base_template_file_data = base_template_file_handle.read().decode('utf-8')
+ base_template_file_handle.close()
+
+ # Get Templates.
+
+ template_path = os.path.join(bluetooth_tests_dir, TEMPLATES_DIR)
+
+ available_templates = []
+ for root, _, files in os.walk(template_path):
+ for template in files:
+ if template.endswith('.js'):
+ available_templates.append(os.path.join(root, template))
+
+ # Generate Test Files
+ for template in available_templates:
+ # Read template
+ template_file_handle = open(template, 'r')
+ template_file_data = template_file_handle.read().decode('utf-8')
+ template_file_handle.close()
+
+ template_name = os.path.splitext(os.path.basename(template))[0]
+
+ # Find function names in multiline pattern: CALLS( [ function_name,function_name2[UUID] ])
+ result = re.search(
+ r'CALLS\(' + # CALLS(
+ r'[^\[]*' + # Any characters not [, allowing for new lines.
+ r'\[' + # [
+ r'(.*?)' + # group matching: function_name(), function_name2[UUID]
+ r'\]\)', # adjacent closing characters: ])
+ template_file_data, re.MULTILINE | re.DOTALL)
+
+ if result is None:
+ raise Exception('Template must contain \'CALLS\' tokens')
+
+ new_test_file_data = base_template_file_data.replace('TEST',
+ template_file_data)
+ # Replace CALLS([...]) with CALLS so that we don't have to replace the
+ # CALLS([...]) for every new test file.
+ new_test_file_data = new_test_file_data.replace(result.group(), 'CALLS')
+
+ # Replace 'PREVIOUS_CALL' with 'CALLS' so that we can replace it while
+ # replacing CALLS.
+ new_test_file_data = new_test_file_data.replace('PREVIOUS_CALL', 'CALLS')
+
+ for call in result.group(1).split('|'):
+ # Parse call
+ call = call.strip()
+ function_name, args, uuid_suffix = re.search(r'(.*?)\((.*)\)(\[UUID\])?', call).groups()
+
+ # Replace template tokens
+ call_test_file_data = new_test_file_data
+ call_test_file_data = call_test_file_data.replace('CALLS', '{}({})'.format(function_name, args))
+ call_test_file_data = call_test_file_data.replace('FUNCTION_NAME', function_name)
+
+ # Get test file name
+ group_dir = os.path.basename(os.path.abspath(os.path.join(template, os.pardir)))
+
+ call_test_file_name = 'gen-{}{}.https.window.js'.format(template_name, '-with-uuid' if uuid_suffix else '')
+ call_test_file_path = os.path.join(bluetooth_tests_dir, group_dir, function_name, call_test_file_name)
+
+ yield GeneratedTest(call_test_file_data, call_test_file_path, template)
+
+def main():
+ logging.basicConfig(level=logging.INFO)
+ previous_generated_files = set()
+ current_path = os.path.dirname(os.path.realpath(__file__))
+ for root, _, filenames in os.walk(current_path):
+ for filename in fnmatch.filter(filenames, 'gen-*.https.window.js'):
+ previous_generated_files.add(os.path.join(root, filename))
+
+ generated_files = set()
+ for generated_test in GetGeneratedTests():
+ prev_len = len(generated_files)
+ generated_files.add(generated_test.path)
+ if prev_len == len(generated_files):
+ logging.info('Generated the same test twice for template:\n%s',
+ generated_test.template)
+
+ # Create or open test file
+ directory = os.path.dirname(generated_test.path)
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+ test_file_handle = open(generated_test.path, 'wb')
+
+ # Write contents
+ test_file_handle.write(generated_test.data.encode('utf-8'))
+ test_file_handle.close()
+
+ new_generated_files = generated_files - previous_generated_files
+ if len(new_generated_files) != 0:
+ logging.info('Newly generated tests:')
+ for generated_file in new_generated_files:
+ logging.info(generated_file)
+
+ obsolete_files = previous_generated_files - generated_files
+ if len(obsolete_files) != 0:
+ logging.warning('The following files might be obsolete:')
+ for generated_file in obsolete_files:
+ logging.warning(generated_file)
+
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/testing/web-platform/tests/bluetooth/generate_test.py b/testing/web-platform/tests/bluetooth/generate_test.py
new file mode 100755
index 0000000000..6f46ae913a
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/generate_test.py
@@ -0,0 +1,56 @@
+#!/usr/bin/python
+
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+#
+# TODO(50903): Delete the file in LayoutTests/bluetooth after all the tests have
+# been migrated to this directory.
+"""Test that the set of gen-* files is the same as the generated files."""
+
+import fnmatch
+import os
+import sys
+import generate
+import logging
+
+UPDATE_TIP = 'To update the generated tests, run:\n' \
+ '$ python third_party/WebKit/LayoutTests/bluetooth/generate.py'
+
+
+def main():
+ logging.basicConfig(level=logging.INFO)
+ logging.info(UPDATE_TIP)
+ generated_files = set()
+ # Tests data in gen-* files is the same as the data generated.
+ for generated_test in generate.GetGeneratedTests():
+ generated_files.add(generated_test.path)
+ try:
+ with open(generated_test.path, 'r') as f:
+ data = f.read().decode('utf-8')
+ if data != generated_test.data:
+ logging.error('%s does not match template', generated_test.path)
+ return -1
+ except IOError as e:
+ if e.errno == 2:
+ logging.error('Missing generated test:\n%s\nFor template:\n%s',
+ generated_test.path,
+ generated_test.template)
+ return -1
+
+ # Tests that there are no obsolete generated files.
+ previous_generated_files = set()
+ current_path = os.path.dirname(os.path.realpath(__file__))
+ for root, _, filenames in os.walk(current_path):
+ for filename in fnmatch.filter(filenames, 'gen-*.https.window.js'):
+ previous_generated_files.add(os.path.join(root, filename))
+
+ if previous_generated_files != generated_files:
+ logging.error('There are extra generated tests. Please remove them.')
+ for test_path in previous_generated_files - generated_files:
+ logging.error('%s', test_path)
+ return -1
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/testing/web-platform/tests/bluetooth/getAvailability/reject_opaque_origin.https.html b/testing/web-platform/tests/bluetooth/getAvailability/reject_opaque_origin.https.html
new file mode 100644
index 0000000000..8745fc9551
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/getAvailability/reject_opaque_origin.https.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+
+ promise_test(async (t) => {
+ await promise_rejects_dom(
+ t, 'SecurityError', navigator.bluetooth.getAvailability(),
+ 'getAvailability() should throw a SecurityError DOMException when called from a context where the top-level document has an opaque origin.');
+ }, 'Calls to Bluetooth APIs from an origin with opaque top origin get blocked.');
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/getAvailability/reject_opaque_origin.https.html.headers b/testing/web-platform/tests/bluetooth/getAvailability/reject_opaque_origin.https.html.headers
new file mode 100644
index 0000000000..c7e4e7cc5b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/getAvailability/reject_opaque_origin.https.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/getAvailability/sandboxed_iframe.https.window.js b/testing/web-platform/tests/bluetooth/getAvailability/sandboxed_iframe.https.window.js
new file mode 100644
index 0000000000..c5e3d1e890
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/getAvailability/sandboxed_iframe.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+'use strict';
+
+let iframe = document.createElement('iframe');
+
+bluetooth_test(async () => {
+ await getConnectedHealthThermometerDevice();
+ await new Promise(resolve => {
+ iframe.src = '/bluetooth/resources/health-thermometer-iframe.html';
+ iframe.sandbox.add('allow-scripts');
+ iframe.allow = 'bluetooth';
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve);
+ });
+ await new Promise(resolve => {
+ iframe.contentWindow.postMessage({type: 'GetAvailability'}, '*');
+
+ window.addEventListener('message', (messageEvent) => {
+ assert_false(/^FAIL: .*/.test(messageEvent.data));
+ resolve();
+ });
+ });
+}, 'Calls to Bluetooth APIs from a sandboxed iframe are valid.'); \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/getDevices/granted-devices-with-services.https.window.js b/testing/web-platform/tests/bluetooth/getDevices/granted-devices-with-services.https.window.js
new file mode 100644
index 0000000000..3228543617
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/getDevices/granted-devices-with-services.https.window.js
@@ -0,0 +1,72 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'getDevices() resolves with permitted devices that can be ' +
+ 'GATT connected to.';
+
+bluetooth_test(async () => {
+ // Set up two connectable Bluetooth devices with their services discovered.
+ // One device is a Health Thermometer device with the 'health_thermometer'
+ // service while the other is a Heart Rate device with the 'heart_rate'
+ // service. Both devices contain the 'generic_access' service.
+ let fake_peripherals = await setUpHealthThermometerAndHeartRateDevices();
+ for (let fake_peripheral of fake_peripherals) {
+ await fake_peripheral.setNextGATTConnectionResponse({code: HCI_SUCCESS});
+ await fake_peripheral.addFakeService({uuid: 'generic_access'});
+ if (fake_peripheral.address === '09:09:09:09:09:09')
+ await fake_peripheral.addFakeService({uuid: 'health_thermometer'});
+ else
+ await fake_peripheral.addFakeService({uuid: 'heart_rate'});
+ await fake_peripheral.setNextGATTDiscoveryResponse({code: HCI_SUCCESS});
+ }
+
+ // Request the Health Thermometer device with access to its 'generic_access'
+ // service.
+ await requestDeviceWithTrustedClick(
+ {filters: [{name: 'Health Thermometer', services: ['generic_access']}]});
+ let devices = await navigator.bluetooth.getDevices();
+ assert_equals(
+ devices.length, 1,
+ `getDevices() should return the 'Health Thermometer' device.`);
+
+ // Only the 'generic_access' service can be accessed.
+ try {
+ await devices[0].gatt.connect();
+ await devices[0].gatt.getPrimaryService('generic_access');
+ assert_promise_rejects_with_message(
+ devices[0].gatt.getPrimaryService('health_thermometer'),
+ {name: 'SecurityError'});
+ } catch (err) {
+ assert_unreached(`${err.name}: ${err.message}`);
+ }
+
+ // Request the Heart Rate device with access to both of its services.
+ await requestDeviceWithTrustedClick({
+ filters: [{name: 'Heart Rate', services: ['generic_access', 'heart_rate']}]
+ });
+ devices = await navigator.bluetooth.getDevices();
+ assert_equals(
+ devices.length, 2,
+ `getDevices() should return the 'Health Thermometer' and 'Health ` +
+ `Monitor' devices`);
+
+ // All of Heart Rate device's services can be accessed, while only the
+ // 'generic_access' service can be accessed on Health Thermometer.
+ try {
+ for (let device of devices) {
+ await device.gatt.connect();
+ await device.gatt.getPrimaryService('generic_access');
+ if (device.name === 'Heart Rate') {
+ await device.gatt.getPrimaryService('heart_rate');
+ } else {
+ assert_promise_rejects_with_message(
+ devices[0].gatt.getPrimaryService('health_thermometer'),
+ {name: 'SecurityError'});
+ }
+ }
+ } catch (err) {
+ assert_unreached(`${err.name}: ${err.message}`);
+ }
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/getDevices/no-granted-devices.https.window.js b/testing/web-platform/tests/bluetooth/getDevices/no-granted-devices.https.window.js
new file mode 100644
index 0000000000..304aa3820d
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/getDevices/no-granted-devices.https.window.js
@@ -0,0 +1,15 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'getDevices() resolves with empty array if no device ' +
+ 'permissions have been granted.';
+
+bluetooth_test(async () => {
+ await navigator.bluetooth.test.simulateCentral({state: 'powered-on'});
+ let devices = await navigator.bluetooth.getDevices();
+
+ assert_equals(
+ 0, devices.length, 'getDevices() should resolve with an empty array');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/getDevices/reject_opaque_origin.https.html b/testing/web-platform/tests/bluetooth/getDevices/reject_opaque_origin.https.html
new file mode 100644
index 0000000000..64b2808fbc
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/getDevices/reject_opaque_origin.https.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+
+ promise_test(async (t) => {
+ await promise_rejects_dom(
+ t, 'SecurityError', navigator.bluetooth.getDevices(),
+ 'getDevices() should throw a SecurityError DOMException when called from a context where the top-level document has an opaque origin.');
+ }, 'Calls to Bluetooth APIs from an origin with opaque top origin get blocked.');
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/getDevices/reject_opaque_origin.https.html.headers b/testing/web-platform/tests/bluetooth/getDevices/reject_opaque_origin.https.html.headers
new file mode 100644
index 0000000000..c7e4e7cc5b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/getDevices/reject_opaque_origin.https.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/getDevices/returns-same-bluetooth-device-object.https.window.js b/testing/web-platform/tests/bluetooth/getDevices/returns-same-bluetooth-device-object.https.window.js
new file mode 100644
index 0000000000..81c0f6a97e
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/getDevices/returns-same-bluetooth-device-object.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'multiple calls to getDevices() resolves with the same' +
+ 'BluetoothDevice objects for each granted Bluetooth device.';
+
+bluetooth_test(async () => {
+ await getConnectedHealthThermometerDevice();
+ let firstDevices = await navigator.bluetooth.getDevices();
+ assert_equals(
+ firstDevices.length, 1, 'getDevices() should return the granted device.');
+
+ let secondDevices = await navigator.bluetooth.getDevices();
+ assert_equals(
+ secondDevices.length, 1,
+ 'getDevices() should return the granted device.');
+ assert_equals(
+ firstDevices[0], secondDevices[0],
+ 'getDevices() should produce the same BluetoothDevice objects for a ' +
+ 'given Bluetooth device.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/getDevices/sandboxed_iframe.https.window.js b/testing/web-platform/tests/bluetooth/getDevices/sandboxed_iframe.https.window.js
new file mode 100644
index 0000000000..22cfd17d46
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/getDevices/sandboxed_iframe.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+'use strict';
+
+let iframe = document.createElement('iframe');
+
+bluetooth_test(async () => {
+ await getConnectedHealthThermometerDevice();
+ await new Promise(resolve => {
+ iframe.src = '/bluetooth/resources/health-thermometer-iframe.html';
+ iframe.sandbox.add('allow-scripts');
+ iframe.allow = 'bluetooth';
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve);
+ });
+ await new Promise(resolve => {
+ iframe.contentWindow.postMessage({type: 'GetDevices'}, '*');
+
+ window.addEventListener('message', (messageEvent) => {
+ assert_false(/^FAIL: .*/.test(messageEvent.data));
+ resolve();
+ });
+ });
+}, 'Calls to Bluetooth APIs from a sandboxed iframe are valid.'); \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/idl/idl-Bluetooth.https.window.js b/testing/web-platform/tests/bluetooth/idl/idl-Bluetooth.https.window.js
new file mode 100644
index 0000000000..2b40eaff49
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/idl/idl-Bluetooth.https.window.js
@@ -0,0 +1,18 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+'use strict';
+const test_desc = 'Bluetooth IDL test';
+
+test(() => {
+ assert_throws_js(
+ TypeError, () => new Bluetooth(),
+ 'the constructor should not be callable with "new"');
+ assert_throws_js(
+ TypeError, () => Bluetooth(),
+ 'the constructor should not be callable');
+
+ // Bluetooth implements BluetoothDiscovery;
+ assert_true('requestDevice' in navigator.bluetooth);
+ assert_true('getDevices' in navigator.bluetooth);
+ assert_equals(navigator.bluetooth.requestDevice.length, 0);
+}, test_desc); \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/idl/idl-BluetoothDevice.https.window.js b/testing/web-platform/tests/bluetooth/idl/idl-BluetoothDevice.https.window.js
new file mode 100644
index 0000000000..c715926df3
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/idl/idl-BluetoothDevice.https.window.js
@@ -0,0 +1,37 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc_idl = 'BluetoothDevice IDL test.';
+
+test(() => {
+ assert_throws_js(
+ TypeError, () => new BluetoothDevice(),
+ 'the constructor should not be callable with "new"');
+ assert_throws_js(
+ TypeError, () => BluetoothDevice(),
+ 'the constructor should not be callable');
+}, test_desc_idl);
+
+const test_desc_attr = 'BluetoothDevice attributes.';
+let device;
+
+bluetooth_test(async () => {
+ let {device} = await getConnectedHealthThermometerDevice();
+
+ assert_equals(device.constructor.name, 'BluetoothDevice');
+ var old_device_id = device.id;
+ assert_throws_js(
+ TypeError, () => device.id = 'overwritten',
+ 'the device id should not be writable');
+ assert_throws_js(
+ TypeError, () => device.name = 'overwritten',
+ 'the device name should not be writable');
+ assert_throws_js(
+ TypeError, () => device.watchingAdvertisements = true,
+ 'the device watchingAdvertisements should not be writable');
+ assert_equals(device.id, old_device_id);
+ assert_equals(device.name, 'Health Thermometer');
+ assert_equals(device.watchingAdvertisements, false);
+}, test_desc_attr);
diff --git a/testing/web-platform/tests/bluetooth/idl/idl-BluetoothUUID.window.js b/testing/web-platform/tests/bluetooth/idl/idl-BluetoothUUID.window.js
new file mode 100644
index 0000000000..cf9c14bb2c
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/idl/idl-BluetoothUUID.window.js
@@ -0,0 +1,177 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+'use strict'
+
+var base_uuid = '00000000-0000-1000-8000-00805f9b34fb'
+
+test(() => {
+ let base_alias = 0x0
+ assert_equals(BluetoothUUID.getService(base_alias), base_uuid);
+ assert_equals(BluetoothUUID.getCharacteristic(base_alias), base_uuid);
+ assert_equals(BluetoothUUID.getDescriptor(base_alias), base_uuid);
+}, '0x0 should produce valid UUID.');
+
+test(() => {
+ assert_equals(BluetoothUUID.getService(NaN), base_uuid);
+ assert_equals(BluetoothUUID.getCharacteristic(NaN), base_uuid);
+ assert_equals(BluetoothUUID.getDescriptor(NaN), base_uuid);
+}, 'NaN returns basic uuid');
+
+test(
+ () => {
+ let max_uuid = 'ffffffff-0000-1000-8000-00805f9b34fb';
+ let nine_digits = 0xfffffffff;
+ let thirteen_digits = 0xfffffffffffff;
+ let fourteen_digits = 0xffffffffffffff;
+ assert_equals(BluetoothUUID.getService(nine_digits), max_uuid);
+ assert_equals(BluetoothUUID.getCharacteristic(nine_digits), max_uuid);
+ assert_equals(BluetoothUUID.getDescriptor(nine_digits), max_uuid);
+ assert_equals(BluetoothUUID.getService(thirteen_digits), max_uuid);
+ assert_equals(BluetoothUUID.getCharacteristic(thirteen_digits), max_uuid);
+ assert_equals(BluetoothUUID.getDescriptor(thirteen_digits), max_uuid);
+ assert_equals(BluetoothUUID.getService(fourteen_digits), base_uuid);
+ assert_equals(
+ BluetoothUUID.getCharacteristic(fourteen_digits), base_uuid);
+ assert_equals(BluetoothUUID.getDescriptor(fourteen_digits), base_uuid);
+ },
+ 'Values between 0xfffffffff (8 digits) and 0xffffffffffffff (14 digits)' +
+ 'should return max UUID');
+
+test(() => {
+ assert_equals(BluetoothUUID.getService(Infinity), base_uuid);
+ assert_equals(BluetoothUUID.getCharacteristic(Infinity), base_uuid);
+ assert_equals(BluetoothUUID.getDescriptor(Infinity), base_uuid);
+}, 'Infinity returns base UUID');
+
+test(() => {
+ let deadbeef_alias = 0xDEADBEEF;
+ let deadbeef_uuid = 'deadbeef-0000-1000-8000-00805f9b34fb';
+ assert_equals(BluetoothUUID.getService(deadbeef_alias), deadbeef_uuid);
+ assert_equals(BluetoothUUID.getCharacteristic(deadbeef_alias), deadbeef_uuid);
+ assert_equals(BluetoothUUID.getDescriptor(deadbeef_alias), deadbeef_uuid);
+}, '0xdeadbeef should produce valid UUID.');
+
+test(() => {
+ let adeadbeef_alias = 0xADEADBEEF;
+ let adeadbeef_uuid = 'deadbeef-0000-1000-8000-00805f9b34fb';
+ assert_equals(BluetoothUUID.getService(adeadbeef_alias), adeadbeef_uuid);
+ assert_equals(
+ BluetoothUUID.getCharacteristic(adeadbeef_alias), adeadbeef_uuid);
+ assert_equals(BluetoothUUID.getDescriptor(adeadbeef_alias), adeadbeef_uuid);
+}, 'Only first 32bits should be used.');
+
+test(() => {
+ let basic_uuid = '1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d';
+ assert_equals(BluetoothUUID.getService(basic_uuid), basic_uuid);
+ assert_equals(BluetoothUUID.getCharacteristic(basic_uuid), basic_uuid);
+ assert_equals(BluetoothUUID.getDescriptor(basic_uuid), basic_uuid);
+}, 'A valid UUID String should return the same UUID.');
+
+test(() => {
+ let all_caps_uuid = '1A2B3C4D-5E6F-7A8B-9C0D-1E2F3A4B5C6D';
+ assert_throws_js(TypeError, () => BluetoothUUID.getService(all_caps_uuid));
+ assert_throws_js(
+ TypeError, () => BluetoothUUID.getCharacteristic(all_caps_uuid));
+ assert_throws_js(TypeError, () => BluetoothUUID.getDescriptor(all_caps_uuid));
+}, 'A UUID String with uppercase letters is an invalid UUID.');
+
+test(() => {
+ let string_alias = 'deadbeef';
+ assert_throws_js(TypeError, () => BluetoothUUID.getService(string_alias));
+ assert_throws_js(
+ TypeError, () => BluetoothUUID.getCharacteristic(string_alias));
+ assert_throws_js(TypeError, () => BluetoothUUID.getDescriptor(string_alias));
+}, 'A 32bit *String* alias is invalid.');
+
+test(() => {
+ let invalid_character_uuid = '0000000g-0000-1000-8000-00805f9b34fb';
+ assert_throws_js(
+ TypeError, () => BluetoothUUID.getService(invalid_character_uuid));
+ assert_throws_js(
+ TypeError,
+ () => BluetoothUUID.getCharacteristic(invalid_character_uuid));
+ assert_throws_js(
+ TypeError, () => BluetoothUUID.getDescriptor(invalid_character_uuid));
+}, 'A UUID with invalid characters is an invalid UUID.');
+
+test(() => {
+ assert_equals(
+ BluetoothUUID.getService('alert_notification'),
+ '00001811-0000-1000-8000-00805f9b34fb');
+ assert_equals(
+ BluetoothUUID.getCharacteristic('aerobic_heart_rate_lower_limit'),
+ '00002a7e-0000-1000-8000-00805f9b34fb');
+ assert_equals(
+ BluetoothUUID.getDescriptor('gatt.characteristic_extended_properties'),
+ '00002900-0000-1000-8000-00805f9b34fb');
+}, 'A valid UUID from a name.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ BluetoothUUID.getService('aerobic_heart_rate_lower_limit');
+ });
+ assert_throws_js(TypeError, () => {
+ BluetoothUUID.getService('gatt.characteristic_extended_properties');
+ });
+ assert_throws_js(TypeError, () => {
+ BluetoothUUID.getCharacteristic('alert_notification');
+ });
+ assert_throws_js(TypeError, () => {
+ BluetoothUUID.getCharacteristic('gatt.characteristic_extended_properties');
+ });
+ assert_throws_js(TypeError, () => {
+ BluetoothUUID.getDescriptor('alert_notification');
+ });
+ assert_throws_js(TypeError, () => {
+ BluetoothUUID.getDescriptor('aerobic_heart_rate_lower_limit');
+ });
+}, 'Make sure attributes don\'t share a map');
+
+test(() => {
+ let wrong_name = 'wrong_name';
+ assert_throws_js(TypeError, () => BluetoothUUID.getService(wrong_name));
+ assert_throws_js(TypeError, () => BluetoothUUID.getCharacteristic(wrong_name));
+ assert_throws_js(TypeError, () => BluetoothUUID.getDescriptor(wrong_name));
+}, 'Invalid Descriptor name');
+
+test(() => {
+ let object = {};
+ let array = [];
+ let func = () => {};
+
+ // cannonicalUUID
+ assert_throws_js(TypeError, () => BluetoothUUID.canonicalUUID(object));
+ // [] converts to '', which converts to 0 before the range check.
+ assert_equals(BluetoothUUID.canonicalUUID(array), base_uuid);
+ assert_throws_js(TypeError, () => BluetoothUUID.canonicalUUID(func));
+ assert_throws_js(TypeError, () => BluetoothUUID.canonicalUUID(undefined));
+ assert_equals(BluetoothUUID.canonicalUUID(null), base_uuid);
+ assert_equals(BluetoothUUID.canonicalUUID(false), base_uuid);
+ assert_equals(
+ BluetoothUUID.canonicalUUID(true), BluetoothUUID.canonicalUUID(1));
+ assert_throws_js(TypeError, () => BluetoothUUID.canonicalUUID(NaN));
+
+ // getService
+ assert_throws_js(TypeError, () => BluetoothUUID.getService(object));
+ assert_throws_js(TypeError, () => BluetoothUUID.getService(array));
+ assert_throws_js(TypeError, () => BluetoothUUID.getService(func));
+ assert_throws_js(TypeError, () => BluetoothUUID.getService(undefined));
+ assert_throws_js(TypeError, () => BluetoothUUID.getService(null));
+ assert_throws_js(TypeError, () => BluetoothUUID.getService(false));
+
+ // getCharacteristic
+ assert_throws_js(TypeError, () => BluetoothUUID.getCharacteristic(object));
+ assert_throws_js(TypeError, () => BluetoothUUID.getCharacteristic(array));
+ assert_throws_js(TypeError, () => BluetoothUUID.getCharacteristic(func));
+ assert_throws_js(TypeError, () => BluetoothUUID.getCharacteristic(undefined));
+ assert_throws_js(TypeError, () => BluetoothUUID.getCharacteristic(null));
+ assert_throws_js(TypeError, () => BluetoothUUID.getCharacteristic(false));
+
+ // getDescriptor
+ assert_throws_js(TypeError, () => BluetoothUUID.getDescriptor(object));
+ assert_throws_js(TypeError, () => BluetoothUUID.getDescriptor(array));
+ assert_throws_js(TypeError, () => BluetoothUUID.getDescriptor(func));
+ assert_throws_js(TypeError, () => BluetoothUUID.getDescriptor(undefined));
+ assert_throws_js(TypeError, () => BluetoothUUID.getDescriptor(null));
+ assert_throws_js(TypeError, () => BluetoothUUID.getDescriptor(false));
+}, 'Non-number and non-strings');
diff --git a/testing/web-platform/tests/bluetooth/idl/idl-NavigatorBluetooth.https.window.js b/testing/web-platform/tests/bluetooth/idl/idl-NavigatorBluetooth.https.window.js
new file mode 100644
index 0000000000..a087d30896
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/idl/idl-NavigatorBluetooth.https.window.js
@@ -0,0 +1,12 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+'use strict';
+const test_desc = '[SameObject] test for navigator.bluetooth';
+
+test(() => {
+ assert_true('bluetooth' in navigator, 'navigator.bluetooth exists.');
+}, 'navigator.bluetooth IDL test');
+
+test(() => {
+ assert_equals(navigator.bluetooth, navigator.bluetooth);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/idl/idl-NavigatorBluetooth.window.js b/testing/web-platform/tests/bluetooth/idl/idl-NavigatorBluetooth.window.js
new file mode 100644
index 0000000000..db6bf89f9f
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/idl/idl-NavigatorBluetooth.window.js
@@ -0,0 +1,7 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+'use strict';
+
+test(() => {
+ assert_false('bluetooth' in navigator);
+}, 'navigator.bluetooth not available in insecure contexts');
diff --git a/testing/web-platform/tests/bluetooth/idl/idlharness.tentative.https.window.js b/testing/web-platform/tests/bluetooth/idl/idlharness.tentative.https.window.js
new file mode 100644
index 0000000000..a632060e20
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/idl/idlharness.tentative.https.window.js
@@ -0,0 +1,25 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: timeout=long
+
+'use strict';
+
+// https://webbluetoothcg.github.io/web-bluetooth/
+
+idl_test(
+ ['web-bluetooth'],
+ ['dom', 'html', 'permissions'],
+ idl_array => {
+ try {
+ self.event = new BluetoothAdvertisingEvent('type');
+ } catch(e) {
+ // Surfaced when 'event' is undefined below.
+ }
+
+ idl_array.add_objects({
+ Navigator: ['navigator'],
+ Bluetooth: ['navigator.bluetooth'],
+ BluetoothAdvertisingEvent: ['event'],
+ });
+ }
+);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/device-with-empty-name.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/device-with-empty-name.https.window.js
new file mode 100644
index 0000000000..15bde6a933
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/device-with-empty-name.https.window.js
@@ -0,0 +1,19 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Device with empty name and no UUIDs nearby. Should be ' +
+ 'found if acceptAllDevices is true.';
+
+bluetooth_test(async () => {
+ let { device } = await setUpPreconnectedFakeDevice({
+ fakeDeviceOptions: {
+ name: ''
+ },
+ requestDeviceOptions: {
+ acceptAllDevices: true
+ }
+ });
+ assert_equals(device.name, '');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/device-with-name.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/device-with-name.https.window.js
new file mode 100644
index 0000000000..f3373a6bb6
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/device-with-name.https.window.js
@@ -0,0 +1,20 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+const test_desc =
+ 'A device with name and no UUIDs nearby. Should be found if ' +
+ 'acceptAllDevices is true.';
+const name = 'LE Device';
+
+bluetooth_test(async () => {
+ let { device } = await setUpPreconnectedFakeDevice({
+ fakeDeviceOptions: {
+ name: name
+ },
+ requestDeviceOptions: {
+ acceptAllDevices: true
+ }
+ });
+ assert_equals(device.name, name);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/optional-services-missing.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/optional-services-missing.https.window.js
new file mode 100644
index 0000000000..5226a645a8
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/optional-services-missing.https.window.js
@@ -0,0 +1,21 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'requestDevice called with acceptAllDevices: true and ' +
+ 'with no optionalServices. Should not get access to any services.';
+const expected = new DOMException(
+ 'Origin is not allowed to access any service. ' +
+ 'Tip: Add the service UUID to \'optionalServices\' in ' +
+ 'requestDevice() options. https://goo.gl/HxfxSQ',
+ 'SecurityError');
+
+bluetooth_test(
+ async () => {
+ let { device } = await getConnectedHealthThermometerDevice(
+ { acceptAllDevices: true });
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(), expected);
+ },
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/optional-services-present.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/optional-services-present.https.window.js
new file mode 100644
index 0000000000..7c200d03f1
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/acceptAllDevices/optional-services-present.https.window.js
@@ -0,0 +1,25 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'requestDevice called with acceptAllDevices: true and with ' +
+ 'optionalServices. Should get access to services.';
+
+bluetooth_test(
+ async () => {
+ await getTwoHealthThermometerServicesDevice()
+ let device = await requestDeviceWithTrustedClick({
+ acceptAllDevices: true,
+ optionalServices: ['health_thermometer']
+ });
+ let gattServer = await device.gatt.connect();
+ let services = await gattServer.getPrimaryServices();
+ assert_equals(services.length, 2);
+ services.forEach(service => {
+ assert_equals(
+ service.uuid,
+ BluetoothUUID.getService('health_thermometer'));
+ });
+ },
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/blocklisted-manufacturer-data-in-filter.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/blocklisted-manufacturer-data-in-filter.https.window.js
new file mode 100644
index 0000000000..2dae7f4cc6
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/blocklisted-manufacturer-data-in-filter.https.window.js
@@ -0,0 +1,29 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Reject with SecurityError if requesting a blocklisted ' +
+ 'manufacturer data.';
+
+const expected = new DOMException(
+ 'requestDevice() called with a filter containing a blocklisted UUID ' +
+ 'or manufacturer data. https://goo.gl/4NeimX',
+ 'SecurityError');
+
+bluetooth_test(async () => {
+ await assert_promise_rejects_with_message(
+ setUpPreconnectedFakeDevice({
+ fakeDeviceOptions: {knownServiceUUIDs: ['heart_rate']},
+ requestDeviceOptions: {
+ filters: [{
+ services: ['heart_rate'],
+ manufacturerData: [{
+ companyIdentifier: blocklistedManufacturerId,
+ dataPrefix: blocklistedManufacturerData,
+ }],
+ }]
+ }
+ }),
+ expected, 'Requesting blocklisted service rejects.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/blocklisted-service-in-filter.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/blocklisted-service-in-filter.https.window.js
new file mode 100644
index 0000000000..80eaf14447
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/blocklisted-service-in-filter.https.window.js
@@ -0,0 +1,21 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Reject with SecurityError if requesting a blocklisted ' +
+ 'service.';
+const expected = new DOMException(
+ 'requestDevice() called with a filter containing a blocklisted UUID ' +
+ 'or manufacturer data. https://goo.gl/4NeimX',
+ 'SecurityError');
+
+bluetooth_test(async () => {
+ await assert_promise_rejects_with_message(
+ setUpPreconnectedFakeDevice({
+ fakeDeviceOptions: {knownServiceUUIDs: ['human_interface_device']},
+ requestDeviceOptions:
+ {filters: [{services: ['human_interface_device']}]}
+ }),
+ expected, 'Requesting blocklisted service rejects.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/blocklisted-service-in-optionalServices.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/blocklisted-service-in-optionalServices.https.window.js
new file mode 100644
index 0000000000..4c01974e55
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/blocklisted-service-in-optionalServices.https.window.js
@@ -0,0 +1,29 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Blocklisted UUID in optionalServices is removed and ' +
+ 'access not granted.';
+const expected = new DOMException(
+ 'Origin is not allowed to access the ' +
+ 'service. Tip: Add the service UUID to \'optionalServices\' in ' +
+ 'requestDevice() options. https://goo.gl/HxfxSQ',
+ 'SecurityError');
+
+bluetooth_test(async () => {
+ let {device, fake_peripheral} = await getDiscoveredHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['human_interface_device']
+ });
+ await fake_peripheral.setNextGATTConnectionResponse({code: HCI_SUCCESS});
+ await device.gatt.connect();
+ Promise.all([
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService('human_interface_device'), expected,
+ 'Blocklisted service not accessible.'),
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices('human_interface_device'), expected,
+ 'Blocklisted services not accessible.')
+ ])
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/data-prefix-and-mask-size.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/data-prefix-and-mask-size.https.window.js
new file mode 100644
index 0000000000..fa2645093a
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/data-prefix-and-mask-size.https.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc =
+ 'Manufacturer data mask size must be equal to dataPrefix size.';
+
+bluetooth_test(async (t) => {
+ const companyIdentifier = 0x0001;
+ const dataPrefix = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
+ const mask = new Uint8Array([0xff]);
+
+ await promise_rejects_js(
+ t, TypeError,
+ requestDeviceWithTrustedClick(
+ {filters: [{manufacturerData: [{companyIdentifier, mask}]}]}));
+ await promise_rejects_js(
+ t, TypeError, requestDeviceWithTrustedClick({
+ filters: [{manufacturerData: [{companyIdentifier, dataPrefix, mask}]}]
+ }));
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/dataPrefix-buffer-is-detached.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/dataPrefix-buffer-is-detached.https.window.js
new file mode 100644
index 0000000000..936ca4735c
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/dataPrefix-buffer-is-detached.https.window.js
@@ -0,0 +1,33 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'dataPrefix value buffer must not be detached';
+
+function detachBuffer(buffer) {
+ window.postMessage('', '*', [buffer]);
+}
+
+bluetooth_test(async (t) => {
+ const companyIdentifier = 0x0001;
+
+ const typed_array = Uint8Array.of(1, 2);
+ detachBuffer(typed_array.buffer);
+
+ await promise_rejects_dom(
+ t, 'InvalidStateError', requestDeviceWithTrustedClick({
+ filters:
+ [{manufacturerData: [{companyIdentifier, dataPrefix: typed_array}]}]
+ }));
+
+ const array_buffer = Uint8Array.of(3, 4).buffer;
+ detachBuffer(array_buffer);
+
+ await promise_rejects_dom(
+ t, 'InvalidStateError', requestDeviceWithTrustedClick({
+ filters: [
+ {manufacturerData: [{companyIdentifier, dataPrefix: array_buffer}]}
+ ]
+ }));
+}, test_desc); \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/device-name-longer-than-29-bytes.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/device-name-longer-than-29-bytes.https.window.js
new file mode 100644
index 0000000000..20ed383d39
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/device-name-longer-than-29-bytes.https.window.js
@@ -0,0 +1,16 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A device name between 29 and 248 bytes is valid.';
+const DEVICE_NAME = 'a_device_name_that_is_longer_than_29_bytes_but_' +
+ 'shorter_than_248_bytes';
+
+bluetooth_test(async () => {
+ let {device} = await setUpPreconnectedFakeDevice({
+ fakeDeviceOptions: {name: DEVICE_NAME},
+ requestDeviceOptions: {filters: [{name: DEVICE_NAME}]}
+ });
+ assert_equals(device.name, DEVICE_NAME)
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-dataPrefix.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-dataPrefix.https.window.js
new file mode 100644
index 0000000000..75e12219cc
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-dataPrefix.https.window.js
@@ -0,0 +1,16 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'dataPrefix when present must be non-empty';
+
+bluetooth_test(async (t) => {
+ await promise_rejects_js(
+ t, TypeError, requestDeviceWithTrustedClick({
+ filters: [{
+ manufacturerData:
+ [{companyIdentifier: 1, dataPrefix: new Uint8Array()}]
+ }]
+ }));
+}, test_desc); \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-filter.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-filter.https.window.js
new file mode 100644
index 0000000000..bfe94f2721
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-filter.https.window.js
@@ -0,0 +1,12 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A filter must restrict the devices in some way.';
+const expected = new TypeError();
+
+bluetooth_test(
+ () => assert_promise_rejects_with_message(
+ requestDeviceWithTrustedClick({filters: [{}]}), expected),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-filters-member.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-filters-member.https.window.js
new file mode 100644
index 0000000000..3265e54fd8
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-filters-member.https.window.js
@@ -0,0 +1,16 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'An empty |filters| member should result in a TypeError';
+const expected = new DOMException(
+ 'Failed to execute \'requestDevice\' on ' +
+ '\'Bluetooth\': \'filters\' member must be non-empty to ' +
+ 'find any devices.',
+ new TypeError());
+
+bluetooth_test(
+ () => assert_promise_rejects_with_message(
+ requestDeviceWithTrustedClick({filters: []}), expected),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-manufacturerData-member.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-manufacturerData-member.https.window.js
new file mode 100644
index 0000000000..0996137f51
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-manufacturerData-member.https.window.js
@@ -0,0 +1,35 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'requestDevice with empty manufacturerData. ' +
+ 'Should reject with TypeError.';
+const test_specs = [
+ {filters: [{manufacturerData: []}]},
+ {filters: [{manufacturerData: [], name: 'Name'}]},
+ {filters: [{manufacturerData: [], services: ['heart_rate']}]},
+ {filters: [{manufacturerData: [], name: 'Name', services: ['heart_rate']}]},
+ {filters: [{manufacturerData: []}], optionalServices: ['heart_rate']}, {
+ filters: [{manufacturerData: [], name: 'Name'}],
+ optionalServices: ['heart_rate']
+ },
+ {
+ filters: [{manufacturerData: [], services: ['heart_rate']}],
+ optionalServices: ['heart_rate']
+ },
+ {
+ filters: [{manufacturerData: [], name: 'Name', services: ['heart_rate']}],
+ optionalServices: ['heart_rate']
+ }
+];
+
+bluetooth_test((t) => {
+ let test_promises = Promise.resolve();
+ test_specs.forEach(args => {
+ test_promises = test_promises.then(
+ () => promise_rejects_js(
+ t, TypeError, requestDeviceWithTrustedClick(args)));
+ });
+ return test_promises;
+}, test_desc); \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-namePrefix.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-namePrefix.https.window.js
new file mode 100644
index 0000000000..8ce2e64967
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-namePrefix.https.window.js
@@ -0,0 +1,33 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'requestDevice with empty namePrefix. ' +
+ 'Should reject with TypeError.';
+const expected = new TypeError();
+const test_specs = [
+ {filters: [{namePrefix: ''}]}, {filters: [{namePrefix: '', name: 'Name'}]},
+ {filters: [{namePrefix: '', services: ['heart_rate']}]},
+ {filters: [{namePrefix: '', name: 'Name', services: ['heart_rate']}]},
+ {filters: [{namePrefix: ''}], optionalServices: ['heart_rate']},
+ {filters: [{namePrefix: '', name: 'Name'}], optionalServices: ['heart_rate']},
+ {
+ filters: [{namePrefix: '', services: ['heart_rate']}],
+ optionalServices: ['heart_rate']
+ },
+ {
+ filters: [{namePrefix: '', name: 'Name', services: ['heart_rate']}],
+ optionalServices: ['heart_rate']
+ }
+];
+
+bluetooth_test(() => {
+ let test_promises = Promise.resolve();
+ test_specs.forEach(args => {
+ test_promises = test_promises.then(
+ () => assert_promise_rejects_with_message(
+ requestDeviceWithTrustedClick(args), expected));
+ });
+ return test_promises;
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-services-member.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-services-member.https.window.js
new file mode 100644
index 0000000000..a24611631d
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/empty-services-member.https.window.js
@@ -0,0 +1,18 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Services member must contain at least one service.';
+const expected = new TypeError();
+
+bluetooth_test(() => {
+ let test_promises = Promise.resolve();
+ generateRequestDeviceArgsWithServices([]).forEach(
+ args => {
+ test_promises = test_promises.then(
+ () => assert_promise_rejects_with_message(
+ requestDeviceWithTrustedClick(args), expected,
+ 'Services member must contain at least one service'))});
+ return test_promises;
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/filters-xor-acceptAllDevices.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/filters-xor-acceptAllDevices.https.window.js
new file mode 100644
index 0000000000..a6c48f2962
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/filters-xor-acceptAllDevices.https.window.js
@@ -0,0 +1,26 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'RequestDeviceOptions should have exactly one of ' +
+ '\'filters\' or \'acceptAllDevices:true\'. Reject with TypeError if not.';
+const expected = new DOMException(
+ 'Failed to execute \'requestDevice\' on \'Bluetooth\': ' +
+ 'Either \'filters\' should be present or ' +
+ '\'acceptAllDevices\' should be true, but not both.',
+ new TypeError());
+const test_specs = [
+ {}, {optionalServices: ['heart_rate']}, {filters: [], acceptAllDevices: true},
+ {filters: [], acceptAllDevices: true, optionalServices: ['heart_rate']}
+];
+
+bluetooth_test(() => {
+ let test_promises = Promise.resolve();
+ test_specs.forEach(
+ args => {
+ test_promises = test_promises.then(
+ () => assert_promise_rejects_with_message(
+ requestDeviceWithTrustedClick(args), expected))});
+ return test_promises;
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/invalid-companyIdentifier.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/invalid-companyIdentifier.https.window.js
new file mode 100644
index 0000000000..18cdbb4b4a
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/invalid-companyIdentifier.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'companyIdentifier must be in the [0, 65535] range';
+
+bluetooth_test(async (t) => {
+ await promise_rejects_js(
+ t, TypeError,
+ requestDeviceWithTrustedClick(
+ {filters: [{manufacturerData: [{companyIdentifier: -1}]}]}));
+ await promise_rejects_js(
+ t, TypeError,
+ requestDeviceWithTrustedClick(
+ {filters: [{manufacturerData: [{companyIdentifier: 65536}]}]}));
+}, test_desc); \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/mask-buffer-is-detached.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/mask-buffer-is-detached.https.window.js
new file mode 100644
index 0000000000..502e2e4057
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/mask-buffer-is-detached.https.window.js
@@ -0,0 +1,36 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'mask value buffer must not be detached';
+
+function detachBuffer(buffer) {
+ window.postMessage('', '*', [buffer]);
+}
+
+bluetooth_test(async (t) => {
+ const companyIdentifier = 0x0001;
+ const dataPrefix = Uint8Array.of(1, 2);
+
+ const typed_array = Uint8Array.of(1, 2);
+ detachBuffer(typed_array.buffer);
+
+ await promise_rejects_dom(
+ t, 'InvalidStateError', requestDeviceWithTrustedClick({
+ filters: [{
+ manufacturerData: [{companyIdentifier, dataPrefix, mask: typed_array}]
+ }]
+ }));
+
+ const array_buffer = Uint8Array.of(3, 4).buffer;
+ detachBuffer(array_buffer);
+
+ await promise_rejects_dom(
+ t, 'InvalidStateError', requestDeviceWithTrustedClick({
+ filters: [{
+ manufacturerData:
+ [{companyIdentifier, dataPrefix, mask: array_buffer}]
+ }]
+ }));
+}, test_desc); \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-name-unicode.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-name-unicode.https.window.js
new file mode 100644
index 0000000000..23de63d293
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-name-unicode.https.window.js
@@ -0,0 +1,20 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Unicode string with utf8 representation longer than 248 ' +
+ 'bytes in \'name\' must throw TypeError.';
+const expected = new DOMException(
+ 'Failed to execute \'requestDevice\' on \'Bluetooth\': ' +
+ 'A device name can\'t be longer than 248 bytes.',
+ new TypeError());
+// \u2764's UTF-8 respresentation is 3 bytes long.
+// 83 chars * 3 bytes/char = 249 bytes
+const unicode_name = '\u2764'.repeat(83);
+
+bluetooth_test(
+ () => assert_promise_rejects_with_message(
+ requestDeviceWithTrustedClick({filters: [{name: unicode_name}]}),
+ expected),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-name.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-name.https.window.js
new file mode 100644
index 0000000000..f14f78fe7d
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-name.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A device name longer than 248 must reject.';
+const expected = new DOMException(
+ 'Failed to execute \'requestDevice\' on \'Bluetooth\': A device ' +
+ 'name can\'t be longer than 248 bytes.',
+ new TypeError());
+const name_too_long = 'a'.repeat(249);
+
+bluetooth_test(
+ () => assert_promise_rejects_with_message(
+ requestDeviceWithTrustedClick({filters: [{name: name_too_long}]}),
+ expected, 'Device name longer than 248'),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-namePrefix-unicode.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-namePrefix-unicode.https.window.js
new file mode 100644
index 0000000000..aa832b2e76
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-namePrefix-unicode.https.window.js
@@ -0,0 +1,20 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Unicode string with utf8 representation longer than 248 ' +
+ 'bytes in \'namePrefix\' must throw NotFoundError.';
+const expected = new DOMException(
+ 'Failed to execute \'requestDevice\' on \'Bluetooth\': ' +
+ 'A device name can\'t be longer than 248 bytes.',
+ new TypeError());
+// \u2764's UTF-8 respresentation is 3 bytes long.
+// 83 chars * 3 bytes/char = 249 bytes
+const unicode_name = '\u2764'.repeat(83);
+
+bluetooth_test(
+ () => assert_promise_rejects_with_message(
+ requestDeviceWithTrustedClick({filters: [{namePrefix: unicode_name}]}),
+ expected),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-namePrefix.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-namePrefix.https.window.js
new file mode 100644
index 0000000000..5d27629eaa
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-exceeded-namePrefix.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A device name prefix longer than 248 must reject.';
+const expected = new DOMException(
+ 'Failed to execute \'requestDevice\' on \'Bluetooth\': A device ' +
+ 'name can\'t be longer than 248 bytes.',
+ new TypeError());
+const name_too_long = 'a'.repeat(249);
+
+bluetooth_test(
+ () => assert_promise_rejects_with_message(
+ requestDeviceWithTrustedClick({filters: [{namePrefix: name_too_long}]}),
+ expected, 'Device name longer than 248'),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-name-unicode.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-name-unicode.https.window.js
new file mode 100644
index 0000000000..1232a030e9
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-name-unicode.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A unicode device name of 248 bytes is valid.';
+// \u00A1's UTF-8 respresentation is 2 bytes long.
+// 124 chars * 2 bytes/char = 248 bytes
+const DEVICE_NAME = '\u00A1'.repeat(124);
+
+bluetooth_test(async () => {
+ let {device} = await setUpPreconnectedFakeDevice({
+ fakeDeviceOptions: {name: DEVICE_NAME},
+ requestDeviceOptions: {filters: [{name: DEVICE_NAME}]}
+ });
+ device => assert_equals(device.name, DEVICE_NAME)
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-name.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-name.https.window.js
new file mode 100644
index 0000000000..7ede93ce72
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-name.https.window.js
@@ -0,0 +1,15 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A device name of 248 bytes is valid.';
+const DEVICE_NAME = 'a'.repeat(248);
+
+bluetooth_test(async () => {
+ let {device} = await setUpPreconnectedFakeDevice({
+ fakeDeviceOptions: {name: DEVICE_NAME},
+ requestDeviceOptions: {filters: [{name: DEVICE_NAME}]}
+ });
+ device => assert_equals(device.name, DEVICE_NAME)
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-namePrefix-unicode.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-namePrefix-unicode.https.window.js
new file mode 100644
index 0000000000..2932dc1cde
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-namePrefix-unicode.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A unicode device namePrefix of 248 bytes is valid.';
+// \u00A1's UTF-8 respresentation is 2 bytes long.
+// 124 chars * 2 bytes/char = 248 bytes
+const DEVICE_NAME = '\u00A1'.repeat(124);
+
+bluetooth_test(async () => {
+ let {device} = await setUpPreconnectedFakeDevice({
+ fakeDeviceOptions: {name: DEVICE_NAME},
+ requestDeviceOptions: {filters: [{namePrefix: DEVICE_NAME}]}
+ });
+ device => assert_equals(device.name, DEVICE_NAME)
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-namePrefix.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-namePrefix.https.window.js
new file mode 100644
index 0000000000..f922bb2f0d
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/max-length-namePrefix.https.window.js
@@ -0,0 +1,15 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A device namePrefix of 248 bytes is valid.';
+const DEVICE_NAME = 'a'.repeat(248);
+
+bluetooth_test(async () => {
+ let {device} = await setUpPreconnectedFakeDevice({
+ fakeDeviceOptions: {name: DEVICE_NAME},
+ requestDeviceOptions: {filters: [{namePrefix: DEVICE_NAME}]}
+ });
+ device => assert_equals(device.name, DEVICE_NAME)
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/no-arguments.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/no-arguments.https.window.js
new file mode 100644
index 0000000000..075a97f1a9
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/no-arguments.https.window.js
@@ -0,0 +1,12 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'requestDevice() requires an argument.';
+const expected = new TypeError();
+
+promise_test(
+ () => assert_promise_rejects_with_message(
+ requestDeviceWithTrustedClick(), expected),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/same-company-identifier.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/same-company-identifier.https.window.js
new file mode 100644
index 0000000000..41f851adc5
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/same-company-identifier.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Manufacturer data company identifier must be unique.';
+const expected = new TypeError();
+
+let filters = [{
+ manufacturerData: [
+ {
+ companyIdentifier: 0x0001,
+ },
+ {
+ companyIdentifier: 0x0001,
+ }
+ ]
+}];
+
+bluetooth_test(
+ (t) => promise_rejects_js(
+ t, TypeError, requestDeviceWithTrustedClick({filters})),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/unicode-valid-length-name-name.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/unicode-valid-length-name-name.https.window.js
new file mode 100644
index 0000000000..cd10288ddb
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/unicode-valid-length-name-name.https.window.js
@@ -0,0 +1,18 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A name containing unicode characters whose utf8 length ' +
+ 'is less than 30 must not throw an error.';
+// \u2764's UTF-8 representation is 3 bytes long.
+// 9 chars * 3 bytes/char = 27 bytes
+const valid_unicode_name = '\u2764'.repeat(9);
+
+bluetooth_test(async () => {
+ let {device} = await setUpPreconnectedFakeDevice({
+ fakeDeviceOptions: {name: valid_unicode_name},
+ requestDeviceOptions: {filters: [{name: valid_unicode_name}]}
+ });
+ device => assert_equals(device.name, valid_unicode_name)
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/unicode-valid-length-name-namePrefix.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/unicode-valid-length-name-namePrefix.https.window.js
new file mode 100644
index 0000000000..494f324ee2
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/unicode-valid-length-name-namePrefix.https.window.js
@@ -0,0 +1,18 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'A namePrefix containing unicode characters whose utf8 ' +
+ 'length is less than 30 must not throw an error.';
+// \u2764's UTF-8 representation is 3 bytes long.
+// 9 chars * 3 bytes/char = 27 bytes
+const valid_unicode_name = '\u2764'.repeat(9);
+
+bluetooth_test(async () => {
+ let {device} = await setUpPreconnectedFakeDevice({
+ fakeDeviceOptions: {name: valid_unicode_name},
+ requestDeviceOptions: {filters: [{namePrefix: valid_unicode_name}]}
+ });
+ device => assert_equals(device.name, valid_unicode_name)
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/wrong-service-in-optionalServices-member.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/wrong-service-in-optionalServices-member.https.window.js
new file mode 100644
index 0000000000..bfba220f47
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/wrong-service-in-optionalServices-member.https.window.js
@@ -0,0 +1,37 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Invalid optional service must reject the promise.';
+const expected = new TypeError();
+const test_specs = [
+ {optionalServices: ['wrong_service'], filters: [{services: ['heart_rate']}]},
+ {
+ optionalServices: ['wrong_service'],
+ filters: [{services: ['heart_rate'], name: 'Name'}]
+ },
+ {
+ optionalServices: ['wrong_service'],
+ filters: [{services: ['heart_rate'], namePrefix: 'Pre'}]
+ },
+ {
+ optionalServices: ['wrong_service'],
+ filters: [{services: ['heart_rate'], name: 'Name', namePrefix: 'Pre'}]
+ },
+ {optionalServices: ['wrong_service'], filters: [{name: 'Name'}]}, {
+ optionalServices: ['wrong_service'],
+ filters: [{name: 'Name', namePrefix: 'Pre'}]
+ },
+ {optionalServices: ['wrong_service'], filters: [{namePrefix: 'Pre'}]}
+];
+
+bluetooth_test(() => {
+ let test_promises = Promise.resolve();
+ test_specs.forEach(args => {
+ test_promises = test_promises.then(
+ () => assert_promise_rejects_with_message(
+ requestDeviceWithTrustedClick(args), expected));
+ });
+ return test_promises;
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/wrong-service-in-services-member.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/wrong-service-in-services-member.https.window.js
new file mode 100644
index 0000000000..352437d0e5
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/canonicalizeFilter/wrong-service-in-services-member.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Invalid service must reject the promise.';
+const expected = new TypeError();
+
+bluetooth_test(() => {
+ let test_promises = Promise.resolve();
+ generateRequestDeviceArgsWithServices(['wrong_service']).forEach(args => {
+ test_promises = test_promises.then(
+ () => assert_promise_rejects_with_message(
+ requestDeviceWithTrustedClick(args), expected));
+ });
+ return test_promises;
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/cross-origin-iframe.sub.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/cross-origin-iframe.sub.https.window.js
new file mode 100644
index 0000000000..d802a86279
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/cross-origin-iframe.sub.https.window.js
@@ -0,0 +1,28 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Request device from a unique origin. ' +
+ 'Should reject with SecurityError.';
+const cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' +
+ '/bluetooth/resources/health-thermometer-iframe.html'
+let iframe = document.createElement('iframe');
+
+bluetooth_test(async (t) => {
+ await setUpHealthThermometerDevice();
+
+ // 1. Load the iframe.
+ const iframeWatcher = new EventWatcher(t, iframe, ['load']);
+ iframe.src = cross_origin_src;
+ document.body.appendChild(iframe);
+ await iframeWatcher.wait_for('load');
+
+ // 2. Request the device from the iframe.
+ const windowWatcher = new EventWatcher(t, window, ['message']);
+ iframe.contentWindow.postMessage({type: 'RequestDevice'}, '*');
+ const messageEvent = await windowWatcher.wait_for('message');
+ assert_equals(
+ messageEvent.data,
+ 'FAIL: SecurityError: Failed to execute \'requestDevice\' on \'Bluetooth\': Access to the feature "bluetooth" is disallowed by permissions policy.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/discovery-succeeds.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/discovery-succeeds.https.window.js
new file mode 100644
index 0000000000..4941d185ca
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/discovery-succeeds.https.window.js
@@ -0,0 +1,31 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Discover a device using alias, name, or UUID.';
+
+const test_specs = [
+ {
+ filters: [{services: [health_thermometer.alias]}],
+ },
+ {
+ filters: [{services: [health_thermometer.name]}],
+ },
+ {
+ filters: [{services: [health_thermometer.uuid]}],
+ },
+];
+
+bluetooth_test(
+ () => setUpHealthThermometerDevice().then(() => {
+ let test_promises = Promise.resolve();
+ test_specs.forEach(args => {
+ test_promises = test_promises.then(async () => {
+ const device = await requestDeviceWithTrustedClick(args);
+ assert_equals(device.constructor.name, 'BluetoothDevice');
+ });
+ });
+ return test_promises;
+ }),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/doesnt-consume-user-gesture.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/doesnt-consume-user-gesture.https.window.js
new file mode 100644
index 0000000000..9c742733e1
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/doesnt-consume-user-gesture.https.window.js
@@ -0,0 +1,24 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'requestDevice calls do not consume user gestures.';
+
+bluetooth_test(
+ () => setUpHealthThermometerAndHeartRateDevices().then(
+ () => callWithTrustedClick(() => {
+ let first = navigator.bluetooth.requestDevice(
+ {filters: [{services: ['heart_rate']}]});
+ let second = navigator.bluetooth.requestDevice(
+ {filters: [{services: ['heart_rate']}]});
+ return Promise.all([
+ first.then(
+ device =>
+ assert_equals(device.constructor.name, 'BluetoothDevice')),
+ second.then(
+ device =>
+ assert_equals(device.constructor.name, 'BluetoothDevice')),
+ ]);
+ })),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/filter-matches.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/filter-matches.https.window.js
new file mode 100644
index 0000000000..1a0f52ac30
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/filter-matches.https.window.js
@@ -0,0 +1,76 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Matches a filter if all present members match.';
+let matching_services = [health_thermometer.uuid];
+let matching_name = 'Health Thermometer';
+let matching_namePrefix = 'Health';
+let matching_manufacturerData = [{companyIdentifier: 0x0001}];
+
+let test_specs = [
+ {
+ filters: [{
+ services: matching_services,
+ }]
+ },
+ {
+ filters: [{
+ services: matching_services,
+ name: matching_name,
+ }]
+ },
+ {filters: [{services: matching_services, namePrefix: matching_namePrefix}]}, {
+ filters: [
+ {services: matching_services, manufacturerData: matching_manufacturerData}
+ ]
+ },
+ {
+ filters: [{
+ name: matching_name,
+ }],
+ optionalServices: matching_services
+ },
+ {
+ filters: [{namePrefix: matching_namePrefix}],
+ optionalServices: matching_services
+ },
+ {
+ filters: [{manufacturerData: matching_manufacturerData}],
+ optionalServices: matching_services
+ },
+ {
+ filters: [{
+ name: matching_name,
+ namePrefix: matching_namePrefix,
+ manufacturerData: matching_manufacturerData
+ }],
+ optionalServices: matching_services
+ },
+ {
+ filters: [{
+ services: matching_services,
+ name: matching_name,
+ namePrefix: matching_namePrefix,
+ manufacturerData: matching_manufacturerData
+ }]
+ }
+];
+
+bluetooth_test(
+ () => setUpHealthThermometerDevice().then(() => {
+ let test_promises = Promise.resolve();
+ test_specs.forEach(args => {
+ test_promises =
+ test_promises.then(() => requestDeviceWithTrustedClick(args))
+ .then(device => {
+ // We always have access to the services in matching_services
+ // because we include them in a filter or in optionalServices.
+ assert_equals(device.name, matching_name);
+ assert_true(device.name.startsWith(matching_namePrefix));
+ });
+ });
+ return test_promises;
+ }),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/le-not-supported.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/le-not-supported.https.window.js
new file mode 100644
index 0000000000..c961ab4492
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/le-not-supported.https.window.js
@@ -0,0 +1,15 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Reject with NotFoundError if Bluetooth is not supported.';
+const expected =
+ new DOMException('Bluetooth Low Energy not available.', 'NotFoundError');
+
+bluetooth_test(
+ () => navigator.bluetooth.test.setLESupported(false).then(
+ () => assert_promise_rejects_with_message(
+ requestDeviceWithTrustedClick({acceptAllDevices: true}), expected,
+ 'Bluetooth Low Energy is not supported.')),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/manufacturer-data-filter-matches.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/manufacturer-data-filter-matches.https.window.js
new file mode 100644
index 0000000000..c4c0e80532
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/manufacturer-data-filter-matches.https.window.js
@@ -0,0 +1,139 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Matches a filter when manufacturer data match.';
+
+let test_specs = [
+ {
+ filters: [{
+ manufacturerData: [{
+ companyIdentifier: 0x0001,
+ }],
+ }],
+ },
+ {
+ filters: [{
+ manufacturerData: [{
+ companyIdentifier: 0x0001,
+ dataPrefix: new Uint8Array([0x01]),
+ }],
+ }],
+ },
+ {
+ filters: [{
+ manufacturerData: [{
+ companyIdentifier: 0x0001,
+ dataPrefix: new Uint8Array([0x01]),
+ mask: new Uint8Array([0xff]),
+ }],
+ }],
+ },
+ {
+ filters: [{
+ manufacturerData: [{
+ companyIdentifier: 0x0001,
+ dataPrefix: new Uint8Array([0x01, 0x02]),
+ }],
+ }],
+ },
+ {
+ filters: [{
+ manufacturerData: [{
+ companyIdentifier: 0x0001,
+ dataPrefix: new Uint8Array([0x01, 0x02]),
+ mask: new Uint8Array([0xff, 0x01]),
+ }],
+ }],
+ },
+ {
+ filters: [{
+ manufacturerData: [
+ {
+ companyIdentifier: 0x0001,
+ dataPrefix: new Uint8Array([0x01, 0x02]),
+ mask: new Uint8Array([0xff, 0x01]),
+ },
+ {
+ companyIdentifier: 0x0002,
+ }
+ ],
+ }],
+ },
+ {
+ filters: [{
+ manufacturerData: [
+ {
+ companyIdentifier: 0x0001,
+ dataPrefix: new Uint8Array([0x01, 0x02]),
+ mask: new Uint8Array([0xff, 0x01]),
+ },
+ {
+ companyIdentifier: 0x0002,
+ dataPrefix: new Uint8Array([0x03]),
+ }
+ ],
+ }],
+ },
+ {
+ filters: [{
+ manufacturerData: [
+ {
+ companyIdentifier: 0x0001,
+ dataPrefix: new Uint8Array([0x01, 0x02]),
+ mask: new Uint8Array([0xff, 0x01]),
+ },
+ {
+ companyIdentifier: 0x0002,
+ dataPrefix: new Uint8Array([0x03]),
+ mask: new Uint8Array([0xff]),
+ }
+ ],
+ }],
+ },
+ {
+ filters: [{
+ manufacturerData: [
+ {
+ companyIdentifier: 0x0001,
+ dataPrefix: new Uint8Array([0x01, 0x02]),
+ mask: new Uint8Array([0xff, 0x01]),
+ },
+ {
+ companyIdentifier: 0x0002,
+ dataPrefix: new Uint8Array([0x03, 0x04]),
+ }
+ ],
+ }],
+ },
+ {
+ filters: [{
+ manufacturerData: [
+ {
+ companyIdentifier: 0x0001,
+ dataPrefix: new Uint8Array([0x01, 0x02]),
+ mask: new Uint8Array([0xff, 0x01]),
+ },
+ {
+ companyIdentifier: 0x0002,
+ dataPrefix: new Uint8Array([0x03, 0x04]),
+ mask: new Uint8Array([0xff, 0xff])
+ }
+ ],
+ }],
+ },
+];
+
+bluetooth_test(
+ () => setUpHealthThermometerDevice().then(() => {
+ let test_promises = Promise.resolve();
+ test_specs.forEach(args => {
+ test_promises = test_promises.then(async () => {
+ const device = await requestDeviceWithTrustedClick(args);
+ assert_equals(device.name, 'Health Thermometer');
+ });
+ });
+ return test_promises;
+ }),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/name-empty-device-from-name-empty-filter.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/name-empty-device-from-name-empty-filter.https.window.js
new file mode 100644
index 0000000000..2ff22cb702
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/name-empty-device-from-name-empty-filter.https.window.js
@@ -0,0 +1,14 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'An empty name device can be obtained by empty name filter.'
+
+bluetooth_test(async () => {
+ let {device} = await setUpPreconnectedFakeDevice({
+ fakeDeviceOptions: {name: ''},
+ requestDeviceOptions: {filters: [{name: ''}]}
+ });
+ assert_equals(device.name, '');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/not-processing-user-gesture.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/not-processing-user-gesture.https.window.js
new file mode 100644
index 0000000000..a063b61163
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/not-processing-user-gesture.https.window.js
@@ -0,0 +1,18 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Requires a user gesture.';
+const expected = new DOMException(
+ 'Failed to execute \'requestDevice\' on \'Bluetooth\': ' +
+ 'Must be handling a user gesture to show a permission request.',
+ 'SecurityError');
+
+bluetooth_test(
+ () => setUpHealthThermometerAndHeartRateDevices().then(
+ () => assert_promise_rejects_with_message(
+ navigator.bluetooth.requestDevice(
+ {filters: [{services: ['heart_rate']}]}),
+ expected, 'User gesture is required')),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/radio-not-present.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/radio-not-present.https.window.js
new file mode 100644
index 0000000000..b55d63c6ff
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/radio-not-present.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Reject with NotFoundError if there is no BT radio present.';
+const expected =
+ new DOMException('Bluetooth adapter not available.', 'NotFoundError');
+
+bluetooth_test(
+ () => navigator.bluetooth.test.simulateCentral({state: 'absent'})
+ .then(
+ () => assert_promise_rejects_with_message(
+ requestDeviceWithTrustedClick(
+ {filters: [{services: ['generic_access']}]}),
+ expected, 'Bluetooth adapter is not present.')),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/reject_opaque_origin.https.html b/testing/web-platform/tests/bluetooth/requestDevice/reject_opaque_origin.https.html
new file mode 100644
index 0000000000..df348dd39e
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/reject_opaque_origin.https.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+
+ promise_test(async (t) => {
+ await promise_rejects_dom(
+ t, 'SecurityError', navigator.bluetooth.requestDevice(),
+ 'requestDevice() should throw a SecurityError DOMException when called from a context where the top-level document has an opaque origin.');
+ }, 'Calls to Bluetooth APIs from an origin with opaque top origin get blocked.');
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/reject_opaque_origin.https.html.headers b/testing/web-platform/tests/bluetooth/requestDevice/reject_opaque_origin.https.html.headers
new file mode 100644
index 0000000000..c7e4e7cc5b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/reject_opaque_origin.https.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/request-from-iframe.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/request-from-iframe.https.window.js
new file mode 100644
index 0000000000..d3f3cf897f
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/request-from-iframe.https.window.js
@@ -0,0 +1,43 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Concurrent requestDevice calls in iframes work.';
+const iframes = [];
+for (let i = 0; i < 5; i++) {
+ iframes.push(document.createElement('iframe'));
+}
+
+bluetooth_test(
+ () => setUpHealthThermometerAndHeartRateDevices()
+ // 1. Load the iframes.
+ .then(() => {
+ let promises = [];
+ for (let iframe of iframes) {
+ iframe.src =
+ '/bluetooth/resources/health-thermometer-iframe.html';
+ document.body.appendChild(iframe);
+ promises.push(new Promise(
+ resolve => iframe.addEventListener('load', resolve)));
+ }
+ return Promise.all(promises);
+ })
+ // 2. Request the device from the iframes.
+ .then(() => new Promise(async (resolve) => {
+ let numMessages = 0;
+ window.onmessage =
+ messageEvent => {
+ assert_equals(messageEvent.data, 'Success');
+ if (++numMessages === iframes.length) {
+ resolve();
+ }
+ }
+
+ for (let iframe of iframes) {
+ await callWithTrustedClick(
+ () => iframe.contentWindow.postMessage(
+ {type: 'RequestDevice'}, '*'));
+ }
+ })),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/request-from-sandboxed-iframe.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/request-from-sandboxed-iframe.https.window.js
new file mode 100644
index 0000000000..2101cf0d6b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/request-from-sandboxed-iframe.https.window.js
@@ -0,0 +1,35 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Request device from a unique origin. ' +
+ 'Should reject with SecurityError.';
+const expected =
+ 'FAIL: SecurityError: Failed to execute \'requestDevice\' on ' +
+ '\'Bluetooth\': Access to the feature "bluetooth" is disallowed by ' +
+ 'permissions policy.';
+
+let iframe = document.createElement('iframe');
+
+bluetooth_test(
+ () => getConnectedHealthThermometerDevice()
+ // 1. Load the iframe.
+ .then(() => new Promise(resolve => {
+ iframe.sandbox.add('allow-scripts');
+ iframe.src =
+ '/bluetooth/resources/health-thermometer-iframe.html';
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve);
+ }))
+ // 2. Request the device from the iframe.
+ .then(() => new Promise(resolve => {
+ iframe.contentWindow.postMessage(
+ {type: 'RequestDevice'}, '*');
+
+ window.onmessage = messageEvent => {
+ assert_equals(messageEvent.data, expected);
+ resolve();
+ }
+ })),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/same-device.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/same-device.https.window.js
new file mode 100644
index 0000000000..41a42cf4c8
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/same-device.https.window.js
@@ -0,0 +1,19 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Returned device should always be the same.';
+let devices = [];
+
+bluetooth_test(async () => {
+ await setUpHealthThermometerAndHeartRateDevices();
+ devices.push(await requestDeviceWithTrustedClick(
+ {filters: [{services: [heart_rate.alias]}]}));
+ devices.push(await requestDeviceWithTrustedClick(
+ {filters: [{services: [heart_rate.name]}]}));
+ devices.push(await requestDeviceWithTrustedClick(
+ {filters: [{services: [heart_rate.uuid]}]}));
+ assert_equals(devices[0], devices[1]);
+ assert_equals(devices[1], devices[2]);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/sandboxed_iframe.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/sandboxed_iframe.https.window.js
new file mode 100644
index 0000000000..e9192a9305
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/sandboxed_iframe.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+'use strict';
+
+let iframe = document.createElement('iframe');
+
+bluetooth_test(async () => {
+ await getConnectedHealthThermometerDevice();
+ await new Promise(resolve => {
+ iframe.src = '/bluetooth/resources/health-thermometer-iframe.html';
+ iframe.sandbox.add('allow-scripts');
+ iframe.allow = 'bluetooth';
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve);
+ });
+ await new Promise(resolve => {
+ iframe.contentWindow.postMessage({type: 'RequestDevice'}, '*');
+
+ window.addEventListener('message', (messageEvent) => {
+ assert_false(/^FAIL: .*/.test(messageEvent.data));
+ resolve();
+ });
+ });
+}, 'Calls to Bluetooth APIs from a sandboxed iframe are valid.'); \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/requestDevice/single-filter-single-service.https.window.js b/testing/web-platform/tests/bluetooth/requestDevice/single-filter-single-service.https.window.js
new file mode 100644
index 0000000000..67afad0b93
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestDevice/single-filter-single-service.https.window.js
@@ -0,0 +1,14 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Simple filter selects matching device.';
+
+bluetooth_test(
+ () => setUpHealthThermometerAndHeartRateDevices()
+ .then(
+ () => requestDeviceWithTrustedClick(
+ {filters: [{services: ['health_thermometer']}]}))
+ .then(device => assert_equals(device.name, 'Health Thermometer')),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/requestLEScan/reject_opaque_origin.https.html b/testing/web-platform/tests/bluetooth/requestLEScan/reject_opaque_origin.https.html
new file mode 100644
index 0000000000..272c5aa760
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestLEScan/reject_opaque_origin.https.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+
+ promise_test(async (t) => {
+ await promise_rejects_dom(
+ t, 'SecurityError', navigator.bluetooth.requestLEScan(),
+ 'requestLEScan() should throw a SecurityError DOMException when called from a context where the top-level document has an opaque origin.');
+ }, 'Calls to Bluetooth APIs from an origin with opaque top origin get blocked.');
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/requestLEScan/reject_opaque_origin.https.html.headers b/testing/web-platform/tests/bluetooth/requestLEScan/reject_opaque_origin.https.html.headers
new file mode 100644
index 0000000000..c7e4e7cc5b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestLEScan/reject_opaque_origin.https.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/requestLEScan/sandboxed_iframe.https.window.js b/testing/web-platform/tests/bluetooth/requestLEScan/sandboxed_iframe.https.window.js
new file mode 100644
index 0000000000..32d1e74b77
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/requestLEScan/sandboxed_iframe.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+'use strict';
+
+let iframe = document.createElement('iframe');
+
+bluetooth_test(async () => {
+ await getConnectedHealthThermometerDevice();
+ await new Promise(resolve => {
+ iframe.src = '/bluetooth/resources/health-thermometer-iframe.html';
+ iframe.sandbox.add('allow-scripts');
+ iframe.allow = 'bluetooth';
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve);
+ });
+ await new Promise(resolve => {
+ iframe.contentWindow.postMessage({type: 'RequestLEScan'}, '*');
+
+ window.addEventListener('message', (messageEvent) => {
+ assert_false(/^FAIL: .*/.test(messageEvent.data));
+ resolve();
+ });
+ });
+}, 'Calls to Bluetooth APIs from a sandboxed iframe are valid.'); \ No newline at end of file
diff --git a/testing/web-platform/tests/bluetooth/resources/bluetooth-fake-devices.js b/testing/web-platform/tests/bluetooth/resources/bluetooth-fake-devices.js
new file mode 100644
index 0000000000..b718ab579a
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/resources/bluetooth-fake-devices.js
@@ -0,0 +1,1203 @@
+'use strict';
+
+/* Bluetooth Constants */
+
+/**
+ * HCI Error Codes.
+ * Used for simulateGATT{Dis}ConnectionResponse. For a complete list of
+ * possible error codes see BT 4.2 Vol 2 Part D 1.3 List Of Error Codes.
+ */
+const HCI_SUCCESS = 0x0000;
+const HCI_CONNECTION_TIMEOUT = 0x0008;
+
+/**
+ * GATT Error codes.
+ * Used for GATT operations responses. BT 4.2 Vol 3 Part F 3.4.1.1 Error
+ * Response
+ */
+const GATT_SUCCESS = 0x0000;
+const GATT_INVALID_HANDLE = 0x0001;
+
+/* Bluetooth UUID Constants */
+
+/* Service UUIDs */
+var blocklist_test_service_uuid = '611c954a-263b-4f4a-aab6-01ddb953f985';
+var request_disconnection_service_uuid = '01d7d889-7451-419f-aeb8-d65e7b9277af';
+
+/* Characteristic UUIDs */
+var blocklist_exclude_reads_characteristic_uuid =
+ 'bad1c9a2-9a5b-4015-8b60-1579bbbf2135';
+var request_disconnection_characteristic_uuid =
+ '01d7d88a-7451-419f-aeb8-d65e7b9277af';
+
+/* Descriptor UUIDs */
+var blocklist_test_descriptor_uuid = 'bad2ddcf-60db-45cd-bef9-fd72b153cf7c';
+var blocklist_exclude_reads_descriptor_uuid =
+ 'bad3ec61-3cc3-4954-9702-7977df514114';
+
+/**
+ * Helper objects that associate Bluetooth names, aliases, and UUIDs. These are
+ * useful for tests that check that the same result is produces when using all
+ * three methods of referring to a Bluetooth UUID.
+ */
+var generic_access = {
+ alias: 0x1800,
+ name: 'generic_access',
+ uuid: '00001800-0000-1000-8000-00805f9b34fb'
+};
+var device_name = {
+ alias: 0x2a00,
+ name: 'gap.device_name',
+ uuid: '00002a00-0000-1000-8000-00805f9b34fb'
+};
+var reconnection_address = {
+ alias: 0x2a03,
+ name: 'gap.reconnection_address',
+ uuid: '00002a03-0000-1000-8000-00805f9b34fb'
+};
+var heart_rate = {
+ alias: 0x180d,
+ name: 'heart_rate',
+ uuid: '0000180d-0000-1000-8000-00805f9b34fb'
+};
+var health_thermometer = {
+ alias: 0x1809,
+ name: 'health_thermometer',
+ uuid: '00001809-0000-1000-8000-00805f9b34fb'
+};
+var body_sensor_location = {
+ alias: 0x2a38,
+ name: 'body_sensor_location',
+ uuid: '00002a38-0000-1000-8000-00805f9b34fb'
+};
+var glucose = {
+ alias: 0x1808,
+ name: 'glucose',
+ uuid: '00001808-0000-1000-8000-00805f9b34fb'
+};
+var battery_service = {
+ alias: 0x180f,
+ name: 'battery_service',
+ uuid: '0000180f-0000-1000-8000-00805f9b34fb'
+};
+var battery_level = {
+ alias: 0x2A19,
+ name: 'battery_level',
+ uuid: '00002a19-0000-1000-8000-00805f9b34fb'
+};
+var user_description = {
+ alias: 0x2901,
+ name: 'gatt.characteristic_user_description',
+ uuid: '00002901-0000-1000-8000-00805f9b34fb'
+};
+var client_characteristic_configuration = {
+ alias: 0x2902,
+ name: 'gatt.client_characteristic_configuration',
+ uuid: '00002902-0000-1000-8000-00805f9b34fb'
+};
+var measurement_interval = {
+ alias: 0x2a21,
+ name: 'measurement_interval',
+ uuid: '00002a21-0000-1000-8000-00805f9b34fb'
+};
+
+/**
+ * An advertisement packet object that simulates a Health Thermometer device.
+ * @type {ScanResult}
+ */
+const health_thermometer_ad_packet = {
+ deviceAddress: '09:09:09:09:09:09',
+ rssi: -10,
+ scanRecord: {
+ name: 'Health Thermometer',
+ uuids: [health_thermometer.uuid],
+ },
+};
+
+/**
+ * An advertisement packet object that simulates a Heart Rate device.
+ * @type {ScanResult}
+ */
+const heart_rate_ad_packet = {
+ deviceAddress: '08:08:08:08:08:08',
+ rssi: -10,
+ scanRecord: {
+ name: 'Heart Rate',
+ uuids: [heart_rate.uuid],
+ },
+};
+
+const uuid1234 = BluetoothUUID.getService(0x1234);
+const uuid5678 = BluetoothUUID.getService(0x5678);
+const uuidABCD = BluetoothUUID.getService(0xABCD);
+const manufacturer1Data = new Uint8Array([1, 2]);
+const manufacturer2Data = new Uint8Array([3, 4]);
+const uuid1234Data = new Uint8Array([5, 6]);
+const uuid5678Data = new Uint8Array([7, 8]);
+const uuidABCDData = new Uint8Array([9, 10]);
+
+// TODO(crbug.com/1163207): Add the blocklist link.
+// Fake manufacturer data following iBeacon format listed in
+// https://en.wikipedia.org/wiki/IBeacon, which will be blocked according to [TBD blocklist link].
+const blocklistedManufacturerId = 0x4c;
+const blocklistedManufacturerData = new Uint8Array([
+ 0x02, 0x15, 0xb3, 0xeb, 0x8d, 0xb1, 0x30, 0xa5, 0x44, 0x8d, 0xb4, 0xac,
+ 0xfb, 0x68, 0xc9, 0x23, 0xa3, 0x0e, 0x00, 0x00, 0x00, 0x00, 0xbf
+]);
+// Fake manufacturer data that is not in [TBD blocklist link].
+const nonBlocklistedManufacturerId = 0x0001;
+const nonBlocklistedManufacturerData = new Uint8Array([1, 2]);
+
+/**
+ * An advertisement packet object that simulates a device that advertises
+ * service and manufacturer data.
+ * @type {ScanResult}
+ */
+const service_and_manufacturer_data_ad_packet = {
+ deviceAddress: '07:07:07:07:07:07',
+ rssi: -10,
+ scanRecord: {
+ name: 'LE Device',
+ uuids: [uuid1234],
+ manufacturerData: {0x0001: manufacturer1Data, 0x0002: manufacturer2Data},
+ serviceData: {
+ [uuid1234]: uuid1234Data,
+ [uuid5678]: uuid5678Data,
+ [uuidABCD]: uuidABCDData
+ }
+ }
+};
+
+/** Bluetooth Helpers */
+
+/**
+ * Helper class to create a BluetoothCharacteristicProperties object using an
+ * array of strings corresponding to the property bit to set.
+ */
+class TestCharacteristicProperties {
+ /** @param {Array<string>} properties */
+ constructor(properties) {
+ this.broadcast = false;
+ this.read = false;
+ this.writeWithoutResponse = false;
+ this.write = false;
+ this.notify = false;
+ this.indicate = false;
+ this.authenticatedSignedWrites = false;
+ this.reliableWrite = false;
+ this.writableAuxiliaries = false;
+
+ properties.forEach(val => {
+ if (this.hasOwnProperty(val))
+ this[val] = true;
+ else
+ throw `Invalid member '${val}'`;
+ });
+ }
+}
+
+/**
+ * Produces an array of BluetoothLEScanFilterInit objects containing the list of
+ * services in |services| and various permutations of the other
+ * BluetoothLEScanFilterInit properties. This method is used to test that the
+ * |services| are valid so the other properties do not matter.
+ * @param {BluetoothServiceUUID} services
+ * @returns {Array<RequestDeviceOptions>} A list of options containing
+ * |services| and various permutations of other options.
+ */
+function generateRequestDeviceArgsWithServices(services = ['heart_rate']) {
+ return [
+ {filters: [{services: services}]},
+ {filters: [{services: services, name: 'Name'}]},
+ {filters: [{services: services, namePrefix: 'Pre'}]}, {
+ filters: [
+ {services: services, manufacturerData: [{companyIdentifier: 0x0001}]}
+ ]
+ },
+ {
+ filters: [{
+ services: services,
+ name: 'Name',
+ namePrefix: 'Pre',
+ manufacturerData: [{companyIdentifier: 0x0001}]
+ }]
+ },
+ {filters: [{services: services}], optionalServices: ['heart_rate']}, {
+ filters: [{services: services, name: 'Name'}],
+ optionalServices: ['heart_rate']
+ },
+ {
+ filters: [{services: services, namePrefix: 'Pre'}],
+ optionalServices: ['heart_rate']
+ },
+ {
+ filters: [
+ {services: services, manufacturerData: [{companyIdentifier: 0x0001}]}
+ ],
+ optionalServices: ['heart_rate']
+ },
+ {
+ filters: [{
+ services: services,
+ name: 'Name',
+ namePrefix: 'Pre',
+ manufacturerData: [{companyIdentifier: 0x0001}]
+ }],
+ optionalServices: ['heart_rate']
+ }
+ ];
+}
+
+/**
+ * Causes |fake_peripheral| to disconnect and returns a promise that resolves
+ * once `gattserverdisconnected` has been fired on |device|.
+ * @param {BluetoothDevice} device The device to check if the
+ * `gattserverdisconnected` promise was fired.
+ * @param {FakePeripheral} fake_peripheral The device fake that represents
+ * |device|.
+ * @returns {Promise<Array<Object>>} A promise that resolves when the device has
+ * successfully disconnected.
+ */
+function simulateGATTDisconnectionAndWait(device, fake_peripheral) {
+ return Promise.all([
+ eventPromise(device, 'gattserverdisconnected'),
+ fake_peripheral.simulateGATTDisconnection(),
+ ]);
+}
+
+/** @type {FakeCentral} The fake adapter for the current test. */
+let fake_central = null;
+
+async function initializeFakeCentral({state = 'powered-on'}) {
+ if (!fake_central) {
+ fake_central = await navigator.bluetooth.test.simulateCentral({state});
+ }
+}
+
+/**
+ * A dictionary for specifying fake Bluetooth device setup options.
+ * @typedef {{address: !string, name: !string,
+ * manufacturerData: !Object<uint16,Array<uint8>>,
+ * knownServiceUUIDs: !Array<string>, connectable: !boolean,
+ * serviceDiscoveryComplete: !boolean}}
+ */
+let FakeDeviceOptions;
+
+/**
+ * @typedef {{fakeDeviceOptions: FakeDeviceOptions,
+ * requestDeviceOptions: RequestDeviceOptions}}
+ */
+let SetupOptions;
+
+/**
+ * Default options for setting up a Bluetooth device.
+ * @type {FakeDeviceOptions}
+ */
+const fakeDeviceOptionsDefault = {
+ address: '00:00:00:00:00:00',
+ name: 'LE Device',
+ manufacturerData: {},
+ knownServiceUUIDs: [],
+ connectable: false,
+ serviceDiscoveryComplete: false,
+};
+
+/**
+ * A dictionary containing the fake Bluetooth device object. The dictionary can
+ * optionally contain its fake services and its BluetoothDevice counterpart.
+ * @typedef {{fake_peripheral: !FakePeripheral,
+ * fake_services: Object<string, FakeService>,
+ * device: BluetoothDevice}}
+ */
+let FakeDevice;
+
+/**
+ * Creates a SetupOptions object using |setupOptionsDefault| as the base options
+ * object with the options from |setupOptionsOverride| overriding these
+ * defaults.
+ * @param {SetupOptions} setupOptionsDefault The default options object to use
+ * as the base.
+ * @param {SetupOptions} setupOptionsOverride The options to override the
+ * defaults with.
+ * @returns {SetupOptions} The merged setup options containing the defaults with
+ * the overrides applied.
+ */
+function createSetupOptions(setupOptionsDefault, setupOptionsOverride) {
+ // Merge the properties of |setupOptionsDefault| and |setupOptionsOverride|
+ // without modifying |setupOptionsDefault|.
+ let fakeDeviceOptions = Object.assign(
+ {...setupOptionsDefault.fakeDeviceOptions},
+ setupOptionsOverride.fakeDeviceOptions);
+ let requestDeviceOptions = Object.assign(
+ {...setupOptionsDefault.requestDeviceOptions},
+ setupOptionsOverride.requestDeviceOptions);
+
+ return {fakeDeviceOptions, requestDeviceOptions};
+}
+
+/**
+ * Adds a preconnected device with the given options. A preconnected device is a
+ * device that has been paired with the system previously. This can be done if,
+ * for example, the user pairs the device using the OS'es settings.
+ *
+ * By default, the preconnected device will be set up using the
+ * |fakeDeviceOptionsDefault| and will not use a RequestDeviceOption object.
+ * This means that the device will not be requested during the setup.
+ *
+ * If |setupOptionsOverride| is provided, these options will override the
+ * defaults. If |setupOptionsOverride| includes the requestDeviceOptions
+ * property, then the device will be requested using those options.
+ * @param {SetupOptions} setupOptionsOverride An object containing options for
+ * setting up a fake Bluetooth device and for requesting the device.
+ * @returns {Promise<FakeDevice>} The device fake initialized with the
+ * parameter values.
+ */
+async function setUpPreconnectedFakeDevice(setupOptionsOverride) {
+ await initializeFakeCentral({state: 'powered-on'});
+
+ let setupOptions = createSetupOptions(
+ {fakeDeviceOptions: fakeDeviceOptionsDefault}, setupOptionsOverride);
+
+ // Simulate the fake peripheral.
+ let preconnectedDevice = {};
+ preconnectedDevice.fake_peripheral =
+ await fake_central.simulatePreconnectedPeripheral({
+ address: setupOptions.fakeDeviceOptions.address,
+ name: setupOptions.fakeDeviceOptions.name,
+ manufacturerData: setupOptions.fakeDeviceOptions.manufacturerData,
+ knownServiceUUIDs: setupOptions.fakeDeviceOptions.knownServiceUUIDs,
+ });
+
+ if (setupOptions.fakeDeviceOptions.connectable) {
+ await preconnectedDevice.fake_peripheral.setNextGATTConnectionResponse(
+ {code: HCI_SUCCESS});
+ }
+
+ // Add known services.
+ preconnectedDevice.fake_services = new Map();
+ for (let service of setupOptions.fakeDeviceOptions.knownServiceUUIDs) {
+ let fake_service = await preconnectedDevice.fake_peripheral.addFakeService(
+ {uuid: service});
+ preconnectedDevice.fake_services.set(service, fake_service);
+ }
+
+ // Request the device if the request option isn't empty.
+ if (Object.keys(setupOptions.requestDeviceOptions).length !== 0) {
+ preconnectedDevice.device =
+ await requestDeviceWithTrustedClick(setupOptions.requestDeviceOptions);
+ }
+
+ // Set up services discovered state.
+ if (setupOptions.fakeDeviceOptions.serviceDiscoveryComplete) {
+ await preconnectedDevice.fake_peripheral.setNextGATTDiscoveryResponse(
+ {code: HCI_SUCCESS});
+ }
+
+ return preconnectedDevice;
+}
+
+/** Blocklisted GATT Device Helper Methods */
+
+/** @type {FakeDeviceOptions} */
+const blocklistFakeDeviceOptionsDefault = {
+ address: '11:11:11:11:11:11',
+ name: 'Blocklist Device',
+ knownServiceUUIDs: ['generic_access', blocklist_test_service_uuid],
+ connectable: true,
+ serviceDiscoveryComplete: true
+};
+
+/** @type {RequestDeviceOptions} */
+const blocklistRequestDeviceOptionsDefault = {
+ filters: [{services: [blocklist_test_service_uuid]}]
+};
+
+/** @type {SetupOptions} */
+const blocklistSetupOptionsDefault = {
+ fakeDeviceOptions: blocklistFakeDeviceOptionsDefault,
+ requestDeviceOptions: blocklistRequestDeviceOptionsDefault
+};
+
+/**
+ * Returns an object containing a BluetoothDevice discovered using |options|,
+ * its corresponding FakePeripheral and FakeRemoteGATTServices.
+ * The simulated device is called 'Blocklist Device' and it has one known
+ * service UUID |blocklist_test_service_uuid|. The |blocklist_test_service_uuid|
+ * service contains two characteristics:
+ * - |blocklist_exclude_reads_characteristic_uuid| (read, write)
+ * - 'gap.peripheral_privacy_flag' (read, write)
+ * The 'gap.peripheral_privacy_flag' characteristic contains three descriptors:
+ * - |blocklist_test_descriptor_uuid|
+ * - |blocklist_exclude_reads_descriptor_uuid|
+ * - 'gatt.client_characteristic_configuration'
+ * These are special UUIDs that have been added to the blocklist found at
+ * https://github.com/WebBluetoothCG/registries/blob/master/gatt_blocklist.txt
+ * There are also test UUIDs that have been added to the test environment which
+ * other implementations should add as test UUIDs as well.
+ * The device has been connected to and its attributes are ready to be
+ * discovered.
+ * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral,
+ * fake_blocklist_test_service: FakeRemoteGATTService,
+ * fake_blocklist_exclude_reads_characteristic:
+ * FakeRemoteGATTCharacteristic,
+ * fake_blocklist_exclude_writes_characteristic:
+ * FakeRemoteGATTCharacteristic,
+ * fake_blocklist_descriptor: FakeRemoteGATTDescriptor,
+ * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor,
+ * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor}>} An
+ * object containing the BluetoothDevice object and its corresponding
+ * GATT fake objects.
+ */
+async function getBlocklistDevice(setupOptionsOverride = {}) {
+ let setupOptions =
+ createSetupOptions(blocklistSetupOptionsDefault, setupOptionsOverride);
+ let fakeDevice = await setUpPreconnectedFakeDevice(setupOptions);
+ await fakeDevice.device.gatt.connect();
+
+ let fake_blocklist_test_service =
+ fakeDevice.fake_services.get(blocklist_test_service_uuid);
+
+ let fake_blocklist_exclude_reads_characteristic =
+ await fake_blocklist_test_service.addFakeCharacteristic({
+ uuid: blocklist_exclude_reads_characteristic_uuid,
+ properties: ['read', 'write'],
+ });
+ let fake_blocklist_exclude_writes_characteristic =
+ await fake_blocklist_test_service.addFakeCharacteristic({
+ uuid: 'gap.peripheral_privacy_flag',
+ properties: ['read', 'write'],
+ });
+
+ let fake_blocklist_descriptor =
+ await fake_blocklist_exclude_writes_characteristic.addFakeDescriptor(
+ {uuid: blocklist_test_descriptor_uuid});
+ let fake_blocklist_exclude_reads_descriptor =
+ await fake_blocklist_exclude_writes_characteristic.addFakeDescriptor(
+ {uuid: blocklist_exclude_reads_descriptor_uuid});
+ let fake_blocklist_exclude_writes_descriptor =
+ await fake_blocklist_exclude_writes_characteristic.addFakeDescriptor(
+ {uuid: 'gatt.client_characteristic_configuration'});
+ return {
+ device: fakeDevice.device,
+ fake_peripheral: fakeDevice.fake_peripheral,
+ fake_blocklist_test_service,
+ fake_blocklist_exclude_reads_characteristic,
+ fake_blocklist_exclude_writes_characteristic,
+ fake_blocklist_descriptor,
+ fake_blocklist_exclude_reads_descriptor,
+ fake_blocklist_exclude_writes_descriptor,
+ };
+}
+
+/**
+ * Returns an object containing a Blocklist Test BluetoothRemoteGattService and
+ * its corresponding FakeRemoteGATTService.
+ * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral,
+ * fake_blocklist_test_service: FakeRemoteGATTService,
+ * fake_blocklist_exclude_reads_characteristic:
+ * FakeRemoteGATTCharacteristic,
+ * fake_blocklist_exclude_writes_characteristic:
+ * FakeRemoteGATTCharacteristic,
+ * fake_blocklist_descriptor: FakeRemoteGATTDescriptor,
+ * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor,
+ * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor,
+ * service: BluetoothRemoteGATTService,
+ * fake_service: FakeBluetoothRemoteGATTService}>} An object containing the
+ * BluetoothDevice object and its corresponding GATT fake objects.
+ */
+async function getBlocklistTestService() {
+ let result = await getBlocklistDevice();
+ let service =
+ await result.device.gatt.getPrimaryService(blocklist_test_service_uuid);
+ return Object.assign(result, {
+ service,
+ fake_service: result.fake_blocklist_test_service,
+ });
+}
+
+/**
+ * Returns an object containing a blocklisted BluetoothRemoteGATTCharacteristic
+ * that excludes reads and its corresponding FakeRemoteGATTCharacteristic.
+ * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral,
+ * fake_blocklist_test_service: FakeRemoteGATTService,
+ * fake_blocklist_exclude_reads_characteristic:
+ * FakeRemoteGATTCharacteristic,
+ * fake_blocklist_exclude_writes_characteristic:
+ * FakeRemoteGATTCharacteristic,
+ * fake_blocklist_descriptor: FakeRemoteGATTDescriptor,
+ * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor,
+ * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor,
+ * service: BluetoothRemoteGATTService,
+ * fake_service: FakeBluetoothRemoteGATTService,
+ * characteristic: BluetoothRemoteGATTCharacteristic,
+ * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic}>} An object
+ * containing the BluetoothDevice object and its corresponding GATT fake
+ * objects.
+ */
+async function getBlocklistExcludeReadsCharacteristic() {
+ let result = await getBlocklistTestService();
+ let characteristic = await result.service.getCharacteristic(
+ blocklist_exclude_reads_characteristic_uuid);
+ return Object.assign(result, {
+ characteristic,
+ fake_characteristic: result.fake_blocklist_exclude_reads_characteristic
+ });
+}
+
+/**
+ * Returns an object containing a blocklisted BluetoothRemoteGATTCharacteristic
+ * that excludes writes and its corresponding FakeRemoteGATTCharacteristic.
+ * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral,
+ * fake_blocklist_test_service: FakeRemoteGATTService,
+ * fake_blocklist_exclude_reads_characteristic:
+ * FakeRemoteGATTCharacteristic,
+ * fake_blocklist_exclude_writes_characteristic:
+ * FakeRemoteGATTCharacteristic,
+ * fake_blocklist_descriptor: FakeRemoteGATTDescriptor,
+ * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor,
+ * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor,
+ * service: BluetoothRemoteGATTService,
+ * fake_service: FakeBluetoothRemoteGATTService,
+ * characteristic: BluetoothRemoteGATTCharacteristic,
+ * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic}>} An object
+ * containing the BluetoothDevice object and its corresponding GATT fake
+ * objects.
+ */
+async function getBlocklistExcludeWritesCharacteristic() {
+ let result = await getBlocklistTestService();
+ let characteristic =
+ await result.service.getCharacteristic('gap.peripheral_privacy_flag');
+ return Object.assign(result, {
+ characteristic,
+ fake_characteristic: result.fake_blocklist_exclude_writes_characteristic
+ });
+}
+
+/**
+ * Returns an object containing a blocklisted BluetoothRemoteGATTDescriptor that
+ * excludes reads and its corresponding FakeRemoteGATTDescriptor.
+ * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral,
+ * fake_blocklist_test_service: FakeRemoteGATTService,
+ * fake_blocklist_exclude_reads_characteristic:
+ * FakeRemoteGATTCharacteristic,
+ * fake_blocklist_exclude_writes_characteristic:
+ * FakeRemoteGATTCharacteristic,
+ * fake_blocklist_descriptor: FakeRemoteGATTDescriptor,
+ * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor,
+ * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor,
+ * service: BluetoothRemoteGATTService,
+ * fake_service: FakeBluetoothRemoteGATTService,
+ * characteristic: BluetoothRemoteGATTCharacteristic,
+ * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic,
+ * descriptor: BluetoothRemoteGATTDescriptor,
+ * fake_descriptor: FakeBluetoothRemoteGATTDescriptor}>} An object
+ * containing the BluetoothDevice object and its corresponding GATT fake
+ * objects.
+ */
+async function getBlocklistExcludeReadsDescriptor() {
+ let result = await getBlocklistExcludeWritesCharacteristic();
+ let descriptor = await result.characteristic.getDescriptor(
+ blocklist_exclude_reads_descriptor_uuid);
+ return Object.assign(result, {
+ descriptor,
+ fake_descriptor: result.fake_blocklist_exclude_reads_descriptor
+ });
+}
+
+/**
+ * Returns an object containing a blocklisted BluetoothRemoteGATTDescriptor that
+ * excludes writes and its corresponding FakeRemoteGATTDescriptor.
+ * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral,
+ * fake_blocklist_test_service: FakeRemoteGATTService,
+ * fake_blocklist_exclude_reads_characteristic:
+ * FakeRemoteGATTCharacteristic,
+ * fake_blocklist_exclude_writes_characteristic:
+ * FakeRemoteGATTCharacteristic,
+ * fake_blocklist_descriptor: FakeRemoteGATTDescriptor,
+ * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor,
+ * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor,
+ * service: BluetoothRemoteGATTService,
+ * fake_service: FakeBluetoothRemoteGATTService,
+ * characteristic: BluetoothRemoteGATTCharacteristic,
+ * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic,
+ * descriptor: BluetoothRemoteGATTDescriptor,
+ * fake_descriptor: FakeBluetoothRemoteGATTDescriptor}>} An object
+ * containing the BluetoothDevice object and its corresponding GATT fake
+ * objects.
+ */
+async function getBlocklistExcludeWritesDescriptor() {
+ let result = await getBlocklistExcludeWritesCharacteristic();
+ let descriptor = await result.characteristic.getDescriptor(
+ 'gatt.client_characteristic_configuration');
+ return Object.assign(result, {
+ descriptor: descriptor,
+ fake_descriptor: result.fake_blocklist_exclude_writes_descriptor,
+ });
+}
+
+/** Bluetooth HID Device Helper Methods */
+
+/** @type {FakeDeviceOptions} */
+const connectedHIDFakeDeviceOptionsDefault = {
+ address: '10:10:10:10:10:10',
+ name: 'HID Device',
+ knownServiceUUIDs: [
+ 'generic_access',
+ 'device_information',
+ 'human_interface_device',
+ ],
+ connectable: true,
+ serviceDiscoveryComplete: false
+};
+
+/** @type {RequestDeviceOptions} */
+const connectedHIDRequestDeviceOptionsDefault = {
+ filters: [{services: ['device_information']}],
+ optionalServices: ['human_interface_device']
+};
+
+/** @type {SetupOptions} */
+const connectedHIDSetupOptionsDefault = {
+ fakeDeviceOptions: connectedHIDFakeDeviceOptionsDefault,
+ requestDeviceOptions: connectedHIDRequestDeviceOptionsDefault
+};
+
+/**
+ * Similar to getHealthThermometerDevice except the GATT discovery
+ * response has not been set yet so more attributes can still be added.
+ * TODO(crbug.com/719816): Add descriptors.
+ * @param {RequestDeviceOptions} options The options for requesting a Bluetooth
+ * Device.
+ * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object
+ * containing a requested BluetoothDevice and its fake counter part.
+ */
+async function getConnectedHIDDevice(
+ requestDeviceOptionsOverride, fakeDeviceOptionsOverride) {
+ let setupOptions = createSetupOptions(connectedHIDSetupOptionsDefault, {
+ fakeDeviceOptions: fakeDeviceOptionsOverride,
+ requestDeviceOptions: requestDeviceOptionsOverride
+ });
+
+ let fakeDevice = await setUpPreconnectedFakeDevice(setupOptions);
+ await fakeDevice.device.gatt.connect();
+
+ // Blocklisted Characteristic:
+ // https://github.com/WebBluetoothCG/registries/blob/master/gatt_blocklist.txt
+ let dev_info = fakeDevice.fake_services.get('device_information');
+ await dev_info.addFakeCharacteristic({
+ uuid: 'serial_number_string',
+ properties: ['read'],
+ });
+ return fakeDevice;
+}
+
+/**
+ * Returns a BluetoothDevice discovered using |options| and its
+ * corresponding FakePeripheral.
+ * The simulated device is called 'HID Device' it has three known service
+ * UUIDs: 'generic_access', 'device_information', 'human_interface_device'.
+ * The primary service with 'device_information' UUID has a characteristics
+ * with UUID 'serial_number_string'. The device has been connected to and its
+ * attributes are ready to be discovered.
+ * @param {RequestDeviceOptions} options The options for requesting a Bluetooth
+ * Device.
+ * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object
+ * containing a requested BluetoothDevice and its fake counter part.
+ */
+async function getHIDDevice(options) {
+ let result =
+ await getConnectedHIDDevice(options, {serviceDiscoveryComplete: true});
+ return result;
+}
+
+/** Health Thermometer Bluetooth Device Helper Methods */
+
+/** @type {FakeDeviceOptions} */
+const healthTherometerFakeDeviceOptionsDefault = {
+ address: '09:09:09:09:09:09',
+ name: 'Health Thermometer',
+ manufacturerData: {0x0001: manufacturer1Data, 0x0002: manufacturer2Data},
+ knownServiceUUIDs: ['generic_access', 'health_thermometer'],
+};
+
+/**
+ * Returns a FakeDevice that corresponds to a simulated pre-connected device
+ * called 'Health Thermometer'. The device has two known serviceUUIDs:
+ * 'generic_access' and 'health_thermometer' and some fake manufacturer data.
+ * @returns {Promise<FakeDevice>} The device fake initialized as a Health
+ * Thermometer device.
+ */
+async function setUpHealthThermometerDevice(setupOptionsOverride = {}) {
+ let setupOptions = createSetupOptions(
+ {fakeDeviceOptions: healthTherometerFakeDeviceOptionsDefault},
+ setupOptionsOverride);
+ return await setUpPreconnectedFakeDevice(setupOptions);
+}
+
+/**
+ * Returns the same fake device as setUpHealthThermometerDevice() except
+ * that connecting to the peripheral will succeed.
+ * @returns {Promise<FakeDevice>} The device fake initialized as a
+ * connectable Health Thermometer device.
+ */
+async function setUpConnectableHealthThermometerDevice() {
+ let fake_device = await setUpHealthThermometerDevice(
+ {fakeDeviceOptions: {connectable: true}});
+ return fake_device;
+}
+
+/**
+ * Populates a fake_device with various fakes appropriate for a health
+ * thermometer. This resolves to an associative array composed of the fakes,
+ * including the |fake_peripheral|.
+ * @param {FakeDevice} fake_device The Bluetooth fake to populate GATT
+ * services, characteristics, and descriptors on.
+ * @returns {Promise<{fake_peripheral: FakePeripheral,
+ * fake_generic_access: FakeRemoteGATTService,
+ * fake_health_thermometer: FakeRemoteGATTService,
+ * fake_measurement_interval: FakeRemoteGATTCharacteristic,
+ * fake_cccd: FakeRemoteGATTDescriptor,
+ * fake_user_description: FakeRemoteGATTDescriptor,
+ * fake_temperature_measurement: FakeRemoteGATTCharacteristic,
+ * fake_temperature_type: FakeRemoteGATTCharacteristic}>} The FakePeripheral
+ * passed into this method along with the fake GATT services, characteristics,
+ * and descriptors added to it.
+ */
+async function populateHealthThermometerFakes(fake_device) {
+ let fake_peripheral = fake_device.fake_peripheral;
+ let fake_generic_access = fake_device.fake_services.get('generic_access');
+ let fake_health_thermometer =
+ fake_device.fake_services.get('health_thermometer');
+ let fake_measurement_interval =
+ await fake_health_thermometer.addFakeCharacteristic({
+ uuid: 'measurement_interval',
+ properties: ['read', 'write', 'indicate'],
+ });
+ let fake_user_description =
+ await fake_measurement_interval.addFakeDescriptor({
+ uuid: 'gatt.characteristic_user_description',
+ });
+ let fake_cccd = await fake_measurement_interval.addFakeDescriptor({
+ uuid: 'gatt.client_characteristic_configuration',
+ });
+ let fake_temperature_measurement =
+ await fake_health_thermometer.addFakeCharacteristic({
+ uuid: 'temperature_measurement',
+ properties: ['indicate'],
+ });
+ let fake_temperature_type =
+ await fake_health_thermometer.addFakeCharacteristic({
+ uuid: 'temperature_type',
+ properties: ['read'],
+ });
+ return {
+ fake_peripheral,
+ fake_generic_access,
+ fake_health_thermometer,
+ fake_measurement_interval,
+ fake_cccd,
+ fake_user_description,
+ fake_temperature_measurement,
+ fake_temperature_type,
+ };
+}
+
+/**
+ * Returns the same device and fake peripheral as getHealthThermometerDevice()
+ * after another frame (an iframe we insert) discovered the device,
+ * connected to it and discovered its services.
+ * @param {RequestDeviceOptions} options The options for requesting a Bluetooth
+ * Device.
+ * @returns {Promise<{device: BluetoothDevice, fakes: {
+ * fake_peripheral: FakePeripheral,
+ * fake_generic_access: FakeRemoteGATTService,
+ * fake_health_thermometer: FakeRemoteGATTService,
+ * fake_measurement_interval: FakeRemoteGATTCharacteristic,
+ * fake_cccd: FakeRemoteGATTDescriptor,
+ * fake_user_description: FakeRemoteGATTDescriptor,
+ * fake_temperature_measurement: FakeRemoteGATTCharacteristic,
+ * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object
+ * containing a requested BluetoothDevice and all of the GATT fake
+ * objects.
+ */
+async function getHealthThermometerDeviceWithServicesDiscovered(options) {
+ let iframe = document.createElement('iframe');
+ let fake_device = await setUpConnectableHealthThermometerDevice();
+ let fakes = populateHealthThermometerFakes(fake_device);
+ await fake_device.fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_SUCCESS,
+ });
+ await new Promise(resolve => {
+ let src = '/bluetooth/resources/health-thermometer-iframe.html';
+ // TODO(509038): Can be removed once LayoutTests/bluetooth/* that
+ // use health-thermometer-iframe.html have been moved to
+ // LayoutTests/external/wpt/bluetooth/*
+ if (window.location.pathname.includes('/LayoutTests/')) {
+ src =
+ '../../../external/wpt/bluetooth/resources/health-thermometer-iframe.html';
+ }
+ iframe.src = src;
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve);
+ });
+ await new Promise((resolve, reject) => {
+ callWithTrustedClick(() => {
+ iframe.contentWindow.postMessage(
+ {type: 'DiscoverServices', options: options}, '*');
+ });
+
+ function messageHandler(messageEvent) {
+ if (messageEvent.data == 'DiscoveryComplete') {
+ window.removeEventListener('message', messageHandler);
+ resolve();
+ } else {
+ reject(new Error(`Unexpected message: ${messageEvent.data}`));
+ }
+ }
+ window.addEventListener('message', messageHandler);
+ });
+ let device = await requestDeviceWithTrustedClick(options);
+ await device.gatt.connect();
+ return Object.assign({device}, fakes);
+}
+
+/**
+ * Returns the device requested and connected in the given iframe context and
+ * fakes from populateHealthThermometerFakes().
+ * @param {object} iframe The iframe element set up by the caller document.
+ * @returns {Promise<{device: BluetoothDevice, fakes: {
+ * fake_peripheral: FakePeripheral,
+ * fake_generic_access: FakeRemoteGATTService,
+ * fake_health_thermometer: FakeRemoteGATTService,
+ * fake_measurement_interval: FakeRemoteGATTCharacteristic,
+ * fake_cccd: FakeRemoteGATTDescriptor,
+ * fake_user_description: FakeRemoteGATTDescriptor,
+ * fake_temperature_measurement: FakeRemoteGATTCharacteristic,
+ * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object
+ * containing a requested BluetoothDevice and all of the GATT fake
+ * objects.
+ */
+async function getHealthThermometerDeviceFromIframe(iframe) {
+ const fake_device = await setUpConnectableHealthThermometerDevice();
+ const fakes = await populateHealthThermometerFakes(fake_device);
+ await new Promise(resolve => {
+ let src = '/bluetooth/resources/health-thermometer-iframe.html';
+ iframe.src = src;
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve, {once: true});
+ });
+ await new Promise((resolve, reject) => {
+ callWithTrustedClick(() => {
+ iframe.contentWindow.postMessage(
+ {
+ type: 'RequestAndConnect',
+ options: {filters: [{services: [health_thermometer.name]}]}
+ },
+ '*');
+ });
+
+ function messageHandler(messageEvent) {
+ if (messageEvent.data == 'Connected') {
+ window.removeEventListener('message', messageHandler);
+ resolve();
+ } else {
+ reject(new Error(`Unexpected message: ${messageEvent.data}`));
+ }
+ }
+ window.addEventListener('message', messageHandler, {once: true});
+ });
+ const devices = await iframe.contentWindow.navigator.bluetooth.getDevices();
+ assert_equals(devices.length, 1);
+ return Object.assign({device: devices[0]}, {fakes});
+}
+
+/**
+ * Similar to getHealthThermometerDevice() except the device
+ * is not connected and thus its services have not been
+ * discovered.
+ * @param {RequestDeviceOptions} options The options for requesting a Bluetooth
+ * Device.
+ * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object
+ * containing a requested BluetoothDevice and its fake counter part.
+ */
+async function getDiscoveredHealthThermometerDevice(options = {
+ filters: [{services: ['health_thermometer']}]
+}) {
+ return await setUpHealthThermometerDevice({requestDeviceOptions: options});
+}
+
+/**
+ * Similar to getHealthThermometerDevice() except the device has no services,
+ * characteristics, or descriptors.
+ * @param {RequestDeviceOptions} options The options for requesting a Bluetooth
+ * Device.
+ * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object
+ * containing a requested BluetoothDevice and its fake counter part.
+ */
+async function getEmptyHealthThermometerDevice(options) {
+ let fake_device = await getDiscoveredHealthThermometerDevice(options);
+ let fake_generic_access = fake_device.fake_services.get('generic_access');
+ let fake_health_thermometer =
+ fake_device.fake_services.get('health_thermometer');
+ // Remove services that have been set up by previous steps.
+ await fake_generic_access.remove();
+ await fake_health_thermometer.remove();
+ await fake_device.fake_peripheral.setNextGATTConnectionResponse(
+ {code: HCI_SUCCESS});
+ await fake_device.device.gatt.connect();
+ await fake_device.fake_peripheral.setNextGATTDiscoveryResponse(
+ {code: HCI_SUCCESS});
+ return fake_device;
+}
+
+/**
+ * Similar to getHealthThermometerService() except the service has no
+ * characteristics or included services.
+ * @param {RequestDeviceOptions} options The options for requesting a Bluetooth
+ * Device.
+ * @returns {service: BluetoothRemoteGATTService,
+ * fake_health_thermometer: FakeRemoteGATTService} An object containing the
+ * health themometer service object and its corresponding fake.
+ */
+async function getEmptyHealthThermometerService(options) {
+ let result = await getDiscoveredHealthThermometerDevice(options);
+ await result.fake_peripheral.setNextGATTConnectionResponse(
+ {code: HCI_SUCCESS});
+ await result.device.gatt.connect();
+ let fake_health_thermometer =
+ await result.fake_peripheral.addFakeService({uuid: 'health_thermometer'});
+ await result.fake_peripheral.setNextGATTDiscoveryResponse(
+ {code: HCI_SUCCESS});
+ let service =
+ await result.device.gatt.getPrimaryService('health_thermometer');
+ return {
+ service: service,
+ fake_health_thermometer: fake_health_thermometer,
+ };
+}
+
+/**
+ * Similar to getHealthThermometerDevice except the GATT discovery
+ * response has not been set yet so more attributes can still be added.
+ * @param {RequestDeviceOptions} options The options for requesting a Bluetooth
+ * Device.
+ * @returns {Promise<{device: BluetoothDevice, fakes: {
+ * fake_peripheral: FakePeripheral,
+ * fake_generic_access: FakeRemoteGATTService,
+ * fake_health_thermometer: FakeRemoteGATTService,
+ * fake_measurement_interval: FakeRemoteGATTCharacteristic,
+ * fake_cccd: FakeRemoteGATTDescriptor,
+ * fake_user_description: FakeRemoteGATTDescriptor,
+ * fake_temperature_measurement: FakeRemoteGATTCharacteristic,
+ * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object
+ * containing a requested BluetoothDevice and all of the GATT fake
+ * objects.
+ */
+async function getConnectedHealthThermometerDevice(options) {
+ let fake_device = await getDiscoveredHealthThermometerDevice(options);
+ await fake_device.fake_peripheral.setNextGATTConnectionResponse({
+ code: HCI_SUCCESS,
+ });
+ let fakes = await populateHealthThermometerFakes(fake_device);
+ await fake_device.device.gatt.connect();
+ return Object.assign({device: fake_device.device}, fakes);
+}
+
+/**
+ * Returns an object containing a BluetoothDevice discovered using |options|,
+ * its corresponding FakePeripheral and FakeRemoteGATTServices.
+ * The simulated device is called 'Health Thermometer' it has two known service
+ * UUIDs: 'generic_access' and 'health_thermometer' which correspond to two
+ * services with the same UUIDs. The 'health thermometer' service contains three
+ * characteristics:
+ * - 'temperature_measurement' (indicate),
+ * - 'temperature_type' (read),
+ * - 'measurement_interval' (read, write, indicate)
+ * The 'measurement_interval' characteristic contains a
+ * 'gatt.client_characteristic_configuration' descriptor and a
+ * 'characteristic_user_description' descriptor.
+ * The device has been connected to and its attributes are ready to be
+ * discovered.
+ * @param {RequestDeviceOptions} options The options for requesting a Bluetooth
+ * Device.
+ * @returns {Promise<{device: BluetoothDevice, fakes: {
+ * fake_peripheral: FakePeripheral,
+ * fake_generic_access: FakeRemoteGATTService,
+ * fake_health_thermometer: FakeRemoteGATTService,
+ * fake_measurement_interval: FakeRemoteGATTCharacteristic,
+ * fake_cccd: FakeRemoteGATTDescriptor,
+ * fake_user_description: FakeRemoteGATTDescriptor,
+ * fake_temperature_measurement: FakeRemoteGATTCharacteristic,
+ * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object
+ * containing a requested BluetoothDevice and all of the GATT fake
+ * objects.
+ */
+async function getHealthThermometerDevice(options) {
+ let result = await getConnectedHealthThermometerDevice(options);
+ await result.fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_SUCCESS,
+ });
+ return result;
+}
+
+/**
+ * Similar to getHealthThermometerDevice except that the peripheral has two
+ * 'health_thermometer' services.
+ * @param {RequestDeviceOptions} options The options for requesting a Bluetooth
+ * Device.
+ * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral,
+ * fake_generic_access: FakeRemoteGATTService, fake_health_thermometer1:
+ * FakeRemoteGATTService, fake_health_thermometer2: FakeRemoteGATTService}>} An
+ * object containing a requested Bluetooth device and two fake health
+ * thermometer GATT services.
+ */
+async function getTwoHealthThermometerServicesDevice(options) {
+ let result = await getConnectedHealthThermometerDevice(options);
+ let fake_health_thermometer2 =
+ await result.fake_peripheral.addFakeService({uuid: 'health_thermometer'});
+ await result.fake_peripheral.setNextGATTDiscoveryResponse(
+ {code: HCI_SUCCESS});
+ return {
+ device: result.device,
+ fake_peripheral: result.fake_peripheral,
+ fake_generic_access: result.fake_generic_access,
+ fake_health_thermometer1: result.fake_health_thermometer,
+ fake_health_thermometer2: fake_health_thermometer2
+ };
+}
+
+/**
+ * Returns an object containing a Health Thermometer BluetoothRemoteGattService
+ * and its corresponding FakeRemoteGATTService.
+ * @returns {Promise<{device: BluetoothDevice, fakes: {
+ * fake_peripheral: FakePeripheral,
+ * fake_generic_access: FakeRemoteGATTService,
+ * fake_health_thermometer: FakeRemoteGATTService,
+ * fake_measurement_interval: FakeRemoteGATTCharacteristic,
+ * fake_cccd: FakeRemoteGATTDescriptor,
+ * fake_user_description: FakeRemoteGATTDescriptor,
+ * fake_temperature_measurement: FakeRemoteGATTCharacteristic,
+ * fake_temperature_type: FakeRemoteGATTCharacteristic,
+ * service: BluetoothRemoteGATTService,
+ * fake_service: FakeRemoteGATTService}}>} An object
+ * containing a requested BluetoothDevice and all of the GATT fake
+ * objects.
+ */
+async function getHealthThermometerService() {
+ let result = await getHealthThermometerDevice();
+ let service =
+ await result.device.gatt.getPrimaryService('health_thermometer');
+ return Object.assign(result, {
+ service,
+ fake_service: result.fake_health_thermometer,
+ });
+}
+
+/**
+ * Returns an object containing a Measurement Interval
+ * BluetoothRemoteGATTCharacteristic and its corresponding
+ * FakeRemoteGATTCharacteristic.
+ * @returns {Promise<{device: BluetoothDevice, fakes: {
+ * fake_peripheral: FakePeripheral,
+ * fake_generic_access: FakeRemoteGATTService,
+ * fake_health_thermometer: FakeRemoteGATTService,
+ * fake_measurement_interval: FakeRemoteGATTCharacteristic,
+ * fake_cccd: FakeRemoteGATTDescriptor,
+ * fake_user_description: FakeRemoteGATTDescriptor,
+ * fake_temperature_measurement: FakeRemoteGATTCharacteristic,
+ * fake_temperature_type: FakeRemoteGATTCharacteristic,
+ * service: BluetoothRemoteGATTService,
+ * fake_service: FakeRemoteGATTService,
+ * characteristic: BluetoothRemoteGATTCharacteristic,
+ * fake_characteristic: FakeRemoteGATTCharacteristic}}>} An object
+ * containing a requested BluetoothDevice and all of the GATT fake
+ * objects.
+ */
+async function getMeasurementIntervalCharacteristic() {
+ let result = await getHealthThermometerService();
+ let characteristic =
+ await result.service.getCharacteristic('measurement_interval');
+ return Object.assign(result, {
+ characteristic,
+ fake_characteristic: result.fake_measurement_interval,
+ });
+}
+
+/**
+ * Returns an object containing a User Description
+ * BluetoothRemoteGATTDescriptor and its corresponding
+ * FakeRemoteGATTDescriptor.
+ * @returns {Promise<{device: BluetoothDevice, fakes: {
+ * fake_peripheral: FakePeripheral,
+ * fake_generic_access: FakeRemoteGATTService,
+ * fake_health_thermometer: FakeRemoteGATTService,
+ * fake_measurement_interval: FakeRemoteGATTCharacteristic,
+ * fake_cccd: FakeRemoteGATTDescriptor,
+ * fake_user_description: FakeRemoteGATTDescriptor,
+ * fake_temperature_measurement: FakeRemoteGATTCharacteristic,
+ * fake_temperature_type: FakeRemoteGATTCharacteristic,
+ * service: BluetoothRemoteGATTService,
+ * fake_service: FakeRemoteGATTService,
+ * characteristic: BluetoothRemoteGATTCharacteristic,
+ * fake_characteristic: FakeRemoteGATTCharacteristic
+ * descriptor: BluetoothRemoteGATTDescriptor,
+ * fake_descriptor: FakeRemoteGATTDescriptor}}>} An object
+ * containing a requested BluetoothDevice and all of the GATT fake
+ * objects.
+ */
+async function getUserDescriptionDescriptor() {
+ let result = await getMeasurementIntervalCharacteristic();
+ let descriptor = await result.characteristic.getDescriptor(
+ 'gatt.characteristic_user_description');
+ return Object.assign(result, {
+ descriptor,
+ fake_descriptor: result.fake_user_description,
+ });
+}
+
+/** Heart Rate Bluetooth Device Helper Methods */
+
+/** @type {FakeDeviceOptions} */
+const heartRateFakeDeviceOptionsDefault = {
+ address: '08:08:08:08:08:08',
+ name: 'Heart Rate',
+ knownServiceUUIDs: ['generic_access', 'heart_rate'],
+ connectable: false,
+ serviceDiscoveryComplete: false,
+};
+
+/** @type {RequestDeviceOptions} */
+const heartRateRequestDeviceOptionsDefault = {
+ filters: [{services: ['heart_rate']}]
+};
+
+async function getHeartRateDevice(setupOptionsOverride) {
+ let setupOptions = createSetupOptions(
+ {fakeDeviceOptions: heartRateFakeDeviceOptionsDefault},
+ setupOptionsOverride);
+ return await setUpPreconnectedFakeDevice(setupOptions);
+}
+
+/**
+ * Returns an array containing two FakePeripherals corresponding
+ * to the simulated devices.
+ * @returns {Promise<Array<FakePeripheral>>} The device fakes initialized as
+ * Health Thermometer and Heart Rate devices.
+ */
+async function setUpHealthThermometerAndHeartRateDevices() {
+ await initializeFakeCentral({state: 'powered-on'});
+ return Promise.all([
+ fake_central.simulatePreconnectedPeripheral({
+ address: '09:09:09:09:09:09',
+ name: 'Health Thermometer',
+ manufacturerData: {},
+ knownServiceUUIDs: ['generic_access', 'health_thermometer'],
+ }),
+ fake_central.simulatePreconnectedPeripheral({
+ address: '08:08:08:08:08:08',
+ name: 'Heart Rate',
+ manufacturerData: {},
+ knownServiceUUIDs: ['generic_access', 'heart_rate'],
+ })
+ ]);
+}
diff --git a/testing/web-platform/tests/bluetooth/resources/bluetooth-scanning-helpers.js b/testing/web-platform/tests/bluetooth/resources/bluetooth-scanning-helpers.js
new file mode 100644
index 0000000000..f474c9c306
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/resources/bluetooth-scanning-helpers.js
@@ -0,0 +1,42 @@
+'use strict';
+
+const company_id = '224';
+const data = new TextEncoder().encode('foo');
+const manufacturerDataMap = {[company_id]: data};
+const health_uuid = health_thermometer.uuid;
+const serviceDataMap = {[health_uuid]: data};
+const scanRecord = {
+ name: 'Health Thermometer',
+ uuids: ['generic_access', health_uuid],
+ txPower: 20,
+ appearance: 100,
+ manufacturerData: manufacturerDataMap,
+ serviceData: serviceDataMap,
+};
+const scanResult = {
+ deviceAddress: '09:09:09:09:09:09',
+ rssi: 100,
+ scanRecord: scanRecord,
+};
+
+function verifyBluetoothAdvertisingEvent(e) {
+ assert_equals(e.constructor.name, 'BluetoothAdvertisingEvent')
+ assert_equals(e.device.name, scanRecord.name)
+ assert_equals(e.name, scanRecord.name)
+ assert_array_equals(e.uuids,
+ ["00001800-0000-1000-8000-00805f9b34fb",
+ "00001809-0000-1000-8000-00805f9b34fb"])
+ assert_equals(e.txPower, 20)
+ assert_equals(e.rssi, 100)
+
+ assert_equals(e.manufacturerData.constructor.name,
+ 'BluetoothManufacturerDataMap')
+ assert_equals(data[0], e.manufacturerData.get(224).getUint8(0))
+ assert_equals(data[1], e.manufacturerData.get(224).getUint8(1))
+ assert_equals(data[2], e.manufacturerData.get(224).getUint8(2))
+
+ assert_equals(e.serviceData.constructor.name, 'BluetoothServiceDataMap')
+ assert_equals(data[0], e.serviceData.get(health_uuid).getUint8(0))
+ assert_equals(data[1], e.serviceData.get(health_uuid).getUint8(1))
+ assert_equals(data[2], e.serviceData.get(health_uuid).getUint8(2))
+}
diff --git a/testing/web-platform/tests/bluetooth/resources/bluetooth-test.js b/testing/web-platform/tests/bluetooth/resources/bluetooth-test.js
new file mode 100644
index 0000000000..7ad1b937e1
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/resources/bluetooth-test.js
@@ -0,0 +1,363 @@
+'use strict';
+
+/**
+ * Test Setup Helpers
+ */
+
+/**
+ * Loads a script by creating a <script> element pointing to |path|.
+ * @param {string} path The path of the script to load.
+ * @returns {Promise<void>} Resolves when the script has finished loading.
+ */
+function loadScript(path) {
+ let script = document.createElement('script');
+ let promise = new Promise(resolve => script.onload = resolve);
+ script.src = path;
+ script.async = false;
+ document.head.appendChild(script);
+ return promise;
+}
+
+/**
+ * Performs the Chromium specific setup necessary to run the tests in the
+ * Chromium browser. This test file is shared between Web Platform Tests and
+ * Blink Web Tests, so this method figures out the correct paths to use for
+ * loading scripts.
+ *
+ * TODO(https://crbug.com/569709): Update this description when all Web
+ * Bluetooth Blink Web Tests have been migrated into this repository.
+ * @returns {Promise<void>} Resolves when Chromium specific setup is complete.
+ */
+async function performChromiumSetup() {
+ // Determine path prefixes.
+ let resPrefix = '/resources';
+ const chromiumResources = ['/resources/chromium/web-bluetooth-test.js'];
+ const pathname = window.location.pathname;
+ if (pathname.includes('/wpt_internal/')) {
+ chromiumResources.push(
+ '/wpt_internal/bluetooth/resources/bluetooth-fake-adapter.js');
+ }
+
+ await loadScript(`${resPrefix}/test-only-api.js`);
+ if (!isChromiumBased) {
+ return;
+ }
+
+ for (const path of chromiumResources) {
+ await loadScript(path);
+ }
+
+ await initializeChromiumResources();
+
+ // Call setBluetoothFakeAdapter() to clean up any fake adapters left over by
+ // legacy tests. Legacy tests that use setBluetoothFakeAdapter() sometimes
+ // fail to clean their fake adapter. This is not a problem for these tests
+ // because the next setBluetoothFakeAdapter() will clean it up anyway but it
+ // is a problem for the new tests that do not use setBluetoothFakeAdapter().
+ // TODO(https://crbug.com/569709): Remove once setBluetoothFakeAdapter is no
+ // longer used.
+ if (typeof setBluetoothFakeAdapter !== 'undefined') {
+ setBluetoothFakeAdapter('');
+ }
+}
+
+/**
+ * These tests rely on the User Agent providing an implementation of the Web
+ * Bluetooth Testing API.
+ * https://docs.google.com/document/d/1Nhv_oVDCodd1pEH_jj9k8gF4rPGb_84VYaZ9IG8M_WY/edit?ts=59b6d823#heading=h.7nki9mck5t64
+ * @param {function{*}: Promise<*>} test_function The Web Bluetooth test to run.
+ * @param {string} name The name or description of the test.
+ * @param {object} properties An object containing extra options for the test.
+ * @param {Boolean} validate_response_consumed Whether to validate all response
+ * consumed or not.
+ * @returns {Promise<void>} Resolves if Web Bluetooth test ran successfully, or
+ * rejects if the test failed.
+ */
+function bluetooth_test(
+ test_function, name, properties, validate_response_consumed = true) {
+ return promise_test(async (t) => {
+ assert_implements(navigator.bluetooth, 'missing navigator.bluetooth');
+ // Trigger Chromium-specific setup.
+ await performChromiumSetup();
+ assert_implements(
+ navigator.bluetooth.test, 'missing navigator.bluetooth.test');
+ await test_function(t);
+ if (validate_response_consumed) {
+ let consumed = await navigator.bluetooth.test.allResponsesConsumed();
+ assert_true(consumed);
+ }
+ }, name, properties);
+}
+
+/**
+ * Test Helpers
+ */
+
+/**
+ * Waits until the document has finished loading.
+ * @returns {Promise<void>} Resolves if the document is already completely
+ * loaded or when the 'onload' event is fired.
+ */
+function waitForDocumentReady() {
+ return new Promise(resolve => {
+ if (document.readyState === 'complete') {
+ resolve();
+ }
+
+ window.addEventListener('load', () => {
+ resolve();
+ }, {once: true});
+ });
+}
+
+/**
+ * Simulates a user activation prior to running |callback|.
+ * @param {Function} callback The function to run after the user activation.
+ * @returns {Promise<*>} Resolves when the user activation has been simulated
+ * with the result of |callback|.
+ */
+async function callWithTrustedClick(callback) {
+ await waitForDocumentReady();
+ return new Promise(resolve => {
+ let button = document.createElement('button');
+ button.textContent = 'click to continue test';
+ button.style.display = 'block';
+ button.style.fontSize = '20px';
+ button.style.padding = '10px';
+ button.onclick = () => {
+ document.body.removeChild(button);
+ resolve(callback());
+ };
+ document.body.appendChild(button);
+ test_driver.click(button);
+ });
+}
+
+/**
+ * Calls requestDevice() in a context that's 'allowed to show a popup'.
+ * @returns {Promise<BluetoothDevice>} Resolves with a Bluetooth device if
+ * successful or rejects with an error.
+ */
+function requestDeviceWithTrustedClick() {
+ let args = arguments;
+ return callWithTrustedClick(
+ () => navigator.bluetooth.requestDevice.apply(navigator.bluetooth, args));
+}
+
+/**
+ * Calls requestLEScan() in a context that's 'allowed to show a popup'.
+ * @returns {Promise<BluetoothLEScan>} Resolves with the properties of the scan
+ * if successful or rejects with an error.
+ */
+function requestLEScanWithTrustedClick() {
+ let args = arguments;
+ return callWithTrustedClick(
+ () => navigator.bluetooth.requestLEScan.apply(navigator.bluetooth, args));
+}
+
+/**
+ * Function to test that a promise rejects with the expected error type and
+ * message.
+ * @param {Promise} promise
+ * @param {object} expected
+ * @param {string} description
+ * @returns {Promise<void>} Resolves if |promise| rejected with |expected|
+ * error.
+ */
+function assert_promise_rejects_with_message(promise, expected, description) {
+ return promise.then(
+ () => {
+ assert_unreached('Promise should have rejected: ' + description);
+ },
+ error => {
+ assert_equals(error.name, expected.name, 'Unexpected Error Name:');
+ if (expected.message) {
+ assert_equals(
+ error.message, expected.message, 'Unexpected Error Message:');
+ }
+ });
+}
+
+/**
+ * Helper class that can be created to check that an event has fired.
+ */
+class EventCatcher {
+ /**
+ * @param {EventTarget} object The object to listen for events on.
+ * @param {string} event The type of event to listen for.
+ */
+ constructor(object, event) {
+ /** @type {boolean} */
+ this.eventFired = false;
+
+ /** @type {function()} */
+ let event_listener = () => {
+ object.removeEventListener(event, event_listener);
+ this.eventFired = true;
+ };
+ object.addEventListener(event, event_listener);
+ }
+}
+
+/**
+ * Notifies when the event |type| has fired.
+ * @param {EventTarget} target The object to listen for the event.
+ * @param {string} type The type of event to listen for.
+ * @param {object} options Characteristics about the event listener.
+ * @returns {Promise<Event>} Resolves when an event of |type| has fired.
+ */
+function eventPromise(target, type, options) {
+ return new Promise(resolve => {
+ let wrapper = function(event) {
+ target.removeEventListener(type, wrapper);
+ resolve(event);
+ };
+ target.addEventListener(type, wrapper, options);
+ });
+}
+
+/**
+ * The action that should occur first in assert_promise_event_order_().
+ * @enum {string}
+ */
+const ShouldBeFirst = {
+ EVENT: 'event',
+ PROMISE_RESOLUTION: 'promiseresolved',
+};
+
+/**
+ * Helper function to assert that events are fired and a promise resolved
+ * in the correct order.
+ * 'event' should be passed as |should_be_first| to indicate that the events
+ * should be fired first, otherwise 'promiseresolved' should be passed.
+ * Attaches |num_listeners| |event| listeners to |object|. If all events have
+ * been fired and the promise resolved in the correct order, returns a promise
+ * that fulfills with the result of |object|.|func()| and |event.target.value|
+ * of each of event listeners. Otherwise throws an error.
+ * @param {ShouldBeFirst} should_be_first Indicates whether |func| should
+ * resolve before |event| is fired.
+ * @param {EventTarget} object The target object to add event listeners to.
+ * @param {function(*): Promise<*>} func The function to test the resolution
+ * order for.
+ * @param {string} event The event type to listen for.
+ * @param {number} num_listeners The number of events to listen for.
+ * @returns {Promise<*>} The return value of |func|.
+ */
+function assert_promise_event_order_(
+ should_be_first, object, func, event, num_listeners) {
+ let order = [];
+ let event_promises = [];
+ for (let i = 0; i < num_listeners; i++) {
+ event_promises.push(new Promise(resolve => {
+ let event_listener = (e) => {
+ object.removeEventListener(event, event_listener);
+ order.push(ShouldBeFirst.EVENT);
+ resolve(e.target.value);
+ };
+ object.addEventListener(event, event_listener);
+ }));
+ }
+
+ let func_promise = object[func]().then(result => {
+ order.push(ShouldBeFirst.PROMISE_RESOLUTION);
+ return result;
+ });
+
+ return Promise.all([func_promise, ...event_promises]).then((result) => {
+ if (should_be_first !== order[0]) {
+ throw should_be_first === ShouldBeFirst.PROMISE_RESOLUTION ?
+ `'${event}' was fired before promise resolved.` :
+ `Promise resolved before '${event}' was fired.`;
+ }
+
+ if (order[0] !== ShouldBeFirst.PROMISE_RESOLUTION &&
+ order[order.length - 1] !== ShouldBeFirst.PROMISE_RESOLUTION) {
+ throw 'Promise resolved in between event listeners.';
+ }
+
+ return result;
+ });
+}
+
+/**
+ * Asserts that the promise returned by |func| resolves before events of type
+ * |event| are fired |num_listeners| times on |object|. See
+ * assert_promise_event_order_ above for more details.
+ * @param {EventTarget} object The target object to add event listeners to.
+ * @param {function(*): Promise<*>} func The function whose promise should
+ * resolve first.
+ * @param {string} event The event type to listen for.
+ * @param {number} num_listeners The number of events to listen for.
+ * @returns {Promise<*>} The return value of |func|.
+ */
+function assert_promise_resolves_before_event(
+ object, func, event, num_listeners = 1) {
+ return assert_promise_event_order_(
+ ShouldBeFirst.PROMISE_RESOLUTION, object, func, event, num_listeners);
+}
+
+/**
+ * Asserts that the promise returned by |func| resolves after events of type
+ * |event| are fired |num_listeners| times on |object|. See
+ * assert_promise_event_order_ above for more details.
+ * @param {EventTarget} object The target object to add event listeners to.
+ * @param {function(*): Promise<*>} func The function whose promise should
+ * resolve first.
+ * @param {string} event The event type to listen for.
+ * @param {number} num_listeners The number of events to listen for.
+ * @returns {Promise<*>} The return value of |func|.
+ */
+function assert_promise_resolves_after_event(
+ object, func, event, num_listeners = 1) {
+ return assert_promise_event_order_(
+ ShouldBeFirst.EVENT, object, func, event, num_listeners);
+}
+
+/**
+ * Returns a promise that resolves after 100ms unless the the event is fired on
+ * the object in which case the promise rejects.
+ * @param {EventTarget} object The target object to listen for events.
+ * @param {string} event_name The event type to listen for.
+ * @returns {Promise<void>} Resolves if no events were fired.
+ */
+function assert_no_events(object, event_name) {
+ return new Promise((resolve) => {
+ let event_listener = (e) => {
+ object.removeEventListener(event_name, event_listener);
+ assert_unreached('Object should not fire an event.');
+ };
+ object.addEventListener(event_name, event_listener);
+ // TODO: Remove timeout.
+ // http://crbug.com/543884
+ step_timeout(() => {
+ object.removeEventListener(event_name, event_listener);
+ resolve();
+ }, 100);
+ });
+}
+
+/**
+ * Asserts that |properties| contains the same properties in
+ * |expected_properties| with equivalent values.
+ * @param {object} properties Actual object to compare.
+ * @param {object} expected_properties Expected object to compare with.
+ */
+function assert_properties_equal(properties, expected_properties) {
+ for (let key in expected_properties) {
+ assert_equals(properties[key], expected_properties[key]);
+ }
+}
+
+/**
+ * Asserts that |data_map| contains |expected_key|, and that the uint8 values
+ * for |expected_key| matches |expected_value|.
+ */
+function assert_data_maps_equal(data_map, expected_key, expected_value) {
+ assert_true(data_map.has(expected_key));
+
+ const value = new Uint8Array(data_map.get(expected_key).buffer);
+ assert_equals(value.length, expected_value.length);
+ for (let i = 0; i < value.length; ++i) {
+ assert_equals(value[i], expected_value[i]);
+ }
+}
diff --git a/testing/web-platform/tests/bluetooth/resources/health-thermometer-iframe.html b/testing/web-platform/tests/bluetooth/resources/health-thermometer-iframe.html
new file mode 100644
index 0000000000..f9f7a6f0d7
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/resources/health-thermometer-iframe.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<body>
+<button>Click me!</button>
+<script>
+let device, gatt;
+
+test_driver.set_test_context(parent);
+
+function requestDeviceWithOptionsAndConnect(options) {
+ return test_driver.click(document.getElementsByTagName("button")[0])
+ .then(() => navigator.bluetooth.requestDevice(options))
+ .then(device => device.gatt.connect());
+}
+
+window.addEventListener('message', (messageEvent) => {
+ switch (messageEvent.data.type) {
+ case 'GetAvailability':
+ navigator.bluetooth.getAvailability()
+ .then(availability => parent.postMessage(availability, '*'))
+ .catch(err => parent.postMessage(`FAIL: ${err}`, '*'));
+ break;
+ case 'GetDevices':
+ navigator.bluetooth.getDevices()
+ .then(devices => parent.postMessage('Success', '*'))
+ .catch(err => parent.postMessage(`FAIL: ${err}`, '*'));
+ break;
+ case 'RequestDevice':
+ test_driver.click(document.getElementsByTagName('button')[0])
+ .then(
+ () => navigator.bluetooth.requestDevice(
+ {filters: [{services: ['generic_access']}]}))
+ .then(device => {
+ if (device.constructor.name === 'BluetoothDevice') {
+ parent.postMessage('Success', '*');
+ } else {
+ parent.postMessage(
+ `FAIL: requestDevice in iframe returned ${device.name}`, '*');
+ }
+ })
+ .catch(err => parent.postMessage(`FAIL: ${err.name}: ${err.message}`, '*'));
+ break;
+ case 'RequestLEScan':
+ test_driver.click(document.getElementsByTagName('button')[0])
+ .then(
+ () => navigator.bluetooth.requestLEScan(
+ {filters: [{name: 'Health Thermometer'}]}))
+ .then(leScan => {
+ if (leScan.active) {
+ parent.postMessage('Success', '*');
+ leScan.stop();
+ } else {
+ parent.postMessage(`FAIL: the LE scan hasn't been initiated.`, '*');
+ }
+ })
+ .catch(err => parent.postMessage(`FAIL: ${err.name}: ${err.message}`, '*'));
+ break;
+ case 'RequestAndConnect':
+ requestDeviceWithOptionsAndConnect(messageEvent.data.options)
+ .then(_ => {
+ gatt = _;
+ device = gatt.device;
+ parent.postMessage('Connected', '*');
+ })
+ .catch(err => {
+ parent.postMessage(`FAIL: ${err}`, '*');
+ });
+ break;
+ case 'DiscoverServices':
+ requestDeviceWithOptionsAndConnect(messageEvent.data.options)
+ .then(gatt => gatt.getPrimaryServices())
+ .then(() => parent.postMessage('DiscoveryComplete', '*'))
+ .catch(err => {
+ parent.postMessage(`FAIL: ${err}`, '*');
+ });
+ break;
+ case 'GetService':
+ if (typeof gatt === 'undefined') {
+ parent.postMessage('FAIL: no GATT server', '*');
+ break;
+ }
+ gatt.getPrimaryService(messageEvent.data.options)
+ .then(() => parent.postMessage('ServiceReceived', '*'))
+ .catch(err => parent.postMessage(`FAIL: ${err}`, '*'));
+ break;
+ default:
+ parent.postMessage(
+ `FAIL: Bad message type: ${messageEvent.data.type}`, '*');
+ }
+});
+</script>
diff --git a/testing/web-platform/tests/bluetooth/script-tests/base_test_js.template b/testing/web-platform/tests/bluetooth/script-tests/base_test_js.template
new file mode 100644
index 0000000000..04c7a70ba4
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/base_test_js.template
@@ -0,0 +1,7 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+TEST
diff --git a/testing/web-platform/tests/bluetooth/script-tests/characteristic/characteristic-is-removed.js b/testing/web-platform/tests/bluetooth/script-tests/characteristic/characteristic-is-removed.js
new file mode 100644
index 0000000000..48aaec3e93
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/characteristic/characteristic-is-removed.js
@@ -0,0 +1,24 @@
+'use strict';
+const test_desc = 'Characteristic gets removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Characteristic no longer exists.',
+ 'InvalidStateError');
+let fake_peripheral, characteristic, fake_characteristic;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({fake_peripheral, characteristic, fake_characteristic} = _))
+ .then(() => characteristic.getDescriptor(user_description.name))
+ .then(() => null, (e) => assert_unreached('Caught error unexpectedly.', e))
+ .then(() => fake_characteristic.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ characteristic.CALLS([
+ getDescriptor(user_description.name)|
+ getDescriptors(user_description.name)[UUID]|
+ getDescriptors()|
+ readValue()|
+ writeValue(new Uint8Array(1))|
+ writeValueWithResponse(new Uint8Array(1))|
+ writeValueWithoutResponse(new Uint8Array(1))|
+ startNotifications()
+ ]), expected)),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/characteristic/descriptor-get-same-object.js b/testing/web-platform/tests/bluetooth/script-tests/characteristic/descriptor-get-same-object.js
new file mode 100644
index 0000000000..4e6bc3519b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/characteristic/descriptor-get-same-object.js
@@ -0,0 +1,32 @@
+'use strict';
+const test_desc = 'Calls to FUNCTION_NAME should return the same object.';
+let characteristic;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({characteristic} = _))
+ .then(() => Promise.all([
+ characteristic.CALLS([
+ getDescriptor(user_description.alias)|
+ getDescriptors(user_description.alias)
+ ]),
+ characteristic.FUNCTION_NAME(user_description.name),
+ characteristic.FUNCTION_NAME(user_description.uuid)
+ ]))
+ .then(descriptors_arrays => {
+ assert_true(descriptors_arrays.length > 0)
+
+ // Convert to arrays if necessary.
+ for (let i = 0; i < descriptors_arrays.length; i++) {
+ descriptors_arrays[i] = [].concat(descriptors_arrays[i]);
+ }
+
+ for (let i = 1; i < descriptors_arrays.length; i++) {
+ assert_equals(descriptors_arrays[0].length,
+ descriptors_arrays[i].length);
+ }
+
+ let base_set = new Set(descriptors_arrays[0]);
+ for (let descriptors of descriptors_arrays) {
+ descriptors.forEach(descriptor => assert_true(base_set.has(descriptor)));
+ }
+ }), test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/characteristic/service-is-removed.js b/testing/web-platform/tests/bluetooth/script-tests/characteristic/service-is-removed.js
new file mode 100644
index 0000000000..2f5824082b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/characteristic/service-is-removed.js
@@ -0,0 +1,20 @@
+// TODO(https://crbug.com/672127) Use this test case to test the rest of
+// characteristic functions.
+'use strict';
+const test_desc = 'Service is removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Service no longer exists.',
+ 'InvalidStateError');
+let characteristic, fake_peripheral, fake_service;
+
+bluetooth_test(() => getMeasurementIntervalCharacteristic()
+ .then(_ => ({characteristic, fake_peripheral, fake_service} = _))
+ .then(() => fake_service.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ characteristic.CALLS([
+ getDescriptor(user_description.name)|
+ getDescriptors(user_description.uuid)[UUID]|
+ getDescriptors(user_description.name)]),
+ expected,
+ 'Service got removed.')),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/descriptor/service-is-removed.js b/testing/web-platform/tests/bluetooth/script-tests/descriptor/service-is-removed.js
new file mode 100644
index 0000000000..5373364399
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/descriptor/service-is-removed.js
@@ -0,0 +1,18 @@
+'use strict';
+const test_desc = 'Service gets removed. Reject with InvalidStateError.';
+const expected = new DOMException('GATT Service no longer exists.',
+ 'InvalidStateError');
+let descriptor, fake_peripheral, fake_service;
+
+bluetooth_test(() => getUserDescriptionDescriptor()
+ .then(_ => ({descriptor, fake_peripheral, fake_service} = _))
+ .then(() => fake_service.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ descriptor.CALLS([
+ readValue()|
+ writeValue(new ArrayBuffer(1 /* length */))
+ ]),
+ expected,
+ 'Service got removed.')),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-called-before.js b/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-called-before.js
new file mode 100644
index 0000000000..57704ee299
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-called-before.js
@@ -0,0 +1,22 @@
+'use strict';
+const test_desc = 'disconnect() called before FUNCTION_NAME. ' +
+ 'Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.',
+ 'NetworkError');
+let device;
+
+bluetooth_test(() => getConnectedHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.disconnect())
+ .then(() => assert_promise_rejects_with_message(
+ device.gatt.CALLS([
+ getPrimaryService('health_thermometer')|
+ getPrimaryServices()|
+ getPrimaryServices('health_thermometer')[UUID]]),
+ expected)),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-called-during-error.js b/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-called-during-error.js
new file mode 100644
index 0000000000..edabb07bcc
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-called-during-error.js
@@ -0,0 +1,22 @@
+'use strict';
+const test_desc = 'disconnect() called during a FUNCTION_NAME ' +
+ 'call that fails. Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.', 'NetworkError');
+let device;
+
+bluetooth_test(() => getEmptyHealthThermometerDevice()
+ .then(_ => ({device} = _))
+ .then(() => {
+ let promise = assert_promise_rejects_with_message(
+ device.gatt.CALLS([
+ getPrimaryService('health_thermometer')|
+ getPrimaryServices()|
+ getPrimaryServices('health_thermometer')[UUID]
+ ]),
+ expected)
+ device.gatt.disconnect();
+ return promise;
+ }),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-called-during-success.js b/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-called-during-success.js
new file mode 100644
index 0000000000..84157a0693
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-called-during-success.js
@@ -0,0 +1,23 @@
+'use strict';
+const test_desc = 'disconnect() called during a FUNCTION_NAME call that ' +
+ 'succeeds. Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.',
+ 'NetworkError');
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(({device}) => {
+ let promise = assert_promise_rejects_with_message(
+ device.gatt.CALLS([
+ getPrimaryService('health_thermometer')|
+ getPrimaryServices()|
+ getPrimaryServices('health_thermometer')[UUID]
+ ]),
+ expected);
+ device.gatt.disconnect();
+ return promise;
+ }), test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-discovery-timeout.js b/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-discovery-timeout.js
new file mode 100644
index 0000000000..718e290950
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-discovery-timeout.js
@@ -0,0 +1,42 @@
+'use strict';
+const test_desc =
+ 'Calls to FUNCTION_NAME when device disconnects and discovery' +
+ ' times out should reject promise rather than get stuck.';
+let device;
+
+bluetooth_test(
+ async (t) => {
+ let {device, fake_peripheral} =
+ await getConnectedHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ });
+
+ await fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_CONNECTION_TIMEOUT,
+ });
+ await Promise.all([
+ fake_peripheral.simulateGATTDisconnection({
+ code: HCI_SUCCESS,
+ }),
+ // Using promise_rejects_dom here rather than
+ // assert_promise_rejects_with_message as the race between
+ // simulateGATTDisconnection and getPrimaryServices might end up giving
+ // slightly different exception message (i.e has "Failed to execute ...
+ // on
+ // ... " prefix when disconnected state is reflected on the renderer
+ // side). The point of the test is no matter how race between them, the
+ // promise will be rejected as opposed to get stuck.
+ promise_rejects_dom(t, 'NetworkError', device.gatt.CALLS([
+ getPrimaryService('health_thermometer') | getPrimaryServices() |
+ getPrimaryServices('health_thermometer')[UUID]
+ ])),
+ ]);
+ },
+ test_desc, '',
+ // As specified above there is a race condition between
+ // simulateGATTDisconnection and getPrimaryServices, the artificial
+ // GATTDiscoveryResponse might not be consumed in case
+ // simulateGATTDisconnection happens first. As a result explicitly skip
+ // all response consumed validation at the end of the test.
+ /*validate_response_consumed=*/ false);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-invalidates-objects.js b/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-invalidates-objects.js
new file mode 100644
index 0000000000..995fda3441
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/disconnect-invalidates-objects.js
@@ -0,0 +1,39 @@
+'use strict';
+const test_desc = 'Calls on services after we disconnect and connect again. '+
+ 'Should reject with InvalidStateError.';
+let device, services;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.CALLS([
+ getPrimaryService('health_thermometer')|
+ getPrimaryServices()|
+ getPrimaryServices('health_thermometer')[UUID]]))
+ // Convert to array if necessary.
+ .then(s => services = [].concat(s))
+ .then(() => device.gatt.disconnect())
+ .then(() => device.gatt.connect())
+ .then(() => {
+ let promises = Promise.resolve();
+ for (let service of services) {
+ let error = new DOMException(
+ `Service with UUID ${service.uuid} is no longer valid. Remember ` +
+ `to retrieve the service again after reconnecting.`,
+ 'InvalidStateError');
+ promises = promises.then(() =>
+ assert_promise_rejects_with_message(
+ service.getCharacteristic('measurement_interval'),
+ error));
+ promises = promises.then(() =>
+ assert_promise_rejects_with_message(
+ service.getCharacteristics(),
+ error));
+ promises = promises.then(() =>
+ assert_promise_rejects_with_message(
+ service.getCharacteristics('measurement_interval'),
+ error));
+ }
+ return promises;
+ }), test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/disconnected-device.js b/testing/web-platform/tests/bluetooth/script-tests/server/disconnected-device.js
new file mode 100644
index 0000000000..2b6011642b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/disconnected-device.js
@@ -0,0 +1,20 @@
+'use strict';
+const test_desc = 'FUNCTION_NAME called before connecting. Reject with ' +
+ 'NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.',
+ 'NetworkError');
+
+bluetooth_test(() => getDiscoveredHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.CALLS([
+ getPrimaryService('health_thermometer')|
+ getPrimaryServices()|
+ getPrimaryServices('health_thermometer')[UUID]
+ ]),
+ expected)),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/discovery-complete-no-permission-absent-service.js b/testing/web-platform/tests/bluetooth/script-tests/server/discovery-complete-no-permission-absent-service.js
new file mode 100644
index 0000000000..e9e972359a
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/discovery-complete-no-permission-absent-service.js
@@ -0,0 +1,25 @@
+'use strict';
+const test_desc = 'Request for absent service without permission. Should ' +
+ 'Reject with SecurityError even if services have been discovered already.';
+const expected = new DOMException(
+ 'Origin is not allowed to access the service. Tip: Add the service ' +
+ 'UUID to \'optionalServices\' in requestDevice() options. ' +
+ 'https://goo.gl/HxfxSQ',
+ 'SecurityError');
+let device;
+
+bluetooth_test(() => getHealthThermometerDeviceWithServicesDiscovered({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(_ => ({device} = _))
+ .then(() => Promise.all([
+ assert_promise_rejects_with_message(
+ device.gatt.CALLS([
+ getPrimaryService(glucose.alias)|
+ getPrimaryServices(glucose.alias)[UUID]
+ ]), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.FUNCTION_NAME(glucose.name), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.FUNCTION_NAME(glucose.uuid), expected)])),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/discovery-complete-service-not-found.js b/testing/web-platform/tests/bluetooth/script-tests/server/discovery-complete-service-not-found.js
new file mode 100644
index 0000000000..6b745d7e2a
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/discovery-complete-service-not-found.js
@@ -0,0 +1,16 @@
+'use strict';
+const test_desc = 'Request for absent service. Must reject with ' +
+ 'NotFoundError even when the services have previously been discovered.';
+
+bluetooth_test(() => getHealthThermometerDeviceWithServicesDiscovered({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['glucose']})
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.CALLS([
+ getPrimaryService('glucose')|
+ getPrimaryServices('glucose')[UUID]
+ ]),
+ new DOMException(
+ `No Services matching UUID ${glucose.uuid} found in Device.`,
+ 'NotFoundError'))),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/garbage-collection-ran-during-error.js b/testing/web-platform/tests/bluetooth/script-tests/server/garbage-collection-ran-during-error.js
new file mode 100644
index 0000000000..cf508a928e
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/garbage-collection-ran-during-error.js
@@ -0,0 +1,25 @@
+'use strict';
+const test_desc = 'Garbage Collection ran during a FUNCTION_NAME ' +
+ 'call that failed. Should not crash.'
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect first ' +
+ 'with `device.gatt.connect`.',
+ 'NetworkError');
+let promise;
+
+bluetooth_test(() => getEmptyHealthThermometerDevice()
+ .then(({device}) => {
+ promise = assert_promise_rejects_with_message(
+ device.gatt.CALLS([
+ getPrimaryService('health_thermometer')|
+ getPrimaryServices()|
+ getPrimaryServices('health_thermometer')[UUID]
+ ]),
+ expected);
+ // Disconnect called to clear attributeInstanceMap and allow the
+ // object to get garbage collected.
+ device.gatt.disconnect();
+ return garbageCollect();
+ })
+ .then(() => promise),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/garbage-collection-ran-during-success.js b/testing/web-platform/tests/bluetooth/script-tests/server/garbage-collection-ran-during-success.js
new file mode 100644
index 0000000000..bb472fcca4
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/garbage-collection-ran-during-success.js
@@ -0,0 +1,24 @@
+'use strict';
+const test_desc = 'Garbage Collection ran during a FUNCTION_NAME call that ' +
+ 'succeeds. Should not crash.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. ' +
+ '(Re)connect first with `device.gatt.connect`.',
+ 'NetworkError');
+let promise;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(({device}) => {
+ promise = assert_promise_rejects_with_message(
+ device.gatt.CALLS([
+ getPrimaryService('health_thermometer') |
+ getPrimaryServices() |
+ getPrimaryServices('health_thermometer')[UUID]]),
+ expected);
+ device.gatt.disconnect();
+ return garbageCollect();
+ })
+ .then(() => promise),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/get-different-service-after-reconnection.js b/testing/web-platform/tests/bluetooth/script-tests/server/get-different-service-after-reconnection.js
new file mode 100644
index 0000000000..e72128a76f
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/get-different-service-after-reconnection.js
@@ -0,0 +1,35 @@
+'use strict';
+const test_desc = 'Calls to FUNCTION_NAME after a disconnection should return ' +
+ 'a different object.';
+let device, services_first_connection, services_second_connection;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.CALLS([
+ getPrimaryService('health_thermometer')|
+ getPrimaryServices()|
+ getPrimaryServices('health_thermometer')[UUID]]))
+ .then(services => services_first_connection = services)
+ .then(() => device.gatt.disconnect())
+ .then(() => device.gatt.connect())
+ .then(() => device.gatt.PREVIOUS_CALL)
+ .then(services => services_second_connection = services)
+ .then(() => {
+ // Convert to arrays if necessary.
+ services_first_connection = [].concat(services_first_connection);
+ services_second_connection = [].concat(services_second_connection);
+
+ assert_equals(services_first_connection.length,
+ services_second_connection.length);
+
+ let first_connection_set = new Set(services_first_connection);
+ let second_connection_set = new Set(services_second_connection);
+
+ // The two sets should be disjoint.
+ let common_services = services_first_connection.filter(
+ val => second_connection_set.has(val));
+ assert_equals(common_services.length, 0);
+ }), test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/get-same-object.js b/testing/web-platform/tests/bluetooth/script-tests/server/get-same-object.js
new file mode 100644
index 0000000000..3b3bdd19d2
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/get-same-object.js
@@ -0,0 +1,33 @@
+'use strict';
+const test_desc = 'Calls to FUNCTION_NAME should return the same object.';
+let device;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']})
+ .then(({device}) => Promise.all([
+ device.gatt.CALLS([
+ getPrimaryService('health_thermometer')|
+ getPrimaryServices()|
+ getPrimaryServices('health_thermometer')[UUID]]),
+ device.gatt.PREVIOUS_CALL]))
+ .then(([services_first_call, services_second_call]) => {
+ // Convert to arrays if necessary.
+ services_first_call = [].concat(services_first_call);
+ services_second_call = [].concat(services_second_call);
+
+ assert_equals(services_first_call.length, services_second_call.length);
+
+ let first_call_set = new Set(services_first_call);
+ assert_equals(services_first_call.length, first_call_set.size);
+ let second_call_set = new Set(services_second_call);
+ assert_equals(services_second_call.length, second_call_set.size);
+
+ services_first_call.forEach(service => {
+ assert_true(second_call_set.has(service))
+ });
+
+ services_second_call.forEach(service => {
+ assert_true(first_call_set.has(service));
+ });
+ }), test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/invalid-service-name.js b/testing/web-platform/tests/bluetooth/script-tests/server/invalid-service-name.js
new file mode 100644
index 0000000000..52cbb24f4a
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/invalid-service-name.js
@@ -0,0 +1,22 @@
+'use strict';
+const test_desc = 'Wrong Service name. Reject with TypeError.';
+const expected = new DOMException(
+ "Failed to execute 'FUNCTION_NAME' on " +
+ "'BluetoothRemoteGATTServer': Invalid Service name: " +
+ "'wrong_name'. It must be a valid UUID alias (e.g. 0x1234), " +
+ "UUID (lowercase hex characters e.g. " +
+ "'00001234-0000-1000-8000-00805f9b34fb'), " +
+ "or recognized standard name from " +
+ "https://www.bluetooth.com/specifications/gatt/services" +
+ " e.g. 'alert_notification'.",
+ 'TypeError');
+
+bluetooth_test(() => getConnectedHealthThermometerDevice()
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.CALLS([
+ getPrimaryService('wrong_name')|
+ getPrimaryServices('wrong_name')
+ ]),
+ expected,
+ 'Wrong Service name passed.')),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/no-permission-absent-service.js b/testing/web-platform/tests/bluetooth/script-tests/server/no-permission-absent-service.js
new file mode 100644
index 0000000000..200dab3e93
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/no-permission-absent-service.js
@@ -0,0 +1,23 @@
+'use strict';
+const test_desc = 'Request for absent service without permission. ' +
+ 'Reject with SecurityError.';
+const expected = new DOMException(
+ 'Origin is not allowed to access the service. Tip: Add the service UUID ' +
+ 'to \'optionalServices\' in requestDevice() options. ' +
+ 'https://goo.gl/HxfxSQ',
+ 'SecurityError');
+
+bluetooth_test(() => getConnectedHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(({device}) => Promise.all([
+ assert_promise_rejects_with_message(
+ device.gatt.CALLS([
+ getPrimaryService(glucose.alias)|
+ getPrimaryServices(glucose.alias)[UUID]
+ ]), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.FUNCTION_NAME(glucose.name), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.FUNCTION_NAME(glucose.uuid), expected)])),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/no-permission-for-any-service.js b/testing/web-platform/tests/bluetooth/script-tests/server/no-permission-for-any-service.js
new file mode 100644
index 0000000000..60e3ef0080
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/no-permission-for-any-service.js
@@ -0,0 +1,17 @@
+'use strict';
+const test_desc = 'Request for present service without permission to access ' +
+ 'any service. Reject with SecurityError.';
+const expected = new DOMException(
+ 'Origin is not allowed to access any service. Tip: Add the service ' +
+ 'UUID to \'optionalServices\' in requestDevice() options. ' +
+ 'https://goo.gl/HxfxSQ',
+ 'SecurityError');
+
+bluetooth_test(() => getConnectedHealthThermometerDevice({acceptAllDevices: true})
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.CALLS([
+ getPrimaryService('heart_rate')|
+ getPrimaryServices()|
+ getPrimaryServices('heart_rate')[UUID]]),
+ expected)),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/no-permission-present-service.js b/testing/web-platform/tests/bluetooth/script-tests/server/no-permission-present-service.js
new file mode 100644
index 0000000000..3257410685
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/no-permission-present-service.js
@@ -0,0 +1,22 @@
+'use strict';
+const test_desc = 'Request for present service without permission. ' +
+ 'Reject with SecurityError.';
+const expected = new DOMException(
+ 'Origin is not allowed to access the service. Tip: Add the service UUID ' +
+ 'to \'optionalServices\' in requestDevice() options. https://goo.gl/HxfxSQ',
+ 'SecurityError');
+
+bluetooth_test(() => getConnectedHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(({device}) => Promise.all([
+ assert_promise_rejects_with_message(
+ device.gatt.CALLS([
+ getPrimaryService(generic_access.alias)|
+ getPrimaryServices(generic_access.alias)[UUID]
+ ]), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.FUNCTION_NAME(generic_access.name), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.FUNCTION_NAME(generic_access.uuid), expected)])),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/server/service-not-found.js b/testing/web-platform/tests/bluetooth/script-tests/server/service-not-found.js
new file mode 100644
index 0000000000..0fd2dace78
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/server/service-not-found.js
@@ -0,0 +1,16 @@
+'use strict';
+const test_desc = 'Request for absent service. Reject with NotFoundError.';
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['glucose']
+ })
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.CALLS([
+ getPrimaryService('glucose')|
+ getPrimaryServices('glucose')[UUID]
+ ]),
+ new DOMException(
+ `No Services matching UUID ${glucose.uuid} found in Device.`,
+ 'NotFoundError'))),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/service/blocklisted-characteristic.js b/testing/web-platform/tests/bluetooth/script-tests/service/blocklisted-characteristic.js
new file mode 100644
index 0000000000..b26f039a70
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/service/blocklisted-characteristic.js
@@ -0,0 +1,19 @@
+'use strict';
+const test_desc = 'Serial Number String characteristic is blocklisted. ' +
+ 'Should reject with SecurityError.';
+const expected = new DOMException(
+ 'getCharacteristic(s) called with blocklisted UUID. https://goo.gl/4NeimX',
+ 'SecurityError');
+
+bluetooth_test(() => getHIDDevice({
+ filters: [{services: ['device_information']}]
+})
+ .then(({device}) => device.gatt.getPrimaryService('device_information'))
+ .then(service => assert_promise_rejects_with_message(
+ service.CALLS([
+ getCharacteristic('serial_number_string')|
+ getCharacteristics('serial_number_string')[UUID]
+ ]),
+ expected,
+ 'Serial Number String characteristic is blocklisted.')),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/service/characteristic-not-found.js b/testing/web-platform/tests/bluetooth/script-tests/service/characteristic-not-found.js
new file mode 100644
index 0000000000..366e046774
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/service/characteristic-not-found.js
@@ -0,0 +1,15 @@
+'use strict';
+const test_desc = 'Request for absent characteristics with UUID. ' +
+ 'Reject with NotFoundError.';
+
+bluetooth_test(() => getEmptyHealthThermometerService()
+ .then(({service}) => assert_promise_rejects_with_message(
+ service.CALLS([
+ getCharacteristic('battery_level')|
+ getCharacteristics('battery_level')[UUID]
+ ]),
+ new DOMException(
+ `No Characteristics matching UUID ${battery_level.uuid} found ` +
+ `in Service with UUID ${health_thermometer.uuid}.`,
+ 'NotFoundError'))),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/service/garbage-collection-ran-during-error.js b/testing/web-platform/tests/bluetooth/script-tests/service/garbage-collection-ran-during-error.js
new file mode 100644
index 0000000000..7ed4aaa962
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/service/garbage-collection-ran-during-error.js
@@ -0,0 +1,24 @@
+'use strict';
+const test_desc = 'Garbage Collection ran during FUNCTION_NAME ' +
+ 'call that fails. Should not crash';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve characteristics. ' +
+ '(Re)connect first with `device.gatt.connect`.',
+ 'NetworkError');
+let promise;
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(({service}) => {
+ promise = assert_promise_rejects_with_message(
+ service.CALLS([
+ getCharacteristic('measurement_interval')|
+ getCharacteristics()|
+ getCharacteristics('measurement_interval')[UUID]
+ ]), expected);
+ // Disconnect called to clear attributeInstanceMap and allow the object to
+ // get garbage collected.
+ service.device.gatt.disconnect();
+ })
+ .then(garbageCollect)
+ .then(() => promise),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/service/get-same-object.js b/testing/web-platform/tests/bluetooth/script-tests/service/get-same-object.js
new file mode 100644
index 0000000000..db9d740c83
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/service/get-same-object.js
@@ -0,0 +1,24 @@
+'use strict';
+const test_desc = 'Calls to FUNCTION_NAME should return the same object.';
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(({service}) => Promise.all([
+ service.CALLS([
+ getCharacteristic('measurement_interval')|
+ getCharacteristics()|
+ getCharacteristics('measurement_interval')[UUID]]),
+ service.PREVIOUS_CALL]))
+ .then(([characteristics_first_call, characteristics_second_call]) => {
+ // Convert to arrays if necessary.
+ characteristics_first_call = [].concat(characteristics_first_call);
+ characteristics_second_call = [].concat(characteristics_second_call);
+
+ let first_call_set = new Set(characteristics_first_call);
+ assert_equals(characteristics_first_call.length, first_call_set.size);
+ let second_call_set = new Set(characteristics_second_call);
+ assert_equals(characteristics_second_call.length, second_call_set.size);
+
+ characteristics_first_call.forEach(characteristic => {
+ assert_true(second_call_set.has(characteristic));
+ });
+ }), test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/service/invalid-characteristic-name.js b/testing/web-platform/tests/bluetooth/script-tests/service/invalid-characteristic-name.js
new file mode 100644
index 0000000000..74cba7ec43
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/service/invalid-characteristic-name.js
@@ -0,0 +1,23 @@
+'use strict';
+const test_desc = 'Wrong Characteristic name. Reject with TypeError.';
+const expected = new DOMException(
+ "Failed to execute 'FUNCTION_NAME' on " +
+ "'BluetoothRemoteGATTService': Invalid Characteristic name: " +
+ "'wrong_name'. " +
+ "It must be a valid UUID alias (e.g. 0x1234), " +
+ "UUID (lowercase hex characters e.g. " +
+ "'00001234-0000-1000-8000-00805f9b34fb'), " +
+ "or recognized standard name from " +
+ "https://www.bluetooth.com/specifications/gatt/characteristics" +
+ " e.g. 'aerobic_heart_rate_lower_limit'.",
+ 'TypeError');
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(({service}) => assert_promise_rejects_with_message(
+ service.CALLS([
+ getCharacteristic('wrong_name')|
+ getCharacteristics('wrong_name')
+ ]),
+ expected,
+ 'Wrong Characteristic name passed.')),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/service/reconnect-during.js b/testing/web-platform/tests/bluetooth/script-tests/service/reconnect-during.js
new file mode 100644
index 0000000000..cc71547ac2
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/service/reconnect-during.js
@@ -0,0 +1,36 @@
+'use strict';
+const test_desc = 'disconnect() and connect() called during ' +
+ 'FUNCTION_NAME. Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve characteristics. ' +
+ '(Re)connect first with `device.gatt.connect`.',
+ 'NetworkError');
+let device;
+
+bluetooth_test(() => getHealthThermometerDeviceWithServicesDiscovered({
+ filters: [{services: [health_thermometer.name]}],
+})
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.getPrimaryService(health_thermometer.name))
+ .then(service => Promise.all([
+ // 1. Make a call to service.FUNCTION_NAME, while the service is still
+ // valid.
+ assert_promise_rejects_with_message(service.CALLS([
+ getCharacteristic(measurement_interval.name)|
+ getCharacteristics()|
+ getCharacteristics(measurement_interval.name)[UUID]
+ ]), expected),
+
+ // 2. disconnect() and connect before the initial call completes.
+ // This is accomplished by making the calls without waiting for the
+ // earlier promises to resolve.
+ // connect() guarantees on OS-level connection, but disconnect()
+ // only disconnects the current instance.
+ // getHealthThermometerDeviceWithServicesDiscovered holds another
+ // connection in an iframe, so disconnect() and connect() are certain to
+ // reconnect. However, disconnect() will invalidate the service object so
+ // the subsequent calls made to it will fail, even after reconnecting.
+ device.gatt.disconnect(),
+ device.gatt.connect()
+ ])),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/script-tests/service/service-is-removed.js b/testing/web-platform/tests/bluetooth/script-tests/service/service-is-removed.js
new file mode 100644
index 0000000000..aaf0f14436
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/script-tests/service/service-is-removed.js
@@ -0,0 +1,20 @@
+'use strict';
+const test_desc = 'Service is removed before FUNCTION_NAME call. ' +
+ 'Reject with InvalidStateError.';
+const expected = new DOMException('GATT Service no longer exists.',
+ 'InvalidStateError');
+let service, fake_service, fake_peripheral;
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(_ => ({service, fake_service, fake_peripheral} = _))
+ .then(() => fake_service.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ service.CALLS([
+ getCharacteristic('measurement_interval')|
+ getCharacteristics()|
+ getCharacteristics('measurement_interval')[UUID]
+ ]),
+ expected,
+ 'Service got removed.')),
+ test_desc);
diff --git a/testing/web-platform/tests/bluetooth/server/connect/connection-succeeds.https.window.js b/testing/web-platform/tests/bluetooth/server/connect/connection-succeeds.https.window.js
new file mode 100644
index 0000000000..90b62b9265
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/connect/connection-succeeds.https.window.js
@@ -0,0 +1,13 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Device will connect';
+
+bluetooth_test(async () => {
+ let {device, fake_peripheral} = await getDiscoveredHealthThermometerDevice();
+ await fake_peripheral.setNextGATTConnectionResponse({code: HCI_SUCCESS});
+ let gatt = await device.gatt.connect();
+ assert_true(gatt.connected);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/server/connect/detachedIframe.https.window.js b/testing/web-platform/tests/bluetooth/server/connect/detachedIframe.https.window.js
new file mode 100644
index 0000000000..2332cef707
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/connect/detachedIframe.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ let iframe = document.createElement('iframe');
+ let error;
+
+ const {device} = await getHealthThermometerDeviceFromIframe(iframe);
+
+ iframe.remove();
+ // Set iframe to null to ensure that the GC cleans up as much as possible.
+ iframe = null;
+ await garbageCollect();
+
+ try {
+ await device.gatt.connect();
+ } catch (e) {
+ // Cannot use promise_rejects_dom() because |e| is thrown from a different
+ // global.
+ error = e;
+ }
+ assert_not_equals(error, undefined);
+ assert_equals(error.name, 'NetworkError');
+}, 'connect() rejects in a detached context');
diff --git a/testing/web-platform/tests/bluetooth/server/connect/garbage-collection-ran-during-success.https.window.js b/testing/web-platform/tests/bluetooth/server/connect/garbage-collection-ran-during-success.https.window.js
new file mode 100644
index 0000000000..2d2211dec3
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/connect/garbage-collection-ran-during-success.https.window.js
@@ -0,0 +1,19 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Garbage Collection ran during a connect call that ' +
+ 'succeeds. Should not crash.';
+
+bluetooth_test(async () => {
+ let connectPromise;
+ {
+ let {device, fake_peripheral} =
+ await getDiscoveredHealthThermometerDevice();
+ await fake_peripheral.setNextGATTConnectionResponse({code: HCI_SUCCESS});
+ connectPromise = device.gatt.connect();
+ }
+ await Promise.all([connectPromise, garbageCollect()]);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/server/connect/get-same-gatt-server.https.window.js b/testing/web-platform/tests/bluetooth/server/connect/get-same-gatt-server.https.window.js
new file mode 100644
index 0000000000..59d7243a65
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/connect/get-same-gatt-server.https.window.js
@@ -0,0 +1,19 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Multiple connects should return the same gatt object.';
+
+bluetooth_test(async () => {
+ let {device, fake_peripheral} = await getDiscoveredHealthThermometerDevice();
+ await fake_peripheral.setNextGATTConnectionResponse({code: HCI_SUCCESS});
+ // No second response is necessary because an ATT Bearer
+ // already exists from the first connection.
+ // See
+ // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothremotegattserver-connect
+ // step 5.1.
+ let gatt1 = await device.gatt.connect();
+ let gatt2 = await device.gatt.connect();
+ assert_equals(gatt1, gatt2);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/server/device-same-object.https.window.js b/testing/web-platform/tests/bluetooth/server/device-same-object.https.window.js
new file mode 100644
index 0000000000..f9a66d9b69
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/device-same-object.https.window.js
@@ -0,0 +1,13 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = '[SameObject] test for BluetoothRemoteGATTServer\'s device.';
+
+bluetooth_test(async () => {
+ let {device, fake_peripheral} = await getDiscoveredHealthThermometerDevice();
+ await fake_peripheral.setNextGATTConnectionResponse({code: HCI_SUCCESS});
+ let gatt = await device.gatt.connect();
+ assert_equals(gatt.device, device);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/server/disconnect/connect-disconnect-twice.https.window.js b/testing/web-platform/tests/bluetooth/server/disconnect/connect-disconnect-twice.https.window.js
new file mode 100644
index 0000000000..5d9908df4c
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/disconnect/connect-disconnect-twice.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Connect + Disconnect twice still results in ' +
+ '\'connected\' being false.';
+let device, fake_peripheral;
+
+// TODO(569716): Test that the disconnect signal was sent to the device.
+bluetooth_test(async () => {
+ let {device, fake_peripheral} = await getDiscoveredHealthThermometerDevice();
+ await fake_peripheral.setNextGATTConnectionResponse({
+ code: HCI_SUCCESS,
+ });
+ let gattServer = await device.gatt.connect();
+ await gattServer.disconnect();
+ assert_false(gattServer.connected);
+
+ gattServer = await device.gatt.connect();
+ await gattServer.disconnect();
+ assert_false(gattServer.connected);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/server/disconnect/detach-gc.https.window.js b/testing/web-platform/tests/bluetooth/server/disconnect/detach-gc.https.window.js
new file mode 100644
index 0000000000..b934b37973
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/disconnect/detach-gc.https.window.js
@@ -0,0 +1,34 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Detach frame then garbage collect. We shouldn\'t crash.';
+let iframe = document.createElement('iframe');
+
+bluetooth_test(async () => {
+ await setUpConnectableHealthThermometerDevice();
+ // 1. Load the iframe.
+ await new Promise(resolve => {
+ iframe.src = '/bluetooth/resources/health-thermometer-iframe.html';
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve);
+ });
+ // 2. Connect device, detach the iframe, and run garbage collection.
+ await new Promise(resolve => {
+ callWithTrustedClick(() => {
+ iframe.contentWindow.postMessage(
+ {
+ type: 'RequestAndConnect',
+ options: {filters: [{services: ['health_thermometer']}]}
+ },
+ '*');
+ });
+ window.onmessage = messageEvent => {
+ assert_equals(messageEvent.data, 'Connected');
+ iframe.remove();
+ garbageCollect().then(resolve);
+ }
+ })
+}, test_desc)
diff --git a/testing/web-platform/tests/bluetooth/server/disconnect/detachedIframe.https.window.js b/testing/web-platform/tests/bluetooth/server/disconnect/detachedIframe.https.window.js
new file mode 100644
index 0000000000..04e0ca0117
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/disconnect/detachedIframe.https.window.js
@@ -0,0 +1,28 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ let iframe = document.createElement('iframe');
+ let error;
+
+ const {device} = await getHealthThermometerDeviceFromIframe(iframe);
+ await device.gatt.connect();
+
+ iframe.remove();
+ // Set iframe to null to ensure that the GC cleans up as much as possible.
+ iframe = null;
+ await garbageCollect();
+
+ try {
+ await device.gatt.disconnect();
+ } catch (e) {
+ // Cannot use promise_rejects_dom() because |e| is thrown from a different
+ // global.
+ error = e;
+ }
+ assert_not_equals(error, undefined);
+ assert_equals(error.name, 'NetworkError');
+}, 'disconnect() rejects in a detached context');
diff --git a/testing/web-platform/tests/bluetooth/server/disconnect/disconnect-twice-in-a-row.https.window.js b/testing/web-platform/tests/bluetooth/server/disconnect/disconnect-twice-in-a-row.https.window.js
new file mode 100644
index 0000000000..acca9796d5
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/disconnect/disconnect-twice-in-a-row.https.window.js
@@ -0,0 +1,20 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Calling disconnect twice in a row still results in ' +
+ '\'connected\' being false.';
+
+// TODO(569716): Test that the disconnect signal was sent to the device.
+bluetooth_test(async () => {
+ let {device, fake_peripheral} = await getDiscoveredHealthThermometerDevice();
+ await fake_peripheral.setNextGATTConnectionResponse({
+ code: HCI_SUCCESS,
+ });
+ let gattServer = await device.gatt.connect();
+ await gattServer.disconnect();
+ assert_false(gattServer.connected);
+ await gattServer.disconnect();
+ assert_false(gattServer.connected);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/server/disconnect/gc-detach.https.window.js b/testing/web-platform/tests/bluetooth/server/disconnect/gc-detach.https.window.js
new file mode 100644
index 0000000000..1c062a7759
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/disconnect/gc-detach.https.window.js
@@ -0,0 +1,36 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Garbage collect then detach frame. We shouldn\'t crash.';
+let iframe = document.createElement('iframe');
+
+bluetooth_test(async () => {
+ await setUpConnectableHealthThermometerDevice();
+ // 1. Load the iframe.
+ await new Promise(resolve => {
+ iframe.src = '/bluetooth/resources/health-thermometer-iframe.html';
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve);
+ });
+ // 2. Connect device, run garbage collection, and detach iframe.
+ await new Promise(resolve => {
+ callWithTrustedClick(() => {
+ iframe.contentWindow.postMessage(
+ {
+ type: 'RequestAndConnect',
+ options: {filters: [{services: ['health_thermometer']}]}
+ },
+ '*');
+ });
+ window.onmessage = messageEvent => {
+ assert_equals(messageEvent.data, 'Connected');
+ garbageCollect().then(() => {
+ iframe.remove();
+ resolve();
+ });
+ }
+ })
+}, test_desc)
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-called-before.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-called-before.https.window.js
new file mode 100644
index 0000000000..631545a385
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-called-before.https.window.js
@@ -0,0 +1,26 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'disconnect() called before getPrimaryService. ' +
+ 'Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.',
+ 'NetworkError');
+let device;
+
+bluetooth_test(() => getConnectedHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.disconnect())
+ .then(() => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService('health_thermometer'),
+ expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-called-during-error.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-called-during-error.https.window.js
new file mode 100644
index 0000000000..bcf19665d5
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-called-during-error.https.window.js
@@ -0,0 +1,25 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'disconnect() called during a getPrimaryService ' +
+ 'call that fails. Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.', 'NetworkError');
+let device;
+
+bluetooth_test(() => getEmptyHealthThermometerDevice()
+ .then(_ => ({device} = _))
+ .then(() => {
+ let promise = assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService('health_thermometer'),
+ expected)
+ device.gatt.disconnect();
+ return promise;
+ }),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-called-during-success.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-called-during-success.https.window.js
new file mode 100644
index 0000000000..0d2fc1044a
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-called-during-success.https.window.js
@@ -0,0 +1,26 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'disconnect() called during a getPrimaryService call that ' +
+ 'succeeds. Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.',
+ 'NetworkError');
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(({device}) => {
+ let promise = assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService('health_thermometer'),
+ expected);
+ device.gatt.disconnect();
+ return promise;
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-discovery-timeout.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-discovery-timeout.https.window.js
new file mode 100644
index 0000000000..03b0c9d0f3
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-discovery-timeout.https.window.js
@@ -0,0 +1,46 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc =
+ 'Calls to getPrimaryService when device disconnects and discovery' +
+ ' times out should reject promise rather than get stuck.';
+let device;
+
+bluetooth_test(
+ async (t) => {
+ let {device, fake_peripheral} =
+ await getConnectedHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ });
+
+ await fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_CONNECTION_TIMEOUT,
+ });
+ await Promise.all([
+ fake_peripheral.simulateGATTDisconnection({
+ code: HCI_SUCCESS,
+ }),
+ // Using promise_rejects_dom here rather than
+ // assert_promise_rejects_with_message as the race between
+ // simulateGATTDisconnection and getPrimaryServices might end up giving
+ // slightly different exception message (i.e has "Failed to execute ...
+ // on
+ // ... " prefix when disconnected state is reflected on the renderer
+ // side). The point of the test is no matter how race between them, the
+ // promise will be rejected as opposed to get stuck.
+ promise_rejects_dom(t, 'NetworkError', device.gatt.getPrimaryService('health_thermometer')),
+ ]);
+ },
+ test_desc, '',
+ // As specified above there is a race condition between
+ // simulateGATTDisconnection and getPrimaryServices, the artificial
+ // GATTDiscoveryResponse might not be consumed in case
+ // simulateGATTDisconnection happens first. As a result explicitly skip
+ // all response consumed validation at the end of the test.
+ /*validate_response_consumed=*/ false);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-invalidates-objects.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-invalidates-objects.https.window.js
new file mode 100644
index 0000000000..56468b24ea
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnect-invalidates-objects.https.window.js
@@ -0,0 +1,43 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Calls on services after we disconnect and connect again. '+
+ 'Should reject with InvalidStateError.';
+let device, services;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.getPrimaryService('health_thermometer'))
+ // Convert to array if necessary.
+ .then(s => services = [].concat(s))
+ .then(() => device.gatt.disconnect())
+ .then(() => device.gatt.connect())
+ .then(() => {
+ let promises = Promise.resolve();
+ for (let service of services) {
+ let error = new DOMException(
+ `Service with UUID ${service.uuid} is no longer valid. Remember ` +
+ `to retrieve the service again after reconnecting.`,
+ 'InvalidStateError');
+ promises = promises.then(() =>
+ assert_promise_rejects_with_message(
+ service.getCharacteristic('measurement_interval'),
+ error));
+ promises = promises.then(() =>
+ assert_promise_rejects_with_message(
+ service.getCharacteristics(),
+ error));
+ promises = promises.then(() =>
+ assert_promise_rejects_with_message(
+ service.getCharacteristics('measurement_interval'),
+ error));
+ }
+ return promises;
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnected-device.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnected-device.https.window.js
new file mode 100644
index 0000000000..741b2db5ee
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-disconnected-device.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'getPrimaryService called before connecting. Reject with ' +
+ 'NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.',
+ 'NetworkError');
+
+bluetooth_test(() => getDiscoveredHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService('health_thermometer'),
+ expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-discovery-complete-no-permission-absent-service.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-discovery-complete-no-permission-absent-service.https.window.js
new file mode 100644
index 0000000000..e2f5c87630
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-discovery-complete-no-permission-absent-service.https.window.js
@@ -0,0 +1,29 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for absent service without permission. Should ' +
+ 'Reject with SecurityError even if services have been discovered already.';
+const expected = new DOMException(
+ 'Origin is not allowed to access the service. Tip: Add the service ' +
+ 'UUID to \'optionalServices\' in requestDevice() options. ' +
+ 'https://goo.gl/HxfxSQ',
+ 'SecurityError');
+let device;
+
+bluetooth_test(() => getHealthThermometerDeviceWithServicesDiscovered({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(_ => ({device} = _))
+ .then(() => Promise.all([
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService(glucose.alias), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService(glucose.name), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService(glucose.uuid), expected)])),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-discovery-complete-service-not-found.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-discovery-complete-service-not-found.https.window.js
new file mode 100644
index 0000000000..8e9166b41a
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-discovery-complete-service-not-found.https.window.js
@@ -0,0 +1,20 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for absent service. Must reject with ' +
+ 'NotFoundError even when the services have previously been discovered.';
+
+bluetooth_test(() => getHealthThermometerDeviceWithServicesDiscovered({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['glucose']})
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService('glucose'),
+ new DOMException(
+ `No Services matching UUID ${glucose.uuid} found in Device.`,
+ 'NotFoundError'))),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-garbage-collection-ran-during-error.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-garbage-collection-ran-during-error.https.window.js
new file mode 100644
index 0000000000..df182fe8ff
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-garbage-collection-ran-during-error.https.window.js
@@ -0,0 +1,28 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Garbage Collection ran during a getPrimaryService ' +
+ 'call that failed. Should not crash.'
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect first ' +
+ 'with `device.gatt.connect`.',
+ 'NetworkError');
+let promise;
+
+bluetooth_test(() => getEmptyHealthThermometerDevice()
+ .then(({device}) => {
+ promise = assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService('health_thermometer'),
+ expected);
+ // Disconnect called to clear attributeInstanceMap and allow the
+ // object to get garbage collected.
+ device.gatt.disconnect();
+ return garbageCollect();
+ })
+ .then(() => promise),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-garbage-collection-ran-during-success.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-garbage-collection-ran-during-success.https.window.js
new file mode 100644
index 0000000000..8e278af224
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-garbage-collection-ran-during-success.https.window.js
@@ -0,0 +1,28 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Garbage Collection ran during a getPrimaryService call that ' +
+ 'succeeds. Should not crash.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. ' +
+ '(Re)connect first with `device.gatt.connect`.',
+ 'NetworkError');
+let promise;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(({device}) => {
+ promise = assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService('health_thermometer'),
+ expected);
+ device.gatt.disconnect();
+ return garbageCollect();
+ })
+ .then(() => promise),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-get-different-service-after-reconnection.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-get-different-service-after-reconnection.https.window.js
new file mode 100644
index 0000000000..d4557f6753
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-get-different-service-after-reconnection.https.window.js
@@ -0,0 +1,39 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Calls to getPrimaryService after a disconnection should return ' +
+ 'a different object.';
+let device, services_first_connection, services_second_connection;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.getPrimaryService('health_thermometer'))
+ .then(services => services_first_connection = services)
+ .then(() => device.gatt.disconnect())
+ .then(() => device.gatt.connect())
+ .then(() => device.gatt.getPrimaryService('health_thermometer'))
+ .then(services => services_second_connection = services)
+ .then(() => {
+ // Convert to arrays if necessary.
+ services_first_connection = [].concat(services_first_connection);
+ services_second_connection = [].concat(services_second_connection);
+
+ assert_equals(services_first_connection.length,
+ services_second_connection.length);
+
+ let first_connection_set = new Set(services_first_connection);
+ let second_connection_set = new Set(services_second_connection);
+
+ // The two sets should be disjoint.
+ let common_services = services_first_connection.filter(
+ val => second_connection_set.has(val));
+ assert_equals(common_services.length, 0);
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-get-same-object.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-get-same-object.https.window.js
new file mode 100644
index 0000000000..b43cefb567
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-get-same-object.https.window.js
@@ -0,0 +1,37 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Calls to getPrimaryService should return the same object.';
+let device;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']})
+ .then(({device}) => Promise.all([
+ device.gatt.getPrimaryService('health_thermometer'),
+ device.gatt.getPrimaryService('health_thermometer')]))
+ .then(([services_first_call, services_second_call]) => {
+ // Convert to arrays if necessary.
+ services_first_call = [].concat(services_first_call);
+ services_second_call = [].concat(services_second_call);
+
+ assert_equals(services_first_call.length, services_second_call.length);
+
+ let first_call_set = new Set(services_first_call);
+ assert_equals(services_first_call.length, first_call_set.size);
+ let second_call_set = new Set(services_second_call);
+ assert_equals(services_second_call.length, second_call_set.size);
+
+ services_first_call.forEach(service => {
+ assert_true(second_call_set.has(service))
+ });
+
+ services_second_call.forEach(service => {
+ assert_true(first_call_set.has(service));
+ });
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-invalid-service-name.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-invalid-service-name.https.window.js
new file mode 100644
index 0000000000..cf4ab6c665
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-invalid-service-name.https.window.js
@@ -0,0 +1,26 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Wrong Service name. Reject with TypeError.';
+const expected = new DOMException(
+ "Failed to execute 'getPrimaryService' on " +
+ "'BluetoothRemoteGATTServer': Invalid Service name: " +
+ "'wrong_name'. It must be a valid UUID alias (e.g. 0x1234), " +
+ "UUID (lowercase hex characters e.g. " +
+ "'00001234-0000-1000-8000-00805f9b34fb'), " +
+ "or recognized standard name from " +
+ "https://www.bluetooth.com/specifications/gatt/services" +
+ " e.g. 'alert_notification'.",
+ 'TypeError');
+
+bluetooth_test(() => getConnectedHealthThermometerDevice()
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService('wrong_name'),
+ expected,
+ 'Wrong Service name passed.')),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-no-permission-absent-service.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-no-permission-absent-service.https.window.js
new file mode 100644
index 0000000000..3466ded4f9
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-no-permission-absent-service.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for absent service without permission. ' +
+ 'Reject with SecurityError.';
+const expected = new DOMException(
+ 'Origin is not allowed to access the service. Tip: Add the service UUID ' +
+ 'to \'optionalServices\' in requestDevice() options. ' +
+ 'https://goo.gl/HxfxSQ',
+ 'SecurityError');
+
+bluetooth_test(() => getConnectedHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(({device}) => Promise.all([
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService(glucose.alias), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService(glucose.name), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService(glucose.uuid), expected)])),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-no-permission-for-any-service.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-no-permission-for-any-service.https.window.js
new file mode 100644
index 0000000000..6576ef20a3
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-no-permission-for-any-service.https.window.js
@@ -0,0 +1,21 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for present service without permission to access ' +
+ 'any service. Reject with SecurityError.';
+const expected = new DOMException(
+ 'Origin is not allowed to access any service. Tip: Add the service ' +
+ 'UUID to \'optionalServices\' in requestDevice() options. ' +
+ 'https://goo.gl/HxfxSQ',
+ 'SecurityError');
+
+bluetooth_test(() => getConnectedHealthThermometerDevice({acceptAllDevices: true})
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService('heart_rate'),
+ expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-no-permission-present-service.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-no-permission-present-service.https.window.js
new file mode 100644
index 0000000000..3d0b460bc3
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-no-permission-present-service.https.window.js
@@ -0,0 +1,26 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for present service without permission. ' +
+ 'Reject with SecurityError.';
+const expected = new DOMException(
+ 'Origin is not allowed to access the service. Tip: Add the service UUID ' +
+ 'to \'optionalServices\' in requestDevice() options. https://goo.gl/HxfxSQ',
+ 'SecurityError');
+
+bluetooth_test(() => getConnectedHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(({device}) => Promise.all([
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService(generic_access.alias), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService(generic_access.name), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService(generic_access.uuid), expected)])),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-service-not-found.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-service-not-found.https.window.js
new file mode 100644
index 0000000000..6e0d2c446b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/gen-service-not-found.https.window.js
@@ -0,0 +1,20 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for absent service. Reject with NotFoundError.';
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['glucose']
+ })
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryService('glucose'),
+ new DOMException(
+ `No Services matching UUID ${glucose.uuid} found in Device.`,
+ 'NotFoundError'))),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/service-found.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/service-found.https.window.js
new file mode 100644
index 0000000000..b8a930d10c
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/service-found.https.window.js
@@ -0,0 +1,28 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Request for service. Should return right service';
+
+bluetooth_test(async () => {
+ let {device} = await getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ });
+ let services = await Promise.all([
+ device.gatt.getPrimaryService(generic_access.alias),
+ device.gatt.getPrimaryService(generic_access.name),
+ device.gatt.getPrimaryService(generic_access.uuid)
+ ]);
+ services.forEach(service => {
+ assert_equals(
+ service.uuid, generic_access.uuid,
+ 'Service UUID should be the same as requested UUID.');
+ assert_true(
+ service.isPrimary,
+ 'getPrimaryService should return a primary service.');
+ assert_equals(
+ service.device, device, 'Service device should be the same as device.');
+ })
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryService/two-iframes-from-same-origin.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryService/two-iframes-from-same-origin.https.window.js
new file mode 100644
index 0000000000..b7f23a1491
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryService/two-iframes-from-same-origin.https.window.js
@@ -0,0 +1,88 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Two iframes in the same origin should be able to access ' +
+ 'each other\'s services';
+
+const iframe1 = document.createElement('iframe');
+const iframe2 = document.createElement('iframe');
+
+function add_iframe(iframe) {
+ let promise =
+ new Promise(resolve => iframe.addEventListener('load', resolve));
+ iframe.src = '/bluetooth/resources/health-thermometer-iframe.html';
+ document.body.appendChild(iframe);
+ return promise;
+}
+
+function send_message(iframe, command, arg, assert_func) {
+ let promise = new Promise((resolve, reject) => {
+ window.addEventListener('message', (messageEvent) => {
+ try {
+ assert_func(messageEvent.data);
+ } catch (e) {
+ reject(e);
+ }
+ resolve();
+ }, {once: true});
+ });
+ if (command === 'RequestAndConnect') {
+ arg = {filters: [{services: [arg]}]};
+ }
+ callWithTrustedClick(
+ () => iframe.contentWindow.postMessage(
+ {
+ type: command,
+ options: arg,
+ },
+ '*'));
+ return promise;
+}
+
+bluetooth_test(async () => {
+ await getHealthThermometerDevice();
+ // 1. Add the first iframe.
+ await add_iframe(iframe1);
+ // 2. Connect with the first iframe, requesting the health
+ // thermometer service.
+ await send_message(
+ iframe1, 'RequestAndConnect', 'health_thermometer',
+ msg => assert_equals(msg, 'Connected'));
+ // 3. Access the health thermometer service with the first iframe
+ // (successfully).
+ await send_message(
+ iframe1, 'GetService', 'health_thermometer',
+ msg => assert_equals(msg, 'ServiceReceived'));
+ // 4. Access the generic access service with the first iframe
+ // (unsuccessfully).
+ await send_message(iframe1, 'GetService', 'generic_access', msg => {
+ let split_msg = msg.split(': ');
+ assert_equals(split_msg[0], 'FAIL');
+ assert_equals(split_msg[1], 'SecurityError');
+ });
+ // 5. Add the second iframe.
+ await add_iframe(iframe2);
+ // 6. Connect with the second iframe, requesting the generic
+ // access service.
+ await send_message(
+ iframe2, 'RequestAndConnect', 'generic_access',
+ msg => assert_equals(msg, 'Connected'));
+ // 7. Access the health thermometer service with the second iframe
+ // (successfully). Both iframes should have access to both
+ // services at this point since they have the same origin.
+ await send_message(
+ iframe2, 'GetService', 'health_thermometer',
+ msg => assert_equals(msg, 'ServiceReceived'));
+ // 8. Access the generic access service with the second iframe
+ // (unsuccessfully).
+ await send_message(
+ iframe2, 'GetService', 'generic_access',
+ msg => assert_equals(msg, 'ServiceReceived'));
+ // 9. Access the generic access service with the first iframe
+ // (successfully).
+ await send_message(
+ iframe1, 'GetService', 'generic_access',
+ msg => assert_equals(msg, 'ServiceReceived'));
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/blocklisted-services-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/blocklisted-services-with-uuid.https.window.js
new file mode 100644
index 0000000000..ccc913e5bf
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/blocklisted-services-with-uuid.https.window.js
@@ -0,0 +1,20 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Request for services. Does not return blocklisted service.';
+const expected = new DOMException(
+ 'Origin is not allowed to access the service. Tip: Add the service ' +
+ 'UUID to \'optionalServices\' in requestDevice() options. ' +
+ 'https://goo.gl/HxfxSQ',
+ 'SecurityError');
+
+bluetooth_test(async () => {
+ let {device} = await getConnectedHIDDevice({
+ filters: [{services: ['device_information']}],
+ optionalServices: ['human_interface_device']
+ });
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices('human_interface_device'), expected)
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/blocklisted-services.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/blocklisted-services.https.window.js
new file mode 100644
index 0000000000..ae6be90994
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/blocklisted-services.https.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Request for services. Does not return blocklisted service.';
+
+bluetooth_test(async () => {
+ let {device} = await getHIDDevice({
+ filters: [{services: ['device_information']}],
+ optionalServices: ['generic_access', 'human_interface_device']
+ })
+ let services = await device.gatt.getPrimaryServices();
+ assert_equals(services.length, 2);
+ let uuid_set = new Set(services.map(s => s.uuid));
+
+ assert_equals(uuid_set.size, 2);
+ assert_true(uuid_set.has(BluetoothUUID.getService('generic_access')));
+ assert_true(uuid_set.has(BluetoothUUID.getService('device_information')));
+ assert_false(
+ uuid_set.has(BluetoothUUID.getService('human_interface_device')));
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/correct-services.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/correct-services.https.window.js
new file mode 100644
index 0000000000..f3d883dd2e
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/correct-services.https.window.js
@@ -0,0 +1,30 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Find correct services with UUID.';
+let device, fake_peripheral;
+
+bluetooth_test(async () => {
+ let {device, fake_peripheral} = await getConnectedHealthThermometerDevice(
+ {filters: [{services: ['health_thermometer']}]});
+ let fake_service =
+ await fake_peripheral.addFakeService({uuid: 'health_thermometer'});
+ await Promise.all([
+ fake_service.addFakeCharacteristic(
+ {uuid: 'temperature_measurement', properties: ['indicate']}),
+ fake_service.addFakeCharacteristic(
+ {uuid: 'temperature_measurement', properties: ['indicate']})
+ ]);
+ await fake_peripheral.setNextGATTDiscoveryResponse({code: HCI_SUCCESS});
+ let services = await device.gatt.getPrimaryServices('health_thermometer');
+ let [characteristics1, characteristics2] = await Promise.all(
+ [services[0].getCharacteristics(), services[1].getCharacteristics()]);
+ if (characteristics1.length === 2)
+ assert_equals(characteristics2.length, 3);
+ else if (characteristics2.length === 2)
+ assert_equals(characteristics1.length, 3);
+ else
+ assert_unreached('Invalid lengths.');
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-before-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-before-with-uuid.https.window.js
new file mode 100644
index 0000000000..21b561375d
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-before-with-uuid.https.window.js
@@ -0,0 +1,26 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'disconnect() called before getPrimaryServices. ' +
+ 'Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.',
+ 'NetworkError');
+let device;
+
+bluetooth_test(() => getConnectedHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.disconnect())
+ .then(() => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices('health_thermometer'),
+ expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-before.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-before.https.window.js
new file mode 100644
index 0000000000..8e5fea83ab
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-before.https.window.js
@@ -0,0 +1,26 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'disconnect() called before getPrimaryServices. ' +
+ 'Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.',
+ 'NetworkError');
+let device;
+
+bluetooth_test(() => getConnectedHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.disconnect())
+ .then(() => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(),
+ expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-error-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-error-with-uuid.https.window.js
new file mode 100644
index 0000000000..5c28716b90
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-error-with-uuid.https.window.js
@@ -0,0 +1,25 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'disconnect() called during a getPrimaryServices ' +
+ 'call that fails. Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.', 'NetworkError');
+let device;
+
+bluetooth_test(() => getEmptyHealthThermometerDevice()
+ .then(_ => ({device} = _))
+ .then(() => {
+ let promise = assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices('health_thermometer'),
+ expected)
+ device.gatt.disconnect();
+ return promise;
+ }),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-error.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-error.https.window.js
new file mode 100644
index 0000000000..ddc3124791
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-error.https.window.js
@@ -0,0 +1,25 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'disconnect() called during a getPrimaryServices ' +
+ 'call that fails. Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.', 'NetworkError');
+let device;
+
+bluetooth_test(() => getEmptyHealthThermometerDevice()
+ .then(_ => ({device} = _))
+ .then(() => {
+ let promise = assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(),
+ expected)
+ device.gatt.disconnect();
+ return promise;
+ }),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-success-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-success-with-uuid.https.window.js
new file mode 100644
index 0000000000..13e3806d31
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-success-with-uuid.https.window.js
@@ -0,0 +1,26 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'disconnect() called during a getPrimaryServices call that ' +
+ 'succeeds. Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.',
+ 'NetworkError');
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(({device}) => {
+ let promise = assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices('health_thermometer'),
+ expected);
+ device.gatt.disconnect();
+ return promise;
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-success.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-success.https.window.js
new file mode 100644
index 0000000000..d6b31936c6
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-called-during-success.https.window.js
@@ -0,0 +1,26 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'disconnect() called during a getPrimaryServices call that ' +
+ 'succeeds. Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.',
+ 'NetworkError');
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(({device}) => {
+ let promise = assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(),
+ expected);
+ device.gatt.disconnect();
+ return promise;
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-discovery-timeout-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-discovery-timeout-with-uuid.https.window.js
new file mode 100644
index 0000000000..77f7bc81d9
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-discovery-timeout-with-uuid.https.window.js
@@ -0,0 +1,46 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc =
+ 'Calls to getPrimaryServices when device disconnects and discovery' +
+ ' times out should reject promise rather than get stuck.';
+let device;
+
+bluetooth_test(
+ async (t) => {
+ let {device, fake_peripheral} =
+ await getConnectedHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ });
+
+ await fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_CONNECTION_TIMEOUT,
+ });
+ await Promise.all([
+ fake_peripheral.simulateGATTDisconnection({
+ code: HCI_SUCCESS,
+ }),
+ // Using promise_rejects_dom here rather than
+ // assert_promise_rejects_with_message as the race between
+ // simulateGATTDisconnection and getPrimaryServices might end up giving
+ // slightly different exception message (i.e has "Failed to execute ...
+ // on
+ // ... " prefix when disconnected state is reflected on the renderer
+ // side). The point of the test is no matter how race between them, the
+ // promise will be rejected as opposed to get stuck.
+ promise_rejects_dom(t, 'NetworkError', device.gatt.getPrimaryServices('health_thermometer')),
+ ]);
+ },
+ test_desc, '',
+ // As specified above there is a race condition between
+ // simulateGATTDisconnection and getPrimaryServices, the artificial
+ // GATTDiscoveryResponse might not be consumed in case
+ // simulateGATTDisconnection happens first. As a result explicitly skip
+ // all response consumed validation at the end of the test.
+ /*validate_response_consumed=*/ false);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-discovery-timeout.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-discovery-timeout.https.window.js
new file mode 100644
index 0000000000..ea55b7b495
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-discovery-timeout.https.window.js
@@ -0,0 +1,46 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc =
+ 'Calls to getPrimaryServices when device disconnects and discovery' +
+ ' times out should reject promise rather than get stuck.';
+let device;
+
+bluetooth_test(
+ async (t) => {
+ let {device, fake_peripheral} =
+ await getConnectedHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ });
+
+ await fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_CONNECTION_TIMEOUT,
+ });
+ await Promise.all([
+ fake_peripheral.simulateGATTDisconnection({
+ code: HCI_SUCCESS,
+ }),
+ // Using promise_rejects_dom here rather than
+ // assert_promise_rejects_with_message as the race between
+ // simulateGATTDisconnection and getPrimaryServices might end up giving
+ // slightly different exception message (i.e has "Failed to execute ...
+ // on
+ // ... " prefix when disconnected state is reflected on the renderer
+ // side). The point of the test is no matter how race between them, the
+ // promise will be rejected as opposed to get stuck.
+ promise_rejects_dom(t, 'NetworkError', device.gatt.getPrimaryServices()),
+ ]);
+ },
+ test_desc, '',
+ // As specified above there is a race condition between
+ // simulateGATTDisconnection and getPrimaryServices, the artificial
+ // GATTDiscoveryResponse might not be consumed in case
+ // simulateGATTDisconnection happens first. As a result explicitly skip
+ // all response consumed validation at the end of the test.
+ /*validate_response_consumed=*/ false);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-invalidates-objects-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-invalidates-objects-with-uuid.https.window.js
new file mode 100644
index 0000000000..8cdb83e3ad
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-invalidates-objects-with-uuid.https.window.js
@@ -0,0 +1,43 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Calls on services after we disconnect and connect again. '+
+ 'Should reject with InvalidStateError.';
+let device, services;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.getPrimaryServices('health_thermometer'))
+ // Convert to array if necessary.
+ .then(s => services = [].concat(s))
+ .then(() => device.gatt.disconnect())
+ .then(() => device.gatt.connect())
+ .then(() => {
+ let promises = Promise.resolve();
+ for (let service of services) {
+ let error = new DOMException(
+ `Service with UUID ${service.uuid} is no longer valid. Remember ` +
+ `to retrieve the service again after reconnecting.`,
+ 'InvalidStateError');
+ promises = promises.then(() =>
+ assert_promise_rejects_with_message(
+ service.getCharacteristic('measurement_interval'),
+ error));
+ promises = promises.then(() =>
+ assert_promise_rejects_with_message(
+ service.getCharacteristics(),
+ error));
+ promises = promises.then(() =>
+ assert_promise_rejects_with_message(
+ service.getCharacteristics('measurement_interval'),
+ error));
+ }
+ return promises;
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-invalidates-objects.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-invalidates-objects.https.window.js
new file mode 100644
index 0000000000..9fd536f051
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnect-invalidates-objects.https.window.js
@@ -0,0 +1,43 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Calls on services after we disconnect and connect again. '+
+ 'Should reject with InvalidStateError.';
+let device, services;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.getPrimaryServices())
+ // Convert to array if necessary.
+ .then(s => services = [].concat(s))
+ .then(() => device.gatt.disconnect())
+ .then(() => device.gatt.connect())
+ .then(() => {
+ let promises = Promise.resolve();
+ for (let service of services) {
+ let error = new DOMException(
+ `Service with UUID ${service.uuid} is no longer valid. Remember ` +
+ `to retrieve the service again after reconnecting.`,
+ 'InvalidStateError');
+ promises = promises.then(() =>
+ assert_promise_rejects_with_message(
+ service.getCharacteristic('measurement_interval'),
+ error));
+ promises = promises.then(() =>
+ assert_promise_rejects_with_message(
+ service.getCharacteristics(),
+ error));
+ promises = promises.then(() =>
+ assert_promise_rejects_with_message(
+ service.getCharacteristics('measurement_interval'),
+ error));
+ }
+ return promises;
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnected-device-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnected-device-with-uuid.https.window.js
new file mode 100644
index 0000000000..e0393d5e69
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnected-device-with-uuid.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'getPrimaryServices called before connecting. Reject with ' +
+ 'NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.',
+ 'NetworkError');
+
+bluetooth_test(() => getDiscoveredHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices('health_thermometer'),
+ expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnected-device.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnected-device.https.window.js
new file mode 100644
index 0000000000..87d74c6ab1
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-disconnected-device.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'getPrimaryServices called before connecting. Reject with ' +
+ 'NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect ' +
+ 'first with `device.gatt.connect`.',
+ 'NetworkError');
+
+bluetooth_test(() => getDiscoveredHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(),
+ expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-discovery-complete-no-permission-absent-service-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-discovery-complete-no-permission-absent-service-with-uuid.https.window.js
new file mode 100644
index 0000000000..6e179dc5d9
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-discovery-complete-no-permission-absent-service-with-uuid.https.window.js
@@ -0,0 +1,29 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for absent service without permission. Should ' +
+ 'Reject with SecurityError even if services have been discovered already.';
+const expected = new DOMException(
+ 'Origin is not allowed to access the service. Tip: Add the service ' +
+ 'UUID to \'optionalServices\' in requestDevice() options. ' +
+ 'https://goo.gl/HxfxSQ',
+ 'SecurityError');
+let device;
+
+bluetooth_test(() => getHealthThermometerDeviceWithServicesDiscovered({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(_ => ({device} = _))
+ .then(() => Promise.all([
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(glucose.alias), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(glucose.name), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(glucose.uuid), expected)])),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-discovery-complete-service-not-found-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-discovery-complete-service-not-found-with-uuid.https.window.js
new file mode 100644
index 0000000000..66cfb491c0
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-discovery-complete-service-not-found-with-uuid.https.window.js
@@ -0,0 +1,20 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for absent service. Must reject with ' +
+ 'NotFoundError even when the services have previously been discovered.';
+
+bluetooth_test(() => getHealthThermometerDeviceWithServicesDiscovered({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['glucose']})
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices('glucose'),
+ new DOMException(
+ `No Services matching UUID ${glucose.uuid} found in Device.`,
+ 'NotFoundError'))),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-error-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-error-with-uuid.https.window.js
new file mode 100644
index 0000000000..a235cf5d18
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-error-with-uuid.https.window.js
@@ -0,0 +1,28 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Garbage Collection ran during a getPrimaryServices ' +
+ 'call that failed. Should not crash.'
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect first ' +
+ 'with `device.gatt.connect`.',
+ 'NetworkError');
+let promise;
+
+bluetooth_test(() => getEmptyHealthThermometerDevice()
+ .then(({device}) => {
+ promise = assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices('health_thermometer'),
+ expected);
+ // Disconnect called to clear attributeInstanceMap and allow the
+ // object to get garbage collected.
+ device.gatt.disconnect();
+ return garbageCollect();
+ })
+ .then(() => promise),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-error.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-error.https.window.js
new file mode 100644
index 0000000000..f174d4aef9
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-error.https.window.js
@@ -0,0 +1,28 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Garbage Collection ran during a getPrimaryServices ' +
+ 'call that failed. Should not crash.'
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. (Re)connect first ' +
+ 'with `device.gatt.connect`.',
+ 'NetworkError');
+let promise;
+
+bluetooth_test(() => getEmptyHealthThermometerDevice()
+ .then(({device}) => {
+ promise = assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(),
+ expected);
+ // Disconnect called to clear attributeInstanceMap and allow the
+ // object to get garbage collected.
+ device.gatt.disconnect();
+ return garbageCollect();
+ })
+ .then(() => promise),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-success-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-success-with-uuid.https.window.js
new file mode 100644
index 0000000000..cf5dfb246f
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-success-with-uuid.https.window.js
@@ -0,0 +1,28 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Garbage Collection ran during a getPrimaryServices call that ' +
+ 'succeeds. Should not crash.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. ' +
+ '(Re)connect first with `device.gatt.connect`.',
+ 'NetworkError');
+let promise;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(({device}) => {
+ promise = assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices('health_thermometer'),
+ expected);
+ device.gatt.disconnect();
+ return garbageCollect();
+ })
+ .then(() => promise),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-success.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-success.https.window.js
new file mode 100644
index 0000000000..f1c080a946
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-garbage-collection-ran-during-success.https.window.js
@@ -0,0 +1,28 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Garbage Collection ran during a getPrimaryServices call that ' +
+ 'succeeds. Should not crash.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve services. ' +
+ '(Re)connect first with `device.gatt.connect`.',
+ 'NetworkError');
+let promise;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(({device}) => {
+ promise = assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(),
+ expected);
+ device.gatt.disconnect();
+ return garbageCollect();
+ })
+ .then(() => promise),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-different-service-after-reconnection-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-different-service-after-reconnection-with-uuid.https.window.js
new file mode 100644
index 0000000000..2e40d580f3
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-different-service-after-reconnection-with-uuid.https.window.js
@@ -0,0 +1,39 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Calls to getPrimaryServices after a disconnection should return ' +
+ 'a different object.';
+let device, services_first_connection, services_second_connection;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.getPrimaryServices('health_thermometer'))
+ .then(services => services_first_connection = services)
+ .then(() => device.gatt.disconnect())
+ .then(() => device.gatt.connect())
+ .then(() => device.gatt.getPrimaryServices('health_thermometer'))
+ .then(services => services_second_connection = services)
+ .then(() => {
+ // Convert to arrays if necessary.
+ services_first_connection = [].concat(services_first_connection);
+ services_second_connection = [].concat(services_second_connection);
+
+ assert_equals(services_first_connection.length,
+ services_second_connection.length);
+
+ let first_connection_set = new Set(services_first_connection);
+ let second_connection_set = new Set(services_second_connection);
+
+ // The two sets should be disjoint.
+ let common_services = services_first_connection.filter(
+ val => second_connection_set.has(val));
+ assert_equals(common_services.length, 0);
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-different-service-after-reconnection.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-different-service-after-reconnection.https.window.js
new file mode 100644
index 0000000000..ee1fc971bf
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-different-service-after-reconnection.https.window.js
@@ -0,0 +1,39 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Calls to getPrimaryServices after a disconnection should return ' +
+ 'a different object.';
+let device, services_first_connection, services_second_connection;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ })
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.getPrimaryServices())
+ .then(services => services_first_connection = services)
+ .then(() => device.gatt.disconnect())
+ .then(() => device.gatt.connect())
+ .then(() => device.gatt.getPrimaryServices())
+ .then(services => services_second_connection = services)
+ .then(() => {
+ // Convert to arrays if necessary.
+ services_first_connection = [].concat(services_first_connection);
+ services_second_connection = [].concat(services_second_connection);
+
+ assert_equals(services_first_connection.length,
+ services_second_connection.length);
+
+ let first_connection_set = new Set(services_first_connection);
+ let second_connection_set = new Set(services_second_connection);
+
+ // The two sets should be disjoint.
+ let common_services = services_first_connection.filter(
+ val => second_connection_set.has(val));
+ assert_equals(common_services.length, 0);
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-same-object-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-same-object-with-uuid.https.window.js
new file mode 100644
index 0000000000..b589056a23
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-same-object-with-uuid.https.window.js
@@ -0,0 +1,37 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Calls to getPrimaryServices should return the same object.';
+let device;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']})
+ .then(({device}) => Promise.all([
+ device.gatt.getPrimaryServices('health_thermometer'),
+ device.gatt.getPrimaryServices('health_thermometer')]))
+ .then(([services_first_call, services_second_call]) => {
+ // Convert to arrays if necessary.
+ services_first_call = [].concat(services_first_call);
+ services_second_call = [].concat(services_second_call);
+
+ assert_equals(services_first_call.length, services_second_call.length);
+
+ let first_call_set = new Set(services_first_call);
+ assert_equals(services_first_call.length, first_call_set.size);
+ let second_call_set = new Set(services_second_call);
+ assert_equals(services_second_call.length, second_call_set.size);
+
+ services_first_call.forEach(service => {
+ assert_true(second_call_set.has(service))
+ });
+
+ services_second_call.forEach(service => {
+ assert_true(first_call_set.has(service));
+ });
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-same-object.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-same-object.https.window.js
new file mode 100644
index 0000000000..63739add91
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-get-same-object.https.window.js
@@ -0,0 +1,37 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Calls to getPrimaryServices should return the same object.';
+let device;
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']})
+ .then(({device}) => Promise.all([
+ device.gatt.getPrimaryServices(),
+ device.gatt.getPrimaryServices()]))
+ .then(([services_first_call, services_second_call]) => {
+ // Convert to arrays if necessary.
+ services_first_call = [].concat(services_first_call);
+ services_second_call = [].concat(services_second_call);
+
+ assert_equals(services_first_call.length, services_second_call.length);
+
+ let first_call_set = new Set(services_first_call);
+ assert_equals(services_first_call.length, first_call_set.size);
+ let second_call_set = new Set(services_second_call);
+ assert_equals(services_second_call.length, second_call_set.size);
+
+ services_first_call.forEach(service => {
+ assert_true(second_call_set.has(service))
+ });
+
+ services_second_call.forEach(service => {
+ assert_true(first_call_set.has(service));
+ });
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-invalid-service-name.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-invalid-service-name.https.window.js
new file mode 100644
index 0000000000..a9b1262e6a
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-invalid-service-name.https.window.js
@@ -0,0 +1,26 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Wrong Service name. Reject with TypeError.';
+const expected = new DOMException(
+ "Failed to execute 'getPrimaryServices' on " +
+ "'BluetoothRemoteGATTServer': Invalid Service name: " +
+ "'wrong_name'. It must be a valid UUID alias (e.g. 0x1234), " +
+ "UUID (lowercase hex characters e.g. " +
+ "'00001234-0000-1000-8000-00805f9b34fb'), " +
+ "or recognized standard name from " +
+ "https://www.bluetooth.com/specifications/gatt/services" +
+ " e.g. 'alert_notification'.",
+ 'TypeError');
+
+bluetooth_test(() => getConnectedHealthThermometerDevice()
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices('wrong_name'),
+ expected,
+ 'Wrong Service name passed.')),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-absent-service-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-absent-service-with-uuid.https.window.js
new file mode 100644
index 0000000000..27ad9f008e
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-absent-service-with-uuid.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for absent service without permission. ' +
+ 'Reject with SecurityError.';
+const expected = new DOMException(
+ 'Origin is not allowed to access the service. Tip: Add the service UUID ' +
+ 'to \'optionalServices\' in requestDevice() options. ' +
+ 'https://goo.gl/HxfxSQ',
+ 'SecurityError');
+
+bluetooth_test(() => getConnectedHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(({device}) => Promise.all([
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(glucose.alias), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(glucose.name), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(glucose.uuid), expected)])),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-for-any-service-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-for-any-service-with-uuid.https.window.js
new file mode 100644
index 0000000000..d5f06c23da
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-for-any-service-with-uuid.https.window.js
@@ -0,0 +1,21 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for present service without permission to access ' +
+ 'any service. Reject with SecurityError.';
+const expected = new DOMException(
+ 'Origin is not allowed to access any service. Tip: Add the service ' +
+ 'UUID to \'optionalServices\' in requestDevice() options. ' +
+ 'https://goo.gl/HxfxSQ',
+ 'SecurityError');
+
+bluetooth_test(() => getConnectedHealthThermometerDevice({acceptAllDevices: true})
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices('heart_rate'),
+ expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-for-any-service.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-for-any-service.https.window.js
new file mode 100644
index 0000000000..8aa730d2ed
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-for-any-service.https.window.js
@@ -0,0 +1,21 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for present service without permission to access ' +
+ 'any service. Reject with SecurityError.';
+const expected = new DOMException(
+ 'Origin is not allowed to access any service. Tip: Add the service ' +
+ 'UUID to \'optionalServices\' in requestDevice() options. ' +
+ 'https://goo.gl/HxfxSQ',
+ 'SecurityError');
+
+bluetooth_test(() => getConnectedHealthThermometerDevice({acceptAllDevices: true})
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(),
+ expected)),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-present-service-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-present-service-with-uuid.https.window.js
new file mode 100644
index 0000000000..a2047a0e8f
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-no-permission-present-service-with-uuid.https.window.js
@@ -0,0 +1,26 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for present service without permission. ' +
+ 'Reject with SecurityError.';
+const expected = new DOMException(
+ 'Origin is not allowed to access the service. Tip: Add the service UUID ' +
+ 'to \'optionalServices\' in requestDevice() options. https://goo.gl/HxfxSQ',
+ 'SecurityError');
+
+bluetooth_test(() => getConnectedHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}]
+ })
+ .then(({device}) => Promise.all([
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(generic_access.alias), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(generic_access.name), expected),
+ assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(generic_access.uuid), expected)])),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-service-not-found-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-service-not-found-with-uuid.https.window.js
new file mode 100644
index 0000000000..a2db1edc4b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/gen-service-not-found-with-uuid.https.window.js
@@ -0,0 +1,20 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for absent service. Reject with NotFoundError.';
+
+bluetooth_test(() => getHealthThermometerDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['glucose']
+ })
+ .then(({device}) => assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices('glucose'),
+ new DOMException(
+ `No Services matching UUID ${glucose.uuid} found in Device.`,
+ 'NotFoundError'))),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/services-found-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/services-found-with-uuid.https.window.js
new file mode 100644
index 0000000000..972e6a75ca
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/services-found-with-uuid.https.window.js
@@ -0,0 +1,25 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Request for services. Should return right number of ' +
+ 'services.';
+
+bluetooth_test(async () => {
+ let {device} = await getTwoHealthThermometerServicesDevice(
+ {filters: [{services: ['health_thermometer']}]});
+ let services_arrays = await Promise.all([
+ device.gatt.getPrimaryServices(health_thermometer.alias),
+ device.gatt.getPrimaryServices(health_thermometer.name),
+ device.gatt.getPrimaryServices(health_thermometer.uuid)
+ ]);
+ services_arrays.forEach(services => {
+ assert_equals(services.length, 2);
+ services.forEach(service => {
+ assert_equals(
+ service.uuid, BluetoothUUID.getService('health_thermometer'));
+ assert_true(service.isPrimary);
+ });
+ })
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/services-found.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/services-found.https.window.js
new file mode 100644
index 0000000000..46861175c6
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/services-found.https.window.js
@@ -0,0 +1,25 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Find all services in a device.';
+
+bluetooth_test(async () => {
+ let {device} = await getTwoHealthThermometerServicesDevice({
+ filters: [{services: ['health_thermometer']}],
+ optionalServices: ['generic_access']
+ });
+ let services = await device.gatt.getPrimaryServices();
+ // Expect three service instances.
+ assert_equals(services.length, 3);
+ services.forEach(s => assert_true(s.isPrimary));
+
+ let uuid_set = new Set(services.map(s => s.uuid));
+ // Two of the expected services are 'health_thermometer', so
+ // only 2 unique UUIDs.
+ assert_equals(uuid_set.size, 2);
+
+ assert_true(uuid_set.has(BluetoothUUID.getService('generic_access')));
+ assert_true(uuid_set.has(BluetoothUUID.getService('health_thermometer')));
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/server/getPrimaryServices/services-not-found.https.window.js b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/services-not-found.https.window.js
new file mode 100644
index 0000000000..6350328241
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/server/getPrimaryServices/services-not-found.https.window.js
@@ -0,0 +1,15 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Request for services in a device with no services. Reject ' +
+ 'with NotFoundError.';
+const expected =
+ new DOMException('No Services found in device.', 'NotFoundError');
+
+bluetooth_test(async () => {
+ let {device} = await getEmptyHealthThermometerDevice();
+ return assert_promise_rejects_with_message(
+ device.gatt.getPrimaryServices(), expected);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/service/detachedIframe.https.window.js b/testing/web-platform/tests/bluetooth/service/detachedIframe.https.window.js
new file mode 100644
index 0000000000..f75fc225a7
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/detachedIframe.https.window.js
@@ -0,0 +1,26 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ let iframe = document.createElement('iframe');
+ const {device} = await getHealthThermometerDeviceFromIframe(iframe);
+ let error;
+
+ iframe.remove();
+ // Set iframe to null to ensure that the GC cleans up as much as possible.
+ iframe = null;
+ await garbageCollect();
+
+ try {
+ await device.gatt.getPrimaryService(health_thermometer.name);
+ } catch (e) {
+ // Cannot use promise_rejects_dom() because |e| is thrown from a different
+ // global.
+ error = e;
+ }
+ assert_not_equals(error, undefined);
+ assert_equals(error.name, 'NetworkError');
+}, 'getPrimaryService() rejects in a detached context');
diff --git a/testing/web-platform/tests/bluetooth/service/device-same-from-2-services.https.window.js b/testing/web-platform/tests/bluetooth/service/device-same-from-2-services.https.window.js
new file mode 100644
index 0000000000..5b2ba310d3
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/device-same-from-2-services.https.window.js
@@ -0,0 +1,14 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Same parent device returned from multiple services.';
+
+bluetooth_test(async () => {
+ let {device} = await getTwoHealthThermometerServicesDevice(
+ {filters: [{services: ['health_thermometer']}]});
+ let [service1, service2] =
+ await device.gatt.getPrimaryServices('health_thermometer');
+ assert_equals(service1.device, service2.device);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/service/device-same-object.https.window.js b/testing/web-platform/tests/bluetooth/service/device-same-object.https.window.js
new file mode 100644
index 0000000000..97da769a9e
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/device-same-object.https.window.js
@@ -0,0 +1,13 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = '[SameObject] test for BluetoothRemoteGATTService device.';
+
+bluetooth_test(async () => {
+ let {device} = await getHealthThermometerDevice(
+ {filters: [{services: ['health_thermometer']}]});
+ let service = await device.gatt.getPrimaryService('health_thermometer');
+ assert_equals(service.device, device);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristic/characteristic-found.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristic/characteristic-found.https.window.js
new file mode 100644
index 0000000000..807852ae13
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristic/characteristic-found.https.window.js
@@ -0,0 +1,25 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Request for characteristic. Should return right ' +
+ 'characteristic.';
+
+bluetooth_test(async () => {
+ let {device} = await getHealthThermometerDevice();
+ let service = await device.gatt.getPrimaryService('health_thermometer');
+ let characteristics = await Promise.all([
+ service.getCharacteristic(measurement_interval.alias),
+ service.getCharacteristic(measurement_interval.name),
+ service.getCharacteristic(measurement_interval.uuid)
+ ]);
+ characteristics.forEach(characteristic => {
+ assert_equals(
+ characteristic.uuid, measurement_interval.uuid,
+ 'Characteristic UUID should be the same as requested UUID.');
+ assert_equals(
+ characteristic.service, service,
+ 'Characteristic service should be the same as service.');
+ });
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristic/detachedIframe.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristic/detachedIframe.https.window.js
new file mode 100644
index 0000000000..ea8c96160f
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristic/detachedIframe.https.window.js
@@ -0,0 +1,31 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+
+bluetooth_test(async () => {
+ let iframe = document.createElement('iframe');
+ let error;
+
+ const {device, fakes} = await getHealthThermometerDeviceFromIframe(iframe);
+ await fakes.fake_peripheral.setNextGATTDiscoveryResponse({
+ code: HCI_SUCCESS,
+ });
+ let service = await device.gatt.getPrimaryService(health_thermometer.name);
+
+ iframe.remove();
+ // Set iframe to null to ensure that the GC cleans up as much as possible.
+ iframe = null;
+ await garbageCollect();
+
+ try {
+ await service.getCharacteristic(measurement_interval.alias);
+ } catch (e) {
+ // Cannot use promise_rejects_dom() because |e| is thrown from a different
+ // global.
+ error = e;
+ }
+ assert_not_equals(error, undefined);
+ assert_equals(error.name, 'NetworkError');
+}, 'getCharacteristic() rejects in a detached context');
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-blocklisted-characteristic.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-blocklisted-characteristic.https.window.js
new file mode 100644
index 0000000000..cce302d650
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-blocklisted-characteristic.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Serial Number String characteristic is blocklisted. ' +
+ 'Should reject with SecurityError.';
+const expected = new DOMException(
+ 'getCharacteristic(s) called with blocklisted UUID. https://goo.gl/4NeimX',
+ 'SecurityError');
+
+bluetooth_test(() => getHIDDevice({
+ filters: [{services: ['device_information']}]
+})
+ .then(({device}) => device.gatt.getPrimaryService('device_information'))
+ .then(service => assert_promise_rejects_with_message(
+ service.getCharacteristic('serial_number_string'),
+ expected,
+ 'Serial Number String characteristic is blocklisted.')),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-characteristic-not-found.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-characteristic-not-found.https.window.js
new file mode 100644
index 0000000000..2ed48eb5c6
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-characteristic-not-found.https.window.js
@@ -0,0 +1,19 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for absent characteristics with UUID. ' +
+ 'Reject with NotFoundError.';
+
+bluetooth_test(() => getEmptyHealthThermometerService()
+ .then(({service}) => assert_promise_rejects_with_message(
+ service.getCharacteristic('battery_level'),
+ new DOMException(
+ `No Characteristics matching UUID ${battery_level.uuid} found ` +
+ `in Service with UUID ${health_thermometer.uuid}.`,
+ 'NotFoundError'))),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-garbage-collection-ran-during-error.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-garbage-collection-ran-during-error.https.window.js
new file mode 100644
index 0000000000..1fd70c8fad
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-garbage-collection-ran-during-error.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Garbage Collection ran during getCharacteristic ' +
+ 'call that fails. Should not crash';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve characteristics. ' +
+ '(Re)connect first with `device.gatt.connect`.',
+ 'NetworkError');
+let promise;
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(({service}) => {
+ promise = assert_promise_rejects_with_message(
+ service.getCharacteristic('measurement_interval'), expected);
+ // Disconnect called to clear attributeInstanceMap and allow the object to
+ // get garbage collected.
+ service.device.gatt.disconnect();
+ })
+ .then(garbageCollect)
+ .then(() => promise),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-get-same-object.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-get-same-object.https.window.js
new file mode 100644
index 0000000000..c5176cdc5e
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-get-same-object.https.window.js
@@ -0,0 +1,28 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Calls to getCharacteristic should return the same object.';
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(({service}) => Promise.all([
+ service.getCharacteristic('measurement_interval'),
+ service.getCharacteristic('measurement_interval')]))
+ .then(([characteristics_first_call, characteristics_second_call]) => {
+ // Convert to arrays if necessary.
+ characteristics_first_call = [].concat(characteristics_first_call);
+ characteristics_second_call = [].concat(characteristics_second_call);
+
+ let first_call_set = new Set(characteristics_first_call);
+ assert_equals(characteristics_first_call.length, first_call_set.size);
+ let second_call_set = new Set(characteristics_second_call);
+ assert_equals(characteristics_second_call.length, second_call_set.size);
+
+ characteristics_first_call.forEach(characteristic => {
+ assert_true(second_call_set.has(characteristic));
+ });
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-invalid-characteristic-name.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-invalid-characteristic-name.https.window.js
new file mode 100644
index 0000000000..da0f5bda28
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-invalid-characteristic-name.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Wrong Characteristic name. Reject with TypeError.';
+const expected = new DOMException(
+ "Failed to execute 'getCharacteristic' on " +
+ "'BluetoothRemoteGATTService': Invalid Characteristic name: " +
+ "'wrong_name'. " +
+ "It must be a valid UUID alias (e.g. 0x1234), " +
+ "UUID (lowercase hex characters e.g. " +
+ "'00001234-0000-1000-8000-00805f9b34fb'), " +
+ "or recognized standard name from " +
+ "https://www.bluetooth.com/specifications/gatt/characteristics" +
+ " e.g. 'aerobic_heart_rate_lower_limit'.",
+ 'TypeError');
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(({service}) => assert_promise_rejects_with_message(
+ service.getCharacteristic('wrong_name'),
+ expected,
+ 'Wrong Characteristic name passed.')),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-reconnect-during.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-reconnect-during.https.window.js
new file mode 100644
index 0000000000..8801c152e9
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-reconnect-during.https.window.js
@@ -0,0 +1,39 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'disconnect() and connect() called during ' +
+ 'getCharacteristic. Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve characteristics. ' +
+ '(Re)connect first with `device.gatt.connect`.',
+ 'NetworkError');
+let device;
+
+bluetooth_test(() => getHealthThermometerDeviceWithServicesDiscovered({
+ filters: [{services: [health_thermometer.name]}],
+})
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.getPrimaryService(health_thermometer.name))
+ .then(service => Promise.all([
+ // 1. Make a call to service.getCharacteristic, while the service is still
+ // valid.
+ assert_promise_rejects_with_message(service.getCharacteristic(measurement_interval.name), expected),
+
+ // 2. disconnect() and connect before the initial call completes.
+ // This is accomplished by making the calls without waiting for the
+ // earlier promises to resolve.
+ // connect() guarantees on OS-level connection, but disconnect()
+ // only disconnects the current instance.
+ // getHealthThermometerDeviceWithServicesDiscovered holds another
+ // connection in an iframe, so disconnect() and connect() are certain to
+ // reconnect. However, disconnect() will invalidate the service object so
+ // the subsequent calls made to it will fail, even after reconnecting.
+ device.gatt.disconnect(),
+ device.gatt.connect()
+ ])),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-service-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-service-is-removed.https.window.js
new file mode 100644
index 0000000000..bfeb318c46
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristic/gen-service-is-removed.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Service is removed before getCharacteristic call. ' +
+ 'Reject with InvalidStateError.';
+const expected = new DOMException('GATT Service no longer exists.',
+ 'InvalidStateError');
+let service, fake_service, fake_peripheral;
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(_ => ({service, fake_service, fake_peripheral} = _))
+ .then(() => fake_service.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ service.getCharacteristic('measurement_interval'),
+ expected,
+ 'Service got removed.')),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/blocklisted-characteristics.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/blocklisted-characteristics.https.window.js
new file mode 100644
index 0000000000..408943585a
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/blocklisted-characteristics.https.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'The Device Information service is composed of blocklisted ' +
+ 'characteristics so we shouldn\'t find any.';
+const expected =
+ new DOMException('No Characteristics found in service.', 'NotFoundError');
+
+bluetooth_test(async () => {
+ let {device} =
+ await getHIDDevice({filters: [{services: ['device_information']}]});
+ let service = await device.gatt.getPrimaryService('device_information');
+ return assert_promise_rejects_with_message(
+ service.getCharacteristics(), expected);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/characteristics-found-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/characteristics-found-with-uuid.https.window.js
new file mode 100644
index 0000000000..f11c69c92e
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/characteristics-found-with-uuid.https.window.js
@@ -0,0 +1,39 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Find characteristics with UUID in service.';
+
+bluetooth_test(async () => {
+ let {device, fake_peripheral, fake_services} = await getDiscoveredHealthThermometerDevice();
+ // Setup a device with two measurement intervals.
+ await fake_peripheral.setNextGATTConnectionResponse({code: HCI_SUCCESS});
+ await device.gatt.connect();
+ let fake_health_thermometer = fake_services.get('health_thermometer');
+ await Promise.all([
+ fake_health_thermometer.addFakeCharacteristic({
+ uuid: 'measurement_interval',
+ properties: ['read', 'write', 'indicate']
+ }),
+ fake_health_thermometer.addFakeCharacteristic({
+ uuid: 'measurement_interval',
+ properties: ['read', 'write', 'indicate']
+ }),
+ fake_health_thermometer.addFakeCharacteristic(
+ {uuid: 'temperature_measurement', properties: ['indicate']})
+ ]);
+ await fake_peripheral.setNextGATTDiscoveryResponse({code: HCI_SUCCESS});
+ let service = await device.gatt.getPrimaryService('health_thermometer');
+ // Actual test starts.
+ let characteristics_arrays = await Promise.all([
+ service.getCharacteristics(measurement_interval.alias),
+ service.getCharacteristics(measurement_interval.name),
+ service.getCharacteristics(measurement_interval.uuid)
+ ]);
+ characteristics_arrays.forEach(characteristics => {
+ assert_equals(characteristics.length, 2);
+ assert_equals(characteristics[0].uuid, measurement_interval.uuid);
+ assert_equals(characteristics[1].uuid, measurement_interval.uuid);
+ });
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/characteristics-found.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/characteristics-found.https.window.js
new file mode 100644
index 0000000000..3244dd3e17
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/characteristics-found.https.window.js
@@ -0,0 +1,41 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Find all characteristics in a service.';
+
+bluetooth_test(async () => {
+ let {device, fake_peripheral, fake_services} = await getDiscoveredHealthThermometerDevice();
+ // Setup a device with two measurement intervals.
+ await fake_peripheral.setNextGATTConnectionResponse({code: HCI_SUCCESS});
+ await device.gatt.connect();
+ let fake_health_thermometer = fake_services.get('health_thermometer');
+ await Promise.all([
+ fake_health_thermometer.addFakeCharacteristic({
+ uuid: 'measurement_interval',
+ properties: ['read', 'write', 'indicate']
+ }),
+ fake_health_thermometer.addFakeCharacteristic({
+ uuid: 'measurement_interval',
+ properties: ['read', 'write', 'indicate']
+ }),
+ fake_health_thermometer.addFakeCharacteristic(
+ {uuid: 'temperature_measurement', properties: ['indicate']})
+ ]);
+ await fake_peripheral.setNextGATTDiscoveryResponse({code: HCI_SUCCESS});
+ let service = await device.gatt.getPrimaryService('health_thermometer');
+ // Actual test starts.
+ let characteristics = await service.getCharacteristics();
+ // Expect three characteristic instances.
+ assert_equals(characteristics.length, 3);
+
+ let uuid_set = new Set(characteristics.map(c => c.uuid));
+ // Two of the expected characteristics are
+ // 'measurement_interval', so only 2 unique UUID.
+ assert_equals(uuid_set.size, 2);
+ assert_true(
+ uuid_set.has(BluetoothUUID.getCharacteristic('measurement_interval')));
+ assert_true(
+ uuid_set.has(BluetoothUUID.getCharacteristic('temperature_measurement')));
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/characteristics-not-found.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/characteristics-not-found.https.window.js
new file mode 100644
index 0000000000..5b0c1896d6
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/characteristics-not-found.https.window.js
@@ -0,0 +1,15 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+'use strict';
+const test_desc = 'Request for absent characteristics. Reject with ' +
+ 'NotFoundError.';
+const expected =
+ new DOMException('No Characteristics found in service.', 'NotFoundError');
+
+bluetooth_test(async () => {
+ let {service} = await getEmptyHealthThermometerService();
+ return assert_promise_rejects_with_message(
+ service.getCharacteristics(), expected);
+}, test_desc);
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-blocklisted-characteristic-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-blocklisted-characteristic-with-uuid.https.window.js
new file mode 100644
index 0000000000..79cd01032b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-blocklisted-characteristic-with-uuid.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Serial Number String characteristic is blocklisted. ' +
+ 'Should reject with SecurityError.';
+const expected = new DOMException(
+ 'getCharacteristic(s) called with blocklisted UUID. https://goo.gl/4NeimX',
+ 'SecurityError');
+
+bluetooth_test(() => getHIDDevice({
+ filters: [{services: ['device_information']}]
+})
+ .then(({device}) => device.gatt.getPrimaryService('device_information'))
+ .then(service => assert_promise_rejects_with_message(
+ service.getCharacteristics('serial_number_string'),
+ expected,
+ 'Serial Number String characteristic is blocklisted.')),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-characteristic-not-found-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-characteristic-not-found-with-uuid.https.window.js
new file mode 100644
index 0000000000..8a5e2ab4e4
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-characteristic-not-found-with-uuid.https.window.js
@@ -0,0 +1,19 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Request for absent characteristics with UUID. ' +
+ 'Reject with NotFoundError.';
+
+bluetooth_test(() => getEmptyHealthThermometerService()
+ .then(({service}) => assert_promise_rejects_with_message(
+ service.getCharacteristics('battery_level'),
+ new DOMException(
+ `No Characteristics matching UUID ${battery_level.uuid} found ` +
+ `in Service with UUID ${health_thermometer.uuid}.`,
+ 'NotFoundError'))),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-garbage-collection-ran-during-error-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-garbage-collection-ran-during-error-with-uuid.https.window.js
new file mode 100644
index 0000000000..683b93e352
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-garbage-collection-ran-during-error-with-uuid.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Garbage Collection ran during getCharacteristics ' +
+ 'call that fails. Should not crash';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve characteristics. ' +
+ '(Re)connect first with `device.gatt.connect`.',
+ 'NetworkError');
+let promise;
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(({service}) => {
+ promise = assert_promise_rejects_with_message(
+ service.getCharacteristics('measurement_interval'), expected);
+ // Disconnect called to clear attributeInstanceMap and allow the object to
+ // get garbage collected.
+ service.device.gatt.disconnect();
+ })
+ .then(garbageCollect)
+ .then(() => promise),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-garbage-collection-ran-during-error.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-garbage-collection-ran-during-error.https.window.js
new file mode 100644
index 0000000000..c964781ab4
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-garbage-collection-ran-during-error.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Garbage Collection ran during getCharacteristics ' +
+ 'call that fails. Should not crash';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve characteristics. ' +
+ '(Re)connect first with `device.gatt.connect`.',
+ 'NetworkError');
+let promise;
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(({service}) => {
+ promise = assert_promise_rejects_with_message(
+ service.getCharacteristics(), expected);
+ // Disconnect called to clear attributeInstanceMap and allow the object to
+ // get garbage collected.
+ service.device.gatt.disconnect();
+ })
+ .then(garbageCollect)
+ .then(() => promise),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-get-same-object-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-get-same-object-with-uuid.https.window.js
new file mode 100644
index 0000000000..64b53f4eb3
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-get-same-object-with-uuid.https.window.js
@@ -0,0 +1,28 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Calls to getCharacteristics should return the same object.';
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(({service}) => Promise.all([
+ service.getCharacteristics('measurement_interval'),
+ service.getCharacteristics('measurement_interval')]))
+ .then(([characteristics_first_call, characteristics_second_call]) => {
+ // Convert to arrays if necessary.
+ characteristics_first_call = [].concat(characteristics_first_call);
+ characteristics_second_call = [].concat(characteristics_second_call);
+
+ let first_call_set = new Set(characteristics_first_call);
+ assert_equals(characteristics_first_call.length, first_call_set.size);
+ let second_call_set = new Set(characteristics_second_call);
+ assert_equals(characteristics_second_call.length, second_call_set.size);
+
+ characteristics_first_call.forEach(characteristic => {
+ assert_true(second_call_set.has(characteristic));
+ });
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-get-same-object.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-get-same-object.https.window.js
new file mode 100644
index 0000000000..6aad17c1e6
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-get-same-object.https.window.js
@@ -0,0 +1,28 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Calls to getCharacteristics should return the same object.';
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(({service}) => Promise.all([
+ service.getCharacteristics(),
+ service.getCharacteristics()]))
+ .then(([characteristics_first_call, characteristics_second_call]) => {
+ // Convert to arrays if necessary.
+ characteristics_first_call = [].concat(characteristics_first_call);
+ characteristics_second_call = [].concat(characteristics_second_call);
+
+ let first_call_set = new Set(characteristics_first_call);
+ assert_equals(characteristics_first_call.length, first_call_set.size);
+ let second_call_set = new Set(characteristics_second_call);
+ assert_equals(characteristics_second_call.length, second_call_set.size);
+
+ characteristics_first_call.forEach(characteristic => {
+ assert_true(second_call_set.has(characteristic));
+ });
+ }), test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-invalid-characteristic-name.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-invalid-characteristic-name.https.window.js
new file mode 100644
index 0000000000..c7d439e13a
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-invalid-characteristic-name.https.window.js
@@ -0,0 +1,27 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Wrong Characteristic name. Reject with TypeError.';
+const expected = new DOMException(
+ "Failed to execute 'getCharacteristics' on " +
+ "'BluetoothRemoteGATTService': Invalid Characteristic name: " +
+ "'wrong_name'. " +
+ "It must be a valid UUID alias (e.g. 0x1234), " +
+ "UUID (lowercase hex characters e.g. " +
+ "'00001234-0000-1000-8000-00805f9b34fb'), " +
+ "or recognized standard name from " +
+ "https://www.bluetooth.com/specifications/gatt/characteristics" +
+ " e.g. 'aerobic_heart_rate_lower_limit'.",
+ 'TypeError');
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(({service}) => assert_promise_rejects_with_message(
+ service.getCharacteristics('wrong_name'),
+ expected,
+ 'Wrong Characteristic name passed.')),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-reconnect-during-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-reconnect-during-with-uuid.https.window.js
new file mode 100644
index 0000000000..db373fbca1
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-reconnect-during-with-uuid.https.window.js
@@ -0,0 +1,39 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'disconnect() and connect() called during ' +
+ 'getCharacteristics. Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve characteristics. ' +
+ '(Re)connect first with `device.gatt.connect`.',
+ 'NetworkError');
+let device;
+
+bluetooth_test(() => getHealthThermometerDeviceWithServicesDiscovered({
+ filters: [{services: [health_thermometer.name]}],
+})
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.getPrimaryService(health_thermometer.name))
+ .then(service => Promise.all([
+ // 1. Make a call to service.getCharacteristics, while the service is still
+ // valid.
+ assert_promise_rejects_with_message(service.getCharacteristics(measurement_interval.name), expected),
+
+ // 2. disconnect() and connect before the initial call completes.
+ // This is accomplished by making the calls without waiting for the
+ // earlier promises to resolve.
+ // connect() guarantees on OS-level connection, but disconnect()
+ // only disconnects the current instance.
+ // getHealthThermometerDeviceWithServicesDiscovered holds another
+ // connection in an iframe, so disconnect() and connect() are certain to
+ // reconnect. However, disconnect() will invalidate the service object so
+ // the subsequent calls made to it will fail, even after reconnecting.
+ device.gatt.disconnect(),
+ device.gatt.connect()
+ ])),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-reconnect-during.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-reconnect-during.https.window.js
new file mode 100644
index 0000000000..8b3ba7cc6b
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-reconnect-during.https.window.js
@@ -0,0 +1,39 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'disconnect() and connect() called during ' +
+ 'getCharacteristics. Reject with NetworkError.';
+const expected = new DOMException(
+ 'GATT Server is disconnected. Cannot retrieve characteristics. ' +
+ '(Re)connect first with `device.gatt.connect`.',
+ 'NetworkError');
+let device;
+
+bluetooth_test(() => getHealthThermometerDeviceWithServicesDiscovered({
+ filters: [{services: [health_thermometer.name]}],
+})
+ .then(_ => ({device} = _))
+ .then(() => device.gatt.getPrimaryService(health_thermometer.name))
+ .then(service => Promise.all([
+ // 1. Make a call to service.getCharacteristics, while the service is still
+ // valid.
+ assert_promise_rejects_with_message(service.getCharacteristics(), expected),
+
+ // 2. disconnect() and connect before the initial call completes.
+ // This is accomplished by making the calls without waiting for the
+ // earlier promises to resolve.
+ // connect() guarantees on OS-level connection, but disconnect()
+ // only disconnects the current instance.
+ // getHealthThermometerDeviceWithServicesDiscovered holds another
+ // connection in an iframe, so disconnect() and connect() are certain to
+ // reconnect. However, disconnect() will invalidate the service object so
+ // the subsequent calls made to it will fail, even after reconnecting.
+ device.gatt.disconnect(),
+ device.gatt.connect()
+ ])),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-service-is-removed-with-uuid.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-service-is-removed-with-uuid.https.window.js
new file mode 100644
index 0000000000..2d4db52822
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-service-is-removed-with-uuid.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Service is removed before getCharacteristics call. ' +
+ 'Reject with InvalidStateError.';
+const expected = new DOMException('GATT Service no longer exists.',
+ 'InvalidStateError');
+let service, fake_service, fake_peripheral;
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(_ => ({service, fake_service, fake_peripheral} = _))
+ .then(() => fake_service.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ service.getCharacteristics('measurement_interval'),
+ expected,
+ 'Service got removed.')),
+ test_desc);
+
diff --git a/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-service-is-removed.https.window.js b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-service-is-removed.https.window.js
new file mode 100644
index 0000000000..f922b45cdc
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/service/getCharacteristics/gen-service-is-removed.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/gc.js
+// META: script=/bluetooth/resources/bluetooth-test.js
+// META: script=/bluetooth/resources/bluetooth-fake-devices.js
+// Generated by //third_party/WebKit/LayoutTests/bluetooth/generate.py
+'use strict';
+const test_desc = 'Service is removed before getCharacteristics call. ' +
+ 'Reject with InvalidStateError.';
+const expected = new DOMException('GATT Service no longer exists.',
+ 'InvalidStateError');
+let service, fake_service, fake_peripheral;
+
+bluetooth_test(() => getHealthThermometerService()
+ .then(_ => ({service, fake_service, fake_peripheral} = _))
+ .then(() => fake_service.remove())
+ .then(() => fake_peripheral.simulateGATTServicesChanged())
+ .then(() => assert_promise_rejects_with_message(
+ service.getCharacteristics(),
+ expected,
+ 'Service got removed.')),
+ test_desc);
+