serial: Export manual tests into Web Platform Tests

These tests don't have any automation dependencies (since they are
manual) so can be exported first.

Bug: 884928
Change-Id: I6a3b9240d7ce8d61bfea43e5445d1334725bbd64
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2572745
Commit-Queue: Reilly Grant <reillyg@chromium.org>
Commit-Queue: Joshua Bell <jsbell@chromium.org>
Auto-Submit: Reilly Grant <reillyg@chromium.org>
Reviewed-by: Joshua Bell <jsbell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#833542}
diff --git a/serial/resources/common.js b/serial/resources/common.js
new file mode 100644
index 0000000..5177f83
--- /dev/null
+++ b/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/serial/resources/manual.js b/serial/resources/manual.js
new file mode 100644
index 0000000..4ac46b6
--- /dev/null
+++ b/serial/resources/manual.js
@@ -0,0 +1,38 @@
+let manualTestPort = null;
+
+navigator.serial.addEventListener('disconnect', (e) => {
+  if (e.port === 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_loopback_serial_test(func, name, properties) {
+  promise_test(async (test) => {
+    await func(test, await getPortForManualTest());
+  }, name, properties);
+}
diff --git a/serial/serialPort_disconnect-manual.https.html b/serial/serialPort_disconnect-manual.https.html
new file mode 100644
index 0000000..3a2e134
--- /dev/null
+++ b/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_loopback_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_loopback_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/serial/serialPort_loopback-manual.https.html b/serial/serialPort_loopback-manual.https.html
new file mode 100644
index 0000000..9e7801d
--- /dev/null
+++ b/serial/serialPort_loopback-manual.https.html
@@ -0,0 +1,181 @@
+<!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_loopback_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_loopback_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_loopback_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.');
+
+      manual_loopback_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.');
+
+      manual_loopback_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>