summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webtransport
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /testing/web-platform/tests/webtransport
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/webtransport')
-rw-r--r--testing/web-platform/tests/webtransport/META.yml5
-rw-r--r--testing/web-platform/tests/webtransport/README.md14
-rw-r--r--testing/web-platform/tests/webtransport/bidirectional-cancel-crash.https.html24
-rw-r--r--testing/web-platform/tests/webtransport/close.https.any.js153
-rw-r--r--testing/web-platform/tests/webtransport/connect.https.any.js89
-rw-r--r--testing/web-platform/tests/webtransport/constructor.https.any.js57
-rw-r--r--testing/web-platform/tests/webtransport/csp-fail.https.window.js27
-rw-r--r--testing/web-platform/tests/webtransport/csp-pass.https.window.js18
-rw-r--r--testing/web-platform/tests/webtransport/datagram-cancel-crash.https.window.js12
-rw-r--r--testing/web-platform/tests/webtransport/datagrams.https.any.js377
-rw-r--r--testing/web-platform/tests/webtransport/echo-large-bidirectional-streams.https.html27
-rw-r--r--testing/web-platform/tests/webtransport/handlers/abort-stream-from-server.py27
-rw-r--r--testing/web-platform/tests/webtransport/handlers/client-close.py59
-rw-r--r--testing/web-platform/tests/webtransport/handlers/custom-response.py14
-rw-r--r--testing/web-platform/tests/webtransport/handlers/echo-request-headers.py11
-rw-r--r--testing/web-platform/tests/webtransport/handlers/echo.py33
-rw-r--r--testing/web-platform/tests/webtransport/handlers/echo_datagram_length.py8
-rw-r--r--testing/web-platform/tests/webtransport/handlers/query.py19
-rw-r--r--testing/web-platform/tests/webtransport/handlers/server-close.py16
-rw-r--r--testing/web-platform/tests/webtransport/handlers/server-connection-close.py9
-rw-r--r--testing/web-platform/tests/webtransport/idlharness.https.any.js24
-rw-r--r--testing/web-platform/tests/webtransport/in-removed-iframe.https.html25
-rw-r--r--testing/web-platform/tests/webtransport/resources/webtransport-test-helpers.sub.js112
-rw-r--r--testing/web-platform/tests/webtransport/server-certificate-hashes.https.any.js21
-rw-r--r--testing/web-platform/tests/webtransport/streams-close.https.any.js252
-rw-r--r--testing/web-platform/tests/webtransport/streams-echo.https.any.js282
26 files changed, 1715 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webtransport/META.yml b/testing/web-platform/tests/webtransport/META.yml
new file mode 100644
index 0000000000..3499931112
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/META.yml
@@ -0,0 +1,5 @@
+spec: https://w3c.github.io/webtransport/
+suggested_reviewers:
+ - bashi
+ - nidhijaju
+ - yutakahirano
diff --git a/testing/web-platform/tests/webtransport/README.md b/testing/web-platform/tests/webtransport/README.md
new file mode 100644
index 0000000000..543d876e01
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/README.md
@@ -0,0 +1,14 @@
+# Notes about WebTransport WPTs
+
+### Running the WebTransport WPTs using the test server
+Although these tests do not currently run on the Chromium CI, they can still be
+run using the WebTransport WPT server manually.
+
+Please refer to the following document for detailed instructions:
+[Running WPT WebTransport test server](https://docs.google.com/document/d/1OBoZTcC9vDoLTgv_5WUznRFrmwXP0Gprj7V9oOzH9cU/edit?usp=sharing)
+
+
+### Server Handlers
+The python server handlers are stored under `handlers/` and are written using the
+PEP8 style.
+For details, please see the [style guide](https://www.python.org/dev/peps/pep-0008/).
diff --git a/testing/web-platform/tests/webtransport/bidirectional-cancel-crash.https.html b/testing/web-platform/tests/webtransport/bidirectional-cancel-crash.https.html
new file mode 100644
index 0000000000..6919c90aa8
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/bidirectional-cancel-crash.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<html class="test-wait">
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/webtransport-test-helpers.sub.js"></script>
+<script type="module">
+ const WT_CODE = 127;
+ const HTTP_CODE = webtransport_code_to_http_code(WT_CODE);
+ const wt = new WebTransport(
+ webtransport_url(`abort-stream-from-server.py?code=${HTTP_CODE}`));
+ await wt.ready;
+
+ const bidi = await wt.createBidirectionalStream();
+ const writer = bidi.writable.getWriter();
+
+ const reader = bidi.readable.getReader();
+ reader.read();
+
+ // Write something, to make the stream visible to the server side.
+ await writer.write(new Uint8Array([64]));
+
+ const e = await reader.closed.catch(e => e);
+ document.documentElement.classList.remove("test-wait");
+</script>
diff --git a/testing/web-platform/tests/webtransport/close.https.any.js b/testing/web-platform/tests/webtransport/close.https.any.js
new file mode 100644
index 0000000000..de675036f3
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/close.https.any.js
@@ -0,0 +1,153 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/webtransport-test-helpers.sub.js
+// META: script=/common/utils.js
+// META: timeout=long
+
+promise_test(async t => {
+ const id = token();
+ const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`));
+ add_completion_callback(() => wt.close());
+ await wt.ready;
+
+ wt.close();
+
+ const close_info = await wt.closed;
+
+ assert_equals(close_info.closeCode, 0, 'code');
+ assert_equals(close_info.reason, '', 'reason');
+
+ await wait(10);
+ const data = await query(id);
+
+ assert_own_property(data, 'session-close-info');
+ const info = data['session-close-info']
+
+ assert_false(info.abruptly, 'abruptly');
+ assert_equals(info.close_info.code, 0, 'code');
+ assert_equals(info.close_info.reason, '', 'reason');
+}, 'close');
+
+promise_test(async t => {
+ const id = token();
+ const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`));
+ add_completion_callback(() => wt.close());
+ await wt.ready;
+
+ wt.close({closeCode: 99, reason: 'reason X'});
+
+ const close_info = await wt.closed;
+
+ assert_equals(close_info.closeCode, 99, 'code');
+ assert_equals(close_info.reason, 'reason X', 'reason');
+
+ await wait(10);
+ const data = await query(id);
+
+ assert_own_property(data, 'session-close-info');
+ const info = data['session-close-info']
+
+ assert_false(info.abruptly, 'abruptly');
+ assert_equals(info.close_info.code, 99, 'code');
+ assert_equals(info.close_info.reason, 'reason X', 'reason');
+}, 'close with code and reason');
+
+promise_test(async t => {
+ const id = token();
+ const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`));
+ add_completion_callback(() => wt.close());
+ await wt.ready;
+ const reason = 'あいうえお'.repeat(1000);
+
+ wt.close({closeCode: 11, reason});
+
+ const close_info = await wt.closed;
+
+ assert_equals(close_info.closeCode, 11, 'code');
+ // `close_info.reason` should report the original, non-truncated reason as
+ // step 9 of https://w3c.github.io/webtransport/#dom-webtransport-close
+ // uses the original `closeInfo` to perform `Cleanup`.
+ assert_equals(close_info.reason, reason, 'reason');
+
+ await wait(10);
+ const data = await query(id);
+
+ assert_own_property(data, 'session-close-info');
+ const info = data['session-close-info']
+
+ // Server should have received truncated reason as step 6 of
+ // https://w3c.github.io/webtransport/#dom-webtransport-close specifies.
+ const expected_reason =
+ new TextDecoder().decode(
+ new TextEncoder().encode(reason).slice(0, 1024)).replaceAll('\ufffd', '');
+ assert_false(info.abruptly, 'abruptly');
+ assert_equals(info.close_info.code, 11, 'code');
+ assert_equals(info.close_info.reason, expected_reason, 'reason');
+}, 'close with code and long reason');
+
+promise_test(async t => {
+ const wt = new WebTransport(webtransport_url('server-close.py'));
+
+ const close_info = await wt.closed;
+ assert_equals(close_info.closeCode, 0, 'code');
+ assert_equals(close_info.reason, '', 'reason');
+}, 'server initiated closure without code and reason');
+
+promise_test(async t => {
+ const code = 32;
+ const reason = 'abc';
+ const wt = new WebTransport(
+ webtransport_url(`server-close.py?code=${code}&reason=${reason}`));
+ add_completion_callback(() => wt.close());
+
+ const close_info = await wt.closed;
+ assert_equals(close_info.closeCode, code, 'code');
+ assert_equals(close_info.reason, reason, 'reason');
+}, 'server initiated closure with code and reason');
+
+promise_test(async t => {
+ const wt = new WebTransport(webtransport_url('server-connection-close.py'));
+ add_completion_callback(() => wt.close());
+
+ const streams_reader = wt.incomingBidirectionalStreams.getReader();
+ const { value: bidi } = await streams_reader.read();
+ const writer = bidi.writable.getWriter();
+ const reader = bidi.readable.getReader();
+ try {
+ writer.write(new Uint8Array([65]));
+ } catch (e) {
+ }
+
+ // Sadly we cannot use promise_rejects_dom as the error constructor is
+ // WebTransportError rather than DOMException.
+ // We get a possible error, and then make sure wt.closed is rejected with it.
+ const e = await wt.closed.catch(e => e);
+ await promise_rejects_exactly(t, e, wt.closed, 'wt.closed');
+ await promise_rejects_exactly(t, e, writer.closed, 'writer.closed');
+ await promise_rejects_exactly(t, e, reader.closed, 'reader.closed');
+ assert_true(e instanceof WebTransportError);
+ assert_equals(e.source, 'session', 'source');
+ assert_equals(e.streamErrorCode, null, 'streamErrorCode');
+}, 'server initiated connection closure');
+
+promise_test(async t => {
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ const stream = await wt.createUnidirectionalStream();
+ await wt.ready;
+}, 'opening unidirectional stream before ready');
+
+promise_test(async t => {
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ const stream = await wt.createBidirectionalStream();
+ await wt.ready;
+}, 'opening bidirectional stream before ready');
+
+promise_test(async t => {
+ const wt = new WebTransport(webtransport_url('server-close.py'));
+ promise_rejects_dom(t, "InvalidStateError", wt.createUnidirectionalStream());
+}, 'server initiated closure while opening unidirectional stream before ready');
+
+promise_test(async t => {
+ const wt = new WebTransport(webtransport_url('server-close.py'));
+ promise_rejects_dom(t, "InvalidStateError", wt.createBidirectionalStream());
+}, 'server initiated closure while opening bidirectional stream before ready');
diff --git a/testing/web-platform/tests/webtransport/connect.https.any.js b/testing/web-platform/tests/webtransport/connect.https.any.js
new file mode 100644
index 0000000000..6bd18ae8de
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/connect.https.any.js
@@ -0,0 +1,89 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/webtransport-test-helpers.sub.js
+// META: timeout=long
+
+promise_test(async t => {
+ const wt = new WebTransport(webtransport_url('custom-response.py?:status=200'));
+ await wt.ready;
+}, 'WebTransport session is established with status code 200');
+
+promise_test(async t => {
+ const wt = new WebTransport(webtransport_url('custom-response.py?:status=204'));
+ await wt.ready;
+}, 'WebTransport session is established with status code 204');
+
+promise_test(async t => {
+ const wt = new WebTransport(webtransport_url('custom-response.py?:status=301'));
+ // Sadly we cannot use promise_rejects_dom as the error constructor is
+ // WebTransportError rather than DOMException. Ditto below.
+ // We get a possible error, and then make sure wt.closed is rejected with it.
+ const e = await wt.ready.catch(e => e);
+ await promise_rejects_exactly(t, e, wt.closed, 'closed promise should be rejected');
+ await promise_rejects_exactly(t, e, wt.ready, 'ready promise shoud be rejected');
+ assert_true(e instanceof WebTransportError);
+ assert_equals(e.source, 'session', 'source');
+ assert_equals(e.streamErrorCode, null, 'streamErrorCode');
+}, 'WebTransport session establishment fails with status code 301');
+
+promise_test(async t => {
+ const wt = new WebTransport(webtransport_url('custom-response.py?:status=401'));
+ const e = await wt.ready.catch(e => e);
+ await promise_rejects_exactly(t, e, wt.closed, 'closed promise should be rejected');
+ await promise_rejects_exactly(t, e, wt.ready, 'ready promise shoud be rejected');
+ assert_true(e instanceof WebTransportError);
+ assert_equals(e.source, 'session', 'source');
+ assert_equals(e.streamErrorCode, null, 'streamErrorCode');
+}, 'WebTransport session establishment with status code 401');
+
+promise_test(async t => {
+ const wt = new WebTransport(webtransport_url('custom-response.py?:status=404'));
+ const e = await wt.ready.catch(e => e);
+ await promise_rejects_exactly(t, e, wt.closed, 'closed promise should be rejected');
+ await promise_rejects_exactly(t, e, wt.ready, 'ready promise shoud be rejected');
+ assert_true(e instanceof WebTransportError);
+ assert_equals(e.source, 'session', 'source');
+ assert_equals(e.streamErrorCode, null, 'streamErrorCode');
+}, 'WebTransport session establishment fails with status code 404');
+
+promise_test(async t => {
+ // Create WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo-request-headers.py'));
+ await wt.ready;
+
+ // Read incoming unidirectional stream for echoed request headers.
+ const streams = await wt.incomingUnidirectionalStreams;
+
+ const stream_reader = streams.getReader();
+ const { value: recv_stream } = await stream_reader.read();
+ stream_reader.releaseLock();
+
+ const request_headers = await read_stream_as_json(recv_stream);
+
+ // Check the standard request headers.
+ check_and_remove_standard_headers(request_headers);
+}, 'Echo back request headers');
+
+promise_test(async t => {
+ // Create WebTransport session, and attach "Set-Cookie: foo=bar" to the response of
+ // the handshake.
+ const encodedSetCookie = encodeURIComponent('foo=bar');
+ let wt = new WebTransport(webtransport_url('custom-response.py?set-cookie=' + encodedSetCookie));
+ await wt.ready;
+
+ wt = new WebTransport(webtransport_url('echo-request-headers.py'));
+ await wt.ready;
+
+ // Read incoming unidirectional stream for echoed request headers.
+ const streams = await wt.incomingUnidirectionalStreams;
+
+ const stream_reader = streams.getReader();
+ const { value: recv_stream } = await stream_reader.read();
+ stream_reader.releaseLock();
+
+ const request_headers = await read_stream_as_json(recv_stream);
+
+ // Check cookie header is not echoed back.
+ check_and_remove_standard_headers(request_headers);
+ assert_equals(request_headers['cookie'], undefined);
+}, 'Cookie header is not echoed back');
diff --git a/testing/web-platform/tests/webtransport/constructor.https.any.js b/testing/web-platform/tests/webtransport/constructor.https.any.js
new file mode 100644
index 0000000000..0f39c4993b
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/constructor.https.any.js
@@ -0,0 +1,57 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/webtransport-test-helpers.sub.js
+// META: script=/common/utils.js
+
+const BAD_URLS = [
+ null,
+ '',
+ 'no-scheme',
+ 'http://example.com/' /* scheme is wrong */,
+ 'quic-transport://example.com/' /* scheme is wrong */,
+ 'https:///' /* no host specified */,
+ 'https://example.com/#failing' /* has fragment */,
+ `https://${HOST}:999999/` /* invalid port */,
+];
+
+for (const url of BAD_URLS) {
+ test(() => {
+ assert_throws_dom('SyntaxError', () => new WebTransport(url),
+ 'constructor should throw');
+ }, `WebTransport constructor should reject URL '${url}'`);
+}
+
+const OPTIONS = [
+ { allowPooling: true },
+ { requireUnreliable: true },
+ { allowPooling: true, requireUnreliable: true },
+ { congestionControl: "default" },
+ { congestionControl: "throughput" },
+ { congestionControl: "low-latency" },
+ { allowPooling: true, requireUnreliable: true, congestionControl: "low-latency" },
+ // XXX Need to test serverCertificateHashes
+];
+
+for (const options of OPTIONS) {
+ promise_test(async t => {
+ const id = token();
+ const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`), options );
+ await wt.ready;
+ wt.close();
+ }, "WebTransport constructor should allow options " + JSON.stringify(options));
+}
+
+promise_test(async t => {
+ const wt = new WebTransport(`https://${HOST}:0/`);
+
+ // Sadly we cannot use promise_rejects_dom as the error constructor is
+ // WebTransportError rather than DOMException.
+ // We get a possible error, and then make sure wt.ready is rejected with it.
+ const e = await wt.ready.catch(e => e);
+
+ await promise_rejects_exactly(t, e, wt.ready, 'ready should be rejected');
+ await promise_rejects_exactly(t, e, wt.closed, 'closed should be rejected');
+ assert_true(e instanceof WebTransportError);
+ assert_equals(e.source, 'session', 'source');
+ assert_equals(e.streamErrorCode, null, 'streamErrorCode');
+}, 'Connection to port 0 should fail');
diff --git a/testing/web-platform/tests/webtransport/csp-fail.https.window.js b/testing/web-platform/tests/webtransport/csp-fail.https.window.js
new file mode 100644
index 0000000000..f2e49fad99
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/csp-fail.https.window.js
@@ -0,0 +1,27 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/webtransport-test-helpers.sub.js
+
+function set_csp(destination) {
+ let meta = document.createElement('meta');
+ meta.httpEquiv = 'Content-Security-Policy';
+ meta.content = `connect-src ${destination}`;
+ return meta;
+}
+
+promise_test(async t => {
+ let meta = set_csp("'none'");
+ document.head.appendChild(meta);
+
+ let wt = new WebTransport(webtransport_url('custom-response.py?:status=200'));
+
+ // Sadly we cannot use promise_rejects_dom as the error constructor is
+ // WebTransportError rather than DOMException.
+ const e = await wt.ready.catch(e => e);
+ await promise_rejects_exactly(t, e, wt.ready, 'ready promise should be rejected');
+ await promise_rejects_exactly(t, e, wt.closed, 'closed promise should be rejected');
+ assert_equals(e.name, 'WebTransportError', 'WebTransportError');
+ assert_true(e instanceof WebTransportError);
+ assert_equals(e.source, 'session', 'source');
+ assert_equals(e.streamErrorCode, null, 'streamErrorCode');
+}, 'WebTransport connection should fail when CSP connect-src is set to none and reject the promises');
diff --git a/testing/web-platform/tests/webtransport/csp-pass.https.window.js b/testing/web-platform/tests/webtransport/csp-pass.https.window.js
new file mode 100644
index 0000000000..3c315d078f
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/csp-pass.https.window.js
@@ -0,0 +1,18 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/webtransport-test-helpers.sub.js
+
+function set_csp(destination) {
+ let meta = document.createElement('meta');
+ meta.httpEquiv = 'Content-Security-Policy';
+ meta.content = `connect-src ${destination}`;
+ return meta;
+}
+
+promise_test(async t => {
+ let meta = set_csp(`${BASE}`);
+ document.head.appendChild(meta);
+
+ let wt = new WebTransport(webtransport_url('custom-response.py?:status=200'));
+ await wt.ready;
+}, 'WebTransport connection should succeed when CSP connect-src destination is set to the page');
diff --git a/testing/web-platform/tests/webtransport/datagram-cancel-crash.https.window.js b/testing/web-platform/tests/webtransport/datagram-cancel-crash.https.window.js
new file mode 100644
index 0000000000..fae613e213
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/datagram-cancel-crash.https.window.js
@@ -0,0 +1,12 @@
+// This test reproduces Chromium issue https://crbug.com/1292387. If it doesn't
+// crash then the test passed.
+
+test(() => {
+ const iframeTag = document.createElement('iframe');
+ document.body.appendChild(iframeTag);
+ const wt = new iframeTag.contentWindow.WebTransport('https://example.com/');
+ iframeTag.remove();
+ const datagrams = wt.datagrams;
+ const reader = datagrams.readable;
+ reader.cancel();
+}, 'call cancel() on stream in destroyed realm');
diff --git a/testing/web-platform/tests/webtransport/datagrams.https.any.js b/testing/web-platform/tests/webtransport/datagrams.https.any.js
new file mode 100644
index 0000000000..d5b8e3bbda
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/datagrams.https.any.js
@@ -0,0 +1,377 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/webtransport-test-helpers.sub.js
+
+// Write datagrams until the producer receives the AbortSignal.
+async function write_datagrams(writer, signal) {
+ const encoder = new TextEncoder();
+ let counter = 0;
+ const sentTokens = [];
+ const aborted = new Promise((resolve) => {
+ signal.addEventListener('abort', resolve);
+ });
+ while (true) {
+ await Promise.race([writer.ready, aborted]);
+ if (signal.aborted) {
+ break;
+ }
+ var token = counter.toString();
+ sentTokens.push(token);
+ writer.write(encoder.encode(token));
+ counter++;
+ }
+ return sentTokens;
+}
+
+// Write N datagrams without waiting, then wait for them
+async function write_N_datagrams(writer, n) {
+ const encoder = new TextEncoder();
+ const sentTokens = [];
+ const promises = [];
+ while (sentTokens.length < n) {
+ const token = sentTokens.length.toString();
+ sentTokens.push(token);
+ promises.push(writer.write(encoder.encode(token)));
+ }
+ await Promise.all(promises);
+ return sentTokens;
+}
+
+// Read datagrams until the consumer has received enough i.e. N datagrams. Call
+// abort() after reading.
+async function read_datagrams(reader, controller, N) {
+ const decoder = new TextDecoder();
+ const receivedTokens = [];
+ while (receivedTokens.length < N) {
+ const { value: token, done } = await reader.read();
+ assert_false(done);
+ receivedTokens.push(decoder.decode(token));
+ }
+ controller.abort();
+ return receivedTokens;
+}
+
+// Write numbers until the producer receives the AbortSignal.
+async function write_numbers(writer, signal) {
+ let counter = 0;
+ const sentNumbers = [];
+ const aborted =
+ new Promise((resolve) => signal.addEventListener('abort', resolve));
+ // Counter should be less than 256 because reader stores numbers in Uint8Array.
+ while (counter < 256) {
+ await Promise.race([writer.ready, aborted])
+ if (signal.aborted) {
+ break;
+ }
+ sentNumbers.push(counter);
+ chunk = new Uint8Array(1);
+ chunk[0] = counter;
+ writer.write(chunk);
+ counter++;
+ }
+ return sentNumbers;
+}
+
+// Write large datagrams of size 10 until the producer receives the AbortSignal.
+async function write_large_datagrams(writer, signal) {
+ const aborted = new Promise((resolve) => {
+ signal.addEventListener('abort', resolve);
+ });
+ while (true) {
+ await Promise.race([writer.ready, aborted]);
+ if (signal.aborted) {
+ break;
+ }
+ writer.write(new Uint8Array(10));
+ }
+}
+
+// Read datagrams with BYOB reader until the consumer has received enough i.e. N
+// datagrams. Call abort() after reading.
+async function read_numbers_byob(reader, controller, N) {
+ let buffer = new ArrayBuffer(N);
+ buffer = await readInto(reader, buffer);
+ controller.abort();
+ return Array.from(new Uint8Array(buffer));
+}
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ const writer = wt.datagrams.writable.getWriter();
+ const reader = wt.datagrams.readable.getReader();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ // Write and read datagrams.
+ const N = 5;
+ const [sentTokens, receivedTokens] = await Promise.all([
+ write_datagrams(writer, signal),
+ read_datagrams(reader, controller, N)
+ ]);
+
+ // Check receivedTokens is a subset of sentTokens.
+ const subset = receivedTokens.every(token => sentTokens.includes(token));
+ assert_true(subset);
+}, 'Datagrams are echoed successfully');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ const writer = wt.datagrams.writable.getWriter();
+ const reader = wt.datagrams.readable.getReader({ mode: 'byob' });
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ // Write and read datagrams.
+ // Numbers are less than 256, consider N to be a small number.
+ const N = 5;
+ const [sentNumbers, receiveNumbers] = await Promise.all([
+ write_numbers(writer, signal),
+ read_numbers_byob(reader, controller, N)
+ ]);
+
+ // No duplicated numbers received.
+ assert_equals((new Set(receiveNumbers)).size, N);
+
+ // Check receiveNumbers is a subset of sentNumbers.
+ const subset = receiveNumbers.every(token => sentNumbers.includes(token));
+ assert_true(subset);
+}, 'Successfully reading datagrams with BYOB reader.');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ const writer = wt.datagrams.writable.getWriter();
+ const reader = wt.datagrams.readable.getReader({ mode: 'byob' });
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ // Write datagrams of size 10, but only 1 byte buffer is provided for BYOB
+ // reader. To avoid splitting a datagram, stream will be errored.
+ const buffer = new ArrayBuffer(1);
+ const [error, _] = await Promise.all([
+ reader.read(new Uint8Array(buffer)).catch(e => {
+ controller.abort();
+ return e;
+ }),
+ write_large_datagrams(writer, signal)
+ ]);
+ assert_equals(error.name, 'RangeError');
+}, 'Reading datagrams with insufficient buffer should be rejected.');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo_datagram_length.py'));
+ await wt.ready;
+
+ const writer = wt.datagrams.writable.getWriter();
+ const reader = wt.datagrams.readable.getReader();
+
+ // Write and read max-size datagram.
+ const maxDatagramSize = wt.datagrams.maxDatagramSize;
+ await writer.write(new Uint8Array(maxDatagramSize));
+
+ // the server should echo the datagram length encoded in JSON
+ const { value: token, done } = await reader.read();
+ assert_false(done);
+
+ const decoder = new TextDecoder();
+ const datagramStr = decoder.decode(token);
+ const jsonObject = JSON.parse(datagramStr);
+ assert_equals(jsonObject['length'], maxDatagramSize);
+}, 'Transfer max-size datagram');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ const writer = wt.datagrams.writable.getWriter();
+ const reader = wt.datagrams.readable.getReader();
+
+ // Write and read max-size datagram.
+ await writer.write(new Uint8Array(wt.datagrams.maxDatagramSize+1));
+ // This should resolve with no datagram sent, which is hard to test for.
+ // Wait for incoming datagrams to arrive, and if they do, fail.
+ const result = await Promise.race([reader.read(), wait(500)]);
+ assert_equals(result, undefined);
+}, 'Fail to transfer max-size+1 datagram');
+
+promise_test(async t => {
+ // Make a WebTransport connection, but session is not necessarily established.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+
+ const writer = wt.datagrams.writable.getWriter();
+ const reader = wt.datagrams.readable.getReader();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ // Write and read datagrams.
+ const N = 5;
+ wt.datagrams.outgoingHighWaterMark = N;
+ const [sentTokens, receivedTokens] = await Promise.all([
+ write_N_datagrams(writer, N),
+ read_datagrams(reader, controller, N)
+ ]);
+
+ // Check receivedTokens is a subset of sentTokens.
+ const subset = receivedTokens.every(token => sentTokens.includes(token));
+ assert_true(subset);
+
+ // Make sure WebTransport session is established.
+ await wt.ready;
+}, 'Sending and receiving datagrams is ready to use before session is established');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ const N = 5;
+ wt.datagrams.outgoingHighWaterMark = N;
+
+ const writer = wt.datagrams.writable.getWriter();
+ const encoder = new TextEncoder();
+
+ // Write N-1 datagrams.
+ let counter;
+ for (counter = 0; counter < N-1; counter++) {
+ var datagram = counter.toString();
+ let resolved = false;
+ writer.write(encoder.encode(datagram));
+
+ // Check writer.ready resolves immediately.
+ writer.ready.then(() => resolved = true);
+ // TODO(nidhijaju): The number of `await Promise.resolve()` calls is
+ // implementation dependent, so we should not have this as the final
+ // solution.
+ for (let i = 0; i < 10; i++) {
+ await Promise.resolve();
+ }
+ assert_true(resolved);
+ }
+
+ // Write one more datagram.
+ resolved = false;
+ const last_datagram = counter.toString();
+ writer.write(encoder.encode(last_datagram));
+
+ // Check writer.ready does not resolve immediately.
+ writer.ready.then(() => resolved = true);
+ for (let i = 0; i < 10; i++) {
+ await Promise.resolve();
+ }
+ assert_false(resolved);
+
+ // Make sure writer.ready is resolved eventually.
+ await writer.ready;
+}, 'Datagram\'s outgoingHighWaterMark correctly regulates written datagrams');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ const N = 5;
+ wt.datagrams.incomingHighWaterMark = N;
+
+ const writer = wt.datagrams.writable.getWriter();
+ const encoder = new TextEncoder();
+
+ // Write 10*N datagrams.
+ let counter;
+ for (counter = 0; counter < 10*N; counter++) {
+ var datagram = counter.toString();
+ writer.write(encoder.encode(datagram));
+ await writer.ready;
+ }
+
+ // Wait for incoming datagrams to arrive.
+ wait(500);
+
+ const reader = wt.datagrams.readable.getReader();
+
+ // Read all of the immediately available datagrams.
+ let receivedDatagrams = 0;
+ while (true) {
+ let resolved = false;
+ reader.read().then(() => resolved = true);
+ // TODO(nidhijaju): Find a better solution instead of just having numerous
+ // `await Promise.resolve()` calls.
+ for (let i = 0; i < 10; i++) {
+ await Promise.resolve();
+ }
+ if (!resolved) {
+ break;
+ }
+ receivedDatagrams++;
+ }
+
+ // Check that the receivedDatagrams is less than or equal to the
+ // incomingHighWaterMark.
+ assert_less_than_equal(receivedDatagrams, N);
+}, 'Datagrams read is less than or equal to the incomingHighWaterMark');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ assert_equals(wt.datagrams.incomingMaxAge, Infinity);
+ assert_equals(wt.datagrams.outgoingMaxAge, Infinity);
+
+ wt.datagrams.incomingMaxAge = 5;
+ assert_equals(wt.datagrams.incomingMaxAge, 5);
+ wt.datagrams.outgoingMaxAge = 5;
+ assert_equals(wt.datagrams.outgoingMaxAge, 5);
+
+ assert_throws_js(RangeError, () => { wt.datagrams.incomingMaxAge = -1; });
+ assert_throws_js(RangeError, () => { wt.datagrams.outgoingMaxAge = -1; });
+ assert_throws_js(RangeError, () => { wt.datagrams.incomingMaxAge = NaN; });
+ assert_throws_js(RangeError, () => { wt.datagrams.outgoingMaxAge = NaN; });
+
+ wt.datagrams.incomingMaxAge = 0;
+ assert_equals(wt.datagrams.incomingMaxAge, Infinity);
+ wt.datagrams.outgoingMaxAge = 0;
+ assert_equals(wt.datagrams.outgoingMaxAge, Infinity);
+}, 'Datagram MaxAge getters/setters work correctly');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ // Initial values are implementation-defined
+ assert_greater_than_equal(wt.datagrams.incomingHighWaterMark, 1);
+ assert_greater_than_equal(wt.datagrams.outgoingHighWaterMark, 1);
+
+ wt.datagrams.incomingHighWaterMark = 5;
+ assert_equals(wt.datagrams.incomingHighWaterMark, 5);
+ wt.datagrams.outgoingHighWaterMark = 5;
+ assert_equals(wt.datagrams.outgoingHighWaterMark, 5);
+
+ assert_throws_js(RangeError, () => { wt.datagrams.incomingHighWaterMark = -1; });
+ assert_throws_js(RangeError, () => { wt.datagrams.outgoingHighWaterMark = -1; });
+ assert_throws_js(RangeError, () => { wt.datagrams.incomingHighWaterMark = NaN; });
+ assert_throws_js(RangeError, () => { wt.datagrams.outgoingHighWaterMark = NaN; });
+
+ wt.datagrams.incomingHighWaterMark = 0.5;
+ assert_equals(wt.datagrams.incomingHighWaterMark, 1);
+ wt.datagrams.outgoingHighWaterMark = 0.5;
+ assert_equals(wt.datagrams.outgoingHighWaterMark, 1);
+ wt.datagrams.incomingHighWaterMark = 0;
+ assert_equals(wt.datagrams.incomingHighWaterMark, 1);
+ wt.datagrams.outgoingHighWaterMark = 0;
+ assert_equals(wt.datagrams.outgoingHighWaterMark, 1);
+}, 'Datagram HighWaterMark getters/setters work correctly');
diff --git a/testing/web-platform/tests/webtransport/echo-large-bidirectional-streams.https.html b/testing/web-platform/tests/webtransport/echo-large-bidirectional-streams.https.html
new file mode 100644
index 0000000000..daa7c1b9b3
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/echo-large-bidirectional-streams.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/webtransport-test-helpers.sub.js"></script>
+<script>
+// A test that aims to reproduce https://crbug.com/1369030 -- note that since
+// the bug in question is a race condition, this test will probably be flaky if
+// this is actually broken.
+promise_test(async t => {
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ const numBytes = 1024 * 1024;
+ const numStreams = 5;
+ for (let i = 0; i < numStreams; i++) {
+ const stream = await wt.createBidirectionalStream();
+ const writer = stream.writable.getWriter();
+ await writer.write(new Uint8Array(numBytes));
+ await writer.close();
+ const response = await (new Response(stream.readable).arrayBuffer());
+ assert_equals(response.byteLength, numBytes);
+ }
+}, 'Ensure large bidirectional streams does not cause race condition');
+
+</script>
diff --git a/testing/web-platform/tests/webtransport/handlers/abort-stream-from-server.py b/testing/web-platform/tests/webtransport/handlers/abort-stream-from-server.py
new file mode 100644
index 0000000000..1d92b705e6
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/handlers/abort-stream-from-server.py
@@ -0,0 +1,27 @@
+from typing import Optional
+from urllib.parse import urlsplit, parse_qsl
+
+
+def session_established(session):
+ path: Optional[bytes] = None
+ for key, value in session.request_headers:
+ if key == b':path':
+ path = value
+ assert path is not None
+ qs = dict(parse_qsl(urlsplit(path).query))
+ code = qs[b'code']
+ if code is None:
+ raise Exception('code is missing, path = {}'.format(path))
+ session.dict_for_handlers['code'] = int(code)
+
+
+def stream_data_received(session,
+ stream_id: int,
+ data: bytes,
+ stream_ended: bool):
+ code: int = session.dict_for_handlers['code']
+ if session.stream_is_unidirectional(stream_id):
+ session.stop_stream(stream_id, code)
+ else:
+ session.stop_stream(stream_id, code)
+ session.reset_stream(stream_id, code) \ No newline at end of file
diff --git a/testing/web-platform/tests/webtransport/handlers/client-close.py b/testing/web-platform/tests/webtransport/handlers/client-close.py
new file mode 100644
index 0000000000..01fb14b134
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/handlers/client-close.py
@@ -0,0 +1,59 @@
+from typing import Optional, Tuple
+from urllib.parse import urlsplit, parse_qsl
+
+
+def session_established(session):
+ path: Optional[bytes] = None
+ for key, value in session.request_headers:
+ if key == b':path':
+ path = value
+ assert path is not None
+ qs = dict(parse_qsl(urlsplit(path).query))
+ token = qs[b'token']
+ if token is None:
+ raise Exception('token is missing, path = {}'.format(path))
+ session.dict_for_handlers['token'] = token
+ session.create_bidirectional_stream()
+
+
+def stream_reset(session, stream_id: int, error_code: int) -> None:
+ token = session.dict_for_handlers['token']
+ data = session.stash.take(key=token) or {}
+
+ data['stream-close-info'] = {
+ 'source': 'reset',
+ 'code': error_code
+ }
+ session.stash.put(key=token, value=data)
+
+
+def stream_data_received(session,
+ stream_id: int,
+ data: bytes,
+ stream_ended: bool):
+ if stream_ended:
+ token = session.dict_for_handlers['token']
+ stashed_data = session.stash.take(key=token) or {}
+ stashed_data['stream-close-info'] = {
+ 'source': 'FIN',
+ }
+ session.stash.put(key=token, value=stashed_data)
+
+
+def session_closed(
+ session, close_info: Optional[Tuple[int, bytes]], abruptly: bool) -> None:
+ token = session.dict_for_handlers['token']
+ data = session.stash.take(key=token) or {}
+
+ decoded_close_info: Optional[Dict[str, Any]] = None
+ if close_info:
+ decoded_close_info = {
+ 'code': close_info[0],
+ 'reason': close_info[1].decode()
+ }
+
+ data['session-close-info'] = {
+ 'abruptly': abruptly,
+ 'close_info': decoded_close_info
+ }
+ session.stash.put(key=token, value=data)
diff --git a/testing/web-platform/tests/webtransport/handlers/custom-response.py b/testing/web-platform/tests/webtransport/handlers/custom-response.py
new file mode 100644
index 0000000000..05e4c4ba36
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/handlers/custom-response.py
@@ -0,0 +1,14 @@
+from urllib.parse import urlsplit, parse_qsl
+
+
+def connect_received(request_headers, response_headers):
+ for data in request_headers:
+ if data[0] == b':path':
+ path = data[1].decode('utf-8')
+
+ qs = dict(parse_qsl(urlsplit(path).query))
+ for key, value in qs.items():
+ response_headers.append((key.encode('utf-8'), value.encode('utf-8')))
+
+ break
+ return
diff --git a/testing/web-platform/tests/webtransport/handlers/echo-request-headers.py b/testing/web-platform/tests/webtransport/handlers/echo-request-headers.py
new file mode 100644
index 0000000000..122d6f0601
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/handlers/echo-request-headers.py
@@ -0,0 +1,11 @@
+import json
+
+
+def session_established(session):
+ headers = {}
+ for name, value in session.request_headers:
+ headers[name.decode('utf-8')] = value.decode('utf-8')
+
+ stream_id = session.create_unidirectional_stream()
+ data = json.dumps(headers).encode('utf-8')
+ session.send_stream_data(stream_id, data, end_stream=True)
diff --git a/testing/web-platform/tests/webtransport/handlers/echo.py b/testing/web-platform/tests/webtransport/handlers/echo.py
new file mode 100644
index 0000000000..4347d4aae3
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/handlers/echo.py
@@ -0,0 +1,33 @@
+streams_dict = {}
+
+
+def session_established(session):
+ # When a WebTransport session is established, a bidirectional stream is
+ # created by the server, which is used to echo back stream data from the
+ # client.
+ session.create_bidirectional_stream()
+
+
+def stream_data_received(session,
+ stream_id: int,
+ data: bytes,
+ stream_ended: bool):
+ # If a stream is unidirectional, create a new unidirectional stream and echo
+ # back the data on that stream.
+ if session.stream_is_unidirectional(stream_id):
+ if (session.session_id, stream_id) not in streams_dict.keys():
+ new_stream_id = session.create_unidirectional_stream()
+ streams_dict[(session.session_id, stream_id)] = new_stream_id
+ session.send_stream_data(streams_dict[(session.session_id, stream_id)],
+ data,
+ end_stream=stream_ended)
+ if (stream_ended):
+ del streams_dict[(session.session_id, stream_id)]
+ return
+ # Otherwise (e.g. if the stream is bidirectional), echo back the data on the
+ # same stream.
+ session.send_stream_data(stream_id, data, end_stream=stream_ended)
+
+
+def datagram_received(session, data: bytes):
+ session.send_datagram(data)
diff --git a/testing/web-platform/tests/webtransport/handlers/echo_datagram_length.py b/testing/web-platform/tests/webtransport/handlers/echo_datagram_length.py
new file mode 100644
index 0000000000..f0610b085f
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/handlers/echo_datagram_length.py
@@ -0,0 +1,8 @@
+import json
+
+def datagram_received(session, data: bytes):
+ # encode the received length into a JSON string and send back
+ data_len = len(data)
+ out_datagram_json = json.dumps({'length': data_len})
+ out_data = out_datagram_json.encode()
+ session.send_datagram(out_data)
diff --git a/testing/web-platform/tests/webtransport/handlers/query.py b/testing/web-platform/tests/webtransport/handlers/query.py
new file mode 100644
index 0000000000..75d458255a
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/handlers/query.py
@@ -0,0 +1,19 @@
+from typing import Optional
+from urllib.parse import urlsplit, parse_qsl
+import json
+
+
+def session_established(session):
+ path: Optional[bytes] = None
+ for key, value in session.request_headers:
+ if key == b':path':
+ path = value
+ assert path is not None
+ qs = dict(parse_qsl(urlsplit(path).query))
+ token = qs[b'token']
+ if token is None:
+ raise Exception('token is missing, path = {}'.format(path))
+
+ stream_id = session.create_unidirectional_stream()
+ data = json.dumps(session.stash.take(key=token) or {}).encode('utf-8')
+ session.send_stream_data(stream_id, data, end_stream=True)
diff --git a/testing/web-platform/tests/webtransport/handlers/server-close.py b/testing/web-platform/tests/webtransport/handlers/server-close.py
new file mode 100644
index 0000000000..e9d08f483c
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/handlers/server-close.py
@@ -0,0 +1,16 @@
+from typing import Optional
+from urllib.parse import urlsplit, parse_qsl
+
+
+def session_established(session):
+ path: Optional[bytes] = None
+ for key, value in session.request_headers:
+ if key == b':path':
+ path = value
+ assert path is not None
+ qs = dict(parse_qsl(urlsplit(path).query))
+ code = qs[b'code'] if b'code' in qs else None
+ reason = qs[b'reason'] if b'reason' in qs else b''
+ close_info = None if code is None else (int(code), reason)
+
+ session.close(close_info)
diff --git a/testing/web-platform/tests/webtransport/handlers/server-connection-close.py b/testing/web-platform/tests/webtransport/handlers/server-connection-close.py
new file mode 100644
index 0000000000..9721c67170
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/handlers/server-connection-close.py
@@ -0,0 +1,9 @@
+def session_established(session):
+ session.create_bidirectional_stream()
+
+
+def stream_data_received(session,
+ stream_id: int,
+ data: bytes,
+ stream_ended: bool):
+ session._http._quic.close() \ No newline at end of file
diff --git a/testing/web-platform/tests/webtransport/idlharness.https.any.js b/testing/web-platform/tests/webtransport/idlharness.https.any.js
new file mode 100644
index 0000000000..248bc41abe
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/idlharness.https.any.js
@@ -0,0 +1,24 @@
+// META: global=window,worker
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+'use strict';
+
+idl_test(
+ ['webtransport'],
+ ['webidl', 'streams'],
+ idl_array => {
+ idl_array.add_objects({
+ WebTransport: ['webTransport'],
+ // TODO: The stream APIs below require a working connection to create.
+ // BidirectionalStream
+ // SendStream
+ // ReceiveStream
+ });
+ self.webTransport = new WebTransport("https://example.com/");
+ // `ready` and `closed` promises will be rejected due to connection error.
+ // Catches them to avoid unhandled rejections.
+ self.webTransport.ready.catch(() => {});
+ self.webTransport.closed.catch(() => {});
+ }
+);
diff --git a/testing/web-platform/tests/webtransport/in-removed-iframe.https.html b/testing/web-platform/tests/webtransport/in-removed-iframe.https.html
new file mode 100644
index 0000000000..76204ac6a1
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/in-removed-iframe.https.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+setup({ single_test: true });
+
+function iframeOnLoad() {
+ const target = document.querySelector('#target');
+ const wt = target.contentWindow.wt;
+ target.remove();
+ const streams = wt.incomingBidirectionalStreams;
+ assert_equals(typeof streams, 'object', 'streams should be an object');
+ done();
+}
+</script>
+
+<iframe id=target onload="iframeOnLoad()" srcdoc="
+<!doctype html>
+<script src=/common/get-host-info.sub.js></script>
+<script src=resources/webtransport-test-helpers.sub.js></script>
+<script>
+window.wt = new WebTransport(webtransport_url('echo.py'));
+</script>
+"></iframe>
diff --git a/testing/web-platform/tests/webtransport/resources/webtransport-test-helpers.sub.js b/testing/web-platform/tests/webtransport/resources/webtransport-test-helpers.sub.js
new file mode 100644
index 0000000000..9f9127b22f
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/resources/webtransport-test-helpers.sub.js
@@ -0,0 +1,112 @@
+// The file including this must also include /common/get-host-info.sub.js to
+// pick up the necessary constants.
+
+const HOST = get_host_info().ORIGINAL_HOST;
+const PORT = '{{ports[webtransport-h3][0]}}';
+const BASE = `https://${HOST}:${PORT}`;
+
+// Wait for the given number of milliseconds (ms).
+function wait(ms) { return new Promise(res => step_timeout(res, ms)); }
+
+// Create URL for WebTransport session.
+function webtransport_url(handler) {
+ return `${BASE}/webtransport/handlers/${handler}`;
+}
+
+// Converts WebTransport stream error code to HTTP/3 error code.
+// https://ietf-wg-webtrans.github.io/draft-ietf-webtrans-http3/draft-ietf-webtrans-http3.html#section-4.3
+function webtransport_code_to_http_code(n) {
+ const first = 0x52e4a40fa8db;
+ return first + n + Math.floor(n / 0x1e);
+}
+
+// Read all chunks from |readable_stream| and return as an array of arrays
+async function read_stream(readable_stream) {
+ const reader = readable_stream.getReader();
+
+ let chunks = [];
+ while (true) {
+ const {value: chunk, done} = await reader.read();
+ if (done) {
+ break;
+ }
+ chunks.push(chunk);
+ }
+ reader.releaseLock();
+
+ return chunks;
+}
+
+// Read all chunks from |readable_stream|, decode chunks to a utf-8 string, then
+// return the string.
+async function read_stream_as_string(readable_stream) {
+ const decoder = new TextDecoderStream();
+ const decode_stream = readable_stream.pipeThrough(decoder);
+ const reader = decode_stream.getReader();
+
+ let chunks = '';
+ while (true) {
+ const {value: chunk, done} = await reader.read();
+ if (done) {
+ break;
+ }
+ chunks += chunk;
+ }
+ reader.releaseLock();
+
+ return chunks;
+}
+
+// Decode all chunks in a given ReadableStream, and parse the data using JSON.
+async function read_stream_as_json(readable_stream) {
+ const text = await read_stream_as_string(readable_stream);
+ return JSON.parse(text);
+}
+
+// Check the standard request headers and delete them, leaving any "unique"
+// headers to check in the test.
+function check_and_remove_standard_headers(headers) {
+ assert_equals(headers[':scheme'], 'https');
+ delete headers[':scheme'];
+ assert_equals(headers[':method'], 'CONNECT');
+ delete headers[':method'];
+ assert_equals(headers[':authority'], `${HOST}:${PORT}`);
+ delete headers[':authority'];
+ assert_equals(headers[':path'], '/webtransport/handlers/echo-request-headers.py');
+ delete headers[':path'];
+ assert_equals(headers[':protocol'], 'webtransport');
+ delete headers[':protocol'];
+ assert_equals(headers['origin'], `${get_host_info().ORIGIN}`);
+ delete headers['origin'];
+}
+
+async function query(token) {
+ const wt = new WebTransport(webtransport_url(`query.py?token=${token}`));
+ try {
+ await wt.ready;
+ const streams = await wt.incomingUnidirectionalStreams;
+ const streams_reader = streams.getReader();
+ const { value: readable } = await streams_reader.read();
+ streams_reader.releaseLock();
+
+ return await read_stream_as_json(readable);
+ } finally {
+ wt.close();
+ }
+}
+
+async function readInto(reader, buffer) {
+ let offset = 0;
+
+ while (offset < buffer.byteLength) {
+ const {value: view, done} = await reader.read(
+ new Uint8Array(buffer, offset, buffer.byteLength - offset));
+ buffer = view.buffer;
+ if (done) {
+ break;
+ }
+ offset += view.byteLength;
+ }
+
+ return buffer;
+}
diff --git a/testing/web-platform/tests/webtransport/server-certificate-hashes.https.any.js b/testing/web-platform/tests/webtransport/server-certificate-hashes.https.any.js
new file mode 100644
index 0000000000..01efab1191
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/server-certificate-hashes.https.any.js
@@ -0,0 +1,21 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/webtransport-test-helpers.sub.js
+
+promise_test(async t => {
+ const hashValue = new Uint8Array(32);
+ // The connection fails because the fingerprint does not match.
+ const wt = new WebTransport(webtransport_url('echo.py'), {
+ serverCertificateHashes: [
+ {
+ algorithm: "sha-256",
+ value: hashValue,
+ }
+ ]
+ });
+
+ const e = await wt.ready.catch(e => e);
+ await promise_rejects_exactly(t, e, wt.ready, 'ready should be rejected');
+ await promise_rejects_exactly(t, e, wt.closed, 'closed should be rejected');
+ assert_true(e instanceof WebTransportError);
+}, 'Connection fails due to certificate hash mismatch');
diff --git a/testing/web-platform/tests/webtransport/streams-close.https.any.js b/testing/web-platform/tests/webtransport/streams-close.https.any.js
new file mode 100644
index 0000000000..4871ee8e7b
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/streams-close.https.any.js
@@ -0,0 +1,252 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=resources/webtransport-test-helpers.sub.js
+
+// Note: There is no aioquic event for STOP_SENDING yet, so the server does
+// not support checking this yet. Hence, tests checking from the STOP_SENDING
+// signal cannot be tested yet.
+
+promise_test(async t => {
+ const id = token();
+ const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`));
+ add_completion_callback(() => wt.close());
+ await wt.ready;
+
+ const bidi_stream = await wt.createBidirectionalStream();
+
+ const writable = bidi_stream.writable;
+ writable.close();
+
+ await wait(10);
+ const data = await query(id);
+
+ assert_own_property(data, 'stream-close-info');
+ const info = data['stream-close-info'];
+
+ assert_equals(info.source, 'FIN', 'source');
+}, 'Close outgoing stream / bidi-1');
+
+promise_test(async t => {
+ const id = token();
+ const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`));
+ add_completion_callback(() => wt.close());
+ await wt.ready;
+
+ const streams_reader = wt.incomingBidirectionalStreams.getReader();
+ const {value: bidi} = await streams_reader.read();
+
+ const writable = bidi.writable;
+ writable.close();
+
+ await wait(10);
+ const data = await query(id);
+
+ assert_own_property(data, 'stream-close-info');
+ const info = data['stream-close-info'];
+
+ assert_equals(info.source, 'FIN', 'source');
+}, 'Close outgoing stream / bidi-2');
+
+promise_test(async t => {
+ const id = token();
+ const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`));
+ add_completion_callback(() => wt.close());
+ await wt.ready;
+
+ const writable = await wt.createUnidirectionalStream();
+ writable.close();
+
+ await wait(10);
+ const data = await query(id);
+
+ assert_own_property(data, 'stream-close-info');
+ const info = data['stream-close-info'];
+
+ assert_equals(info.source, 'FIN', 'source');
+}, 'Close outgoing stream / uni');
+
+promise_test(async t => {
+ const id = token();
+ const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`));
+ add_completion_callback(() => wt.close());
+ await wt.ready;
+
+ const bidi_stream = await wt.createBidirectionalStream();
+
+ const writable = bidi_stream.writable;
+
+ const WT_CODE = 139;
+ const HTTP_CODE = webtransport_code_to_http_code(WT_CODE);
+ await writable.abort(
+ new WebTransportError({streamErrorCode: WT_CODE}));
+
+ await wait(10);
+ const data = await query(id);
+
+ // Check that stream is aborted with RESET_STREAM with the code and reason
+ assert_own_property(data, 'stream-close-info');
+ const info = data['stream-close-info'];
+
+ assert_equals(info.source, 'reset', 'reset stream');
+ assert_equals(info.code, HTTP_CODE, 'code');
+}, 'Abort client-created bidirectional stream');
+
+promise_test(async t => {
+ const id = token();
+ const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`));
+ add_completion_callback(() => wt.close());
+ await wt.ready;
+
+ const stream_reader = wt.incomingBidirectionalStreams.getReader();
+ const { value: bidi_stream } = await stream_reader.read();
+ stream_reader.releaseLock();
+
+ const writer = bidi_stream.writable.getWriter();
+
+ const WT_CODE = 52;
+ const HTTP_CODE = webtransport_code_to_http_code(WT_CODE);
+ await writer.abort(
+ new WebTransportError({streamErrorCode: WT_CODE}));
+
+ await wait(10);
+ const data = await query(id);
+
+ // Check that stream is aborted with RESET_STREAM with the code and reason
+ assert_own_property(data, 'stream-close-info');
+ const info = data['stream-close-info'];
+
+ assert_equals(info.source, 'reset', 'reset_stream');
+ assert_equals(info.code, HTTP_CODE, 'code');
+}, 'Abort server-initiated bidirectional stream');
+
+promise_test(async t => {
+ const id = token();
+ const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`));
+ add_completion_callback(() => wt.close());
+ await wt.ready;
+
+ const writable = await wt.createUnidirectionalStream();
+
+ const WT_CODE = 95;
+ const HTTP_CODE = webtransport_code_to_http_code(WT_CODE);
+ await writable.abort(
+ new WebTransportError({streamErrorCode: WT_CODE}));
+
+ await wait(10);
+ const data = await query(id);
+
+ // Check that stream is aborted with RESET_STREAM with the code and reason
+ assert_own_property(data, 'stream-close-info');
+ const info = data['stream-close-info'];
+
+ assert_equals(info.source, 'reset', 'reset_stream');
+ assert_equals(info.code, HTTP_CODE, 'code');
+}, 'Abort unidirectional stream with WebTransportError');
+
+promise_test(async t => {
+ const id = token();
+ const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`));
+ add_completion_callback(() => wt.close());
+ await wt.ready;
+
+ const writable = await wt.createUnidirectionalStream();
+ const writer = writable.getWriter();
+
+ const WT_CODE = 134;
+ const HTTP_CODE = webtransport_code_to_http_code(WT_CODE);
+
+ // We use a large chunk so that sending the FIN signal takes time.
+ const chunk = new Uint8Array(64 * 1024);
+ const e = new WebTransportError({streamErrorCode: WT_CODE});
+ // Write a chunk, close the stream, and then abort the stream immediately to
+ // abort the closing operation.
+ // TODO: Check that the abort promise is correctly rejected/resolved based on
+ // the spec discussion at https://github.com/whatwg/streams/issues/1203.
+ await writer.write(chunk);
+ const close_promise = writer.close();
+ const abort_promise = writer.abort(e);
+
+ await promise_rejects_exactly(t, e, close_promise, 'close_promise');
+ await promise_rejects_exactly(t, e, writer.closed, '.closed');
+ await promise_rejects_exactly(t, e, abort_promise, 'abort_promise');
+ writer.releaseLock();
+
+ await wait(10);
+ const data = await query(id);
+
+ // Check that stream is aborted with RESET_STREAM with the code and reason
+ assert_own_property(data, 'stream-close-info');
+ const info = data['stream-close-info'];
+
+ assert_equals(info.source, 'reset', 'reset_stream');
+ assert_equals(info.code, HTTP_CODE, 'code');
+}, 'Close and abort unidirectional stream');
+
+promise_test(async t => {
+ const id = token();
+ const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`));
+ add_completion_callback(() => wt.close());
+ await wt.ready;
+
+ const writable = await wt.createUnidirectionalStream();
+ await writable.abort();
+
+ await wait(10);
+ const data = await query(id);
+
+ // Check that stream is aborted with RESET_STREAM with the code and reason
+ assert_own_property(data, 'stream-close-info');
+ const info = data['stream-close-info'];
+
+ assert_equals(info.source, 'reset', 'reset_stream');
+ assert_equals(info.code, webtransport_code_to_http_code(0), 'code');
+}, 'Abort unidirectional stream with default error code');
+
+promise_test(async t => {
+ const WT_CODE = 240;
+ const HTTP_CODE = webtransport_code_to_http_code(WT_CODE);
+ const wt = new WebTransport(
+ webtransport_url(`abort-stream-from-server.py?code=${HTTP_CODE}`));
+ add_completion_callback(() => wt.close());
+ await wt.ready;
+
+ const writable = await wt.createUnidirectionalStream();
+ const writer = writable.getWriter();
+
+ // Write something, to make the stream visible to the server side.
+ await writer.write(new Uint8Array([64]));
+
+ // Sadly we cannot use promise_rejects_dom as the error constructor is
+ // WebTransportError rather than DOMException. Ditto below.
+ // We get a possible error, and then make sure wt.closed is rejected with it.
+ const e = await writer.closed.catch(e => e);
+ await promise_rejects_exactly(
+ t, e, writer.closed, 'closed promise should be rejected');
+ assert_true(e instanceof WebTransportError);
+ assert_equals(e.source, 'stream', 'source');
+ assert_equals(e.streamErrorCode, WT_CODE, 'streamErrorCode');
+}, 'STOP_SENDING coming from server');
+
+promise_test(async t => {
+ const WT_CODE = 127;
+ const HTTP_CODE = webtransport_code_to_http_code(WT_CODE);
+ const wt = new WebTransport(
+ webtransport_url(`abort-stream-from-server.py?code=${HTTP_CODE}`));
+ add_completion_callback(() => wt.close());
+ await wt.ready;
+
+ const bidi = await wt.createBidirectionalStream();
+ const writer = bidi.writable.getWriter();
+
+ // Write something, to make the stream visible to the server side.
+ await writer.write(new Uint8Array([64]));
+
+ const reader = bidi.readable.getReader();
+ const e = await reader.closed.catch(e => e);
+ await promise_rejects_exactly(
+ t, e, reader.closed, 'closed promise should be rejected');
+ assert_true(e instanceof WebTransportError);
+ assert_equals(e.source, 'stream', 'source');
+ assert_equals(e.streamErrorCode, WT_CODE, 'streamErrorCode');
+}, 'RESET_STREAM coming from server');
diff --git a/testing/web-platform/tests/webtransport/streams-echo.https.any.js b/testing/web-platform/tests/webtransport/streams-echo.https.any.js
new file mode 100644
index 0000000000..703318d0e7
--- /dev/null
+++ b/testing/web-platform/tests/webtransport/streams-echo.https.any.js
@@ -0,0 +1,282 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/webtransport-test-helpers.sub.js
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ // Create a bidirectional stream.
+ const bidi_stream = await wt.createBidirectionalStream();
+
+ // Write a message to the writable end, and close it.
+ const writer = bidi_stream.writable.getWriter();
+ const encoder = new TextEncoder();
+ await writer.write(encoder.encode('Hello World'));
+ await writer.close();
+
+ // Read the data on the readable end.
+ const reply = await read_stream_as_string(bidi_stream.readable);
+
+ // Check that the message from the readable end matches the writable end.
+ assert_equals(reply, 'Hello World');
+}, 'WebTransport client should be able to create and handle a bidirectional stream');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+
+ // Create a bidirectional stream.
+ const bidi_stream = await wt.createBidirectionalStream();
+
+ // Write a message to the writable end, and close it.
+ const writer = bidi_stream.writable.getWriter();
+ const encoder = new TextEncoder();
+ await writer.write(encoder.encode('Hello World'));
+ await writer.close();
+
+ // Read the data on the readable end.
+ const reply = await read_stream_as_string(bidi_stream.readable);
+
+ // Check that the message from the readable end matches the writable end.
+ assert_equals(reply, 'Hello World');
+}, 'WebTransport client should be able to create and handle a bidirectional stream without waiting for ready');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ // The echo handler creates a bidirectional stream when a WebTransport session
+ // is established. Accept the bidirectional stream.
+ const stream_reader = wt.incomingBidirectionalStreams.getReader();
+ const { value: bidi_stream } = await stream_reader.read();
+ stream_reader.releaseLock();
+
+ // Write a message to the writable end, and close it.
+ const encoder = new TextEncoderStream();
+ encoder.readable.pipeTo(bidi_stream.writable);
+ const writer = encoder.writable.getWriter();
+ await writer.write('Hello World');
+ await writer.close();
+
+ // Read the data on the readable end.
+ const reply = await read_stream_as_string(bidi_stream.readable);
+
+ // Check that the message from the readable end matches the writable end.
+ assert_equals(reply, 'Hello World');
+}, 'WebTransport server should be able to accept and handle a bidirectional stream');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ // Create a unidirectional stream.
+ const writable = await wt.createUnidirectionalStream();
+
+ // Write a message to the writable end, and close it.
+ const encoder = new TextEncoderStream();
+ encoder.readable.pipeTo(writable);
+ const writer = encoder.writable.getWriter();
+ await writer.write('Hello World');
+ await writer.close();
+
+ // The echo handler creates a new unidirectional stream to echo back data from
+ // the server to client. Accept the unidirectional stream.
+ const readable = wt.incomingUnidirectionalStreams;
+ const stream_reader = readable.getReader();
+ const { value: recv_stream } = await stream_reader.read();
+ stream_reader.releaseLock();
+
+ // Read the data on the readable end.
+ const reply = await read_stream_as_string(recv_stream);
+
+ // Make sure the message on the writable and readable ends of the streams
+ // match.
+ assert_equals(reply, 'Hello World');
+}, 'WebTransport client should be able to create, accept, and handle a unidirectional stream');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+
+ // Create a unidirectional stream.
+ const writable = await wt.createUnidirectionalStream();
+
+ // Write a message to the writable end, and close it.
+ const encoder = new TextEncoderStream();
+ encoder.readable.pipeTo(writable);
+ const writer = encoder.writable.getWriter();
+ await writer.write('Hello World');
+ await writer.close();
+
+ // The echo handler creates a new unidirectional stream to echo back data from
+ // the server to client. Accept the unidirectional stream.
+ const readable = wt.incomingUnidirectionalStreams;
+ const stream_reader = readable.getReader();
+ const { value: recv_stream } = await stream_reader.read();
+ stream_reader.releaseLock();
+
+ // Read the data on the readable end.
+ const reply = await read_stream_as_string(recv_stream);
+
+ // Make sure the message on the writable and readable ends of the streams
+ // match.
+ assert_equals(reply, 'Hello World');
+}, 'WebTransport client should be able to create, accept, and handle a unidirectional stream without waiting for ready');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ // The echo handler creates a bidirectional stream when a WebTransport session
+ // is established. Accept the bidirectional stream.
+ const stream_reader = wt.incomingBidirectionalStreams.getReader();
+ const {value: bidi_stream} = await stream_reader.read();
+ stream_reader.releaseLock();
+
+ // Write data to the writable end, and close it.
+ const buffer_size = 256;
+ const data = new Uint8Array(buffer_size);
+ for (let i = 0; i < data.byteLength; ++i) {
+ data[i] = i;
+ }
+ const writer = bidi_stream.writable.getWriter();
+ writer.write(data);
+ await writer.close();
+
+ // Read the data on the readable end and check if it matches the writable end.
+ const reader = bidi_stream.readable.getReader({mode: 'byob'});
+ assert_true(reader instanceof ReadableStreamBYOBReader);
+ const half_buffer_size = buffer_size / 2;
+ for (let i = 0; i < 2; i++) {
+ let buffer = new ArrayBuffer(half_buffer_size);
+ buffer = await readInto(reader, buffer);
+ assert_array_equals(
+ new Uint8Array(buffer),
+ data.subarray(half_buffer_size * i, half_buffer_size * (i + 1)))
+ }
+ reader.releaseLock();
+}, 'Can read data from a bidirectional stream with BYOB reader');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ // Create a unidirectional stream.
+ const writable = await wt.createUnidirectionalStream();
+
+ // Write data to the writable end, and close it.
+ const buffer_size = 256;
+ const data = new Uint8Array(buffer_size);
+ for (let i = 0; i < data.byteLength; ++i) {
+ data[i] = i;
+ }
+ const writer = writable.getWriter();
+ writer.write(data);
+ await writer.close();
+
+ // The echo handler creates a new unidirectional stream to echo back data from
+ // the server to client. Accept the unidirectional stream.
+ const readable = wt.incomingUnidirectionalStreams;
+ const stream_reader = readable.getReader();
+ const {value: recv_stream} = await stream_reader.read();
+ stream_reader.releaseLock();
+
+ // Read the data on the readable end and check if it matches the writable end.
+ const reader = recv_stream.getReader({mode: 'byob'});
+ assert_true(reader instanceof ReadableStreamBYOBReader);
+ const half_buffer_size = buffer_size / 2;
+ let buffer = new ArrayBuffer(half_buffer_size);
+ for (let i = 0; i < 2; i++) {
+ buffer = await readInto(reader, buffer);
+ assert_array_equals(
+ new Uint8Array(buffer),
+ data.subarray(half_buffer_size * i, half_buffer_size * (i + 1)))
+ }
+ reader.releaseLock();
+}, 'Can read data from a unidirectional stream with BYOB reader');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ // Create a bidirectional stream.
+ const bidi_stream = await wt.createBidirectionalStream();
+
+ // Write a message to the writable end, and close it.
+ const writer = bidi_stream.writable.getWriter();
+ const bytes = new Uint8Array(16384);
+ const [reply] = await Promise.all([
+ read_stream(bidi_stream.readable),
+ writer.write(bytes),
+ writer.write(bytes),
+ writer.write(bytes),
+ writer.close()
+ ]);
+ let len = 0;
+ for (chunk of reply) {
+ len += chunk.length;
+ }
+ // Check that the message from the readable end matches the writable end.
+ assert_equals(len, 3*bytes.length);
+}, 'Transfer large chunks of data on a bidirectional stream');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ // Create a unidirectional stream.
+ const uni_stream = await wt.createUnidirectionalStream();
+
+ // Write a message to the writable end, and close it.
+ const writer = uni_stream.getWriter();
+ const bytes = new Uint8Array(16384);
+ await Promise.all([
+ writer.write(bytes),
+ writer.write(bytes),
+ writer.write(bytes),
+ writer.close()
+ ]);
+ // XXX Update once chrome fixes https://crbug.com/929585
+ // The echo handler creates a new unidirectional stream to echo back data from
+ // the server to client. Accept the unidirectional stream.
+ const readable = wt.incomingUnidirectionalStreams;
+ const stream_reader = readable.getReader();
+ const { value: recv_stream } = await stream_reader.read();
+ stream_reader.releaseLock();
+
+ // Read the data on the readable end.
+ const reply = await read_stream(recv_stream);
+ let len = 0;
+ for (chunk of reply) {
+ len += chunk.length;
+ }
+ // Check that the message from the readable end matches the writable end.
+ assert_equals(len, 3*bytes.length);
+}, 'Transfer large chunks of data on a unidirectional stream');
+
+promise_test(async t => {
+ // Establish a WebTransport session.
+ const wt = new WebTransport(webtransport_url('echo.py'));
+ await wt.ready;
+
+ // Create a bidirectional stream.
+ const bidi_stream = await wt.createBidirectionalStream();
+
+ // Close the writable end with no data at all.
+ const writer = bidi_stream.writable.getWriter();
+ writer.close();
+
+ // Read the data on the readable end.
+ const chunks = await read_stream(bidi_stream.readable);
+ assert_equals(chunks.length, 0);
+
+ await bidi_stream.readable.closed;
+}, 'Closing the stream with no data still resolves the read request');