diff options
Diffstat (limited to 'testing/web-platform/tests/websockets/stream')
8 files changed, 436 insertions, 0 deletions
diff --git a/testing/web-platform/tests/websockets/stream/tentative/README.md b/testing/web-platform/tests/websockets/stream/tentative/README.md new file mode 100644 index 0000000000..6c51588774 --- /dev/null +++ b/testing/web-platform/tests/websockets/stream/tentative/README.md @@ -0,0 +1,9 @@ +# WebSocketStream tentative tests + +Tests in this directory are for the proposed "WebSocketStream" interface to the +WebSocket protocol. This is not yet standardised and browsers should not be +expected to pass these tests. + +See the explainer at +https://github.com/ricea/websocketstream-explainer/blob/master/README.md for +more information about the API. diff --git a/testing/web-platform/tests/websockets/stream/tentative/abort.any.js b/testing/web-platform/tests/websockets/stream/tentative/abort.any.js new file mode 100644 index 0000000000..9047f246f9 --- /dev/null +++ b/testing/web-platform/tests/websockets/stream/tentative/abort.any.js @@ -0,0 +1,50 @@ +// META: script=../../constants.sub.js +// META: script=resources/url-constants.js +// META: script=/common/utils.js +// META: global=window,worker +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +promise_test(async t => { + const controller = new AbortController(); + controller.abort(); + const key = token(); + const wsUrl = new URL( + `/fetch/api/resources/stash-put.py?key=${key}&value=connected`, + location.href); + wsUrl.protocol = wsUrl.protocol.replace('http', 'ws'); + // We intentionally use the port for the HTTP server, not the WebSocket + // server, because we don't expect the connection to be performed. + const wss = new WebSocketStream(wsUrl, { signal: controller.signal }); + await promise_rejects_dom( + t, 'AbortError', wss.opened, 'opened should reject'); + await promise_rejects_dom( + t, 'AbortError', wss.closed, 'closed should reject'); + // An incorrect implementation could pass this test due a race condition, + // but it is hard to completely eliminate the possibility. + const response = await fetch(`/fetch/api/resources/stash-take.py?key=${key}`); + assert_equals(await response.text(), 'null', 'response should be null'); +}, 'abort before constructing should prevent connection'); + +promise_test(async t => { + const controller = new AbortController(); + const wss = new WebSocketStream(`${BASEURL}/handshake_sleep_2`, + { signal: controller.signal }); + // Give the connection a chance to start. + await new Promise(resolve => t.step_timeout(resolve, 0)); + controller.abort(); + await promise_rejects_dom( + t, 'AbortError', wss.opened, 'opened should reject'); + await promise_rejects_dom( + t, 'AbortError', wss.closed, 'closed should reject'); +}, 'abort during handshake should work'); + +promise_test(async t => { + const controller = new AbortController(); + const wss = new WebSocketStream(ECHOURL, { signal: controller.signal }); + const { readable, writable } = await wss.opened; + controller.abort(); + writable.getWriter().write('connected'); + const { value } = await readable.getReader().read(); + assert_equals(value, 'connected', 'value should match'); +}, 'abort after connect should do nothing'); diff --git a/testing/web-platform/tests/websockets/stream/tentative/backpressure-receive.any.js b/testing/web-platform/tests/websockets/stream/tentative/backpressure-receive.any.js new file mode 100644 index 0000000000..236bb2e40f --- /dev/null +++ b/testing/web-platform/tests/websockets/stream/tentative/backpressure-receive.any.js @@ -0,0 +1,40 @@ +// META: script=../../constants.sub.js +// META: script=resources/url-constants.js +// META: global=window,worker +// META: timeout=long +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +// Allow for this much timer jitter. +const JITTER_ALLOWANCE_MS = 200; +const LARGE_MESSAGE_COUNT = 16; + +// This test works by using a server WebSocket handler which sends a large +// message, and then sends a second message with the time it measured the first +// message taking. On the browser side, we wait 2 seconds before reading from +// the socket. This should ensure it takes at least 2 seconds to finish sending +// the large message. +promise_test(async t => { + const wss = new WebSocketStream(`${BASEURL}/send-backpressure`); + const { readable } = await wss.opened; + const reader = readable.getReader(); + + // Create backpressure for 2 seconds. + await new Promise(resolve => t.step_timeout(resolve, 2000)); + + // Skip the empty message used to fill the readable queue. + await reader.read(); + + // Skip the large messages. + for (let i = 0; i < LARGE_MESSAGE_COUNT; ++i) { + await reader.read(); + } + + // Read the time it took. + const { value, done } = await reader.read(); + + // A browser can pass this test simply by being slow. This may be a source of + // flakiness for browsers that do not implement backpressure properly. + assert_greater_than_equal(Number(value), 2 - JITTER_ALLOWANCE_MS / 1000, + 'data send should have taken at least 2 seconds'); +}, 'backpressure should be applied to received messages'); diff --git a/testing/web-platform/tests/websockets/stream/tentative/backpressure-send.any.js b/testing/web-platform/tests/websockets/stream/tentative/backpressure-send.any.js new file mode 100644 index 0000000000..e4a80f6e1c --- /dev/null +++ b/testing/web-platform/tests/websockets/stream/tentative/backpressure-send.any.js @@ -0,0 +1,25 @@ +// META: script=../../constants.sub.js +// META: script=resources/url-constants.js +// META: global=window,worker +// META: timeout=long +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +// Allow for this much timer jitter. +const JITTER_ALLOWANCE_MS = 200; + +// The amount of buffering a WebSocket connection has is not standardised, but +// it's reasonable to expect that it will not be as large as 8MB. +const MESSAGE_SIZE = 8 * 1024 * 1024; + +// In this test, the server WebSocket handler waits 2 seconds, and the browser +// times how long it takes to send the first message. +promise_test(async t => { + const wss = new WebSocketStream(`${BASEURL}/receive-backpressure`); + const { writable } = await wss.opened; + const writer = writable.getWriter(); + const start = performance.now(); + await writer.write(new Uint8Array(MESSAGE_SIZE)); + const elapsed = performance.now() - start; + assert_greater_than_equal(elapsed, 2000 - JITTER_ALLOWANCE_MS); +}, 'backpressure should be applied to sent messages'); diff --git a/testing/web-platform/tests/websockets/stream/tentative/close.any.js b/testing/web-platform/tests/websockets/stream/tentative/close.any.js new file mode 100644 index 0000000000..098caf31c8 --- /dev/null +++ b/testing/web-platform/tests/websockets/stream/tentative/close.any.js @@ -0,0 +1,183 @@ +// META: script=../../constants.sub.js +// META: script=resources/url-constants.js +// META: global=window,worker +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +'use strict'; + +promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + await wss.opened; + wss.close({ closeCode: 3456, reason: 'pizza' }); + const { closeCode, reason } = await wss.closed; + assert_equals(closeCode, 3456, 'code should match'); + assert_equals(reason, 'pizza', 'reason should match'); +}, 'close code should be sent to server and reflected back'); + +promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + await wss.opened; + wss.close(); + const { closeCode, reason } = await wss.closed; + assert_equals(closeCode, 1005, 'code should be unset'); + assert_equals(reason, '', 'reason should be empty'); +}, 'no close argument should send empty Close frame'); + +promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + await wss.opened; + wss.close({}); + const { closeCode, reason } = await wss.closed; + assert_equals(closeCode, 1005, 'code should be unset'); + assert_equals(reason, '', 'reason should be empty'); +}, 'unspecified close code should send empty Close frame'); + +promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + await wss.opened; + wss.close({reason: ''}); + const { closeCode, reason } = await wss.closed; + assert_equals(closeCode, 1005, 'code should be unset'); + assert_equals(reason, '', 'reason should be empty'); +}, 'unspecified close code with empty reason should send empty Close frame'); + +promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + await wss.opened; + wss.close({reason: 'non-empty'}); + const { closeCode, reason } = await wss.closed; + assert_equals(closeCode, 1000, 'code should be set'); + assert_equals(reason, 'non-empty', 'reason should match'); +}, 'unspecified close code with non-empty reason should set code to 1000'); + +promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + await wss.opened; + assert_throws_js(TypeError, () => wss.close(true), + 'close should throw a TypeError'); +}, 'close(true) should throw a TypeError'); + +promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + await wss.opened; + const reason = '.'.repeat(124); + assert_throws_dom('SyntaxError', () => wss.close({ reason }), + 'close should throw a SyntaxError'); +}, 'close() with an overlong reason should throw'); + +function IsWebSocketError(e) { + return e.constructor == WebSocketError; +} + +promise_test(t => { + const wss = new WebSocketStream(ECHOURL); + wss.close(); + return Promise.all([ + wss.opened.then(t.unreached_func('should have rejected')).catch(e => assert_true(IsWebSocketError(e))), + wss.closed.then(t.unreached_func('should have rejected')).catch(e => assert_true(IsWebSocketError(e))), + ]); +}, 'close during handshake should work'); + +for (const invalidCode of [999, 1001, 2999, 5000]) { + promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + await wss.opened; + assert_throws_dom('InvalidAccessError', () => wss.close({ closeCode: invalidCode }), + 'close should throw an InvalidAccessError'); + }, `close() with invalid code ${invalidCode} should throw`); +} + +promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + const { writable } = await wss.opened; + writable.getWriter().close(); + const { closeCode, reason } = await wss.closed; + assert_equals(closeCode, 1005, 'code should be unset'); + assert_equals(reason, '', 'reason should be empty'); +}, 'closing the writable should result in a clean close'); + +promise_test(async () => { + const wss = new WebSocketStream(`${BASEURL}/delayed-passive-close`); + const { writable } = await wss.opened; + const startTime = performance.now(); + await writable.getWriter().close(); + const elapsed = performance.now() - startTime; + const jitterAllowance = 100; + assert_greater_than_equal(elapsed, 1000 - jitterAllowance, + 'one second should have elapsed'); +}, 'writer close() promise should not resolve until handshake completes'); + +const abortOrCancel = [ + { + method: 'abort', + voweling: 'aborting', + stream: 'writable', + }, + { + method: 'cancel', + voweling: 'canceling', + stream: 'readable', + }, +]; + +for (const { method, voweling, stream } of abortOrCancel) { + + promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + const info = await wss.opened; + info[stream][method](); + const { closeCode, reason } = await wss.closed; + assert_equals(closeCode, 1005, 'code should be unset'); + assert_equals(reason, '', 'reason should be empty'); + }, `${voweling} the ${stream} should result in a clean close`); + + promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + const info = await wss.opened; + info[stream][method]({ closeCode: 3333, reason: 'obsolete' }); + const { closeCode, reason } = await wss.closed; + assert_equals(closeCode, 1005, 'code should be unset'); + assert_equals(reason, '', 'reason should be empty'); + }, `${voweling} the ${stream} with attributes not wrapped in a WebSocketError should be ignored`); + + promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + const info = await wss.opened; + info[stream][method](new WebSocketError('', { closeCode: 3333 })); + const { closeCode, reason } = await wss.closed; + assert_equals(closeCode, 3333, 'code should be used'); + assert_equals(reason, '', 'reason should be empty'); + }, `${voweling} the ${stream} with a code should send that code`); + + promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + const info = await wss.opened; + info[stream][method](new WebSocketError('', { closeCode: 3456, reason: 'set' })); + const { closeCode, reason } = await wss.closed; + assert_equals(closeCode, 3456, 'code should be used'); + assert_equals(reason, 'set', 'reason should be used'); + }, `${voweling} the ${stream} with a code and reason should use them`); + + promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + const info = await wss.opened; + info[stream][method](new WebSocketError('', { reason: 'specified' })); + const { closeCode, reason } = await wss.closed; + assert_equals(closeCode, 1000, 'code should be defaulted'); + assert_equals(reason, 'specified', 'reason should be used'); + }, `${voweling} the ${stream} with a reason but no code should default the close code`); + + promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + const info = await wss.opened; + const domException = new DOMException('yes', 'DataCloneError'); + domException.closeCode = 1000; + domException.reason = 'should be ignored'; + info[stream][method](domException); + const { closeCode, reason } = await wss.closed; + assert_equals(closeCode, 1005, 'code should be unset'); + assert_equals(reason, '', 'reason should be empty'); + }, `${voweling} the ${stream} with a DOMException not set code or reason`); + +} diff --git a/testing/web-platform/tests/websockets/stream/tentative/constructor.any.js b/testing/web-platform/tests/websockets/stream/tentative/constructor.any.js new file mode 100644 index 0000000000..66df974303 --- /dev/null +++ b/testing/web-platform/tests/websockets/stream/tentative/constructor.any.js @@ -0,0 +1,71 @@ +// META: script=../../constants.sub.js +// META: script=resources/url-constants.js +// META: global=window,worker +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +test(() => { + assert_throws_js(TypeError, () => new WebSocketStream(), + 'constructor should throw'); +}, 'constructing with no URL should throw'); + +test(() => { + assert_throws_dom('SyntaxError', () => new WebSocketStream('invalid:'), + 'constructor should throw'); +}, 'constructing with an invalid URL should throw'); + +test(() => { + assert_throws_js(TypeError, + () => new WebSocketStream(`${BASEURL}/`, true), + 'constructor should throw'); +}, 'constructing with invalid options should throw'); + +test(() => { + assert_throws_js(TypeError, + () => new WebSocketStream(`${BASEURL}/`, {protocols: 'hi'}), + 'constructor should throw'); +}, 'protocols should be required to be a list'); + +promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + await wss.opened; + assert_equals(wss.url, ECHOURL, 'url should match'); + wss.close(); +}, 'constructing with a valid URL should work'); + +promise_test(async () => { + const wss = new WebSocketStream(`${BASEURL}/protocol_array`, + {protocols: ['alpha', 'beta']}); + const { readable, protocol } = await wss.opened; + assert_equals(protocol, 'alpha', 'protocol should be right'); + const reader = readable.getReader(); + const { value, done } = await reader.read(); + assert_equals(value, 'alpha', 'message contents should match'); + wss.close(); +}, 'setting a protocol in the constructor should work'); + +function IsWebSocketError(e) { + return e.constructor == WebSocketError; +} + +promise_test(t => { + const wss = new WebSocketStream(`${BASEURL}/404`); + return Promise.all([ + wss.opened.then(t.unreached_func('should have rejected')).catch(e => assert_true(IsWebSocketError(e))), + wss.closed.then(t.unreached_func('should have rejected')).catch(e => assert_true(IsWebSocketError(e))), + ]); +}, 'connection failure should reject the promises'); + +promise_test(async () => { + const wss = new WebSocketStream(ECHOURL); + const { readable, writable, protocol, extensions} = await wss.opened; + // Verify that |readable| really is a ReadableStream using the getReader() + // brand check. If it doesn't throw the test passes. + ReadableStream.prototype.getReader.call(readable); + // Verify that |writable| really is a WritableStream using the getWriter() + // brand check. If it doesn't throw the test passes. + WritableStream.prototype.getWriter.call(writable); + assert_equals(typeof protocol, 'string', 'protocol should be a string'); + assert_equals(typeof extensions, 'string', 'extensions should be a string'); + wss.close(); +}, 'wss.opened should resolve to the right types'); diff --git a/testing/web-platform/tests/websockets/stream/tentative/resources/url-constants.js b/testing/web-platform/tests/websockets/stream/tentative/resources/url-constants.js new file mode 100644 index 0000000000..fe681af5c4 --- /dev/null +++ b/testing/web-platform/tests/websockets/stream/tentative/resources/url-constants.js @@ -0,0 +1,8 @@ +// The file including this must also include ../constants.sub.js to pick up the +// necessary constants. + +const {BASEURL, ECHOURL} = (() => { + const BASEURL = SCHEME_DOMAIN_PORT; + const ECHOURL = `${BASEURL}/echo`; + return {BASEURL, ECHOURL}; +})(); diff --git a/testing/web-platform/tests/websockets/stream/tentative/websocket-error.any.js b/testing/web-platform/tests/websockets/stream/tentative/websocket-error.any.js new file mode 100644 index 0000000000..b114bbb3e3 --- /dev/null +++ b/testing/web-platform/tests/websockets/stream/tentative/websocket-error.any.js @@ -0,0 +1,50 @@ +// META: global=window,worker + +'use strict'; + +test(() => { + const error = new WebSocketError(); + assert_equals(error.code, 0, 'DOMException code should be 0'); + assert_equals(error.name, 'WebSocketError', 'name should be correct'); + assert_equals(error.message, '', 'DOMException message should be empty'); + assert_equals(error.closeCode, null, 'closeCode should be null'); + assert_equals(error.reason, '', 'reason should be empty'); +}, 'WebSocketError defaults should be correct'); + +test(() => { + const error = new WebSocketError('message', { closeCode: 3456, reason: 'reason' }); + assert_equals(error.code, 0, 'DOMException code should be 0'); + assert_equals(error.name, 'WebSocketError', 'name should be correct'); + assert_equals(error.message, 'message', 'DOMException message should be set'); + assert_equals(error.closeCode, 3456, 'closeCode should match'); + assert_equals(error.reason, 'reason', 'reason should match'); +}, 'WebSocketError should be initialised from arguments'); + +for (const invalidCode of [999, 1001, 2999, 5000]) { + test(() => { + assert_throws_dom('InvalidAccessError', () => new WebSocketError('', { closeCode: invalidCode }), + 'invalid code should throw an InvalidAccessError'); + }, `new WebSocketError with invalid code ${invalidCode} should throw`); +} + +test(() => { + const error = new WebSocketError('', { closeCode: 3333 }); + assert_equals(error.closeCode, 3333, 'code should be used'); + assert_equals(error.reason, '', 'reason should be empty'); +}, 'passing only close code to WebSocketError should work'); + +test(() => { + const error = new WebSocketError('', { reason: 'specified' }); + assert_equals(error.closeCode, 1000, 'code should be defaulted'); + assert_equals(error.reason, 'specified', 'reason should be used'); +}, 'passing a non-empty reason should cause the close code to be set to 1000'); + +test(() => { + assert_throws_dom('SyntaxError', () => new WebSocketError('', { closeCode: 1000, reason: 'x'.repeat(124) }), + 'overlong reason should trigger SyntaxError'); +}, 'overlong reason should throw'); + +test(() => { + assert_throws_dom('SyntaxError', () => new WebSocketError('', { reason: '🔌'.repeat(32) }), + 'overlong reason should throw'); +}, 'reason should be rejected based on utf-8 bytes, not character count'); |