Skip to content

Commit c7b9125

Browse files
committed
Update client_xhr_transport_test to avoid dart:html, updating xhr_transport to support testability
1 parent 93909b7 commit c7b9125

File tree

2 files changed

+136
-32
lines changed

2 files changed

+136
-32
lines changed

lib/src/client/transport/xhr_transport.dart

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,104 @@ class XhrTransportStream implements GrpcTransportStream {
146146
}
147147
}
148148

149+
// XMLHttpRequest is an extension type and can't be extended or implemented.
150+
// This interface is used to allow for mocking XMLHttpRequest in tests of
151+
// XhrClientConnection.
152+
@visibleForTesting
153+
abstract interface class IXMLHttpRequest {
154+
Stream<Event> get onReadyStateChange;
155+
Stream<ProgressEvent> get onProgress;
156+
Stream<ProgressEvent> get onError;
157+
int get readyState;
158+
JSAny? get response;
159+
String get responseText;
160+
Map<String, String> get responseHeaders;
161+
int get status;
162+
163+
set responseType(String responseType);
164+
set withCredentials(bool withCredentials);
165+
166+
void open(
167+
String method,
168+
String url, [
169+
// external default is true
170+
bool async = true,
171+
String? username,
172+
String? password,
173+
]);
174+
void overrideMimeType(String mimeType);
175+
void send([JSAny? body]);
176+
void setRequestHeader(String header, String value);
177+
178+
// This method should only be used in production code.
179+
XMLHttpRequest toXMLHttpRequest();
180+
}
181+
182+
// IXMLHttpRequest that delegates to a real XMLHttpRequest.
183+
class XMLHttpRequestImpl implements IXMLHttpRequest {
184+
final XMLHttpRequest _xhr = XMLHttpRequest();
185+
186+
XMLHttpRequestImpl();
187+
188+
@override
189+
Stream<Event> get onReadyStateChange => _xhr.onReadyStateChange;
190+
@override
191+
Stream<ProgressEvent> get onProgress => _xhr.onProgress;
192+
@override
193+
Stream<ProgressEvent> get onError => _xhr.onError;
194+
@override
195+
int get readyState => _xhr.readyState;
196+
@override
197+
Map<String, String> get responseHeaders => _xhr.responseHeaders;
198+
@override
199+
JSAny? get response => _xhr.response;
200+
@override
201+
String get responseText => _xhr.responseText;
202+
@override
203+
int get status => _xhr.status;
204+
205+
@override
206+
set responseType(String responseType) {
207+
_xhr.responseType = responseType;
208+
}
209+
210+
@override
211+
set withCredentials(bool withCredentials) {
212+
_xhr.withCredentials = withCredentials;
213+
}
214+
215+
@override
216+
void open(
217+
String method,
218+
String url, [
219+
bool async = true,
220+
String? username,
221+
String? password,
222+
]) {
223+
_xhr.open(method, url, async, username, password);
224+
}
225+
226+
@override
227+
void overrideMimeType(String mimeType) {
228+
_xhr.overrideMimeType(mimeType);
229+
}
230+
231+
@override
232+
void setRequestHeader(String header, String value) {
233+
_xhr.setRequestHeader(header, value);
234+
}
235+
236+
@override
237+
void send([JSAny? body]) {
238+
_xhr.send(body);
239+
}
240+
241+
@override
242+
XMLHttpRequest toXMLHttpRequest() {
243+
return _xhr;
244+
}
245+
}
246+
149247
class XhrClientConnection implements ClientConnection {
150248
final Uri uri;
151249

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

162260
void _initializeRequest(
163-
XMLHttpRequest request, Map<String, String> metadata) {
261+
IXMLHttpRequest request, Map<String, String> metadata) {
164262
metadata.forEach(request.setRequestHeader);
165263
// Overriding the mimetype allows us to stream and parse the data
166264
request.overrideMimeType('text/plain; charset=x-user-defined');
167265
request.responseType = 'text';
168266
}
169267

170268
@visibleForTesting
171-
XMLHttpRequest createHttpRequest() => XMLHttpRequest();
269+
IXMLHttpRequest createHttpRequest() => XMLHttpRequestImpl();
172270

173271
@override
174272
GrpcTransportStream makeRequest(String path, Duration? timeout,
@@ -196,11 +294,17 @@ class XhrClientConnection implements ClientConnection {
196294
_initializeRequest(request, metadata);
197295

198296
final transportStream =
199-
XhrTransportStream(request, onError: onError, onDone: _removeStream);
297+
_createXhrTransportStream(request, onError, _removeStream);
200298
_requests.add(transportStream);
201299
return transportStream;
202300
}
203301

302+
XhrTransportStream _createXhrTransportStream(IXMLHttpRequest request,
303+
ErrorHandler onError, void Function(XhrTransportStream stream) onDone) {
304+
return XhrTransportStream(request.toXMLHttpRequest(),
305+
onError: onError, onDone: onDone);
306+
}
307+
204308
void _removeStream(XhrTransportStream stream) {
205309
_requests.remove(stream);
206310
}

test/client_tests/client_xhr_transport_test.dart

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
library;
1717

1818
import 'dart:async';
19-
import 'dart:html';
19+
import 'dart:js_interop';
2020

2121
import 'package:async/async.dart';
2222
import 'package:grpc/src/client/call.dart';
@@ -26,12 +26,13 @@ import 'package:grpc/src/shared/status.dart';
2626
import 'package:mockito/mockito.dart';
2727
import 'package:stream_transform/stream_transform.dart';
2828
import 'package:test/test.dart';
29+
import 'package:web/web.dart';
2930

3031
final readyStateChangeEvent =
31-
Event('readystatechange', canBubble: false, cancelable: false);
32+
Event('readystatechange', EventInit(bubbles: false, cancelable: false));
3233
final progressEvent = ProgressEvent('onloadstart');
3334

34-
class MockHttpRequest extends Mock implements HttpRequest {
35+
class MockHttpRequest extends Mock implements IXMLHttpRequest {
3536
MockHttpRequest({int? code}) : status = code ?? 200;
3637
// ignore: close_sinks
3738
StreamController<Event> readyStateChangeController =
@@ -52,6 +53,10 @@ class MockHttpRequest extends Mock implements HttpRequest {
5253
@override
5354
final int status;
5455

56+
// Some test code expects to call this
57+
set readyState(int state);
58+
set responseText(String text);
59+
5560
@override
5661
int get readyState =>
5762
super.noSuchMethod(Invocation.getter(#readyState), returnValue: -1);
@@ -71,7 +76,7 @@ class MockXhrClientConnection extends XhrClientConnection {
7176
final int _statusCode;
7277

7378
@override
74-
HttpRequest createHttpRequest() {
79+
IXMLHttpRequest createHttpRequest() {
7580
final request = MockHttpRequest(code: _statusCode);
7681
latestRequest = request;
7782
return request;
@@ -208,8 +213,7 @@ void main() {
208213
await stream.terminate();
209214

210215
final expectedData = frame(data);
211-
expect(verify(connection.latestRequest.send(captureAny)).captured.single,
212-
expectedData);
216+
verify(connection.latestRequest.send(expectedData.toJSBox));
213217
});
214218

215219
test('Stream handles headers properly', () async {
@@ -226,15 +230,15 @@ void main() {
226230

227231
when(transport.latestRequest.responseHeaders).thenReturn(responseHeaders);
228232
when(transport.latestRequest.response)
229-
.thenReturn(String.fromCharCodes(frame(<int>[])));
233+
.thenReturn(String.fromCharCodes(frame(<int>[])).toJS);
230234

231235
// Set expectation for request readyState and generate two readyStateChange
232236
// events, so that incomingMessages stream completes.
233-
final readyStates = [HttpRequest.HEADERS_RECEIVED, HttpRequest.DONE];
234-
when(transport.latestRequest.readyState)
235-
.thenAnswer((_) => readyStates.removeAt(0));
237+
final readyStates = [XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE];
238+
transport.latestRequest.readyState = readyStates[0];
236239
transport.latestRequest.readyStateChangeController
237240
.add(readyStateChangeEvent);
241+
transport.latestRequest.readyState = readyStates[1];
238242
transport.latestRequest.readyStateChangeController
239243
.add(readyStateChangeEvent);
240244

@@ -267,16 +271,15 @@ void main() {
267271
final encodedString = String.fromCharCodes(encodedTrailers);
268272

269273
when(connection.latestRequest.responseHeaders).thenReturn(requestHeaders);
270-
when(connection.latestRequest.response).thenReturn(encodedString);
274+
when(connection.latestRequest.response).thenReturn(encodedString.toJS);
271275

272276
// Set expectation for request readyState and generate events so that
273277
// incomingMessages stream completes.
274-
final readyStates = [HttpRequest.HEADERS_RECEIVED, HttpRequest.DONE];
275-
when(connection.latestRequest.readyState)
276-
.thenAnswer((_) => readyStates.removeAt(0));
278+
connection.latestRequest.readyState = XMLHttpRequest.HEADERS_RECEIVED;
277279
connection.latestRequest.readyStateChangeController
278280
.add(readyStateChangeEvent);
279281
connection.latestRequest.progressController.add(progressEvent);
282+
connection.latestRequest.readyState = XMLHttpRequest.DONE;
280283
connection.latestRequest.readyStateChangeController
281284
.add(readyStateChangeEvent);
282285

@@ -303,16 +306,14 @@ void main() {
303306
final encodedString = String.fromCharCodes(encoded);
304307

305308
when(connection.latestRequest.responseHeaders).thenReturn(requestHeaders);
306-
when(connection.latestRequest.response).thenReturn(encodedString);
307-
309+
when(connection.latestRequest.response).thenReturn(encodedString.toJS);
308310
// Set expectation for request readyState and generate events so that
309311
// incomingMessages stream completes.
310-
final readyStates = [HttpRequest.HEADERS_RECEIVED, HttpRequest.DONE];
311-
when(connection.latestRequest.readyState)
312-
.thenAnswer((_) => readyStates.removeAt(0));
312+
connection.latestRequest.readyState = XMLHttpRequest.HEADERS_RECEIVED;
313313
connection.latestRequest.readyStateChangeController
314314
.add(readyStateChangeEvent);
315315
connection.latestRequest.progressController.add(progressEvent);
316+
connection.latestRequest.readyState = XMLHttpRequest.DONE;
316317
connection.latestRequest.readyStateChangeController
317318
.add(readyStateChangeEvent);
318319

@@ -338,16 +339,15 @@ void main() {
338339
final data = List<int>.filled(10, 224);
339340
when(connection.latestRequest.responseHeaders).thenReturn(requestHeaders);
340341
when(connection.latestRequest.response)
341-
.thenReturn(String.fromCharCodes(frame(data)));
342+
.thenReturn(String.fromCharCodes(frame(data)).toJS);
342343

343344
// Set expectation for request readyState and generate events, so that
344345
// incomingMessages stream completes.
345-
final readyStates = [HttpRequest.HEADERS_RECEIVED, HttpRequest.DONE];
346-
when(connection.latestRequest.readyState)
347-
.thenAnswer((_) => readyStates.removeAt(0));
346+
connection.latestRequest.readyState = XMLHttpRequest.HEADERS_RECEIVED;
348347
connection.latestRequest.readyStateChangeController
349348
.add(readyStateChangeEvent);
350349
connection.latestRequest.progressController.add(progressEvent);
350+
connection.latestRequest.readyState = XMLHttpRequest.DONE;
351351
connection.latestRequest.readyStateChangeController
352352
.add(readyStateChangeEvent);
353353

@@ -369,8 +369,8 @@ void main() {
369369
const errorDetails = 'error details';
370370
when(connection.latestRequest.responseHeaders)
371371
.thenReturn({'content-type': 'application/grpc+proto'});
372-
when(connection.latestRequest.readyState).thenReturn(HttpRequest.DONE);
373-
when(connection.latestRequest.responseText).thenReturn(errorDetails);
372+
connection.latestRequest.readyState = XMLHttpRequest.DONE;
373+
connection.latestRequest.responseText = errorDetails;
374374
connection.latestRequest.readyStateChangeController
375375
.add(readyStateChangeEvent);
376376
await errorReceived.future;
@@ -398,20 +398,20 @@ void main() {
398398

399399
when(connection.latestRequest.responseHeaders).thenReturn(metadata);
400400
when(connection.latestRequest.readyState)
401-
.thenReturn(HttpRequest.HEADERS_RECEIVED);
401+
.thenReturn(XMLHttpRequest.HEADERS_RECEIVED);
402402

403403
// At first invocation the response should be the the first message, after
404404
// that first + last messages.
405405
var first = true;
406406
when(connection.latestRequest.response).thenAnswer((_) {
407407
if (first) {
408408
first = false;
409-
return encodedStrings[0];
409+
return encodedStrings[0].toJS;
410410
}
411-
return encodedStrings[0] + encodedStrings[1];
411+
return (encodedStrings[0] + encodedStrings[1]).toJS;
412412
});
413413

414-
final readyStates = [HttpRequest.HEADERS_RECEIVED, HttpRequest.DONE];
414+
final readyStates = [XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE];
415415
when(connection.latestRequest.readyState)
416416
.thenAnswer((_) => readyStates.removeAt(0));
417417

0 commit comments

Comments
 (0)