summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/serial
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/serial')
-rw-r--r--testing/web-platform/tests/serial/META.yml1
-rw-r--r--testing/web-platform/tests/serial/README.md20
-rw-r--r--testing/web-platform/tests/serial/getPorts/reject_opaque_origin.https.html14
-rw-r--r--testing/web-platform/tests/serial/getPorts/reject_opaque_origin.https.html.headers1
-rw-r--r--testing/web-platform/tests/serial/getPorts/sandboxed_iframe.https.window.js20
-rw-r--r--testing/web-platform/tests/serial/idlharness.https.any.js22
-rw-r--r--testing/web-platform/tests/serial/requestPort/reject_opaque_origin.https.html14
-rw-r--r--testing/web-platform/tests/serial/requestPort/reject_opaque_origin.https.html.headers1
-rw-r--r--testing/web-platform/tests/serial/requestPort/sandboxed_iframe.https.window.js24
-rw-r--r--testing/web-platform/tests/serial/resources/automation.js66
-rw-r--r--testing/web-platform/tests/serial/resources/common.js33
-rw-r--r--testing/web-platform/tests/serial/resources/manual.js38
-rw-r--r--testing/web-platform/tests/serial/resources/open-in-iframe.html33
-rw-r--r--testing/web-platform/tests/serial/resources/serial-allowed-by-permissions-policy-worker.js14
-rw-r--r--testing/web-platform/tests/serial/resources/serial-disabled-by-permissions-policy-worker.js17
-rw-r--r--testing/web-platform/tests/serial/serial-allowed-by-permissions-policy-attribute-redirect-on-load.https.sub.html44
-rw-r--r--testing/web-platform/tests/serial/serial-allowed-by-permissions-policy-attribute.https.sub.html46
-rw-r--r--testing/web-platform/tests/serial/serial-allowed-by-permissions-policy.https.sub.html49
-rw-r--r--testing/web-platform/tests/serial/serial-allowed-by-permissions-policy.https.sub.html.headers1
-rw-r--r--testing/web-platform/tests/serial/serial-default-permissions-policy.https.sub.html27
-rw-r--r--testing/web-platform/tests/serial/serial-disabled-by-permissions-policy.https.sub.html74
-rw-r--r--testing/web-platform/tests/serial/serial-disabled-by-permissions-policy.https.sub.html.headers1
-rw-r--r--testing/web-platform/tests/serial/serialPort_close.https.any.js26
-rw-r--r--testing/web-platform/tests/serial/serialPort_disconnect-manual.https.html83
-rw-r--r--testing/web-platform/tests/serial/serialPort_events.https.any.js197
-rw-r--r--testing/web-platform/tests/serial/serialPort_forget.https.any.js43
-rw-r--r--testing/web-platform/tests/serial/serialPort_getInfo.https.any.js24
-rw-r--r--testing/web-platform/tests/serial/serialPort_getSignals.https.any.js63
-rw-r--r--testing/web-platform/tests/serial/serialPort_loopback-manual.https.html97
-rw-r--r--testing/web-platform/tests/serial/serialPort_loopback_BreakError-manual.https.html55
-rw-r--r--testing/web-platform/tests/serial/serialPort_loopback_BufferOverrunError-manual.https.html65
-rw-r--r--testing/web-platform/tests/serial/serialPort_loopback_flowControl-manual.https.html59
-rw-r--r--testing/web-platform/tests/serial/serialPort_ondisconnect.https.any.js40
-rw-r--r--testing/web-platform/tests/serial/serialPort_open.https.any.js95
-rw-r--r--testing/web-platform/tests/serial/serialPort_readable-manual.https.html107
-rw-r--r--testing/web-platform/tests/serial/serialPort_readable_byob.https.any.js65
-rw-r--r--testing/web-platform/tests/serial/serialPort_readable_cancel.https.any.js69
-rw-r--r--testing/web-platform/tests/serial/serialPort_readable_chain.https.any.js30
-rw-r--r--testing/web-platform/tests/serial/serialPort_readable_closeLocked.https.any.js17
-rw-r--r--testing/web-platform/tests/serial/serialPort_readable_disconnect.https.any.js25
-rw-r--r--testing/web-platform/tests/serial/serialPort_readable_largeRead.https.any.js23
-rw-r--r--testing/web-platform/tests/serial/serialPort_readable_open.https.any.js21
-rw-r--r--testing/web-platform/tests/serial/serialPort_readable_parityError.https.any.js52
-rw-r--r--testing/web-platform/tests/serial/serialPort_readable_pipeThrough.https.any.js26
-rw-r--r--testing/web-platform/tests/serial/serialPort_readable_smallRead.https.any.js23
-rw-r--r--testing/web-platform/tests/serial/serialPort_setSignals.https.any.js59
-rw-r--r--testing/web-platform/tests/serial/serialPort_writable.https.any.js289
-rw-r--r--testing/web-platform/tests/serial/serialPort_writable_detachBuffer.https.any.js48
-rw-r--r--testing/web-platform/tests/serial/serial_getPorts.https.any.js25
-rw-r--r--testing/web-platform/tests/serial/serial_onconnect.https.any.js29
-rw-r--r--testing/web-platform/tests/serial/serial_ondisconnect.https.any.js37
-rw-r--r--testing/web-platform/tests/serial/serial_requestPort.https.window.js66
52 files changed, 2418 insertions, 0 deletions
diff --git a/testing/web-platform/tests/serial/META.yml b/testing/web-platform/tests/serial/META.yml
new file mode 100644
index 0000000000..64938c9298
--- /dev/null
+++ b/testing/web-platform/tests/serial/META.yml
@@ -0,0 +1 @@
+spec: https://wicg.github.io/serial/
diff --git a/testing/web-platform/tests/serial/README.md b/testing/web-platform/tests/serial/README.md
new file mode 100644
index 0000000000..7040cf0a9d
--- /dev/null
+++ b/testing/web-platform/tests/serial/README.md
@@ -0,0 +1,20 @@
+# Web Serial Testing
+
+Automated testing for the [Web Serial API] relies on a test-only interface which
+must be provided by browsers under test. This is similar to [WebUSB] however
+there is no separate specification of the API other than the tests themselves
+and the Chromium implementation.
+
+Tests in this suite include `resources/automation.js` to detect and load the
+test API as needed.
+
+The Chromium implementation is provided by
+`../resources/chromium/fake-serial.js` using [MojoJS].
+
+Tests with the "-manual" suffix do not use the test-only interface and expect a
+real hardware device to be connected. The specific characteristics of the device
+are described in each test.
+
+[MojoJS]: https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/testing/web_platform_tests.md#mojojs
+[WebUSB]: ../webusb
+[Web Serial API]: https://wicg.github.io/serial
diff --git a/testing/web-platform/tests/serial/getPorts/reject_opaque_origin.https.html b/testing/web-platform/tests/serial/getPorts/reject_opaque_origin.https.html
new file mode 100644
index 0000000000..b2f630a319
--- /dev/null
+++ b/testing/web-platform/tests/serial/getPorts/reject_opaque_origin.https.html
@@ -0,0 +1,14 @@
+<!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.serial.getPorts(),
+ 'getPorts() should throw a SecurityError DOMException when called ' +
+ 'from a context where the top-level document has an opaque origin.');
+ }, 'Calls to Serial APIs from an origin with opaque top origin get blocked.');
+</script>
diff --git a/testing/web-platform/tests/serial/getPorts/reject_opaque_origin.https.html.headers b/testing/web-platform/tests/serial/getPorts/reject_opaque_origin.https.html.headers
new file mode 100644
index 0000000000..1efcf8c226
--- /dev/null
+++ b/testing/web-platform/tests/serial/getPorts/reject_opaque_origin.https.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts
diff --git a/testing/web-platform/tests/serial/getPorts/sandboxed_iframe.https.window.js b/testing/web-platform/tests/serial/getPorts/sandboxed_iframe.https.window.js
new file mode 100644
index 0000000000..0a99f75aae
--- /dev/null
+++ b/testing/web-platform/tests/serial/getPorts/sandboxed_iframe.https.window.js
@@ -0,0 +1,20 @@
+'use strict';
+
+promise_test(async (t) => {
+ let iframe = document.createElement('iframe');
+ await new Promise(resolve => {
+ iframe.src = '../resources/open-in-iframe.html';
+ iframe.sandbox.add('allow-scripts');
+ iframe.allow = 'serial';
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve);
+ });
+
+ await new Promise(resolve => {
+ window.addEventListener('message', t.step_func(messageEvent => {
+ assert_equals(messageEvent.data, 'Success');
+ resolve();
+ }));
+ iframe.contentWindow.postMessage({type: 'GetPorts'}, '*');
+ });
+}, 'GetPorts from a sandboxed iframe is valid.');
diff --git a/testing/web-platform/tests/serial/idlharness.https.any.js b/testing/web-platform/tests/serial/idlharness.https.any.js
new file mode 100644
index 0000000000..b240da933a
--- /dev/null
+++ b/testing/web-platform/tests/serial/idlharness.https.any.js
@@ -0,0 +1,22 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+'use strict';
+
+idl_test(
+ ['serial'],
+ ['html', 'dom'],
+ idl_array => {
+ idl_array.add_objects({
+ Serial: ['navigator.serial'],
+ // TODO: SerialPort
+ // TODO: SerialPortInfo
+ });
+
+ if (self.GLOBAL.isWorker()) {
+ idl_array.add_objects({ WorkerNavigator: ['navigator'] });
+ } else {
+ idl_array.add_objects({ Navigator: ['navigator'] });
+ }
+ }
+);
diff --git a/testing/web-platform/tests/serial/requestPort/reject_opaque_origin.https.html b/testing/web-platform/tests/serial/requestPort/reject_opaque_origin.https.html
new file mode 100644
index 0000000000..ade8ae7392
--- /dev/null
+++ b/testing/web-platform/tests/serial/requestPort/reject_opaque_origin.https.html
@@ -0,0 +1,14 @@
+<!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.serial.requestPort(),
+ 'requestPort() should throw a SecurityError DOMException when called ' +
+ 'from a context where the top-level document has an opaque origin.');
+ }, 'Calls to Serial APIs from an origin with opaque top origin get blocked.');
+</script>
diff --git a/testing/web-platform/tests/serial/requestPort/reject_opaque_origin.https.html.headers b/testing/web-platform/tests/serial/requestPort/reject_opaque_origin.https.html.headers
new file mode 100644
index 0000000000..1efcf8c226
--- /dev/null
+++ b/testing/web-platform/tests/serial/requestPort/reject_opaque_origin.https.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts
diff --git a/testing/web-platform/tests/serial/requestPort/sandboxed_iframe.https.window.js b/testing/web-platform/tests/serial/requestPort/sandboxed_iframe.https.window.js
new file mode 100644
index 0000000000..f6d7abad10
--- /dev/null
+++ b/testing/web-platform/tests/serial/requestPort/sandboxed_iframe.https.window.js
@@ -0,0 +1,24 @@
+'use strict';
+
+promise_test(async (t) => {
+ let iframe = document.createElement('iframe');
+ await new Promise(resolve => {
+ iframe.src = '../resources/open-in-iframe.html';
+ iframe.sandbox.add('allow-scripts');
+ iframe.allow = 'serial';
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve);
+ });
+
+ await new Promise(resolve => {
+ window.addEventListener('message', t.step_func(messageEvent => {
+ // The failure message of no device chosen is expected. The point here is
+ // to validate not failing because of a sandboxed iframe.
+ assert_equals(
+ 'FAIL: NotFoundError: Failed to execute \'requestPort\' on \'Serial\': No port selected by the user.',
+ messageEvent.data);
+ resolve();
+ }));
+ iframe.contentWindow.postMessage({type: 'RequestPort'}, '*');
+ });
+}, 'RequestPort from a sandboxed iframe is valid.');
diff --git a/testing/web-platform/tests/serial/resources/automation.js b/testing/web-platform/tests/serial/resources/automation.js
new file mode 100644
index 0000000000..e88fdb1a9d
--- /dev/null
+++ b/testing/web-platform/tests/serial/resources/automation.js
@@ -0,0 +1,66 @@
+'use strict';
+
+// These tests rely on the User Agent providing an implementation of the
+// FakeSerialService interface which replaces the platform-specific
+// implementation of the Web Serial API with one that can be automated from
+// Javascript for testing purposes.
+//
+// In Chromium-based browsers this implementation is provided by a polyfill
+// in order to reduce the amount of test-only code shipped to users. To enable
+// these tests the browser must be run with these options:
+//
+// --enable-blink-features=MojoJS,MojoJSTest
+
+let fakeSerialService = undefined;
+
+// Returns a SerialPort instance and associated FakeSerialPort instance.
+async function getFakeSerialPort(fake) {
+ let token = fake.addPort();
+ let fakePort = fake.getFakePort(token);
+
+ let ports = await navigator.serial.getPorts();
+ assert_equals(ports.length, 1);
+
+ let port = ports[0];
+ assert_true(port instanceof SerialPort);
+
+ return { port, fakePort };
+}
+
+function serial_test(func, name, properties) {
+ promise_test(async (test) => {
+ assert_implements(navigator.serial, 'missing navigator.serial');
+ if (fakeSerialService === undefined) {
+ // Try loading a polyfill for the fake serial service.
+ if (isChromiumBased) {
+ const fakes = await import('/resources/chromium/fake-serial.js');
+ fakeSerialService = fakes.fakeSerialService;
+ }
+ }
+ assert_implements(fakeSerialService, 'missing fakeSerialService after initialization');
+
+ fakeSerialService.start();
+ try {
+ await func(test, fakeSerialService);
+ } finally {
+ fakeSerialService.stop();
+ fakeSerialService.reset();
+ }
+ }, name, properties);
+}
+
+function trustedClick() {
+ 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();
+ };
+ document.body.appendChild(button);
+ test_driver.click(button);
+ });
+}
diff --git a/testing/web-platform/tests/serial/resources/common.js b/testing/web-platform/tests/serial/resources/common.js
new file mode 100644
index 0000000000..5177f83a86
--- /dev/null
+++ b/testing/web-platform/tests/serial/resources/common.js
@@ -0,0 +1,33 @@
+// Compare two Uint8Arrays.
+function compareArrays(actual, expected) {
+ assert_true(actual instanceof Uint8Array, 'actual is Uint8Array');
+ assert_true(expected instanceof Uint8Array, 'expected is Uint8Array');
+ assert_equals(actual.byteLength, expected.byteLength, 'lengths equal');
+ for (let i = 0; i < expected.byteLength; ++i)
+ assert_equals(actual[i], expected[i], `Mismatch at position ${i}.`);
+}
+
+// Reads from |reader| until at least |targetLength| is read or the stream is
+// closed. The data is returned as a combined Uint8Array.
+async function readWithLength(reader, targetLength) {
+ const chunks = [];
+ let actualLength = 0;
+
+ while (true) {
+ let {value, done} = await reader.read();
+ chunks.push(value);
+ actualLength += value.byteLength;
+
+ if (actualLength >= targetLength || done) {
+ // It would be better to allocate |buffer| up front with the number of
+ // of bytes expected but this is the best that can be done without a
+ // BYOB reader to control the amount of data read.
+ const buffer = new Uint8Array(actualLength);
+ chunks.reduce((offset, chunk) => {
+ buffer.set(chunk, offset);
+ return offset + chunk.byteLength;
+ }, 0);
+ return buffer;
+ }
+ }
+}
diff --git a/testing/web-platform/tests/serial/resources/manual.js b/testing/web-platform/tests/serial/resources/manual.js
new file mode 100644
index 0000000000..ac19136b35
--- /dev/null
+++ b/testing/web-platform/tests/serial/resources/manual.js
@@ -0,0 +1,38 @@
+let manualTestPort = null;
+
+navigator.serial.addEventListener('disconnect', (e) => {
+ if (e.target === manualTestPort) {
+ manualTestPort = null;
+ }
+})
+
+async function getPortForManualTest() {
+ if (manualTestPort) {
+ return manualTestPort;
+ }
+
+ const button = document.createElement('button');
+ button.textContent = 'Click to select a device';
+ button.style.display = 'block';
+ button.style.fontSize = '20px';
+ button.style.padding = '10px';
+
+ await new Promise((resolve) => {
+ button.onclick = () => {
+ document.body.removeChild(button);
+ resolve();
+ };
+ document.body.appendChild(button);
+ });
+
+ manualTestPort = await navigator.serial.requestPort({filters: []});
+ assert_true(manualTestPort instanceof SerialPort);
+
+ return manualTestPort;
+}
+
+function manual_serial_test(func, name, properties) {
+ promise_test(async (test) => {
+ await func(test, await getPortForManualTest());
+ }, name, properties);
+}
diff --git a/testing/web-platform/tests/serial/resources/open-in-iframe.html b/testing/web-platform/tests/serial/resources/open-in-iframe.html
new file mode 100644
index 0000000000..9bf8beb66a
--- /dev/null
+++ b/testing/web-platform/tests/serial/resources/open-in-iframe.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<body>
+ <button>Fake user gesture</button>
+</body>
+
+<script>
+ 'use strict';
+
+ test_driver.set_test_context(parent);
+
+ window.onmessage = messageEvent => {
+ switch (messageEvent.data.type) {
+ case 'GetPorts':
+ navigator.serial.getPorts()
+ .then(ports => parent.postMessage('Success', '*'))
+ .catch(err => parent.postMessage(`FAIL: ${err}`, '*'));
+ break;
+ case 'RequestPort':
+ test_driver.click(document.getElementsByTagName('button')[0])
+ .then(() => navigator.serial.requestPort({filters: []}))
+ .then(port => parent.postMessage('Success', '*'))
+ .catch(err => parent.postMessage(`FAIL: ${err}`, '*'));
+ break;
+ default:
+ parent.postMessage(
+ `FAIL: Bad message type: ${messageEvent.data}`, '*');
+ };
+ };
+</script>
diff --git a/testing/web-platform/tests/serial/resources/serial-allowed-by-permissions-policy-worker.js b/testing/web-platform/tests/serial/resources/serial-allowed-by-permissions-policy-worker.js
new file mode 100644
index 0000000000..cef0aacdfc
--- /dev/null
+++ b/testing/web-platform/tests/serial/resources/serial-allowed-by-permissions-policy-worker.js
@@ -0,0 +1,14 @@
+'use strict';
+
+importScripts('/resources/testharness.js');
+
+let workerType;
+
+if (typeof postMessage === 'function') {
+ workerType = 'dedicated';
+}
+
+promise_test(() => navigator.serial.getPorts(),
+ `Inherited header permissions policy allows ${workerType} workers.`);
+
+done();
diff --git a/testing/web-platform/tests/serial/resources/serial-disabled-by-permissions-policy-worker.js b/testing/web-platform/tests/serial/resources/serial-disabled-by-permissions-policy-worker.js
new file mode 100644
index 0000000000..afac442974
--- /dev/null
+++ b/testing/web-platform/tests/serial/resources/serial-disabled-by-permissions-policy-worker.js
@@ -0,0 +1,17 @@
+'use strict';
+
+importScripts('/resources/testharness.js');
+
+const header = 'Permissions-Policy header serial=()';
+let workerType;
+
+if (typeof postMessage === 'function') {
+ workerType = 'dedicated';
+}
+
+promise_test(() => navigator.serial.getPorts().then(
+ () => assert_unreached('expected promise to reject with SecurityError'),
+ error => assert_equals(error.name, 'SecurityError')),
+ `Inherited ${header} disallows ${workerType} workers.`);
+
+done();
diff --git a/testing/web-platform/tests/serial/serial-allowed-by-permissions-policy-attribute-redirect-on-load.https.sub.html b/testing/web-platform/tests/serial/serial-allowed-by-permissions-policy-attribute-redirect-on-load.https.sub.html
new file mode 100644
index 0000000000..ac278ff0cf
--- /dev/null
+++ b/testing/web-platform/tests/serial/serial-allowed-by-permissions-policy-attribute-redirect-on-load.https.sub.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/permissions-policy/resources/permissions-policy.js></script>
+<script>
+'use strict';
+const relative_path = '/permissions-policy/resources/permissions-policy-serial.html';
+const base_src = '/permissions-policy/resources/redirect-on-load.html#';
+const relative_worker_frame_path =
+ '/permissions-policy/resources/permissions-policy-serial-worker.html';
+const sub = 'https://{{domains[www]}}:{{ports[https][0]}}';
+const same_origin_src = base_src + relative_path;
+const cross_origin_src = base_src + sub + relative_path;
+const same_origin_worker_frame_src = base_src + relative_worker_frame_path;
+const cross_origin_worker_frame_src = base_src + sub +
+ relative_worker_frame_path;
+const header = 'Permissions-Policy allow="serial"';
+
+async_test(t => {
+ test_feature_availability(
+ 'serial.getPorts()', t, same_origin_src,
+ expect_feature_available_default, 'serial');
+}, header + ' allows same-origin relocation.');
+
+async_test(t => {
+ test_feature_availability(
+ 'serial.getPorts()', t, same_origin_worker_frame_src,
+ expect_feature_available_default, 'serial');
+}, header + ' allows workers in same-origin relocation.');
+
+async_test(t => {
+ test_feature_availability(
+ 'serial.getPorts()', t, cross_origin_src,
+ expect_feature_unavailable_default, 'serial');
+}, header + ' disallows cross-origin relocation.');
+
+async_test(t => {
+ test_feature_availability(
+ 'serial.getPorts()', t, cross_origin_worker_frame_src,
+ expect_feature_unavailable_default, 'serial');
+}, header + ' disallows workers in cross-origin relocation.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/serial/serial-allowed-by-permissions-policy-attribute.https.sub.html b/testing/web-platform/tests/serial/serial-allowed-by-permissions-policy-attribute.https.sub.html
new file mode 100644
index 0000000000..02560b180f
--- /dev/null
+++ b/testing/web-platform/tests/serial/serial-allowed-by-permissions-policy-attribute.https.sub.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/permissions-policy/resources/permissions-policy.js></script>
+<script>
+'use strict';
+const sub = 'https://{{domains[www]}}:{{ports[https][0]}}';
+const same_origin_src = '/permissions-policy/resources/permissions-policy-serial.html';
+const cross_origin_src = sub + same_origin_src;
+const same_origin_worker_frame_src =
+ '/permissions-policy/resources/permissions-policy-serial-worker.html';
+const cross_origin_worker_frame_src = sub + same_origin_worker_frame_src;
+const feature_name = 'Permissions policy "serial"';
+const header = 'allow="serial" attribute';
+
+async_test(t => {
+ test_feature_availability(
+ 'serial.getPorts()', t, same_origin_src,
+ expect_feature_available_default, 'serial');
+}, feature_name + ' can be enabled in same-origin iframe using ' + header);
+
+async_test(t => {
+ test_feature_availability(
+ 'serial.getPorts()', t, same_origin_worker_frame_src,
+ expect_feature_available_default, 'serial');
+}, feature_name + ' can be enabled in a worker in same-origin iframe using ' +
+ header);
+
+async_test(t => {
+ test_feature_availability(
+ 'serial.getPorts()', t, cross_origin_src,
+ expect_feature_available_default, 'serial');
+}, feature_name + ' can be enabled in cross-origin iframe using ' + header);
+
+async_test(t => {
+ test_feature_availability(
+ 'serial.getPorts()', t, cross_origin_worker_frame_src,
+ expect_feature_available_default, 'serial');
+}, feature_name + ' can be enabled in a worker in cross-origin iframe using ' +
+ header);
+
+fetch_tests_from_worker(new Worker(
+ 'resources/serial-allowed-by-permissions-policy-worker.js'));
+</script>
+</body>
diff --git a/testing/web-platform/tests/serial/serial-allowed-by-permissions-policy.https.sub.html b/testing/web-platform/tests/serial/serial-allowed-by-permissions-policy.https.sub.html
new file mode 100644
index 0000000000..1be8eb979f
--- /dev/null
+++ b/testing/web-platform/tests/serial/serial-allowed-by-permissions-policy.https.sub.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/permissions-policy/resources/permissions-policy.js></script>
+<script>
+'use strict';
+const sub = 'https://{{domains[www]}}:{{ports[https][0]}}';
+const same_origin_src = '/permissions-policy/resources/permissions-policy-serial.html';
+const cross_origin_src = sub + same_origin_src;
+const same_origin_worker_frame_src =
+ '/permissions-policy/resources/permissions-policy-serial-worker.html';
+const cross_origin_worker_frame_src = sub + same_origin_worker_frame_src;
+const header = 'Permissions-Policy header serial=*';
+
+promise_test(
+ () => navigator.serial.getPorts(),
+ header + ' allows the top-level document.');
+
+async_test(t => {
+ test_feature_availability('serial.getPorts()', t, same_origin_src,
+ expect_feature_available_default);
+}, header + ' allows same-origin iframes.');
+
+async_test(t => {
+ test_feature_availability('serial.getPorts()', t, same_origin_worker_frame_src,
+ expect_feature_available_default);
+}, header + ' allows workers in same-origin iframes.');
+
+// Set allow="serial" on iframe element to delegate 'serial' to cross origin
+// subframe.
+async_test(t => {
+ test_feature_availability('serial.getPorts()', t, cross_origin_src,
+ expect_feature_available_default, 'serial');
+}, header + ' allows cross-origin iframes.');
+
+// Set allow="serial" on iframe element to delegate 'serial' to cross origin
+// subframe.
+async_test(t => {
+ test_feature_availability('serial.getPorts()', t,
+ cross_origin_worker_frame_src,
+ expect_feature_available_default, 'serial');
+}, header + ' allows workers in cross-origin iframes.');
+
+fetch_tests_from_worker(new Worker(
+ 'resources/serial-allowed-by-permissions-policy-worker.js'));
+</script>
+</body>
diff --git a/testing/web-platform/tests/serial/serial-allowed-by-permissions-policy.https.sub.html.headers b/testing/web-platform/tests/serial/serial-allowed-by-permissions-policy.https.sub.html.headers
new file mode 100644
index 0000000000..27deb82758
--- /dev/null
+++ b/testing/web-platform/tests/serial/serial-allowed-by-permissions-policy.https.sub.html.headers
@@ -0,0 +1 @@
+Permissions-Policy: serial=*
diff --git a/testing/web-platform/tests/serial/serial-default-permissions-policy.https.sub.html b/testing/web-platform/tests/serial/serial-default-permissions-policy.https.sub.html
new file mode 100644
index 0000000000..e3908d9215
--- /dev/null
+++ b/testing/web-platform/tests/serial/serial-default-permissions-policy.https.sub.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/permissions-policy/resources/permissions-policy.js></script>
+<script>
+'use strict';
+var same_origin_src = '/permissions-policy/resources/permissions-policy-serial.html';
+var cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' +
+ same_origin_src;
+var header = 'Default "serial" permissions policy "self"';
+
+promise_test(
+ () => navigator.serial.getPorts(),
+ header + ' allows the top-level document.');
+
+async_test(t => {
+ test_feature_availability('serial.getPorts()', t, same_origin_src,
+ expect_feature_available_default);
+}, header + ' allows same-origin iframes.');
+
+async_test(t => {
+ test_feature_availability('serial.getPorts()', t, cross_origin_src,
+ expect_feature_unavailable_default);
+}, header + ' disallows cross-origin iframes.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/serial/serial-disabled-by-permissions-policy.https.sub.html b/testing/web-platform/tests/serial/serial-disabled-by-permissions-policy.https.sub.html
new file mode 100644
index 0000000000..251bf84f47
--- /dev/null
+++ b/testing/web-platform/tests/serial/serial-disabled-by-permissions-policy.https.sub.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/test-only-api.js"></script>
+<script src="/serial/resources/automation.js"></script>
+<script src="/permissions-policy/resources/permissions-policy.js"></script>
+<script>
+'use strict';
+const sub = 'https://{{domains[www]}}:{{ports[https][0]}}';
+const same_origin_src = '/permissions-policy/resources/permissions-policy-serial.html';
+const cross_origin_src = sub + same_origin_src;
+const same_origin_worker_frame_src =
+ '/permissions-policy/resources/permissions-policy-serial-worker.html';
+const cross_origin_worker_frame_src = sub + same_origin_worker_frame_src;
+const header = 'Permissions-Policy header serial=()';
+
+async function sleep(timeout) {
+ return new Promise(resolve => {
+ step_timeout(() => {
+ resolve();
+ }, timeout);
+ });
+}
+
+promise_test(() => {
+ return navigator.serial.getPorts().then(() => {
+ assert_unreached('expected promise to reject with SecurityError');
+ }, error => {
+ assert_equals(error.name, 'SecurityError');
+ });
+}, header + ' disallows getPorts in the top-level document.');
+
+async_test(t => {
+ test_feature_availability('serial.getPorts()', t, same_origin_src,
+ expect_feature_unavailable_default);
+}, header + ' disallows same-origin iframes.');
+
+async_test(t => {
+ test_feature_availability('serial.getPorts()', t, same_origin_worker_frame_src,
+ expect_feature_unavailable_default);
+}, header + ' disallows workers in same-origin iframes.');
+
+async_test(t => {
+ test_feature_availability('serial.getPorts()', t, cross_origin_src,
+ expect_feature_unavailable_default);
+}, header + ' disallows cross-origin iframes.');
+
+async_test(t => {
+ test_feature_availability('serial.getPorts()', t,
+ cross_origin_worker_frame_src,
+ expect_feature_unavailable_default);
+}, header + ' disallows workers in cross-origin iframes.');
+
+fetch_tests_from_worker(new Worker(
+ 'resources/serial-disabled-by-permissions-policy-worker.js'));
+
+serial_test(async (t, fake) => {
+ let eventWatcher = new EventWatcher(t, navigator.serial, 'connect');
+
+ // This isn't necessary as the expected scenario shouldn't send any mojo
+ // request. However, in order to capture a bug that doesn't reject adding
+ // event listener, time delay here is to allow mojo request to be intercepted
+ // after adding connect event listener.
+ await sleep(100);
+
+ // If device connect event fires, EventWatcher will assert for an unexpected
+ // event.
+ fake.addPort();
+ // Time delay here is to allow event to be fired if any.
+ await sleep(100);
+}, 'Connect event is not fired when serial is disallowed.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/serial/serial-disabled-by-permissions-policy.https.sub.html.headers b/testing/web-platform/tests/serial/serial-disabled-by-permissions-policy.https.sub.html.headers
new file mode 100644
index 0000000000..690b696751
--- /dev/null
+++ b/testing/web-platform/tests/serial/serial-disabled-by-permissions-policy.https.sub.html.headers
@@ -0,0 +1 @@
+Permissions-Policy: serial=()
diff --git a/testing/web-platform/tests/serial/serialPort_close.https.any.js b/testing/web-platform/tests/serial/serialPort_close.https.any.js
new file mode 100644
index 0000000000..ec949a9917
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_close.https.any.js
@@ -0,0 +1,26 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await promise_rejects_dom(t, 'InvalidStateError', port.close());
+}, 'A SerialPort cannot be closed if it was never opened.');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await port.open({baudRate: 9600});
+ await port.close();
+ await promise_rejects_dom(t, 'InvalidStateError', port.close());
+}, 'A SerialPort cannot be closed if it is already closed.');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await port.open({baudRate: 9600});
+ const closePromise = port.close();
+ await promise_rejects_dom(t, 'InvalidStateError', port.close());
+ await closePromise;
+}, 'A SerialPort cannot be closed if it is being closed.');
diff --git a/testing/web-platform/tests/serial/serialPort_disconnect-manual.https.html b/testing/web-platform/tests/serial/serialPort_disconnect-manual.https.html
new file mode 100644
index 0000000000..5386201e6c
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_disconnect-manual.https.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/common.js"></script>
+ <script src="resources/manual.js"></script>
+ </head>
+ <body>
+ <p>
+ These tests require a serial device to be connected and disconnected.
+ </p>
+ <script>
+ manual_serial_test(async (t, port) => {
+ const watcher = new EventWatcher(t, navigator.serial, ['disconnect']);
+
+ await port.open({baudRate: 115200, bufferSize: 1024});
+
+ const disconnectPromise = watcher.wait_for(['disconnect'])
+ const reader = port.readable.getReader();
+
+ const disconnectMessage = document.createElement('div');
+ disconnectMessage.textContent = 'Disconnect the device now.';
+ document.body.appendChild(disconnectMessage);
+
+ try {
+ while (true) {
+ const {value, done} = await reader.read();
+ // Ignore |value| in case the device happens to produce data. It is
+ // not important for this test.
+ assert_false(done);
+ }
+ } catch (e) {
+ assert_equals(e.constructor, DOMException);
+ assert_equals(e.name, 'NetworkError');
+ }
+ reader.releaseLock();
+ assert_equals(port.readable, null);
+
+ let event = await disconnectPromise;
+ assert_equals(event.target, port);
+
+ disconnectMessage.remove();
+
+ await port.close();
+ }, 'Disconnect during read is detected.');
+
+ manual_serial_test(async (t, port) => {
+ const watcher = new EventWatcher(t, navigator.serial, ['disconnect']);
+
+ await port.open({baudRate: 115200, bufferSize: 1024});
+
+ const disconnectPromise = watcher.wait_for(['disconnect'])
+ const writer = port.writable.getWriter();
+
+ const disconnectMessage = document.createElement('div');
+ disconnectMessage.textContent = 'Disconnect the device now.';
+ document.body.appendChild(disconnectMessage);
+
+ const data = new Uint8Array(4096);
+ try {
+ while (true) {
+ await writer.write(data);
+ }
+ } catch (e) {
+ assert_equals(e.constructor, DOMException);
+ assert_equals(e.name, 'NetworkError');
+ }
+ writer.releaseLock();
+ assert_equals(port.writable, null);
+
+ let event = await disconnectPromise;
+ assert_equals(event.target, port);
+
+ disconnectMessage.remove();
+
+ await port.close();
+ }, 'Disconnect during write is detected.');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/serial/serialPort_events.https.any.js b/testing/web-platform/tests/serial/serialPort_events.https.any.js
new file mode 100644
index 0000000000..fa3db6a3b3
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_events.https.any.js
@@ -0,0 +1,197 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ const targets = [navigator.serial, port];
+ const expectedTargets = [navigator.serial];
+
+ const actualTargets = [];
+ function eventHandler(evt) {
+ actualTargets.push(evt.currentTarget);
+
+ if (evt.currentTarget == navigator.serial) {
+ evt.stopPropagation();
+ }
+ }
+
+ targets.forEach((target) => {
+ target.addEventListener('foo', eventHandler, {capture: true});
+ // stopPropagation() during capturing prevents bubbling.
+ target.addEventListener('foo', eventHandler);
+
+ t.add_cleanup(() => {
+ target.removeEventListener('foo', eventHandler, {capture: true});
+ target.removeEventListener('foo', eventHandler);
+ });
+ });
+
+ port.dispatchEvent(new CustomEvent('foo', {bubbles: true}));
+
+ assert_array_equals(actualTargets, expectedTargets, 'actualTargets');
+}, 'stopPropagation() during capturing');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ const targets = [navigator.serial, port];
+ const expectedTargets = [navigator.serial];
+
+ const actualTargets = [];
+ function eventHandler(evt) {
+ actualTargets.push(evt.currentTarget);
+
+ if (evt.currentTarget == navigator.serial) {
+ evt.cancelBubble = true;
+ }
+ }
+
+ targets.forEach((target) => {
+ target.addEventListener('foo', eventHandler, {capture: true});
+ // Setting cancelBubble during capturing prevents bubbling.
+ target.addEventListener('foo', eventHandler);
+
+ t.add_cleanup(() => {
+ target.removeEventListener('foo', eventHandler, {capture: true});
+ target.removeEventListener('foo', eventHandler);
+ });
+ });
+
+ port.dispatchEvent(new CustomEvent('foo', {bubbles: true}));
+
+ assert_array_equals(actualTargets, expectedTargets, 'actualTargets');
+}, 'Set cancelBubble during capturing');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ const targets = [navigator.serial, port];
+ const expectedTargets = [port];
+
+ const actualTargets = [];
+ function eventHandler(evt) {
+ actualTargets.push(evt.currentTarget);
+
+ if (evt.currentTarget == port) {
+ evt.stopPropagation();
+ }
+ }
+
+ targets.forEach((target) => {
+ target.addEventListener('foo', eventHandler);
+
+ t.add_cleanup(() => {
+ target.removeEventListener('foo', eventHandler);
+ });
+ });
+
+ port.dispatchEvent(new CustomEvent('foo', {bubbles: true}));
+
+ assert_array_equals(actualTargets, expectedTargets, 'actualTargets');
+}, 'stopPropagation() during bubbling');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ const targets = [navigator.serial, port];
+ const expectedTargets = [port];
+
+ const actualTargets = [];
+ function eventHandler(evt) {
+ actualTargets.push(evt.currentTarget);
+
+ if (evt.currentTarget == port) {
+ evt.cancelBubble = true;
+ }
+ }
+
+ targets.forEach((target) => {
+ target.addEventListener('foo', eventHandler);
+
+ t.add_cleanup(() => {
+ target.removeEventListener('foo', eventHandler);
+ });
+ });
+
+ port.dispatchEvent(new CustomEvent('foo', {bubbles: true}));
+
+ assert_array_equals(actualTargets, expectedTargets, 'actualTargets');
+}, 'Set cancelBubble during bubbling');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ const targets = [navigator.serial, port];
+ const expectedTargets = [
+ navigator.serial,
+ port,
+ navigator.serial,
+ port,
+ ];
+ const expectedTypes = [
+ 'foo',
+ 'bar',
+ 'bar',
+ 'foo',
+ ];
+
+ const actualTargets = [];
+ const actualTypes = [];
+ function eventHandler(evt) {
+ actualTargets.push(evt.currentTarget);
+ actualTypes.push(evt.type);
+
+ if (evt.currentTarget == navigator.serial && evt.type == 'foo') {
+ port.dispatchEvent(new CustomEvent('bar', {bubbles: true}));
+ }
+ }
+
+ targets.forEach((target) => {
+ target.addEventListener('foo', eventHandler, {capture: true});
+ target.addEventListener('bar', eventHandler);
+
+ t.add_cleanup(() => {
+ target.removeEventListener('foo', eventHandler, {capture: true});
+ target.removeEventListener('bar', eventHandler);
+ });
+ });
+
+ port.dispatchEvent(new CustomEvent('foo', {bubbles: true}));
+
+ assert_array_equals(actualTargets, expectedTargets, 'actualTargets');
+ assert_array_equals(actualTypes, expectedTypes, 'actualTypes');
+}, 'An event dispatched in an event handler is propagated before continuing');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ const targets = [navigator.serial, port];
+ const expected = [
+ 'capturing Serial',
+ 'capturing SerialPort',
+ 'bubbling SerialPort',
+ 'bubbling Serial',
+ ];
+
+ const actual = [];
+ targets.forEach((target) => {
+ const bubblingEventHandler = () => {
+ actual.push(`bubbling ${target.constructor.name}`);
+ };
+ target.addEventListener('foo', bubblingEventHandler);
+ const capturingEventHandler = () => {
+ actual.push(`capturing ${target.constructor.name}`);
+ };
+ target.addEventListener('foo', capturingEventHandler, {capture: true});
+
+ t.add_cleanup(() => {
+ target.removeEventListener('foo', bubblingEventHandler, {capture: true});
+ target.removeEventListener('foo', capturingEventHandler);
+ });
+ });
+
+ port.dispatchEvent(new CustomEvent('foo', {bubbles: true}));
+ assert_array_equals(actual, expected);
+}, 'Capturing and bubbling events delivered to listeners in the expected order');
diff --git a/testing/web-platform/tests/serial/serialPort_forget.https.any.js b/testing/web-platform/tests/serial/serialPort_forget.https.any.js
new file mode 100644
index 0000000000..8c0d96f695
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_forget.https.any.js
@@ -0,0 +1,43 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.forget();
+ return promise_rejects_dom(
+ t, 'NetworkError', port.open({baudRate: 9600}));
+}, 'open() rejects after forget()');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.open({baudRate: 9600});
+ await port.forget();
+ return promise_rejects_dom(t, 'InvalidStateError', port.close());
+}, 'close() rejects after forget()');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.open({baudRate: 9600});
+ await port.forget();
+ return promise_rejects_dom(t, 'InvalidStateError', port.setSignals());
+}, 'setSignals() rejects after forget()');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.open({baudRate: 9600});
+ await port.forget();
+ return promise_rejects_dom(t, 'InvalidStateError', port.getSignals());
+}, 'getSignals() rejects after forget()');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ const portsBeforeForget = await navigator.serial.getPorts();
+ assert_equals(portsBeforeForget.length, 1);
+ assert_equals(portsBeforeForget[0], port);
+
+ await port.forget();
+
+ const portsAfterForget = await navigator.serial.getPorts();
+ assert_equals(portsAfterForget.length, 0);
+}, 'forget() removes the device from getPorts()'); \ No newline at end of file
diff --git a/testing/web-platform/tests/serial/serialPort_getInfo.https.any.js b/testing/web-platform/tests/serial/serialPort_getInfo.https.any.js
new file mode 100644
index 0000000000..2e0df3b8bd
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_getInfo.https.any.js
@@ -0,0 +1,24 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ // Wait for getPorts() to resolve in order to ensure that the Mojo client
+ // interface has been configured.
+ let ports = await navigator.serial.getPorts();
+ assert_equals(ports.length, 0);
+
+ [{},
+ {usbVendorId: 1},
+ {usbProductId: 2},
+ {usbVendorId: 1, usbProductId: 2},
+ ].forEach((expectedInfo) => {
+ serial_test(async (t, fake) => {
+ let watcher = new EventWatcher(t, navigator.serial, ['connect']);
+ fake.addPort(expectedInfo);
+ let evt = await watcher.wait_for(['connect']);
+ let info = evt.target.getInfo();
+ assert_object_equals(expectedInfo, info);
+ }, `getInfo() returns ${JSON.stringify(expectedInfo)}`);
+ });
+}, 'getInfo() meta test');
diff --git a/testing/web-platform/tests/serial/serialPort_getSignals.https.any.js b/testing/web-platform/tests/serial/serialPort_getSignals.https.any.js
new file mode 100644
index 0000000000..a570e35987
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_getSignals.https.any.js
@@ -0,0 +1,63 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await promise_rejects_dom(t, 'InvalidStateError', port.getSignals());
+}, 'getSignals() rejects if the port is not open');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.open({baudRate: 9600});
+
+ let expectedSignals = {
+ dataCarrierDetect: false,
+ clearToSend: false,
+ ringIndicator: false,
+ dataSetReady: false
+ };
+ fakePort.simulateInputSignals(expectedSignals);
+ let signals = await port.getSignals();
+ assert_object_equals(signals, expectedSignals);
+
+ expectedSignals.dataCarrierDetect = true;
+ fakePort.simulateInputSignals(expectedSignals);
+ signals = await port.getSignals();
+ assert_object_equals(signals, expectedSignals, 'DCD set');
+
+ expectedSignals.clearToSend = true;
+ fakePort.simulateInputSignals(expectedSignals);
+ signals = await port.getSignals();
+ assert_object_equals(signals, expectedSignals, 'CTS set');
+
+ expectedSignals.ringIndicator = true;
+ fakePort.simulateInputSignals(expectedSignals);
+ signals = await port.getSignals();
+ assert_object_equals(signals, expectedSignals, 'RI set');
+
+ expectedSignals.dataSetReady = true;
+ fakePort.simulateInputSignals(expectedSignals);
+ signals = await port.getSignals();
+ assert_object_equals(signals, expectedSignals, 'DSR set');
+}, 'getSignals() returns the current state of input control signals');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.open({baudRate: 9600});
+
+ fakePort.simulateInputSignalFailure(true);
+ await promise_rejects_dom(t, 'NetworkError', port.getSignals());
+
+ fakePort.simulateInputSignalFailure(false);
+ const expectedSignals = {
+ dataCarrierDetect: false,
+ clearToSend: false,
+ ringIndicator: false,
+ dataSetReady: false
+ };
+ const signals = await port.getSignals();
+ assert_object_equals(signals, expectedSignals);
+
+ await port.close();
+}, 'getSignals() rejects on failure');
diff --git a/testing/web-platform/tests/serial/serialPort_loopback-manual.https.html b/testing/web-platform/tests/serial/serialPort_loopback-manual.https.html
new file mode 100644
index 0000000000..bf876b6610
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_loopback-manual.https.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/common.js"></script>
+ <script src="resources/manual.js"></script>
+ </head>
+ <body>
+ <p>
+ These tests require a connected serial device configured to act as a
+ "loopback" device, with the transmit and receive pins wired together.
+ </p>
+ <script>
+ manual_serial_test(async (t, port) => {
+ await port.open({baudRate: 115200, bufferSize: 1024});
+
+ // Create something much smaller than bufferSize above.
+ const data = new Uint8Array(64);
+ for (let i = 0; i < data.byteLength; ++i)
+ data[i] = i & 0xff;
+
+ const reader = port.readable.getReader();
+
+ for (let i = 0; i < 10; ++i) {
+ const writer = port.writable.getWriter();
+ writer.write(data);
+ const writePromise = writer.close();
+
+ const value = await readWithLength(reader, data.byteLength);
+ await writePromise;
+
+ compareArrays(value, data);
+ }
+
+ reader.releaseLock();
+ await port.close();
+ }, 'Can perform a series of small writes.');
+
+ manual_serial_test(async (t, port) => {
+ await port.open({baudRate: 115200, bufferSize: 1024});
+
+ // Create something much larger than bufferSize above.
+ const data = new Uint8Array(10 * 1024);
+ for (let i = 0; i < data.byteLength; ++i)
+ data[i] = (i / 1024) & 0xff;
+
+ const reader = port.readable.getReader();
+
+ for (let i = 0; i < 10; ++i) {
+ const writer = port.writable.getWriter();
+ writer.write(data);
+ const writePromise = writer.close();
+
+ const value = await readWithLength(reader, data.byteLength);
+ await writePromise;
+
+ compareArrays(value, data);
+ }
+
+ reader.releaseLock();
+ await port.close();
+ }, 'Can perform a series of large writes.');
+
+ manual_serial_test(async (t, port) => {
+ await port.open({baudRate: 115200, bufferSize: 64});
+
+ const writer = port.writable.getWriter();
+ // |data| is small enough to be completely transmitted.
+ let data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ await writer.write(data);
+
+ // Wait a little bit for the device to process the incoming data.
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ // ...before discarding the receive buffers.
+ await port.readable.cancel();
+
+ data = new Uint8Array([9, 10, 11, 12, 13, 14, 15, 16]);
+ const reader = port.readable.getReader();
+ const readPromise = readWithLength(reader, data.byteLength);
+
+ // The next block of data should be received successfully.
+ await writer.write(data);
+ writer.releaseLock();
+
+ const value = await readPromise;
+ reader.releaseLock();
+
+ compareArrays(value, data);
+
+ await port.close();
+ }, 'Canceling the reader discards buffered data.');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/serial/serialPort_loopback_BreakError-manual.https.html b/testing/web-platform/tests/serial/serialPort_loopback_BreakError-manual.https.html
new file mode 100644
index 0000000000..369fac6155
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_loopback_BreakError-manual.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/common.js"></script>
+ <script src="resources/manual.js"></script>
+ </head>
+ <body>
+ <p>
+ These tests require a connected serial device configured to act as a
+ "loopback" device, with the transmit and receive pins wired together.
+ </p>
+ <script>
+ manual_serial_test(async (t, port) => {
+ await port.open({baudRate: 115200, bufferSize: 1024});
+
+ let reader = port.readable.getReader();
+ let readPromise = (async () => {
+ // A single zero byte will be read before the break is detected.
+ const {value, done} = await reader.read();
+ compareArrays(value, new Uint8Array([0]));
+ assert_false(done);
+
+ try {
+ const {value, done} = await reader.read();
+ assert_unreached(`Expected break, got ${value.byteLength} bytes`);
+ } catch (e) {
+ assert_equals(e.constructor, DOMException);
+ assert_equals(e.name, 'BreakError');
+ }
+ })();
+
+ await port.setSignals({break: true});
+ await readPromise;
+ await port.setSignals({break: false});
+
+ const writer = port.writable.getWriter();
+ // |data| is small enough to be completely transmitted.
+ let data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ await writer.write(data);
+ writer.releaseLock();
+
+ reader = port.readable.getReader();
+ const buffer = await readWithLength(reader, data.byteLength);;
+ compareArrays(buffer, data);
+ reader.releaseLock();
+
+ await port.close();
+ }, 'Break is detected.');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/serial/serialPort_loopback_BufferOverrunError-manual.https.html b/testing/web-platform/tests/serial/serialPort_loopback_BufferOverrunError-manual.https.html
new file mode 100644
index 0000000000..06f8271ac9
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_loopback_BufferOverrunError-manual.https.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/common.js"></script>
+ <script src="resources/manual.js"></script>
+ </head>
+ <body>
+ <p>
+ These tests require a connected serial device configured to act as a
+ "loopback" device, with the transmit and receive pins wired together.
+ </p>
+ <script>
+ manual_serial_test(async (t, port) => {
+ await port.open({baudRate: 115200, bufferSize: 1024});
+
+ // Create something much larger than bufferSize above.
+ const data = new Uint8Array(16 * 1024);
+ for (let i = 0; i < data.byteLength; ++i)
+ data[i] = (i / 1024) & 0xff;
+
+ // Completely write |data| to the port without waiting for it to be
+ // received.
+ const writer = port.writable.getWriter();
+ writer.write(data);
+ await writer.close();
+
+ const reader = port.readable.getReader();
+ const chunks = [];
+ let actualLength = 0;
+ while (true) {
+ try {
+ const {value, done} = await reader.read();
+ if (value) {
+ actualLength += value.byteLength;
+ chunks.push(value);
+ }
+ if (done) {
+ assert_unreached("Unexpected end of stream.");
+ break;
+ }
+ } catch (e) {
+ assert_equals(e.name, 'BufferOverrunError');
+ break;
+ }
+ }
+ reader.releaseLock();
+
+ const buffer = new Uint8Array(actualLength);
+ chunks.reduce((offset, chunk) => {
+ buffer.set(chunk, offset);
+ return offset + chunk.byteLength;
+ }, 0);
+
+ assert_greater_than(actualLength, 0);
+ compareArrays(buffer, data.slice(0, actualLength));
+
+ await port.close();
+ }, 'Overflowing the receive buffer triggers an error.');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/serial/serialPort_loopback_flowControl-manual.https.html b/testing/web-platform/tests/serial/serialPort_loopback_flowControl-manual.https.html
new file mode 100644
index 0000000000..930dab74d6
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_loopback_flowControl-manual.https.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/common.js"></script>
+ <script src="resources/manual.js"></script>
+ </head>
+ <body>
+ <p>
+ These tests require a connected serial device configured to act as a
+ "loopback" device, with the TX and RX pins and RTS and CTS pins wired
+ together.
+ </p>
+ <script>
+ manual_serial_test(async (t, port) => {
+ await port.open({
+ baudRate: 115200,
+ bufferSize: 255,
+ flowControl: 'none'
+ });
+ t.add_cleanup(async () => {
+ await port.close();
+ });
+
+ await port.setSignals({ requestToSend: true });
+ let signals = await port.getSignals();
+ assert_true(signals.clearToSend);
+
+ await port.setSignals({ requestToSend: false });
+ signals = await port.getSignals();
+ assert_false(signals.clearToSend);
+ }, 'Manual RTS control works with no flow control enabled');
+
+ manual_serial_test(async (t, port) => {
+ await port.open({
+ baudRate: 115200,
+ bufferSize: 255,
+ flowControl: 'hardware'
+ });
+
+ const writer = port.writable.getWriter();
+ t.add_cleanup(async () => {
+ writer.releaseLock();
+ await port.close();
+ });
+
+ assert_true((await port.getSignals()).clearToSend);
+
+ const buffer = new Uint8Array(1);
+ while ((await port.getSignals()).clearToSend) {
+ await writer.write(buffer);
+ }
+ }, 'Hardware flow control automatically sets RTS pin');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/serial/serialPort_ondisconnect.https.any.js b/testing/web-platform/tests/serial/serialPort_ondisconnect.https.any.js
new file mode 100644
index 0000000000..b20f69d265
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_ondisconnect.https.any.js
@@ -0,0 +1,40 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ // Don't listen for 'disconnect' events on navigator.serial so we can listen
+ // for them on each SerialPort instance instead.
+ const eventWatcher = new EventWatcher(t, navigator.serial, ['connect']);
+
+ // Wait for getPorts() to resolve in order to ensure that the Mojo client
+ // interface has been configured.
+ let ports = await navigator.serial.getPorts();
+ assert_equals(ports.length, 0);
+
+ // Add ports one at a time so that we can map tokens to ports.
+ const token1 = fake.addPort();
+ const port1 = (await eventWatcher.wait_for(['connect'])).target;
+ const port1Watcher = new EventWatcher(t, port1, ['disconnect']);
+
+ const token2 = fake.addPort();
+ const port2 = (await eventWatcher.wait_for(['connect'])).target;
+ const port2Watcher = new EventWatcher(t, port2, ['disconnect']);
+
+ fake.removePort(token2);
+ const event1 = await port2Watcher.wait_for(['disconnect']);
+ assert_true(event1 instanceof Event);
+ assert_equals(event1.target, port2);
+
+ ports = await navigator.serial.getPorts();
+ assert_equals(ports.length, 1);
+ assert_equals(ports[0], port1);
+
+ fake.removePort(token1);
+ const event2 = await port1Watcher.wait_for(['disconnect']);
+ assert_true(event2 instanceof Event);
+ assert_equals(event2.target, port1);
+
+ ports = await navigator.serial.getPorts();
+ assert_equals(ports.length, 0);
+}, 'A "disconnect" event is fired on ports when they are removed.');
diff --git a/testing/web-platform/tests/serial/serialPort_open.https.any.js b/testing/web-platform/tests/serial/serialPort_open.https.any.js
new file mode 100644
index 0000000000..ca13877088
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_open.https.any.js
@@ -0,0 +1,95 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await port.open({baudRate: 9600});
+ return promise_rejects_dom(
+ t, 'InvalidStateError', port.open({baudRate: 9600}));
+}, 'A SerialPort cannot be opened if it is already open.');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ const firstRequest = port.open({baudRate: 9600});
+ await promise_rejects_dom(
+ t, 'InvalidStateError', port.open({baudRate: 9600}));
+ await firstRequest;
+}, 'Simultaneous calls to open() are disallowed.');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await promise_rejects_js(t, TypeError, port.open({}));
+
+ await Promise.all([-1, 0].map(
+ baudRate => {
+ return promise_rejects_js(t, TypeError, port.open({baudRate}))}));
+}, 'Baud rate is required and must be greater than zero.');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await Promise.all([-1, 0, 6, 9].map(dataBits => {
+ return promise_rejects_js(
+ t, TypeError, port.open({baudRate: 9600, dataBits}));
+ }));
+
+ await[undefined, 7, 8].reduce(async (previousTest, dataBits) => {
+ await previousTest;
+ await port.open({baudRate: 9600, dataBits});
+ await port.close();
+ }, Promise.resolve());
+}, 'Data bits must be 7 or 8');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await Promise.all([0, null, 'cats'].map(parity => {
+ return promise_rejects_js(
+ t, TypeError, port.open({baudRate: 9600, parity}),
+ `Should reject parity option "${parity}"`);
+ }));
+
+ await[undefined, 'none', 'even', 'odd'].reduce(
+ async (previousTest, parity) => {
+ await previousTest;
+ await port.open({baudRate: 9600, parity});
+ await port.close();
+ },
+ Promise.resolve());
+}, 'Parity must be "none", "even" or "odd"');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await Promise.all([-1, 0, 3, 4].map(stopBits => {
+ return promise_rejects_js(
+ t, TypeError, port.open({baudRate: 9600, stopBits}));
+ }));
+
+ await[undefined, 1, 2].reduce(async (previousTest, stopBits) => {
+ await previousTest;
+ await port.open({baudRate: 9600, stopBits});
+ await port.close();
+ }, Promise.resolve());
+}, 'Stop bits must be 1 or 2');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await promise_rejects_js(
+ t, TypeError, port.open({baudRate: 9600, bufferSize: -1}));
+ await promise_rejects_js(
+ t, TypeError, port.open({baudRate: 9600, bufferSize: 0}));
+}, 'Buffer size must be greater than zero.');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ const bufferSize = 1 * 1024 * 1024 * 1024 /* 1 GiB */;
+ return promise_rejects_js(
+ t, TypeError, port.open({baudRate: 9600, bufferSize}));
+}, 'Unreasonably large buffer sizes are rejected.');
diff --git a/testing/web-platform/tests/serial/serialPort_readable-manual.https.html b/testing/web-platform/tests/serial/serialPort_readable-manual.https.html
new file mode 100644
index 0000000000..4e49ef4061
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_readable-manual.https.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/common.js"></script>
+ <script src="resources/manual.js"></script>
+ </head>
+ <body>
+ <p>
+ These tests require a device configured with the following Arduino sketch:
+
+ <pre>
+uint32_t seed = 0;
+uint32_t bytesToSend = 0;
+
+uint8_t nextByte() {
+ seed = (1103515245 * seed + 12345) % 0x8000000;
+ return (seed >> 16) & 0xFF;
+}
+
+void setup() {
+ Serial.begin(115200);
+}
+
+void loop() {
+ if (!Serial) {
+ return;
+ }
+
+ if (bytesToSend == 0) {
+ // Read the seed and number of bytes to send from the host as 32-bit
+ // little-endian values.
+ if (Serial.available() &lt 8) {
+ return;
+ }
+
+ uint8_t buf[8];
+ Serial.readBytes((char*)buf, sizeof buf);
+ seed = (uint32_t)buf[0] |
+ ((uint32_t)buf[1] &lt;&lt; 8) |
+ ((uint32_t)buf[2] &lt;&lt; 16) |
+ ((uint32_t)buf[3] &lt;&lt; 24);
+ bytesToSend = (uint32_t)buf[4] |
+ ((uint32_t)buf[5] &lt;&lt; 8) |
+ ((uint32_t)buf[6] &lt;&lt; 16) |
+ ((uint32_t)buf[7] &lt;&lt; 24);
+ } else {
+ uint8_t buf[64];
+ uint32_t count = min(sizeof buf, bytesToSend);
+ for (uint32_t i = 0; i &lt; count; ++i) {
+ buf[i] = nextByte();
+ }
+ bytesToSend -= count;
+ Serial.write((char*)buf, count);
+ }
+}
+ </pre>
+ </p>
+ <p>
+ <progress id='progress'></progress>
+ </p>
+ <script>
+ let seed = 10;
+ const length = 1024 * 1024 * 10;
+
+ function next_byte() {
+ seed = (Math.imul(1103515245, seed) + 12345) % (1 << 31);
+ return (seed >> 16) & 0xFF;
+ }
+
+ manual_serial_test(async (t, port) => {
+ await port.open({baudRate: 115200, bufferSize: 1024});
+
+ const config = new DataView(new ArrayBuffer(8));
+ config.setUint32(0, seed, /*littleEndian=*/true);
+ config.setUint32(4, length, /*littleEndian=*/true);
+
+ const writer = port.writable.getWriter();
+ writer.write(config);
+
+ const progress = document.getElementById('progress');
+ progress.max = length;
+ progress.value = 0;
+
+ const reader = port.readable.getReader();
+ let bytesRead = 0;
+ while (bytesRead < length) {
+ const { value, done } = await reader.read();
+ assert_false(done);
+ for (let i = 0; i < value.byteLength; ++i) {
+ assert_equals(value[i], next_byte(),
+ `mismatch at byte ${bytesRead + i}`);
+ }
+ bytesRead += value.byteLength;
+ progress.value = bytesRead;
+ }
+
+ writer.releaseLock();
+ reader.releaseLock();
+ await port.close();
+ }, `Reading ${length} bytes from the device succeeds.`);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/serial/serialPort_readable_byob.https.any.js b/testing/web-platform/tests/serial/serialPort_readable_byob.https.any.js
new file mode 100644
index 0000000000..8672cf7124
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_readable_byob.https.any.js
@@ -0,0 +1,65 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+async function readInto(reader, buffer) {
+ let offset = 0;
+ while (offset < buffer.byteLength) {
+ const {value: view, done} =
+ await reader.read(new Uint8Array(buffer, offset));
+ buffer = view.buffer;
+ if (done) {
+ break;
+ }
+ offset += view.byteLength;
+ }
+ return buffer;
+}
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ const bufferSize = 1024;
+ await port.open({baudRate: 9600, bufferSize});
+
+ const reader = port.readable.getReader({mode: 'byob'});
+ assert_true(reader instanceof ReadableStreamBYOBReader);
+
+ await fakePort.writable();
+ const data = new Uint8Array(bufferSize);
+ for (let i = 0; i < data.byteLength; ++i)
+ data[i] = i & 0xff;
+ fakePort.write(data);
+
+ let buffer = new ArrayBuffer(512);
+ buffer = await readInto(reader, buffer);
+ assert_equals(512, buffer.byteLength, 'original size retained');
+ compareArrays(data.subarray(0, 512), new Uint8Array(buffer));
+
+ buffer = await readInto(reader, buffer);
+ assert_equals(512, buffer.byteLength, 'original size retained');
+ compareArrays(data.subarray(512), new Uint8Array(buffer));
+ reader.releaseLock();
+
+ await port.close();
+}, 'Can read specific subsets of the available data');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const reader = port.readable.getReader({mode: 'byob'});
+ assert_true(reader instanceof ReadableStreamBYOBReader);
+
+ await fakePort.writable();
+ const data = new Uint8Array(1024);
+ for (let i = 0; i < data.byteLength; ++i)
+ data[i] = i & 0xff;
+ fakePort.write(data);
+
+ let buffer = new ArrayBuffer(1024);
+ buffer = await readInto(reader, buffer);
+ compareArrays(data, new Uint8Array(buffer));
+ reader.releaseLock();
+
+ await port.close();
+}, 'Can read a large amount of data over multiple iterations');
diff --git a/testing/web-platform/tests/serial/serialPort_readable_cancel.https.any.js b/testing/web-platform/tests/serial/serialPort_readable_cancel.https.any.js
new file mode 100644
index 0000000000..fd1b08f6e7
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_readable_cancel.https.any.js
@@ -0,0 +1,69 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const reader = port.readable.getReader();
+ const readPromise = reader.read();
+ await reader.cancel();
+ const {value, done} = await readPromise;
+ assert_true(done);
+ assert_equals(undefined, value);
+
+ await port.close();
+}, 'Can cancel while reading');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const reader = port.readable.getReader();
+ const closed = (async () => {
+ const {value, done} = await reader.read();
+ assert_true(done);
+ assert_equals(undefined, value);
+ reader.releaseLock();
+ await port.close();
+ assert_equals(port.readable, null);
+ })();
+
+ await reader.cancel();
+ await closed;
+}, 'Can close while canceling');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const reader = port.readable.getReader();
+
+ await fakePort.writable();
+ const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ await fakePort.write(data);
+
+ await reader.cancel();
+ await port.close();
+}, 'Cancel discards a small amount of data waiting to be read');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ // Select a buffer size smaller than the amount of data transferred.
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const reader = port.readable.getReader();
+
+ await fakePort.writable();
+ const data = new Uint8Array(1024);
+ // Writing will fail because there was more data to send than could fit in the
+ // buffer and none of it was read.
+ const writePromise =
+ promise_rejects_dom(t, 'InvalidStateError', fakePort.write(data));
+
+ await reader.cancel();
+ await writePromise;
+
+ await port.close();
+}, 'Cancel discards a large amount of data waiting to be read');
diff --git a/testing/web-platform/tests/serial/serialPort_readable_chain.https.any.js b/testing/web-platform/tests/serial/serialPort_readable_chain.https.any.js
new file mode 100644
index 0000000000..552567cdfe
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_readable_chain.https.any.js
@@ -0,0 +1,30 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ // Select a buffer size larger than the amount of data transferred.
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const decoder = new TextDecoderStream();
+ const streamClosed = port.readable.pipeTo(decoder.writable);
+ const readable = decoder.readable.pipeThrough(new TransformStream())
+ .pipeThrough(new TransformStream())
+ .pipeThrough(new TransformStream())
+ .pipeThrough(new TransformStream());
+ const reader = readable.getReader();
+
+ await fakePort.writable();
+ fakePort.write(new TextEncoder().encode('Hello world!'));
+
+ const {value, done} = await reader.read();
+ assert_false(done);
+ assert_equals('Hello world!', value);
+ await reader.cancel('arbitrary reason');
+ await streamClosed.catch(reason => {
+ assert_equals('arbitrary reason', reason);
+ });
+
+ await port.close();
+}, 'Stream closure is observable through a long chain of transforms');
diff --git a/testing/web-platform/tests/serial/serialPort_readable_closeLocked.https.any.js b/testing/web-platform/tests/serial/serialPort_readable_closeLocked.https.any.js
new file mode 100644
index 0000000000..21196f758b
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_readable_closeLocked.https.any.js
@@ -0,0 +1,17 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await port.open({baudRate: 9600});
+ assert_true(port.readable instanceof ReadableStream);
+
+ const reader = port.readable.getReader();
+ await promise_rejects_js(t, TypeError, port.close());
+
+ reader.releaseLock();
+ await port.close();
+ assert_equals(port.readable, null);
+}, 'Port cannot be closed while readable is locked');
diff --git a/testing/web-platform/tests/serial/serialPort_readable_disconnect.https.any.js b/testing/web-platform/tests/serial/serialPort_readable_disconnect.https.any.js
new file mode 100644
index 0000000000..3532745ae0
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_readable_disconnect.https.any.js
@@ -0,0 +1,25 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ // Select a buffer size smaller than the amount of data transferred.
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const reader = port.readable.getReader();
+
+ await fakePort.writable();
+ const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ fakePort.write(data);
+ fakePort.simulateDisconnectOnRead();
+
+ const {value, done} = await reader.read();
+ assert_false(done);
+ compareArrays(data, value);
+
+ await promise_rejects_dom(t, 'NetworkError', reader.read());
+ assert_equals(port.readable, null);
+
+ await port.close();
+}, 'Disconnect error closes readable and sets it to null');
diff --git a/testing/web-platform/tests/serial/serialPort_readable_largeRead.https.any.js b/testing/web-platform/tests/serial/serialPort_readable_largeRead.https.any.js
new file mode 100644
index 0000000000..f8087514f7
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_readable_largeRead.https.any.js
@@ -0,0 +1,23 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ // Select a buffer size smaller than the amount of data transferred.
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const reader = port.readable.getReader();
+
+ await fakePort.writable();
+ const data = new Uint8Array(1024); // Much larger than bufferSize above.
+ for (let i = 0; i < data.byteLength; ++i)
+ data[i] = i & 0xff;
+ fakePort.write(data);
+
+ const value = await readWithLength(reader, data.byteLength);
+ compareArrays(data, value);
+ reader.releaseLock();
+
+ await port.close();
+}, 'Can read a large amount of data');
diff --git a/testing/web-platform/tests/serial/serialPort_readable_open.https.any.js b/testing/web-platform/tests/serial/serialPort_readable_open.https.any.js
new file mode 100644
index 0000000000..1a12cc2da4
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_readable_open.https.any.js
@@ -0,0 +1,21 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ assert_equals(port.readable, null);
+
+ await port.open({baudRate: 9600});
+ const readable = port.readable;
+ assert_true(readable instanceof ReadableStream);
+
+ await port.close();
+ assert_equals(port.readable, null);
+
+ const reader = readable.getReader();
+ const {value, done} = await reader.read();
+ assert_true(done);
+ assert_equals(value, undefined);
+}, 'SerialPort.readable is set by open() and closes on port close');
diff --git a/testing/web-platform/tests/serial/serialPort_readable_parityError.https.any.js b/testing/web-platform/tests/serial/serialPort_readable_parityError.https.any.js
new file mode 100644
index 0000000000..7afd494dc5
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_readable_parityError.https.any.js
@@ -0,0 +1,52 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+// ParityError is not (as of 2020/03/23) a valid DOMException, so cannot use
+// promise_rejects_dom for it.
+async function promise_rejects_with_parity_error(t, promise) {
+ return promise
+ .then(() => {
+ assert_false('Should have rejected');
+ })
+ .catch(e => {
+ assert_equals(e.constructor, DOMException);
+ assert_equals(e.name, 'ParityError');
+ });
+}
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ // Select a buffer size smaller than the amount of data transferred.
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ let readable = port.readable;
+ let reader = readable.getReader();
+
+ await fakePort.writable();
+ const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ fakePort.write(data);
+ fakePort.simulateParityError();
+
+ let {value, done} = await reader.read();
+ assert_false(done);
+ compareArrays(data, value);
+
+ await promise_rejects_with_parity_error(t, reader.read());
+ assert_not_equals(port.readable, readable);
+
+ readable = port.readable;
+ assert_true(readable instanceof ReadableStream);
+ reader = port.readable.getReader();
+
+ await fakePort.writable();
+ fakePort.write(data);
+
+ ({value, done} = await reader.read());
+ assert_false(done);
+ compareArrays(data, value);
+ reader.releaseLock();
+
+ await port.close();
+ assert_equals(port.readable, null);
+}, 'Parity error closes readable and replaces it with a new stream');
diff --git a/testing/web-platform/tests/serial/serialPort_readable_pipeThrough.https.any.js b/testing/web-platform/tests/serial/serialPort_readable_pipeThrough.https.any.js
new file mode 100644
index 0000000000..13c6d48023
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_readable_pipeThrough.https.any.js
@@ -0,0 +1,26 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ // Select a buffer size larger than the amount of data transferred.
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const decoder = new TextDecoderStream();
+ const streamClosed = port.readable.pipeTo(decoder.writable);
+ const reader = decoder.readable.getReader();
+
+ await fakePort.writable();
+ fakePort.write(new TextEncoder().encode('Hello world!'));
+
+ const {value, done} = await reader.read();
+ assert_false(done);
+ assert_equals('Hello world!', value);
+ await reader.cancel();
+ await streamClosed.catch(reason => {
+ assert_equals(undefined, reason);
+ });
+
+ await port.close();
+}, 'Can pipe readable through a transform stream.')
diff --git a/testing/web-platform/tests/serial/serialPort_readable_smallRead.https.any.js b/testing/web-platform/tests/serial/serialPort_readable_smallRead.https.any.js
new file mode 100644
index 0000000000..b926aa608f
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_readable_smallRead.https.any.js
@@ -0,0 +1,23 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ // Select a buffer size larger than the amount of data transferred.
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const reader = port.readable.getReader();
+ assert_true(reader instanceof ReadableStreamDefaultReader);
+
+ await fakePort.writable();
+ const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ fakePort.write(data);
+
+ let {value, done} = await reader.read();
+ assert_false(done);
+ compareArrays(data, value);
+ reader.releaseLock();
+
+ await port.close();
+}, 'Can read a small amount of data');
diff --git a/testing/web-platform/tests/serial/serialPort_setSignals.https.any.js b/testing/web-platform/tests/serial/serialPort_setSignals.https.any.js
new file mode 100644
index 0000000000..27911ab7db
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_setSignals.https.any.js
@@ -0,0 +1,59 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await promise_rejects_dom(t, 'InvalidStateError', port.setSignals({}));
+}, 'setSignals() rejects if the port is not open');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.open({baudRate: 9600});
+
+ let expectedSignals = {
+ dataTerminalReady: true,
+ requestToSend: false,
+ break: false
+ };
+ assert_object_equals(fakePort.outputSignals, expectedSignals, 'initial');
+
+ await promise_rejects_js(t, TypeError, port.setSignals());
+ assert_object_equals(fakePort.outputSignals, expectedSignals, 'no-op');
+
+ await promise_rejects_js(t, TypeError, port.setSignals({}));
+ assert_object_equals(fakePort.outputSignals, expectedSignals, 'no-op');
+
+ await port.setSignals({dataTerminalReady: false});
+ expectedSignals.dataTerminalReady = false;
+ assert_object_equals(fakePort.outputSignals, expectedSignals, 'clear DTR');
+
+ await port.setSignals({requestToSend: true});
+ expectedSignals.requestToSend = true;
+ assert_object_equals(fakePort.outputSignals, expectedSignals, 'set RTS');
+
+ await port.setSignals({break: true});
+ expectedSignals.break = true;
+ assert_object_equals(fakePort.outputSignals, expectedSignals, 'set BRK');
+
+ await port.setSignals(
+ {dataTerminalReady: true, requestToSend: false, break: false});
+ expectedSignals.dataTerminalReady = true;
+ expectedSignals.requestToSend = false;
+ expectedSignals.break = false;
+ assert_object_equals(fakePort.outputSignals, expectedSignals, 'invert');
+}, 'setSignals() modifies the state of the port');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.open({baudRate: 9600});
+
+ fakePort.simulateOutputSignalFailure(true);
+ await promise_rejects_dom(t, 'NetworkError', port.setSignals({break: true}));
+
+ fakePort.simulateOutputSignalFailure(false);
+ await port.setSignals({break: true});
+ assert_true(fakePort.outputSignals.break);
+
+ await port.close();
+}, 'setSignals() rejects on failure');
diff --git a/testing/web-platform/tests/serial/serialPort_writable.https.any.js b/testing/web-platform/tests/serial/serialPort_writable.https.any.js
new file mode 100644
index 0000000000..087c881500
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_writable.https.any.js
@@ -0,0 +1,289 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ assert_equals(port.writable, null);
+
+ await port.open({baudRate: 9600});
+ const writable = port.writable;
+ assert_true(writable instanceof WritableStream);
+
+ await port.close();
+ assert_equals(port.writable, null);
+
+ const writer = writable.getWriter();
+ const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ await promise_rejects_dom(t, 'InvalidStateError', writer.write(data));
+}, 'open() and close() set and unset SerialPort.writable');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await port.open({baudRate: 9600});
+ assert_true(port.writable instanceof WritableStream);
+
+ const writer = port.writable.getWriter();
+ await promise_rejects_js(t, TypeError, port.close());
+
+ writer.releaseLock();
+ await port.close();
+ assert_equals(port.writable, null);
+}, 'Port cannot be closed while writable is locked');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await port.open({baudRate: 9600});
+ assert_true(port.writable instanceof WritableStream);
+
+ const writer = port.writable.getWriter();
+ const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ let writePromise = writer.write(data);
+ writer.releaseLock();
+
+ await fakePort.readable();
+ let {value, done} = await fakePort.read();
+ await writePromise;
+ compareArrays(value, data);
+
+ await port.close();
+ assert_equals(port.writable, null);
+}, 'Can write a small amount of data');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ // Select a buffer size smaller than the amount of data transferred.
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const writer = port.writable.getWriter();
+ const data = new Uint8Array(1024); // Much larger than bufferSize above.
+ for (let i = 0; i < data.byteLength; ++i)
+ data[i] = i & 0xff;
+ writer.write(data);
+ writer.releaseLock();
+
+ await fakePort.readable();
+ const value = await fakePort.readWithLength(data.byteLength);
+ compareArrays(data, value);
+
+ await port.close();
+}, 'Can write a large amount of data');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.open({baudRate: 9600});
+
+ const writable = port.writable;
+ assert_true(writable instanceof WritableStream);
+ let writer = writable.getWriter();
+
+ await fakePort.readable();
+ fakePort.simulateSystemErrorOnWrite();
+ const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ await promise_rejects_dom(t, 'UnknownError', writer.write(data));
+
+ assert_true(port.writable instanceof WritableStream);
+ assert_not_equals(port.writable, writable);
+
+ writer = port.writable.getWriter();
+ let writePromise = writer.write(data);
+ writer.releaseLock();
+ await fakePort.readable();
+ let {value, done} = await fakePort.read();
+ await writePromise;
+ compareArrays(value, data);
+
+ await port.close();
+ assert_equals(port.writable, null);
+}, 'System error closes writable and replaces it with a new stream');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.open({baudRate: 9600});
+
+ assert_true(port.writable instanceof WritableStream);
+ const writer = port.writable.getWriter();
+
+ await fakePort.readable();
+ fakePort.simulateDisconnectOnWrite();
+ const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ await promise_rejects_dom(t, 'NetworkError', writer.write(data));
+ assert_equals(port.writable, null);
+
+ await port.close();
+}, 'Disconnect error closes writable and sets it to null');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await port.open({baudRate: 9600, bufferSize: 64});
+ const originalWritable = port.writable;
+ assert_true(originalWritable instanceof WritableStream);
+
+ let writer = originalWritable.getWriter();
+ let data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ // The buffer size is large enough to allow this write to complete without
+ // the data being read from the fake port.
+ await writer.write(data);
+ await writer.abort();
+
+ assert_true(port.writable instanceof WritableStream);
+ assert_true(port.writable !== originalWritable);
+ writer = port.writable.getWriter();
+ data = new Uint8Array([9, 10, 11, 12, 13, 14, 15, 16]);
+ const writePromise = writer.write(data);
+ writer.releaseLock();
+
+ await fakePort.readable();
+ const {value, done} = await fakePort.read();
+ await writePromise;
+ compareArrays(value, data);
+
+ await port.close();
+ assert_equals(port.writable, null);
+}, 'abort() discards the write buffer');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ // Select a buffer size smaller than the amount of data transferred.
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const writer = port.writable.getWriter();
+ // Wait for microtasks to execute in order to ensure that the WritableStream
+ // has been set up completely.
+ await Promise.resolve();
+
+ const data = new Uint8Array(1024); // Much larger than bufferSize above.
+ for (let i = 0; i < data.byteLength; ++i)
+ data[i] = i & 0xff;
+ const writePromise =
+ promise_rejects_exactly(t, 'Aborting.', writer.write(data));
+
+ await writer.abort('Aborting.');
+ await writePromise;
+ await port.close();
+ assert_equals(port.writable, null);
+}, 'abort() does not wait for the write buffer to be cleared');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ // Select a buffer size smaller than the amount of data transferred.
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const writer = port.writable.getWriter();
+ const data = new Uint8Array(1024); // Much larger than bufferSize above.
+ for (let i = 0; i < data.byteLength; ++i)
+ data[i] = i & 0xff;
+ const closed = (async () => {
+ await promise_rejects_exactly(t, 'Aborting.', writer.write(data));
+ writer.releaseLock();
+ await port.close();
+ assert_equals(port.writable, null);
+ })();
+
+ await writer.abort('Aborting.');
+ await closed;
+}, 'Can close while aborting');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ // Select a buffer size smaller than the amount of data transferred.
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const writer = port.writable.getWriter();
+ const data = new Uint8Array(1024); // Much larger than bufferSize above.
+ for (let i = 0; i < data.byteLength; ++i)
+ data[i] = i & 0xff;
+ writer.write(data);
+
+ let readComplete = false;
+ let writePromise = writer.close().then(() => {
+ assert_true(readComplete);
+ });
+
+ await fakePort.readable();
+ let readPromise = fakePort.readWithLength(data.byteLength).then(result => {
+ readComplete = true;
+ return result;
+ });
+ const value = await readPromise;
+ compareArrays(data, value);
+ await writePromise;
+
+ await port.close();
+}, 'close() waits for the write buffer to be cleared');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ // Select a buffer size smaller than the amount of data transferred.
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const writer = port.writable.getWriter();
+ // Wait for microtasks to execute in order to ensure that the WritableStream
+ // has been set up completely.
+ await Promise.resolve();
+
+ const data = new Uint8Array(1024); // Much larger than bufferSize above.
+ for (let i = 0; i < data.byteLength; ++i)
+ data[i] = i & 0xff;
+ const writePromise =
+ promise_rejects_exactly(t, 'Aborting.', writer.write(data));
+ const closePromise = promise_rejects_exactly(t, 'Aborting.', writer.close());
+
+ await writer.abort('Aborting.');
+ await writePromise;
+ await closePromise;
+ await port.close();
+ assert_equals(port.writable, null);
+}, 'Can abort while closing');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await port.open({baudRate: 9600});
+ assert_true(port.writable instanceof WritableStream);
+
+ const encoder = new TextEncoderStream();
+ const streamClosed = encoder.readable.pipeTo(port.writable);
+ const writer = encoder.writable.getWriter();
+ const writePromise = writer.write('Hello world!');
+
+ await fakePort.readable();
+ const {value, done} = await fakePort.read();
+ await writePromise;
+ assert_equals('Hello world!', new TextDecoder().decode(value));
+ await writer.close();
+ await streamClosed;
+
+ await port.close();
+ assert_equals(port.writable, null);
+}, 'Can pipe a stream to writable');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+
+ await port.open({baudRate: 9600});
+ assert_true(port.writable instanceof WritableStream);
+
+ const transform = new TransformStream();
+ const readable = transform.readable.pipeThrough(new TextEncoderStream())
+ .pipeThrough(new TransformStream())
+ .pipeThrough(new TransformStream())
+ .pipeThrough(new TransformStream());
+ const streamClosed = readable.pipeTo(port.writable);
+ const writer = transform.writable.getWriter();
+ const writePromise = writer.write('Hello world!');
+
+ await fakePort.readable();
+ const {value, done} = await fakePort.read();
+ await writePromise;
+ assert_equals('Hello world!', new TextDecoder().decode(value));
+ await writer.close();
+ await streamClosed;
+
+ await port.close();
+ assert_equals(port.writable, null);
+}, 'Stream closure is observable through a long chain of transformers');
diff --git a/testing/web-platform/tests/serial/serialPort_writable_detachBuffer.https.any.js b/testing/web-platform/tests/serial/serialPort_writable_detachBuffer.https.any.js
new file mode 100644
index 0000000000..828e877726
--- /dev/null
+++ b/testing/web-platform/tests/serial/serialPort_writable_detachBuffer.https.any.js
@@ -0,0 +1,48 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+function detachBuffer(buffer) {
+ const channel = new MessageChannel();
+ channel.port1.postMessage('', [buffer]);
+}
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ const writer = port.writable.getWriter();
+ const data = new Uint8Array(64);
+ detachBuffer(data.buffer);
+
+ // Writing a detached buffer is equivalent to writing an empty buffer so this
+ // should trivially succeed.
+ await writer.write(data);
+ writer.releaseLock();
+
+ await port.close();
+}, 'Writing a detached buffer is safe');
+
+serial_test(async (t, fake) => {
+ const {port, fakePort} = await getFakeSerialPort(fake);
+ // Select a buffer size smaller than the amount of data transferred.
+ await port.open({baudRate: 9600, bufferSize: 64});
+
+ // Start writing a buffer much larger than bufferSize above so that it can't
+ // all be transfered in a single operation.
+ const writer = port.writable.getWriter();
+ const data = new Uint8Array(1024);
+ const promise = writer.write(data);
+ writer.releaseLock();
+
+ // Read half of the written data and then detach the buffer.
+ await fakePort.readable();
+ await fakePort.readWithLength(data.byteLength / 2);
+ detachBuffer(data.buffer);
+
+ // When the buffer is detached its length becomes zero and so the write should
+ // succeed but it is undefined how much data was written before that happened.
+ await promise;
+
+ await port.close();
+}, 'Detaching a buffer while writing is safe');
diff --git a/testing/web-platform/tests/serial/serial_getPorts.https.any.js b/testing/web-platform/tests/serial/serial_getPorts.https.any.js
new file mode 100644
index 0000000000..03ab5f7e26
--- /dev/null
+++ b/testing/web-platform/tests/serial/serial_getPorts.https.any.js
@@ -0,0 +1,25 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ fake.addPort();
+ fake.addPort();
+
+ let ports = await navigator.serial.getPorts();
+ assert_equals(ports.length, 2);
+ assert_true(ports[0] instanceof SerialPort);
+ assert_true(ports[1] instanceof SerialPort);
+}, 'getPorts() returns the set of configured fake ports');
+
+serial_test(async (t, fake) => {
+ fake.addPort();
+
+ let portsFirst = await navigator.serial.getPorts();
+ assert_equals(portsFirst.length, 1, 'first call returns one port');
+ assert_true(portsFirst[0] instanceof SerialPort);
+ let portsSecond = await navigator.serial.getPorts();
+ assert_equals(portsSecond.length, 1, 'second call returns one port');
+ assert_true(portsSecond[0] instanceof SerialPort);
+ assert_true(portsFirst[0] === portsSecond[0]);
+}, 'getPorts() returns the same port objects every time');
diff --git a/testing/web-platform/tests/serial/serial_onconnect.https.any.js b/testing/web-platform/tests/serial/serial_onconnect.https.any.js
new file mode 100644
index 0000000000..e2ff9a0f0a
--- /dev/null
+++ b/testing/web-platform/tests/serial/serial_onconnect.https.any.js
@@ -0,0 +1,29 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const eventWatcher =
+ new EventWatcher(t, navigator.serial, ['connect', 'disconnect']);
+
+ // Wait for getPorts() to resolve in order to ensure that the Mojo client
+ // interface has been configured.
+ let ports = await navigator.serial.getPorts();
+ assert_equals(ports.length, 0);
+
+ fake.addPort();
+ const event1 = await eventWatcher.wait_for(['connect']);
+ assert_true(event1 instanceof Event);
+ assert_true(event1.target instanceof SerialPort);
+
+ fake.addPort();
+ const event2 = await eventWatcher.wait_for(['connect']);
+ assert_true(event2 instanceof Event);
+ assert_true(event2.target instanceof SerialPort);
+ assert_not_equals(event1.target, event2.target);
+
+ ports = await navigator.serial.getPorts();
+ assert_equals(ports.length, 2);
+ assert_in_array(event1.target, ports);
+ assert_in_array(event2.target, ports);
+}, 'A "connect" event is fired when ports are added.');
diff --git a/testing/web-platform/tests/serial/serial_ondisconnect.https.any.js b/testing/web-platform/tests/serial/serial_ondisconnect.https.any.js
new file mode 100644
index 0000000000..583a9ea52c
--- /dev/null
+++ b/testing/web-platform/tests/serial/serial_ondisconnect.https.any.js
@@ -0,0 +1,37 @@
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test(async (t, fake) => {
+ const eventWatcher =
+ new EventWatcher(t, navigator.serial, ['connect', 'disconnect']);
+
+ // Wait for getPorts() to resolve in order to ensure that the Mojo client
+ // interface has been configured.
+ let ports = await navigator.serial.getPorts();
+ assert_equals(ports.length, 0);
+
+ // Add ports one at a time so that we can map tokens to ports.
+ const token1 = fake.addPort();
+ const port1 = (await eventWatcher.wait_for(['connect'])).target;
+
+ const token2 = fake.addPort();
+ const port2 = (await eventWatcher.wait_for(['connect'])).target;
+
+ fake.removePort(token2);
+ const event1 = await eventWatcher.wait_for(['disconnect']);
+ assert_true(event1 instanceof Event);
+ assert_equals(event1.target, port2);
+
+ ports = await navigator.serial.getPorts();
+ assert_equals(ports.length, 1);
+ assert_equals(ports[0], port1);
+
+ fake.removePort(token1);
+ const event2 = await eventWatcher.wait_for(['disconnect']);
+ assert_true(event2 instanceof Event);
+ assert_equals(event2.target, port1);
+
+ ports = await navigator.serial.getPorts();
+ assert_equals(ports.length, 0);
+}, 'A "disconnect" event is fired when ports are added.');
diff --git a/testing/web-platform/tests/serial/serial_requestPort.https.window.js b/testing/web-platform/tests/serial/serial_requestPort.https.window.js
new file mode 100644
index 0000000000..4edcfa6a46
--- /dev/null
+++ b/testing/web-platform/tests/serial/serial_requestPort.https.window.js
@@ -0,0 +1,66 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/resources/test-only-api.js
+// META: script=/serial/resources/common.js
+// META: script=resources/automation.js
+
+serial_test((t, fake) => {
+ return promise_rejects_dom(
+ t, 'SecurityError', navigator.serial.requestPort());
+}, 'requestPort() rejects without a user gesture');
+
+serial_test(async (t, fake) => {
+ await trustedClick();
+ return promise_rejects_dom(
+ t, 'NotFoundError', navigator.serial.requestPort());
+}, 'requestPort() rejects if no port has been selected');
+
+serial_test(async (t, fake) => {
+ let token = fake.addPort();
+ fake.setSelectedPort(token);
+
+ await trustedClick();
+ let port = await navigator.serial.requestPort();
+ assert_true(port instanceof SerialPort);
+}, 'requestPort() returns the selected port');
+
+serial_test(async (t, fake) => {
+ let token = fake.addPort();
+ fake.setSelectedPort(token);
+
+ await trustedClick();
+ let firstPort = await navigator.serial.requestPort();
+ assert_true(firstPort instanceof SerialPort);
+ let secondPort = await navigator.serial.requestPort();
+ assert_true(secondPort instanceof SerialPort);
+ assert_true(firstPort === secondPort);
+}, 'requestPort() returns the same port object every time');
+
+serial_test(async (t, fake) => {
+ let token = fake.addPort();
+ fake.setSelectedPort(token);
+
+ await trustedClick();
+ let port = await navigator.serial.requestPort({filters: []});
+ assert_true(port instanceof SerialPort);
+}, 'An empty list of filters is valid');
+
+serial_test(async (t, fake) => {
+ let token = fake.addPort();
+ fake.setSelectedPort(token);
+
+ await trustedClick();
+ return promise_rejects_js(t, TypeError, navigator.serial.requestPort({
+ filters: [{}],
+ }));
+}, 'An empty filter is not valid');
+
+serial_test(async (t, fake) => {
+ let token = fake.addPort();
+ fake.setSelectedPort(token);
+
+ await trustedClick();
+ return promise_rejects_js(t, TypeError, navigator.serial.requestPort({
+ filters: [{usbProductId: 0x0001}],
+ }));
+}, 'requestPort() requires a USB vendor ID if a product ID specified');