Skip to content

Commit

Permalink
Update client_xhr_transport_test to avoid dart:html, updating xhr_tra…
Browse files Browse the repository at this point in the history
…nsport to support testability
  • Loading branch information
aran committed Dec 16, 2024
1 parent 93909b7 commit c7b9125
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 32 deletions.
110 changes: 107 additions & 3 deletions lib/src/client/transport/xhr_transport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Event> get onReadyStateChange;
Stream<ProgressEvent> get onProgress;
Stream<ProgressEvent> get onError;
int get readyState;
JSAny? get response;
String get responseText;
Map<String, String> 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<Event> get onReadyStateChange => _xhr.onReadyStateChange;
@override
Stream<ProgressEvent> get onProgress => _xhr.onProgress;
@override
Stream<ProgressEvent> get onError => _xhr.onError;
@override
int get readyState => _xhr.readyState;
@override
Map<String, String> 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;

Expand All @@ -160,15 +258,15 @@ class XhrClientConnection implements ClientConnection {
String get scheme => uri.scheme;

void _initializeRequest(
XMLHttpRequest request, Map<String, String> metadata) {
IXMLHttpRequest request, Map<String, String> metadata) {
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';
}

@visibleForTesting
XMLHttpRequest createHttpRequest() => XMLHttpRequest();
IXMLHttpRequest createHttpRequest() => XMLHttpRequestImpl();

@override
GrpcTransportStream makeRequest(String path, Duration? timeout,
Expand Down Expand Up @@ -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);
}
Expand Down
58 changes: 29 additions & 29 deletions test/client_tests/client_xhr_transport_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<Event> readyStateChangeController =
Expand All @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -226,15 +230,15 @@ void main() {

when(transport.latestRequest.responseHeaders).thenReturn(responseHeaders);
when(transport.latestRequest.response)
.thenReturn(String.fromCharCodes(frame(<int>[])));
.thenReturn(String.fromCharCodes(frame(<int>[])).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);

Expand Down Expand Up @@ -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);

Expand All @@ -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);

Expand All @@ -338,16 +339,15 @@ void main() {
final data = List<int>.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);

Expand All @@ -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;
Expand Down Expand Up @@ -398,20 +398,20 @@ 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.
var first = true;
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));

Expand Down

0 comments on commit c7b9125

Please sign in to comment.