Skip to content

Commit ce82545

Browse files
Add check for JS refs, beef up test coverage to test all ref types with all factory types
1 parent 7ba2721 commit ce82545

File tree

6 files changed

+129
-101
lines changed

6 files changed

+129
-101
lines changed

lib/react_client/react_interop.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,10 @@ class Ref<T> {
8282
T get current {
8383
final jsCurrent = jsRef.current;
8484

85-
if (jsCurrent is! Element) {
86-
final dartCurrent = (jsCurrent as ReactComponent)?.dartComponent;
85+
// Note: this ReactComponent check will pass for many types of JS objects,
86+
// so don't assume for sure that it's a ReactComponent
87+
if (jsCurrent is! Element && jsCurrent is ReactComponent) {
88+
final dartCurrent = jsCurrent.dartComponent;
8789

8890
if (dartCurrent != null) {
8991
return dartCurrent as T;

lib/src/react_client/chain_refs.dart

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:js/js_util.dart';
12
import 'package:react/react_client/react_interop.dart';
23

34
/// Returns a ref that updates both [ref1] and [ref2], effectively
@@ -29,14 +30,14 @@ dynamic chainRefs(dynamic ref1, dynamic ref2) {
2930

3031
/// Like [chainRefs], but takes in a list of [refs].
3132
dynamic chainRefList(List<dynamic> refs) {
33+
final nonNullRefs = refs.where((ref) => ref != null).toList(growable: false);
34+
3235
// Wrap in an assert so iteration doesn't take place unnecessarily
3336
assert(() {
34-
refs.forEach(_validateChainRefsArg);
37+
nonNullRefs.forEach(_validateChainRefsArg);
3538
return true;
3639
}());
3740

38-
final nonNullRefs = refs.where((ref) => ref != null).toList();
39-
4041
// Return null if there are no refs to chain
4142
if (nonNullRefs.isEmpty) return null;
4243

@@ -82,6 +83,15 @@ dynamic chainRefList(List<dynamic> refs) {
8283
}
8384

8485
void _validateChainRefsArg(dynamic ref) {
85-
assert(ref is! Function || ref is Function(Null), 'callback refs must take a single argument');
86-
assert(ref is! String, 'String refs cannot be chained');
86+
if (ref is Function(Null) ||
87+
ref is Ref ||
88+
// Need to duck-type since `is JsRef` will return true for most JS objects.
89+
(ref is JsRef && hasProperty(ref, 'current'))) {
90+
return;
91+
}
92+
93+
if (ref is String) throw AssertionError('String refs cannot be chained');
94+
if (ref is Function) throw AssertionError('callback refs must take a single argument');
95+
96+
throw AssertionError('Invalid ref type: $ref');
8797
}

test/factory/common_factory_tests.dart

Lines changed: 11 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -332,101 +332,26 @@ void refTests<T>(ReactComponentFactoryProxy factory, {void verifyRefValue(dynami
332332
});
333333
});
334334

335-
group('chainRefs works', () {
336-
test('with two typed callback refs', () {
337-
var calls = <Map>[];
338-
339-
rtu.renderIntoDocument(factory({
340-
'ref': chainRefs(
341-
(T ref) => calls.add({'name': 'ref1', 'value': ref}),
342-
(T ref) => calls.add({'name': 'ref2', 'value': ref}),
343-
),
344-
}));
345-
346-
expect(calls, [
347-
{'name': 'ref1', 'value': anything},
348-
{'name': 'ref2', 'value': anything},
349-
]);
350-
verifyRefValue(calls[0]['value']);
351-
verifyRefValue(calls[1]['value']);
352-
});
353-
354-
test('with a typed and untyped callback refs', () {
355-
var calls = <Map>[];
356-
357-
rtu.renderIntoDocument(factory({
358-
'ref': chainRefs(
359-
(T ref) => calls.add({'name': 'ref1', 'value': ref}),
360-
(ref) => calls.add({'name': 'ref2', 'value': ref}),
361-
),
362-
}));
363-
364-
expect(calls, [
365-
{'name': 'ref1', 'value': anything},
366-
{'name': 'ref2', 'value': anything},
367-
]);
368-
verifyRefValue(calls[0]['value']);
369-
verifyRefValue(calls[1]['value']);
370-
});
371-
372-
test('with two createRef refs', () {
373-
final ref1 = react.createRef<T>();
374-
final ref2 = react.createRef<T>();
375-
376-
rtu.renderIntoDocument(factory({
377-
'ref': chainRefs(ref1, ref2),
378-
}));
379-
380-
verifyRefValue(ref1.current);
381-
verifyRefValue(ref2.current);
382-
});
383-
384-
test('with a typed callback ref and a createRef', () {
385-
var callbackRefCallValues = [];
386-
final refObject = react.createRef<T>();
387-
388-
rtu.renderIntoDocument(factory({
389-
'ref': chainRefs(
390-
(T ref) => callbackRefCallValues.add(ref),
391-
refObject,
392-
),
393-
}));
394-
395-
expect(callbackRefCallValues, hasLength(1));
396-
verifyRefValue(callbackRefCallValues[0]);
397-
verifyRefValue(refObject.current);
398-
});
399-
400-
// Other cases tested in chainRefs's own tests
401-
});
402-
403335
group('chainRefList works', () {
404-
test('with a list of refs, ignoring any null values', () {
405-
var calls = <Map>[];
336+
test('with all different types of values, ignoring null', () {
337+
final testCases = RefTestCase.allChainable<T>();
406338

339+
T refValue;
407340
rtu.renderIntoDocument(factory({
408341
'ref': chainRefList([
342+
(ref) => refValue = ref,
409343
null,
410-
(ref) => calls.add({'name': 'ref1', 'value': ref}),
411344
null,
412-
(ref) => calls.add({'name': 'ref2', 'value': ref}),
413-
(ref) => calls.add({'name': 'ref3', 'value': ref}),
414-
null,
415-
null,
416-
(ref) => calls.add({'name': 'ref4', 'value': ref}),
345+
...testCases.map((t) => t.ref),
417346
]),
418347
}));
348+
// Test setup check: verify refValue is correct,
349+
// which we'll use below to verify refs were updated.
350+
verifyRefValue(refValue);
419351

420-
expect(calls, [
421-
{'name': 'ref1', 'value': anything},
422-
{'name': 'ref2', 'value': anything},
423-
{'name': 'ref3', 'value': anything},
424-
{'name': 'ref4', 'value': anything},
425-
]);
426-
verifyRefValue(calls[0]['value']);
427-
verifyRefValue(calls[1]['value']);
428-
verifyRefValue(calls[2]['value']);
429-
verifyRefValue(calls[3]['value']);
352+
for (final testCase in testCases) {
353+
testCase.verifyRefWasUpdated(refValue);
354+
}
430355
});
431356

432357
// Other cases tested in chainRefList's own tests

test/react_client/chain_refs_test.dart

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
@TestOn('browser')
2+
@JS()
23
library react.chain_refs_test;
34

5+
import 'package:js/js.dart';
6+
import 'package:react/react_client.dart';
47
import 'package:react/src/react_client/chain_refs.dart';
58
import 'package:test/test.dart';
69

10+
import '../util.dart';
11+
712
main() {
813
group('Ref chaining utils:', () {
914
testRef(_) {}
1015

1116
group('chainRefs', () {
12-
// Actual chaining is tested in refTests so it can be tested functionally with each component type.
17+
// Chaining and arg validation is tested via chainRefList.
1318

1419
group('skips chaining and passes through arguments when', () {
1520
test('both arguments are null', () {
@@ -24,12 +29,10 @@ main() {
2429
expect(chainRefs(testRef, null), same(testRef));
2530
});
2631
});
27-
28-
// Arg validation is covered by `chainRefList` tests
2932
});
3033

3134
group('chainRefList', () {
32-
// Actual chaining is tested in refTests so it can be tested functionally with each component type.
35+
// Chaining is tested functionally in refTests with each component type.
3336

3437
group('skips chaining and passes through arguments when', () {
3538
test('the list is empty', () {
@@ -49,15 +52,44 @@ main() {
4952
});
5053
});
5154

52-
group('asserts that inputs arg are not', () {
55+
group('raises an assertion when inputs are invalid:', () {
5356
test('strings', () {
5457
expect(() => chainRefList([testRef, 'bad ref']), throwsA(isA<AssertionError>()));
5558
});
5659

5760
test('unsupported function types', () {
5861
expect(() => chainRefList([testRef, () {}]), throwsA(isA<AssertionError>()));
5962
});
63+
64+
test('other objects', () {
65+
expect(() => chainRefList([testRef, Object()]), throwsA(isA<AssertionError>()));
66+
});
67+
68+
// test JS interop objects since type-checking anonymous interop objects
69+
test('non-createRef anonymous JS interop objects', () {
70+
expect(() => chainRefList([testRef, JsTypeAnonymous()]), throwsA(isA<AssertionError>()));
71+
});
72+
73+
// test JS interop objects since type-checking anonymous interop objects
74+
test('non-createRef JS interop objects', () {
75+
expect(() => chainRefList([testRef, JsType()]), throwsA(isA<AssertionError>()));
76+
});
6077
}, tags: 'no-dart2js');
78+
79+
test('does not raise an assertion for valid input refs', () {
80+
expect(() => chainRefList(RefTestCase.allChainable().map((r) => r.ref).toList()), returnsNormally);
81+
});
6182
});
6283
});
6384
}
85+
86+
@JS()
87+
@anonymous
88+
class JsTypeAnonymous {
89+
external factory JsTypeAnonymous();
90+
}
91+
92+
@JS()
93+
class JsType {
94+
external JsType();
95+
}

test/react_client/chain_refs_test.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
<script src="packages/react/react_with_addons.js"></script>
77
<script src="packages/react/react_dom.js"></script>
88

9+
<script>
10+
function JsType() {}
11+
</script>
12+
913
<link rel="x-dart-test" href="chain_refs_test.dart">
1014
<script src="packages/test/dart.js"></script>
1115
</head>

test/util.dart

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ library react.test.util;
66
import 'dart:js_util' show getProperty;
77

88
import 'package:js/js.dart';
9-
import 'package:react/react_client/react_interop.dart';
10-
11-
import 'package:react/react_test_utils.dart' as rtu;
9+
import 'package:meta/meta.dart';
1210
import 'package:react/react.dart' as react;
1311
import 'package:react/react_client/js_backed_map.dart';
12+
import 'package:react/react_client/react_interop.dart';
13+
import 'package:react/react_test_utils.dart' as rtu;
14+
import 'package:test/test.dart';
1415

1516
@JS('Object.keys')
1617
external List _objectKeys(obj);
@@ -54,3 +55,57 @@ Map unmodifiableMap([Map map1, Map map2, Map map3, Map map4]) {
5455
if (map4 != null) merged.addAll(map4);
5556
return new Map.unmodifiable(merged);
5657
}
58+
59+
/// A test case that can be used for consuming a specific kind of ref and verifying
60+
/// it was updated properly when rendered.
61+
class RefTestCase {
62+
final dynamic ref;
63+
final Function(dynamic actualValue) verifyRefWasUpdated;
64+
65+
RefTestCase._({@required this.ref, @required this.verifyRefWasUpdated});
66+
67+
static RefTestCase untypedCallbackRefCase() {
68+
final untypedCallbackRefCalls = [];
69+
return RefTestCase._(
70+
ref: (value) => untypedCallbackRefCalls.add(value),
71+
verifyRefWasUpdated: (actualValue) => expect(untypedCallbackRefCalls, [same(actualValue)]),
72+
);
73+
}
74+
75+
static RefTestCase typedCallbackRefCase<T>() {
76+
final typedCallbackRefCalls = [];
77+
return RefTestCase._(
78+
ref: (T value) => typedCallbackRefCalls.add(value),
79+
verifyRefWasUpdated: (actualValue) => expect(typedCallbackRefCalls, [same(actualValue)]),
80+
);
81+
}
82+
83+
static RefTestCase refObjectCase<T>() {
84+
final refObjectRef = createRef<T>();
85+
return RefTestCase._(
86+
ref: refObjectRef,
87+
verifyRefWasUpdated: (actualValue) => expect(refObjectRef.current, same(actualValue)),
88+
);
89+
}
90+
91+
static RefTestCase jsRefObjectCase<T>() {
92+
final jsRefObjectRef = React.createRef();
93+
return RefTestCase._(
94+
ref: jsRefObjectRef,
95+
verifyRefWasUpdated: (actualValue) => expect(jsRefObjectRef.current, same(actualValue)),
96+
);
97+
}
98+
99+
/// Test cases for each of the valid, chainable ref types:
100+
///
101+
/// 1. callback ref with untyped argument
102+
/// 2. callback ref with typed argument
103+
/// 3. createRef (Dart wrapper)
104+
/// 4. createRef (JS object)
105+
static List<RefTestCase> allChainable<T>() => [
106+
untypedCallbackRefCase(),
107+
typedCallbackRefCase<T>(),
108+
refObjectCase<T>(),
109+
jsRefObjectCase<T>(),
110+
];
111+
}

0 commit comments

Comments
 (0)