From 74897b2cc9e73e6b41340b31508e1d2d295d5263 Mon Sep 17 00:00:00 2001 From: minoic Date: Mon, 16 Dec 2024 09:15:03 -0800 Subject: [PATCH 1/9] update: Migrate off legacy JS/HTML apis --- lib/grpc_or_grpcweb.dart | 4 +- lib/src/client/transport/xhr_transport.dart | 77 +++++++++++---------- pubspec.yaml | 1 + 3 files changed, 43 insertions(+), 39 deletions(-) diff --git a/lib/grpc_or_grpcweb.dart b/lib/grpc_or_grpcweb.dart index b23bed5d..dffbde2e 100644 --- a/lib/grpc_or_grpcweb.dart +++ b/lib/grpc_or_grpcweb.dart @@ -13,8 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'src/client/grpc_or_grpcweb_channel_grpc.dart' - if (dart.library.html) 'src/client/grpc_or_grpcweb_channel_web.dart'; +import 'src/client/grpc_or_grpcweb_channel_web.dart' + if (dart.library.io) 'src/client/grpc_or_grpcweb_channel_grpc.dart'; import 'src/client/http2_channel.dart'; import 'src/client/options.dart'; diff --git a/lib/src/client/transport/xhr_transport.dart b/lib/src/client/transport/xhr_transport.dart index 16b0dca5..d3293e8e 100644 --- a/lib/src/client/transport/xhr_transport.dart +++ b/lib/src/client/transport/xhr_transport.dart @@ -14,10 +14,11 @@ // limitations under the License. import 'dart:async'; -import 'dart:html'; +import 'dart:js_interop'; import 'dart:typed_data'; import 'package:meta/meta.dart'; +import 'package:web/web.dart'; import '../../client/call.dart'; import '../../shared/message.dart'; @@ -30,7 +31,7 @@ import 'web_streams.dart'; const _contentTypeKey = 'Content-Type'; class XhrTransportStream implements GrpcTransportStream { - final HttpRequest _request; + final XMLHttpRequest _request; final ErrorHandler _onError; final Function(XhrTransportStream stream) _onDone; bool _headersReceived = false; @@ -45,23 +46,22 @@ class XhrTransportStream implements GrpcTransportStream { @override StreamSink> get outgoingMessages => _outgoingMessages.sink; - XhrTransportStream(this._request, - {required ErrorHandler onError, required onDone}) + XhrTransportStream(this._request, {required ErrorHandler onError, required onDone}) : _onError = onError, _onDone = onDone { _outgoingMessages.stream .map(frame) - .listen((data) => _request.send(data), cancelOnError: true); + .listen((data) => _request.send(Int8List.fromList(data).toJS), cancelOnError: true, onError: _onError); - _request.onReadyStateChange.listen((data) { + _request.onReadyStateChange.listen((_) { if (_incomingProcessor.isClosed) { return; } switch (_request.readyState) { - case HttpRequest.HEADERS_RECEIVED: + case 2: _onHeadersReceived(); break; - case HttpRequest.DONE: + case 4: _onRequestDone(); _close(); break; @@ -72,8 +72,7 @@ class XhrTransportStream implements GrpcTransportStream { if (_incomingProcessor.isClosed) { return; } - _onError(GrpcError.unavailable('XhrConnection connection-error'), - StackTrace.current); + _onError(GrpcError.unavailable('XhrConnection connection-error'), StackTrace.current); terminate(); }); @@ -81,27 +80,21 @@ class XhrTransportStream implements GrpcTransportStream { if (_incomingProcessor.isClosed) { return; } - // Use response over responseText as most browsers don't support - // using responseText during an onProgress event. - final responseString = _request.response as String; - final bytes = Uint8List.fromList( - responseString.substring(_requestBytesRead).codeUnits) - .buffer; - _requestBytesRead = responseString.length; + final responseText = _request.responseText; + final bytes = Uint8List.fromList(responseText.substring(_requestBytesRead).codeUnits).buffer; + _requestBytesRead = responseText.length; _incomingProcessor.add(bytes); }); _incomingProcessor.stream .transform(GrpcWebDecoder()) .transform(grpcDecompressor()) - .listen(_incomingMessages.add, - onError: _onError, onDone: _incomingMessages.close); + .listen(_incomingMessages.add, onError: _onError, onDone: _incomingMessages.close); } bool _validateResponseState() { try { - validateHttpStatusAndContentType( - _request.status, _request.responseHeaders, + validateHttpStatusAndContentType(_request.status, _parseHeaders(_request.getAllResponseHeaders()), rawResponse: _request.responseText); return true; } catch (e, st) { @@ -115,17 +108,15 @@ class XhrTransportStream implements GrpcTransportStream { if (!_validateResponseState()) { return; } - _incomingMessages.add(GrpcMetadata(_request.responseHeaders)); + _incomingMessages.add(GrpcMetadata(_parseHeaders(_request.getAllResponseHeaders()))); } void _onRequestDone() { if (!_headersReceived && !_validateResponseState()) { return; } - if (_request.response == null) { - _onError( - GrpcError.unavailable('XhrConnection request null response', null, - _request.responseText), + if (_request.status != 200) { + _onError(GrpcError.unavailable('Request failed with status: ${_request.status}', null, _request.responseText), StackTrace.current); return; } @@ -137,6 +128,20 @@ class XhrTransportStream implements GrpcTransportStream { _onDone(this); } + Map _parseHeaders(String rawHeaders) { + final headers = {}; + final lines = rawHeaders.split('\r\n'); + for (var line in lines) { + final index = line.indexOf(': '); + if (index != -1) { + final key = line.substring(0, index); + final value = line.substring(index + 2); + headers[key] = value; + } + } + return headers; + } + @override Future terminate() async { _close(); @@ -153,24 +158,24 @@ class XhrClientConnection implements ClientConnection { @override String get authority => uri.authority; + @override String get scheme => uri.scheme; - void _initializeRequest(HttpRequest request, Map metadata) { - for (final header in metadata.keys) { - request.setRequestHeader(header, metadata[header]!); - } + void _initializeRequest(XMLHttpRequest request, Map metadata) { + metadata.forEach((key, value) { + request.setRequestHeader(key, value); + }); // Overriding the mimetype allows us to stream and parse the data request.overrideMimeType('text/plain; charset=x-user-defined'); request.responseType = 'text'; } @visibleForTesting - HttpRequest createHttpRequest() => HttpRequest(); + XMLHttpRequest createHttpRequest() => XMLHttpRequest(); @override - GrpcTransportStream makeRequest(String path, Duration? timeout, - Map metadata, ErrorHandler onError, + GrpcTransportStream makeRequest(String path, Duration? timeout, Map metadata, ErrorHandler onError, {CallOptions? callOptions}) { // gRPC-web headers. if (_getContentTypeHeader(metadata) == null) { @@ -180,8 +185,7 @@ class XhrClientConnection implements ClientConnection { } var requestUri = uri.resolve(path); - if (callOptions is WebCallOptions && - callOptions.bypassCorsPreflight == true) { + if (callOptions is WebCallOptions && callOptions.bypassCorsPreflight == true) { requestUri = cors.moveHttpHeadersToQueryParam(metadata, requestUri); } @@ -193,8 +197,7 @@ class XhrClientConnection implements ClientConnection { // Must set headers after calling open(). _initializeRequest(request, metadata); - final transportStream = - XhrTransportStream(request, onError: onError, onDone: _removeStream); + final transportStream = XhrTransportStream(request, onError: onError, onDone: _removeStream); _requests.add(transportStream); return transportStream; } diff --git a/pubspec.yaml b/pubspec.yaml index 8c393dba..cde369f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: http2: ^2.2.0 protobuf: '>=2.0.0 <4.0.0' clock: ^1.1.1 + web: ^1.0.0 dev_dependencies: build_runner: ^2.0.0 From d49f9f7414872fae994e8cc490579ad41e72d9bd Mon Sep 17 00:00:00 2001 From: minoic Date: Mon, 16 Dec 2024 09:15:03 -0800 Subject: [PATCH 2/9] update: use dart.library.js_interop in place of dart.library.html --- CHANGELOG.md | 1 + lib/grpc_or_grpcweb.dart | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 684f1d6a..368ef92e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * When the keep alive manager runs into a timeout, it will finish the transport instead of closing the connection, as defined in the gRPC spec. * Upgrade to `package:lints` version 5.0.0 and Dart SDK version 3.5.0. +* Update xhr transport to migrate off legacy JS/HTML apis. ## 4.0.1 diff --git a/lib/grpc_or_grpcweb.dart b/lib/grpc_or_grpcweb.dart index dffbde2e..049ceabf 100644 --- a/lib/grpc_or_grpcweb.dart +++ b/lib/grpc_or_grpcweb.dart @@ -13,8 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'src/client/grpc_or_grpcweb_channel_web.dart' - if (dart.library.io) 'src/client/grpc_or_grpcweb_channel_grpc.dart'; +import 'src/client/grpc_or_grpcweb_channel_grpc.dart' + if (dart.library.js_interop) 'src/client/grpc_or_grpcweb_channel_web.dart'; import 'src/client/http2_channel.dart'; import 'src/client/options.dart'; From 8313a4dc79590de0453e0178de1611b9210cbaf7 Mon Sep 17 00:00:00 2001 From: minoic Date: Mon, 16 Dec 2024 09:15:03 -0800 Subject: [PATCH 3/9] update: dart format xhr_transport.dart and update dart sdk to v3.4.0 in workflows --- lib/src/client/transport/xhr_transport.dart | 44 ++++++++++++++------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/lib/src/client/transport/xhr_transport.dart b/lib/src/client/transport/xhr_transport.dart index d3293e8e..69cb78a8 100644 --- a/lib/src/client/transport/xhr_transport.dart +++ b/lib/src/client/transport/xhr_transport.dart @@ -46,12 +46,14 @@ class XhrTransportStream implements GrpcTransportStream { @override StreamSink> get outgoingMessages => _outgoingMessages.sink; - XhrTransportStream(this._request, {required ErrorHandler onError, required onDone}) + XhrTransportStream(this._request, + {required ErrorHandler onError, required onDone}) : _onError = onError, _onDone = onDone { - _outgoingMessages.stream - .map(frame) - .listen((data) => _request.send(Int8List.fromList(data).toJS), cancelOnError: true, onError: _onError); + _outgoingMessages.stream.map(frame).listen( + (data) => _request.send(Int8List.fromList(data).toJS), + cancelOnError: true, + onError: _onError); _request.onReadyStateChange.listen((_) { if (_incomingProcessor.isClosed) { @@ -72,7 +74,8 @@ class XhrTransportStream implements GrpcTransportStream { if (_incomingProcessor.isClosed) { return; } - _onError(GrpcError.unavailable('XhrConnection connection-error'), StackTrace.current); + _onError(GrpcError.unavailable('XhrConnection connection-error'), + StackTrace.current); terminate(); }); @@ -81,7 +84,9 @@ class XhrTransportStream implements GrpcTransportStream { return; } final responseText = _request.responseText; - final bytes = Uint8List.fromList(responseText.substring(_requestBytesRead).codeUnits).buffer; + final bytes = Uint8List.fromList( + responseText.substring(_requestBytesRead).codeUnits) + .buffer; _requestBytesRead = responseText.length; _incomingProcessor.add(bytes); }); @@ -89,12 +94,14 @@ class XhrTransportStream implements GrpcTransportStream { _incomingProcessor.stream .transform(GrpcWebDecoder()) .transform(grpcDecompressor()) - .listen(_incomingMessages.add, onError: _onError, onDone: _incomingMessages.close); + .listen(_incomingMessages.add, + onError: _onError, onDone: _incomingMessages.close); } bool _validateResponseState() { try { - validateHttpStatusAndContentType(_request.status, _parseHeaders(_request.getAllResponseHeaders()), + validateHttpStatusAndContentType( + _request.status, _parseHeaders(_request.getAllResponseHeaders()), rawResponse: _request.responseText); return true; } catch (e, st) { @@ -108,7 +115,8 @@ class XhrTransportStream implements GrpcTransportStream { if (!_validateResponseState()) { return; } - _incomingMessages.add(GrpcMetadata(_parseHeaders(_request.getAllResponseHeaders()))); + _incomingMessages + .add(GrpcMetadata(_parseHeaders(_request.getAllResponseHeaders()))); } void _onRequestDone() { @@ -116,7 +124,11 @@ class XhrTransportStream implements GrpcTransportStream { return; } if (_request.status != 200) { - _onError(GrpcError.unavailable('Request failed with status: ${_request.status}', null, _request.responseText), + _onError( + GrpcError.unavailable( + 'Request failed with status: ${_request.status}', + null, + _request.responseText), StackTrace.current); return; } @@ -162,7 +174,8 @@ class XhrClientConnection implements ClientConnection { @override String get scheme => uri.scheme; - void _initializeRequest(XMLHttpRequest request, Map metadata) { + void _initializeRequest( + XMLHttpRequest request, Map metadata) { metadata.forEach((key, value) { request.setRequestHeader(key, value); }); @@ -175,7 +188,8 @@ class XhrClientConnection implements ClientConnection { XMLHttpRequest createHttpRequest() => XMLHttpRequest(); @override - GrpcTransportStream makeRequest(String path, Duration? timeout, Map metadata, ErrorHandler onError, + GrpcTransportStream makeRequest(String path, Duration? timeout, + Map metadata, ErrorHandler onError, {CallOptions? callOptions}) { // gRPC-web headers. if (_getContentTypeHeader(metadata) == null) { @@ -185,7 +199,8 @@ class XhrClientConnection implements ClientConnection { } var requestUri = uri.resolve(path); - if (callOptions is WebCallOptions && callOptions.bypassCorsPreflight == true) { + if (callOptions is WebCallOptions && + callOptions.bypassCorsPreflight == true) { requestUri = cors.moveHttpHeadersToQueryParam(metadata, requestUri); } @@ -197,7 +212,8 @@ class XhrClientConnection implements ClientConnection { // Must set headers after calling open(). _initializeRequest(request, metadata); - final transportStream = XhrTransportStream(request, onError: onError, onDone: _removeStream); + final transportStream = + XhrTransportStream(request, onError: onError, onDone: _removeStream); _requests.add(transportStream); return transportStream; } From 609d725e340df61187eea33d6f93a0a4c1c3bf96 Mon Sep 17 00:00:00 2001 From: minoic Date: Mon, 16 Dec 2024 09:15:03 -0800 Subject: [PATCH 4/9] update: use if instead of switch case in xhr_transport.dart --- lib/src/client/transport/xhr_transport.dart | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/src/client/transport/xhr_transport.dart b/lib/src/client/transport/xhr_transport.dart index 69cb78a8..1de0eb78 100644 --- a/lib/src/client/transport/xhr_transport.dart +++ b/lib/src/client/transport/xhr_transport.dart @@ -59,14 +59,12 @@ class XhrTransportStream implements GrpcTransportStream { if (_incomingProcessor.isClosed) { return; } - switch (_request.readyState) { - case 2: - _onHeadersReceived(); - break; - case 4: - _onRequestDone(); - _close(); - break; + // TODO: dart-lang/web#285 use 'if' for now + if (_request.readyState == XMLHttpRequest.HEADERS_RECEIVED) { + _onHeadersReceived(); + } else if (_request.readyState == XMLHttpRequest.DONE) { + _onRequestDone(); + _close(); } }); From 20bd1737c4cf9c20e78cb1222f0c9b984dcab11a Mon Sep 17 00:00:00 2001 From: minoic Date: Mon, 16 Dec 2024 09:15:03 -0800 Subject: [PATCH 5/9] update: upgrade web package to v1.1.0 --- lib/src/client/transport/xhr_transport.dart | 33 +++++++-------------- pubspec.yaml | 2 +- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/lib/src/client/transport/xhr_transport.dart b/lib/src/client/transport/xhr_transport.dart index 1de0eb78..e41a7f39 100644 --- a/lib/src/client/transport/xhr_transport.dart +++ b/lib/src/client/transport/xhr_transport.dart @@ -59,12 +59,14 @@ class XhrTransportStream implements GrpcTransportStream { if (_incomingProcessor.isClosed) { return; } - // TODO: dart-lang/web#285 use 'if' for now - if (_request.readyState == XMLHttpRequest.HEADERS_RECEIVED) { - _onHeadersReceived(); - } else if (_request.readyState == XMLHttpRequest.DONE) { - _onRequestDone(); - _close(); + switch (_request.readyState) { + case XMLHttpRequest.HEADERS_RECEIVED: + _onHeadersReceived(); + break; + case XMLHttpRequest.DONE: + _onRequestDone(); + _close(); + break; } }); @@ -99,7 +101,7 @@ class XhrTransportStream implements GrpcTransportStream { bool _validateResponseState() { try { validateHttpStatusAndContentType( - _request.status, _parseHeaders(_request.getAllResponseHeaders()), + _request.status, _request.responseHeaders, rawResponse: _request.responseText); return true; } catch (e, st) { @@ -113,8 +115,7 @@ class XhrTransportStream implements GrpcTransportStream { if (!_validateResponseState()) { return; } - _incomingMessages - .add(GrpcMetadata(_parseHeaders(_request.getAllResponseHeaders()))); + _incomingMessages.add(GrpcMetadata(_request.responseHeaders)); } void _onRequestDone() { @@ -138,20 +139,6 @@ class XhrTransportStream implements GrpcTransportStream { _onDone(this); } - Map _parseHeaders(String rawHeaders) { - final headers = {}; - final lines = rawHeaders.split('\r\n'); - for (var line in lines) { - final index = line.indexOf(': '); - if (index != -1) { - final key = line.substring(0, index); - final value = line.substring(index + 2); - headers[key] = value; - } - } - return headers; - } - @override Future terminate() async { _close(); diff --git a/pubspec.yaml b/pubspec.yaml index cde369f2..02b3047a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: http2: ^2.2.0 protobuf: '>=2.0.0 <4.0.0' clock: ^1.1.1 - web: ^1.0.0 + web: ^1.1.0 dev_dependencies: build_runner: ^2.0.0 From 00a012a55fb8613c8c0e4e58ee9ab498cffb6a6d Mon Sep 17 00:00:00 2001 From: Aran Donohue Date: Mon, 16 Dec 2024 09:15:03 -0800 Subject: [PATCH 6/9] refactor: use Uint8List for sending data over XHR rather than Int8List --- lib/src/client/transport/xhr_transport.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/client/transport/xhr_transport.dart b/lib/src/client/transport/xhr_transport.dart index e41a7f39..390e4943 100644 --- a/lib/src/client/transport/xhr_transport.dart +++ b/lib/src/client/transport/xhr_transport.dart @@ -51,7 +51,7 @@ class XhrTransportStream implements GrpcTransportStream { : _onError = onError, _onDone = onDone { _outgoingMessages.stream.map(frame).listen( - (data) => _request.send(Int8List.fromList(data).toJS), + (data) => _request.send(Uint8List.fromList(data).toJS), cancelOnError: true, onError: _onError); From 93909b76685e160512cc0ece92f7776844ea7aaa Mon Sep 17 00:00:00 2001 From: Aran Donohue Date: Mon, 16 Dec 2024 09:15:03 -0800 Subject: [PATCH 7/9] refactor: eta-reduction of call to request.setRequestHeader --- lib/src/client/transport/xhr_transport.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/src/client/transport/xhr_transport.dart b/lib/src/client/transport/xhr_transport.dart index 390e4943..5e0c2291 100644 --- a/lib/src/client/transport/xhr_transport.dart +++ b/lib/src/client/transport/xhr_transport.dart @@ -161,9 +161,7 @@ class XhrClientConnection implements ClientConnection { void _initializeRequest( XMLHttpRequest request, Map metadata) { - metadata.forEach((key, value) { - request.setRequestHeader(key, value); - }); + metadata.forEach(request.setRequestHeader); // Overriding the mimetype allows us to stream and parse the data request.overrideMimeType('text/plain; charset=x-user-defined'); request.responseType = 'text'; From c7b912546ac5a5d8cfad9515bdb65c7f4f05c8d7 Mon Sep 17 00:00:00 2001 From: Aran Donohue Date: Mon, 16 Dec 2024 09:16:10 -0800 Subject: [PATCH 8/9] Update client_xhr_transport_test to avoid dart:html, updating xhr_transport to support testability --- lib/src/client/transport/xhr_transport.dart | 110 +++++++++++++++++- .../client_xhr_transport_test.dart | 58 ++++----- 2 files changed, 136 insertions(+), 32 deletions(-) diff --git a/lib/src/client/transport/xhr_transport.dart b/lib/src/client/transport/xhr_transport.dart index 5e0c2291..cb6af6d0 100644 --- a/lib/src/client/transport/xhr_transport.dart +++ b/lib/src/client/transport/xhr_transport.dart @@ -146,6 +146,104 @@ class XhrTransportStream implements GrpcTransportStream { } } +// XMLHttpRequest is an extension type and can't be extended or implemented. +// This interface is used to allow for mocking XMLHttpRequest in tests of +// XhrClientConnection. +@visibleForTesting +abstract interface class IXMLHttpRequest { + Stream get onReadyStateChange; + Stream get onProgress; + Stream get onError; + int get readyState; + JSAny? get response; + String get responseText; + Map get responseHeaders; + int get status; + + set responseType(String responseType); + set withCredentials(bool withCredentials); + + void open( + String method, + String url, [ + // external default is true + bool async = true, + String? username, + String? password, + ]); + void overrideMimeType(String mimeType); + void send([JSAny? body]); + void setRequestHeader(String header, String value); + + // This method should only be used in production code. + XMLHttpRequest toXMLHttpRequest(); +} + +// IXMLHttpRequest that delegates to a real XMLHttpRequest. +class XMLHttpRequestImpl implements IXMLHttpRequest { + final XMLHttpRequest _xhr = XMLHttpRequest(); + + XMLHttpRequestImpl(); + + @override + Stream get onReadyStateChange => _xhr.onReadyStateChange; + @override + Stream get onProgress => _xhr.onProgress; + @override + Stream get onError => _xhr.onError; + @override + int get readyState => _xhr.readyState; + @override + Map get responseHeaders => _xhr.responseHeaders; + @override + JSAny? get response => _xhr.response; + @override + String get responseText => _xhr.responseText; + @override + int get status => _xhr.status; + + @override + set responseType(String responseType) { + _xhr.responseType = responseType; + } + + @override + set withCredentials(bool withCredentials) { + _xhr.withCredentials = withCredentials; + } + + @override + void open( + String method, + String url, [ + bool async = true, + String? username, + String? password, + ]) { + _xhr.open(method, url, async, username, password); + } + + @override + void overrideMimeType(String mimeType) { + _xhr.overrideMimeType(mimeType); + } + + @override + void setRequestHeader(String header, String value) { + _xhr.setRequestHeader(header, value); + } + + @override + void send([JSAny? body]) { + _xhr.send(body); + } + + @override + XMLHttpRequest toXMLHttpRequest() { + return _xhr; + } +} + class XhrClientConnection implements ClientConnection { final Uri uri; @@ -160,7 +258,7 @@ class XhrClientConnection implements ClientConnection { String get scheme => uri.scheme; void _initializeRequest( - XMLHttpRequest request, Map metadata) { + IXMLHttpRequest request, Map metadata) { metadata.forEach(request.setRequestHeader); // Overriding the mimetype allows us to stream and parse the data request.overrideMimeType('text/plain; charset=x-user-defined'); @@ -168,7 +266,7 @@ class XhrClientConnection implements ClientConnection { } @visibleForTesting - XMLHttpRequest createHttpRequest() => XMLHttpRequest(); + IXMLHttpRequest createHttpRequest() => XMLHttpRequestImpl(); @override GrpcTransportStream makeRequest(String path, Duration? timeout, @@ -196,11 +294,17 @@ class XhrClientConnection implements ClientConnection { _initializeRequest(request, metadata); final transportStream = - XhrTransportStream(request, onError: onError, onDone: _removeStream); + _createXhrTransportStream(request, onError, _removeStream); _requests.add(transportStream); return transportStream; } + XhrTransportStream _createXhrTransportStream(IXMLHttpRequest request, + ErrorHandler onError, void Function(XhrTransportStream stream) onDone) { + return XhrTransportStream(request.toXMLHttpRequest(), + onError: onError, onDone: onDone); + } + void _removeStream(XhrTransportStream stream) { _requests.remove(stream); } diff --git a/test/client_tests/client_xhr_transport_test.dart b/test/client_tests/client_xhr_transport_test.dart index 1c58077f..31fe48d2 100644 --- a/test/client_tests/client_xhr_transport_test.dart +++ b/test/client_tests/client_xhr_transport_test.dart @@ -16,7 +16,7 @@ library; import 'dart:async'; -import 'dart:html'; +import 'dart:js_interop'; import 'package:async/async.dart'; import 'package:grpc/src/client/call.dart'; @@ -26,12 +26,13 @@ import 'package:grpc/src/shared/status.dart'; import 'package:mockito/mockito.dart'; import 'package:stream_transform/stream_transform.dart'; import 'package:test/test.dart'; +import 'package:web/web.dart'; final readyStateChangeEvent = - Event('readystatechange', canBubble: false, cancelable: false); + Event('readystatechange', EventInit(bubbles: false, cancelable: false)); final progressEvent = ProgressEvent('onloadstart'); -class MockHttpRequest extends Mock implements HttpRequest { +class MockHttpRequest extends Mock implements IXMLHttpRequest { MockHttpRequest({int? code}) : status = code ?? 200; // ignore: close_sinks StreamController readyStateChangeController = @@ -52,6 +53,10 @@ class MockHttpRequest extends Mock implements HttpRequest { @override final int status; + // Some test code expects to call this + set readyState(int state); + set responseText(String text); + @override int get readyState => super.noSuchMethod(Invocation.getter(#readyState), returnValue: -1); @@ -71,7 +76,7 @@ class MockXhrClientConnection extends XhrClientConnection { final int _statusCode; @override - HttpRequest createHttpRequest() { + IXMLHttpRequest createHttpRequest() { final request = MockHttpRequest(code: _statusCode); latestRequest = request; return request; @@ -208,8 +213,7 @@ void main() { await stream.terminate(); final expectedData = frame(data); - expect(verify(connection.latestRequest.send(captureAny)).captured.single, - expectedData); + verify(connection.latestRequest.send(expectedData.toJSBox)); }); test('Stream handles headers properly', () async { @@ -226,15 +230,15 @@ void main() { when(transport.latestRequest.responseHeaders).thenReturn(responseHeaders); when(transport.latestRequest.response) - .thenReturn(String.fromCharCodes(frame([]))); + .thenReturn(String.fromCharCodes(frame([])).toJS); // Set expectation for request readyState and generate two readyStateChange // events, so that incomingMessages stream completes. - final readyStates = [HttpRequest.HEADERS_RECEIVED, HttpRequest.DONE]; - when(transport.latestRequest.readyState) - .thenAnswer((_) => readyStates.removeAt(0)); + final readyStates = [XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE]; + transport.latestRequest.readyState = readyStates[0]; transport.latestRequest.readyStateChangeController .add(readyStateChangeEvent); + transport.latestRequest.readyState = readyStates[1]; transport.latestRequest.readyStateChangeController .add(readyStateChangeEvent); @@ -267,16 +271,15 @@ void main() { final encodedString = String.fromCharCodes(encodedTrailers); when(connection.latestRequest.responseHeaders).thenReturn(requestHeaders); - when(connection.latestRequest.response).thenReturn(encodedString); + when(connection.latestRequest.response).thenReturn(encodedString.toJS); // Set expectation for request readyState and generate events so that // incomingMessages stream completes. - final readyStates = [HttpRequest.HEADERS_RECEIVED, HttpRequest.DONE]; - when(connection.latestRequest.readyState) - .thenAnswer((_) => readyStates.removeAt(0)); + connection.latestRequest.readyState = XMLHttpRequest.HEADERS_RECEIVED; connection.latestRequest.readyStateChangeController .add(readyStateChangeEvent); connection.latestRequest.progressController.add(progressEvent); + connection.latestRequest.readyState = XMLHttpRequest.DONE; connection.latestRequest.readyStateChangeController .add(readyStateChangeEvent); @@ -303,16 +306,14 @@ void main() { final encodedString = String.fromCharCodes(encoded); when(connection.latestRequest.responseHeaders).thenReturn(requestHeaders); - when(connection.latestRequest.response).thenReturn(encodedString); - + when(connection.latestRequest.response).thenReturn(encodedString.toJS); // Set expectation for request readyState and generate events so that // incomingMessages stream completes. - final readyStates = [HttpRequest.HEADERS_RECEIVED, HttpRequest.DONE]; - when(connection.latestRequest.readyState) - .thenAnswer((_) => readyStates.removeAt(0)); + connection.latestRequest.readyState = XMLHttpRequest.HEADERS_RECEIVED; connection.latestRequest.readyStateChangeController .add(readyStateChangeEvent); connection.latestRequest.progressController.add(progressEvent); + connection.latestRequest.readyState = XMLHttpRequest.DONE; connection.latestRequest.readyStateChangeController .add(readyStateChangeEvent); @@ -338,16 +339,15 @@ void main() { final data = List.filled(10, 224); when(connection.latestRequest.responseHeaders).thenReturn(requestHeaders); when(connection.latestRequest.response) - .thenReturn(String.fromCharCodes(frame(data))); + .thenReturn(String.fromCharCodes(frame(data)).toJS); // Set expectation for request readyState and generate events, so that // incomingMessages stream completes. - final readyStates = [HttpRequest.HEADERS_RECEIVED, HttpRequest.DONE]; - when(connection.latestRequest.readyState) - .thenAnswer((_) => readyStates.removeAt(0)); + connection.latestRequest.readyState = XMLHttpRequest.HEADERS_RECEIVED; connection.latestRequest.readyStateChangeController .add(readyStateChangeEvent); connection.latestRequest.progressController.add(progressEvent); + connection.latestRequest.readyState = XMLHttpRequest.DONE; connection.latestRequest.readyStateChangeController .add(readyStateChangeEvent); @@ -369,8 +369,8 @@ void main() { const errorDetails = 'error details'; when(connection.latestRequest.responseHeaders) .thenReturn({'content-type': 'application/grpc+proto'}); - when(connection.latestRequest.readyState).thenReturn(HttpRequest.DONE); - when(connection.latestRequest.responseText).thenReturn(errorDetails); + connection.latestRequest.readyState = XMLHttpRequest.DONE; + connection.latestRequest.responseText = errorDetails; connection.latestRequest.readyStateChangeController .add(readyStateChangeEvent); await errorReceived.future; @@ -398,7 +398,7 @@ void main() { when(connection.latestRequest.responseHeaders).thenReturn(metadata); when(connection.latestRequest.readyState) - .thenReturn(HttpRequest.HEADERS_RECEIVED); + .thenReturn(XMLHttpRequest.HEADERS_RECEIVED); // At first invocation the response should be the the first message, after // that first + last messages. @@ -406,12 +406,12 @@ void main() { when(connection.latestRequest.response).thenAnswer((_) { if (first) { first = false; - return encodedStrings[0]; + return encodedStrings[0].toJS; } - return encodedStrings[0] + encodedStrings[1]; + return (encodedStrings[0] + encodedStrings[1]).toJS; }); - final readyStates = [HttpRequest.HEADERS_RECEIVED, HttpRequest.DONE]; + final readyStates = [XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE]; when(connection.latestRequest.readyState) .thenAnswer((_) => readyStates.removeAt(0)); From b39a6ff62d518ec035af919bfe5422549248aadd Mon Sep 17 00:00:00 2001 From: Aran Donohue Date: Fri, 14 Feb 2025 08:59:05 -0800 Subject: [PATCH 9/9] fixup tests --- lib/src/client/transport/xhr_transport.dart | 11 +++-- .../client_xhr_transport_test.dart | 48 +++++++++---------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/lib/src/client/transport/xhr_transport.dart b/lib/src/client/transport/xhr_transport.dart index cb6af6d0..693088c2 100644 --- a/lib/src/client/transport/xhr_transport.dart +++ b/lib/src/client/transport/xhr_transport.dart @@ -31,7 +31,7 @@ import 'web_streams.dart'; const _contentTypeKey = 'Content-Type'; class XhrTransportStream implements GrpcTransportStream { - final XMLHttpRequest _request; + final IXMLHttpRequest _request; final ErrorHandler _onError; final Function(XhrTransportStream stream) _onDone; bool _headersReceived = false; @@ -163,6 +163,7 @@ abstract interface class IXMLHttpRequest { set responseType(String responseType); set withCredentials(bool withCredentials); + void abort(); void open( String method, String url, [ @@ -212,6 +213,11 @@ class XMLHttpRequestImpl implements IXMLHttpRequest { _xhr.withCredentials = withCredentials; } + @override + void abort() { + _xhr.abort(); + } + @override void open( String method, @@ -301,8 +307,7 @@ class XhrClientConnection implements ClientConnection { XhrTransportStream _createXhrTransportStream(IXMLHttpRequest request, ErrorHandler onError, void Function(XhrTransportStream stream) onDone) { - return XhrTransportStream(request.toXMLHttpRequest(), - onError: onError, onDone: onDone); + return XhrTransportStream(request, onError: onError, onDone: onDone); } void _removeStream(XhrTransportStream stream) { diff --git a/test/client_tests/client_xhr_transport_test.dart b/test/client_tests/client_xhr_transport_test.dart index f5fad357..84c805ec 100644 --- a/test/client_tests/client_xhr_transport_test.dart +++ b/test/client_tests/client_xhr_transport_test.dart @@ -18,7 +18,7 @@ library; import 'dart:async'; import 'dart:js_interop'; - +import 'dart:typed_data'; import 'package:async/async.dart'; import 'package:grpc/src/client/call.dart'; import 'package:grpc/src/client/transport/xhr_transport.dart'; @@ -54,9 +54,9 @@ class MockHttpRequest extends Mock implements IXMLHttpRequest { @override final int status; - // Some test code expects to call this - set readyState(int state); - set responseText(String text); + @override + String get responseText => + super.noSuchMethod(Invocation.getter(#responseText), returnValue: ''); @override int get readyState => @@ -214,7 +214,8 @@ void main() { await stream.terminate(); final expectedData = frame(data); - verify(connection.latestRequest.send(expectedData.toJSBox)); + verify( + connection.latestRequest.send(Uint8List.fromList(expectedData).toJS)); }); test('Stream handles headers properly', () async { @@ -230,16 +231,15 @@ void main() { (error, _) => fail(error.toString())); when(transport.latestRequest.responseHeaders).thenReturn(responseHeaders); - when(transport.latestRequest.response) - .thenReturn(String.fromCharCodes(frame([])).toJS); + when(transport.latestRequest.responseText) + .thenReturn(String.fromCharCodes(frame([]))); // Set expectation for request readyState and generate two readyStateChange // events, so that incomingMessages stream completes. final readyStates = [XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE]; - transport.latestRequest.readyState = readyStates[0]; + when(transport.latestRequest.readyState).thenReturnInOrder(readyStates); transport.latestRequest.readyStateChangeController .add(readyStateChangeEvent); - transport.latestRequest.readyState = readyStates[1]; transport.latestRequest.readyStateChangeController .add(readyStateChangeEvent); @@ -272,15 +272,15 @@ void main() { final encodedString = String.fromCharCodes(encodedTrailers); when(connection.latestRequest.responseHeaders).thenReturn(requestHeaders); - when(connection.latestRequest.response).thenReturn(encodedString.toJS); + when(connection.latestRequest.responseText).thenReturn(encodedString); // Set expectation for request readyState and generate events so that // incomingMessages stream completes. - connection.latestRequest.readyState = XMLHttpRequest.HEADERS_RECEIVED; + when(connection.latestRequest.readyState).thenReturnInOrder( + [XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE]); connection.latestRequest.readyStateChangeController .add(readyStateChangeEvent); connection.latestRequest.progressController.add(progressEvent); - connection.latestRequest.readyState = XMLHttpRequest.DONE; connection.latestRequest.readyStateChangeController .add(readyStateChangeEvent); @@ -307,14 +307,14 @@ void main() { final encodedString = String.fromCharCodes(encoded); when(connection.latestRequest.responseHeaders).thenReturn(requestHeaders); - when(connection.latestRequest.response).thenReturn(encodedString.toJS); + when(connection.latestRequest.responseText).thenReturn(encodedString); // Set expectation for request readyState and generate events so that // incomingMessages stream completes. - connection.latestRequest.readyState = XMLHttpRequest.HEADERS_RECEIVED; + when(connection.latestRequest.readyState).thenReturnInOrder( + [XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE]); connection.latestRequest.readyStateChangeController .add(readyStateChangeEvent); connection.latestRequest.progressController.add(progressEvent); - connection.latestRequest.readyState = XMLHttpRequest.DONE; connection.latestRequest.readyStateChangeController .add(readyStateChangeEvent); @@ -339,16 +339,16 @@ void main() { requestHeaders, (error, _) => fail(error.toString())); final data = List.filled(10, 224); when(connection.latestRequest.responseHeaders).thenReturn(requestHeaders); - when(connection.latestRequest.response) - .thenReturn(String.fromCharCodes(frame(data)).toJS); + when(connection.latestRequest.responseText) + .thenReturn(String.fromCharCodes(frame(data))); // Set expectation for request readyState and generate events, so that // incomingMessages stream completes. - connection.latestRequest.readyState = XMLHttpRequest.HEADERS_RECEIVED; + when(connection.latestRequest.readyState).thenReturnInOrder( + [XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE]); connection.latestRequest.readyStateChangeController .add(readyStateChangeEvent); connection.latestRequest.progressController.add(progressEvent); - connection.latestRequest.readyState = XMLHttpRequest.DONE; connection.latestRequest.readyStateChangeController .add(readyStateChangeEvent); @@ -370,8 +370,8 @@ void main() { const errorDetails = 'error details'; when(connection.latestRequest.responseHeaders) .thenReturn({'content-type': 'application/grpc+proto'}); - connection.latestRequest.readyState = XMLHttpRequest.DONE; - connection.latestRequest.responseText = errorDetails; + when(connection.latestRequest.readyState).thenReturn(XMLHttpRequest.DONE); + when(connection.latestRequest.responseText).thenReturn(errorDetails); connection.latestRequest.readyStateChangeController .add(readyStateChangeEvent); await errorReceived.future; @@ -404,12 +404,12 @@ void main() { // At first invocation the response should be the the first message, after // that first + last messages. var first = true; - when(connection.latestRequest.response).thenAnswer((_) { + when(connection.latestRequest.responseText).thenAnswer((_) { if (first) { first = false; - return encodedStrings[0].toJS; + return encodedStrings[0]; } - return (encodedStrings[0] + encodedStrings[1]).toJS; + return encodedStrings[0] + encodedStrings[1]; }); final readyStates = [XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE];