Commit 26bef937 authored by Adam Rice's avatar Adam Rice Committed by Commit Bot

Implement WebSocketStream

WebSocketStream is a new API for the WebSocket protocol that uses
streams to support backpressure.

See the explainer for more justification:
https://github.com/ricea/websocketstream-explainer/blob/master/README.md

This CL implements WebSocketStream behind the
experimental-web-platform-features flag.

See the design doc at https://docs.google.com/document/d/1XuxEshh5VYBYm1qRVKordTamCOsR-uGQBCYFcHXP4L0/edit#heading=h.7nki9mck5t64

Bug: 983030
Change-Id: I3002fd52ab151acb579e96a24985988e3fd35053
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1735156Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Reviewed-by: default avatarYutaka Hirano <yhirano@chromium.org>
Commit-Queue: Adam Rice <ricea@chromium.org>
Cr-Commit-Position: refs/heads/master@{#690263}
parent a571bf76
...@@ -428,6 +428,7 @@ jumbo_source_set("unit_tests") { ...@@ -428,6 +428,7 @@ jumbo_source_set("unit_tests") {
"websockets/websocket_channel_impl_test.cc", "websockets/websocket_channel_impl_test.cc",
"websockets/websocket_common_test.cc", "websockets/websocket_common_test.cc",
"websockets/websocket_message_chunk_accumulator_test.cc", "websockets/websocket_message_chunk_accumulator_test.cc",
"websockets/websocket_stream_test.cc",
"worklet/animation_and_paint_worklet_thread_test.cc", "worklet/animation_and_paint_worklet_thread_test.cc",
"worklet/worklet_thread_test_common.cc", "worklet/worklet_thread_test_common.cc",
"worklet/worklet_thread_test_common.h", "worklet/worklet_thread_test_common.h",
......
...@@ -493,6 +493,7 @@ modules_idl_files = ...@@ -493,6 +493,7 @@ modules_idl_files =
"webmidi/midi_port.idl", "webmidi/midi_port.idl",
"websockets/close_event.idl", "websockets/close_event.idl",
"websockets/websocket.idl", "websockets/websocket.idl",
"websockets/websocket_stream.idl",
"webusb/usb.idl", "webusb/usb.idl",
"webusb/usb_alternate_interface.idl", "webusb/usb_alternate_interface.idl",
"webusb/usb_configuration.idl", "webusb/usb_configuration.idl",
...@@ -868,6 +869,9 @@ modules_dictionary_idl_files = ...@@ -868,6 +869,9 @@ modules_dictionary_idl_files =
"webmidi/midi_options.idl", "webmidi/midi_options.idl",
"webshare/share_data.idl", "webshare/share_data.idl",
"websockets/close_event_init.idl", "websockets/close_event_init.idl",
"websockets/websocket_close_info.idl",
"websockets/websocket_connection.idl",
"websockets/websocket_stream_options.idl",
"webusb/usb_connection_event_init.idl", "webusb/usb_connection_event_init.idl",
"webusb/usb_control_transfer_parameters.idl", "webusb/usb_control_transfer_parameters.idl",
"webusb/usb_device_filter.idl", "webusb/usb_device_filter.idl",
......
...@@ -27,5 +27,7 @@ blink_modules_sources("websockets") { ...@@ -27,5 +27,7 @@ blink_modules_sources("websockets") {
"websocket_handle_impl.h", "websocket_handle_impl.h",
"websocket_message_chunk_accumulator.cc", "websocket_message_chunk_accumulator.cc",
"websocket_message_chunk_accumulator.h", "websocket_message_chunk_accumulator.h",
"websocket_stream.cc",
"websocket_stream.h",
] ]
} }
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// TODO(ricea): Add standard link when there is one.
dictionary WebSocketCloseInfo {
[Clamp] unsigned short code;
USVString reason = "";
};
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// TODO(ricea): Add standard link when there is one.
dictionary WebSocketConnection {
ReadableStream readable;
WritableStream writable;
DOMString extensions;
DOMString protocol;
};
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_WEBSOCKETS_WEBSOCKET_STREAM_H_
#define THIRD_PARTY_BLINK_RENDERER_MODULES_WEBSOCKETS_WEBSOCKET_STREAM_H_
#include "third_party/blink/renderer/bindings/core/v8/active_script_wrappable.h"
#include "third_party/blink/renderer/core/execution_context/context_lifecycle_observer.h"
#include "third_party/blink/renderer/modules/modules_export.h"
#include "third_party/blink/renderer/modules/websockets/websocket_channel_client.h"
#include "third_party/blink/renderer/modules/websockets/websocket_common.h"
#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
#include "third_party/blink/renderer/platform/bindings/trace_wrapper_v8_reference.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/wtf/forward.h"
#include "v8/include/v8.h"
namespace blink {
class ExceptionState;
class ExecutionContext;
class ScriptPromise;
class ScriptPromiseResolver;
class ScriptState;
class ScriptValue;
class Visitor;
class WebSocketChannel;
class WebSocketCloseInfo;
class WebSocketStreamOptions;
// Implements of JavaScript-exposed WebSocketStream API. See design doc at
// https://docs.google.com/document/d/1XuxEshh5VYBYm1qRVKordTamCOsR-uGQBCYFcHXP4L0/edit
class MODULES_EXPORT WebSocketStream final
: public ScriptWrappable,
public ActiveScriptWrappable<WebSocketStream>,
public ContextLifecycleObserver,
public WebSocketChannelClient {
DEFINE_WRAPPERTYPEINFO();
USING_GARBAGE_COLLECTED_MIXIN(WebSocketStream);
public:
// IDL constructors
static WebSocketStream* Create(ScriptState*,
const String& url,
WebSocketStreamOptions*,
ExceptionState&);
static WebSocketStream* CreateForTesting(ScriptState*,
const String& url,
WebSocketStreamOptions*,
WebSocketChannel*,
ExceptionState&);
// The constructor only creates the object. It cannot fail. The private
// Connect() method is used by Create() to start connecting.
WebSocketStream(ExecutionContext*, ScriptState*);
~WebSocketStream() override;
// IDL properties
const KURL& url() const { return common_.Url(); }
ScriptPromise connection(ScriptState*) const;
ScriptPromise closed(ScriptState*) const;
// IDL functions
void close(WebSocketCloseInfo*, ExceptionState&);
// Implementation of WebSocketChannelClient
void DidConnect(const String& subprotocol, const String& extensions) override;
void DidReceiveTextMessage(const String&) override;
void DidReceiveBinaryMessage(
const Vector<base::span<const char>>& data) override;
void DidError() override;
void DidConsumeBufferedAmount(uint64_t consumed) override;
void DidStartClosingHandshake() override;
void DidClose(ClosingHandshakeCompletionStatus,
uint16_t /* code */,
const String& /* reason */) override;
// Implementation of ContextLifecycleObserver.
void ContextDestroyed(ExecutionContext*) override;
// Implementation of ActiveScriptWrappable.
bool HasPendingActivity() const override;
void Trace(Visitor*) override;
private:
class UnderlyingSource;
class UnderlyingSink;
static WebSocketStream* CreateInternal(ScriptState*,
const String& url,
WebSocketStreamOptions*,
WebSocketChannel*,
ExceptionState&);
void Connect(ScriptState*,
const String& url,
WebSocketStreamOptions*,
ExceptionState&);
// Closes the connection. If |maybe_reason| is an object with a valid "code"
// property and optionally a valid "reason" property, will use them as the
// code and reason, otherwise will close with unspecified close.
void CloseMaybeWithReason(ScriptValue maybe_reason);
void CloseWithUnspecifiedCode();
void CloseInternal(int code,
const String& reason,
ExceptionState& exception_state);
v8::Local<v8::Value> CreateNetworkErrorDOMException();
static WebSocketCloseInfo* MakeCloseInfo(uint16_t code, const String& reason);
const Member<ScriptState> script_state_;
const Member<ScriptPromiseResolver> connection_resolver_;
const Member<ScriptPromiseResolver> closed_resolver_;
// These need to be cached because the Promise() method on
// ScriptPromiseResolver doesn't work any more once the promise is resolved or
// rejected.
const TraceWrapperV8Reference<v8::Promise> connection_;
const TraceWrapperV8Reference<v8::Promise> closed_;
Member<WebSocketChannel> channel_;
Member<UnderlyingSource> source_;
Member<UnderlyingSink> sink_;
WebSocketCommon common_;
// We need to distinguish between "closing during handshake" and "closing
// after handshake" in order to reject the |connection_resolver_| correctly.
bool was_ever_connected_ = false;
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_WEBSOCKETS_WEBSOCKET_STREAM_H_
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// TODO(ricea): Add standard link when there is one.
[
Constructor(USVString url, optional WebSocketStreamOptions options),
ConstructorCallWith=ScriptState,
Exposed=(Window,Worker),
RaisesException=Constructor,
RuntimeEnabled=WebSocketStream,
ActiveScriptWrappable
] interface WebSocketStream {
readonly attribute USVString url;
[CallWith=ScriptState] readonly attribute Promise<WebSocketConnection> connection;
[CallWith=ScriptState] readonly attribute Promise<WebSocketCloseInfo> closed;
[RaisesException] void close(optional WebSocketCloseInfo closeInfo);
};
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// TODO(ricea): Add standard link when there is one.
dictionary WebSocketStreamOptions {
sequence<USVString> protocols;
AbortSignal signal;
};
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Most testing for WebSocketStream is done via web platform tests. These unit
// tests just cover the most common functionality.
#include "third_party/blink/renderer/modules/websockets/websocket_stream.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/modules/websockets/mock_websocket_channel.h"
#include "third_party/blink/renderer/modules/websockets/websocket_channel.h"
#include "third_party/blink/renderer/modules/websockets/websocket_channel_client.h"
#include "third_party/blink/renderer/modules/websockets/websocket_close_info.h"
#include "third_party/blink/renderer/modules/websockets/websocket_stream_options.h"
#include "third_party/blink/renderer/platform/bindings/exception_code.h"
namespace blink {
namespace {
using ::testing::_;
using ::testing::InSequence;
using ::testing::Return;
typedef testing::StrictMock<testing::MockFunction<void(int)>>
Checkpoint; // NOLINT
class WebSocketStreamTest : public ::testing::Test {
public:
WebSocketStreamTest() : channel_(MockWebSocketChannel::Create()) {}
void TearDown() override {
testing::Mock::VerifyAndClear(channel_);
channel_ = nullptr;
}
// Returns a reference for easy use with EXPECT_CALL(Channel(), ...).
MockWebSocketChannel& Channel() const { return *channel_; }
WebSocketStream* Create(ScriptState* script_state,
const String& url,
ExceptionState& exception_state) {
return Create(script_state, url, WebSocketStreamOptions::Create(),
exception_state);
}
WebSocketStream* Create(ScriptState* script_state,
const String& url,
WebSocketStreamOptions* options,
ExceptionState& exception_state) {
return WebSocketStream::CreateForTesting(script_state, url, options,
channel_, exception_state);
}
private:
Persistent<MockWebSocketChannel> channel_;
};
TEST_F(WebSocketStreamTest, ConstructWithBadURL) {
V8TestingScope scope;
auto& exception_state = scope.GetExceptionState();
auto* stream = Create(scope.GetScriptState(), "bad-scheme:", exception_state);
EXPECT_FALSE(stream);
EXPECT_TRUE(exception_state.HadException());
EXPECT_EQ(DOMExceptionCode::kSyntaxError,
exception_state.CodeAs<DOMExceptionCode>());
EXPECT_EQ(
"The URL's scheme must be either 'ws' or 'wss'. 'bad-scheme' is not "
"allowed.",
scope.GetExceptionState().Message());
}
// Most coverage for bad constructor arguments is provided by
// dom_websocket_test.cc.
// TODO(ricea): Should we duplicate those tests here?
TEST_F(WebSocketStreamTest, Connect) {
V8TestingScope scope;
{
InSequence s;
EXPECT_CALL(Channel(), Connect(KURL("ws://example.com/hoge"), String()))
.WillOnce(Return(true));
}
auto* stream = Create(scope.GetScriptState(), "ws://example.com/hoge",
ASSERT_NO_EXCEPTION);
EXPECT_TRUE(stream);
EXPECT_EQ(KURL("ws://example.com/hoge"), stream->url());
}
TEST_F(WebSocketStreamTest, ConnectWithProtocols) {
V8TestingScope scope;
{
InSequence s;
EXPECT_CALL(Channel(),
Connect(KURL("ws://example.com/chat"), String("chat0, chat1")))
.WillOnce(Return(true));
}
auto* options = WebSocketStreamOptions::Create();
options->setProtocols({"chat0", "chat1"});
auto* stream = Create(scope.GetScriptState(), "ws://example.com/chat",
options, ASSERT_NO_EXCEPTION);
EXPECT_TRUE(stream);
EXPECT_EQ(KURL("ws://example.com/chat"), stream->url());
}
TEST_F(WebSocketStreamTest, ConnectWithFailedHandshake) {
V8TestingScope scope;
{
InSequence s;
EXPECT_CALL(Channel(), Connect(KURL("ws://example.com/chat"), String()))
.WillOnce(Return(true));
EXPECT_CALL(Channel(), Disconnect());
}
auto* stream = Create(scope.GetScriptState(), "ws://example.com/chat",
ASSERT_NO_EXCEPTION);
EXPECT_TRUE(stream);
EXPECT_EQ(KURL("ws://example.com/chat"), stream->url());
stream->DidError();
stream->DidClose(WebSocketChannelClient::kClosingHandshakeIncomplete,
WebSocketChannel::kCloseEventCodeAbnormalClosure, String());
// TODO(ricea): Verify the promises are rejected correctly.
}
TEST_F(WebSocketStreamTest, ConnectWithSuccessfulHandshake) {
V8TestingScope scope;
Checkpoint checkpoint;
{
InSequence s;
EXPECT_CALL(Channel(),
Connect(KURL("ws://example.com/chat"), String("chat")))
.WillOnce(Return(true));
EXPECT_CALL(Channel(), ApplyBackpressure());
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(Channel(), Close(1001, String()));
}
auto* options = WebSocketStreamOptions::Create();
options->setProtocols({"chat"});
auto* stream = Create(scope.GetScriptState(), "ws://example.com/chat",
options, ASSERT_NO_EXCEPTION);
EXPECT_TRUE(stream);
EXPECT_EQ(KURL("ws://example.com/chat"), stream->url());
stream->DidConnect("chat", "permessage-deflate");
// TODO(ricea): Verify the connection promise is resolved correctly.
// Destruction of V8TestingScope causes Close() to be called.
checkpoint.Call(1);
}
TEST_F(WebSocketStreamTest, ConnectThenCloseCleanly) {
V8TestingScope scope;
{
InSequence s;
EXPECT_CALL(Channel(), Connect(KURL("ws://example.com/echo"), String()))
.WillOnce(Return(true));
EXPECT_CALL(Channel(), ApplyBackpressure());
EXPECT_CALL(Channel(), Close(-1, String("")));
EXPECT_CALL(Channel(), Disconnect());
}
auto* stream = Create(scope.GetScriptState(), "ws://example.com/echo",
ASSERT_NO_EXCEPTION);
EXPECT_TRUE(stream);
stream->DidConnect("", "");
stream->close(MakeGarbageCollected<WebSocketCloseInfo>(),
scope.GetExceptionState());
stream->DidClose(WebSocketChannelClient::kClosingHandshakeComplete, 1005, "");
}
TEST_F(WebSocketStreamTest, CloseDuringHandshake) {
V8TestingScope scope;
{
InSequence s;
EXPECT_CALL(Channel(), Connect(KURL("ws://example.com/echo"), String()))
.WillOnce(Return(true));
EXPECT_CALL(
Channel(),
FailMock(
String("WebSocket is closed before the connection is established."),
mojom::ConsoleMessageLevel::kWarning, _));
EXPECT_CALL(Channel(), Disconnect());
}
auto* stream = Create(scope.GetScriptState(), "ws://example.com/echo",
ASSERT_NO_EXCEPTION);
EXPECT_TRUE(stream);
stream->close(MakeGarbageCollected<WebSocketCloseInfo>(),
scope.GetExceptionState());
stream->DidClose(WebSocketChannelClient::kClosingHandshakeIncomplete, 1006,
"");
}
} // namespace
} // namespace blink
...@@ -1763,6 +1763,10 @@ ...@@ -1763,6 +1763,10 @@
status: "experimental", status: "experimental",
depends_on: ["WebShare"], depends_on: ["WebShare"],
}, },
{
name: "WebSocketStream",
status: "experimental",
},
{ {
name: "WebUSB", name: "WebUSB",
status: "stable", status: "stable",
......
#!/usr/bin/python
from mod_pywebsocket import common
import time
def web_socket_do_extra_handshake(request):
pass
def web_socket_transfer_data(request):
# Wait for the close frame to arrive.
request.ws_stream.receive_message()
def web_socket_passive_closing_handshake(request):
# Echo close status code and reason
code, reason = request.ws_close_code, request.ws_close_reason
# No status received is a reserved pseudo code representing an empty code,
# so echo back an empty code in this case.
if code == common.STATUS_NO_STATUS_RECEIVED:
code = None
# The browser may error the connection if the closing handshake takes too
# long, but hopefully no browser will have a timeout this short.
time.sleep(1)
return code, reason
# 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.
// META: script=../websocket.sub.js
// META: script=resources/url-constants.js
// META: global=window,worker
promise_test(async () => {
const wss = new WebSocketStream(ECHOURL);
await wss.connection;
wss.close({code: 3456, reason: 'pizza'});
const { code, reason } = await wss.closed;
assert_equals(code, 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.connection;
wss.close();
const { code, reason } = await wss.closed;
assert_equals(code, 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.connection;
wss.close({});
const { code, reason } = await wss.closed;
assert_equals(code, 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.connection;
wss.close({reason: ''});
const { code, reason } = await wss.closed;
assert_equals(code, 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.connection;
wss.close({reason: 'non-empty'});
const { code, reason } = await wss.closed;
assert_equals(code, 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.connection;
assert_throws(new 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.connection;
const reason = '.'.repeat(124);
assert_throws('SyntaxError', () => wss.close({ reason }),
'close should throw a TypeError');
}, 'close() with an overlong reason should throw');
promise_test(t => {
const wss = new WebSocketStream(ECHOURL);
wss.close();
return Promise.all([
promise_rejects(t, 'NetworkError', wss.connection,
'connection promise should reject'),
promise_rejects(t, 'NetworkError', wss.closed,
'closed promise should reject')]);
}, 'close during handshake should work');
for (const invalidCode of [999, 1001, 2999, 5000]) {
promise_test(async () => {
const wss = new WebSocketStream(ECHOURL);
await wss.connection;
assert_throws('InvalidAccessError', () => wss.close({ code: invalidCode }),
'close should throw a TypeError');
}, `close() with invalid code ${invalidCode} should throw`);
}
promise_test(async () => {
const wss = new WebSocketStream(ECHOURL);
const { writable } = await wss.connection;
writable.getWriter().close();
const { code, reason } = await wss.closed;
assert_equals(code, 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.connection;
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.connection;
info[stream][method]();
const { code, reason } = await wss.closed;
assert_equals(code, 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.connection;
info[stream][method]({ code: 3333 });
const { code, reason } = await wss.closed;
assert_equals(code, 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.connection;
info[stream][method]({ code: 3456, reason: 'set' });
const { code, reason } = await wss.closed;
assert_equals(code, 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.connection;
info[stream][method]({ reason: 'specified' });
const { code, reason } = await wss.closed;
assert_equals(code, 1005, 'code should be unset');
assert_equals(reason, '', 'reason should be empty');
}, `${voweling} the ${stream} with a reason but no code should be ignored`);
promise_test(async () => {
const wss = new WebSocketStream(ECHOURL);
const info = await wss.connection;
info[stream][method]({ code: 999 });
const { code, reason } = await wss.closed;
assert_equals(code, 1005, 'code should be unset');
assert_equals(reason, '', 'reason should be empty');
}, `${voweling} the ${stream} with an invalid code should be ignored`);
promise_test(async () => {
const wss = new WebSocketStream(ECHOURL);
const info = await wss.connection;
info[stream][method]({ code: 1000, reason: 'x'.repeat(128) });
const { code, reason } = await wss.closed;
assert_equals(code, 1005, 'code should be unset');
assert_equals(reason, '', 'reason should be empty');
}, `${voweling} the ${stream} with an invalid reason should be ignored`);
// DOMExceptions are only ignored because the |code| attribute is too small to
// be a valid WebSocket close code.
promise_test(async () => {
const wss = new WebSocketStream(ECHOURL);
const info = await wss.connection;
info[stream][method](new DOMException('yes', 'DataCloneError'));
const { code, reason } = await wss.closed;
assert_equals(code, 1005, 'code should be unset');
assert_equals(reason, '', 'reason should be empty');
}, `${voweling} the ${stream} with a DOMException should be ignored`);
}
// META: script=../websocket.sub.js
// META: script=resources/url-constants.js
// META: global=window,worker
test(() => {
assert_throws(new TypeError(), () => new WebSocketStream(),
'constructor should throw');
}, 'constructing with no URL should throw');
test(() => {
assert_throws(new SyntaxError(), () => new WebSocketStream('invalid:'),
"constructor should throw");
}, 'constructing with an invalid URL should throw');
test(() => {
assert_throws(new TypeError(),
() => new WebSocketStream(`${BASEURL}/`, true),
"constructor should throw");
}, 'constructing with invalid options should throw');
test(() => {
assert_throws(new 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.connection;
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.connection;
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');
promise_test(t => {
const wss = new WebSocketStream(`${BASEURL}/404`);
return Promise.all([
promise_rejects(t, 'NetworkError', wss.connection,
'connection should reject'),
promise_rejects(t, 'NetworkError', wss.closed, 'closed should reject')
]);
}, 'connection failure should reject the promises');
promise_test(async () => {
const wss = new WebSocketStream(ECHOURL);
const { readable, writable, protocol, extensions} = await wss.connection;
// 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.connection should resolve to the right types');
// The file including this must also include ../websocket.sub.js to pick up the
// necessary constants.
const {BASEURL, ECHOURL} = (() => {
const isSecure = location.href.match(/^https:/);
const scheme = isSecure ? "wss:" : "ws:";
const port = isSecure ? __SECURE__PORT : __PORT;
const BASEURL = `${scheme}//${__SERVER__NAME}:${port}`;
const ECHOURL = `${BASEURL}/echo`;
return {BASEURL, ECHOURL};
})();
...@@ -3594,6 +3594,13 @@ interface WebSocket : EventTarget ...@@ -3594,6 +3594,13 @@ interface WebSocket : EventTarget
setter onerror setter onerror
setter onmessage setter onmessage
setter onopen setter onopen
interface WebSocketStream
attribute @@toStringTag
getter closed
getter connection
getter url
method close
method constructor
interface WindowClient : Client interface WindowClient : Client
attribute @@toStringTag attribute @@toStringTag
getter focused getter focused
......
...@@ -3656,6 +3656,13 @@ Starting worker: resources/global-interface-listing-worker.js ...@@ -3656,6 +3656,13 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] setter onerror [Worker] setter onerror
[Worker] setter onmessage [Worker] setter onmessage
[Worker] setter onopen [Worker] setter onopen
[Worker] interface WebSocketStream
[Worker] attribute @@toStringTag
[Worker] getter closed
[Worker] getter connection
[Worker] getter url
[Worker] method close
[Worker] method constructor
[Worker] interface Worker : EventTarget [Worker] interface Worker : EventTarget
[Worker] attribute @@toStringTag [Worker] attribute @@toStringTag
[Worker] getter onerror [Worker] getter onerror
......
...@@ -10624,6 +10624,13 @@ interface WebSocket : EventTarget ...@@ -10624,6 +10624,13 @@ interface WebSocket : EventTarget
setter onerror setter onerror
setter onmessage setter onmessage
setter onopen setter onopen
interface WebSocketStream
attribute @@toStringTag
getter closed
getter connection
getter url
method close
method constructor
interface WheelEvent : MouseEvent interface WheelEvent : MouseEvent
attribute @@toStringTag attribute @@toStringTag
attribute DOM_DELTA_LINE attribute DOM_DELTA_LINE
......
...@@ -3514,6 +3514,13 @@ Starting worker: resources/global-interface-listing-worker.js ...@@ -3514,6 +3514,13 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] setter onerror [Worker] setter onerror
[Worker] setter onmessage [Worker] setter onmessage
[Worker] setter onopen [Worker] setter onopen
[Worker] interface WebSocketStream
[Worker] attribute @@toStringTag
[Worker] getter closed
[Worker] getter connection
[Worker] getter url
[Worker] method close
[Worker] method constructor
[Worker] interface WorkerGlobalScope : EventTarget [Worker] interface WorkerGlobalScope : EventTarget
[Worker] attribute @@toStringTag [Worker] attribute @@toStringTag
[Worker] getter addressSpace [Worker] getter addressSpace
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment