Skip to content

Commit b27803c

Browse files
Merge pull request #381 from Workiva/fix-mocked-event-types
FED-1881 Fix regression in SyntheticEvent mock class type-checking
2 parents 0218473 + 11e485e commit b27803c

File tree

4 files changed

+107
-5
lines changed

4 files changed

+107
-5
lines changed

lib/src/react_client/event_helpers.dart

+19-5
Original file line numberDiff line numberDiff line change
@@ -760,11 +760,25 @@ SyntheticWheelEvent createSyntheticWheelEvent({
760760
}
761761

762762
extension SyntheticEventTypeHelpers on SyntheticEvent {
763-
// Use getProperty(this, 'type') since, although statically we may be dealing with a SyntheticEvent,
764-
// this could be a non-event JS object cast to SyntheticEvent with a null `type`.
765-
// This is unlikely, but is possible, and before the null safety migration this method
766-
// gracefully returned false instead of throwing.
767-
bool _checkEventType(List<String> types) => getProperty(this, 'type') != null && types.any((t) => type.contains(t));
763+
// Access `type` in a try-catch since, although statically we may be dealing with a SyntheticEvent,
764+
// this could be an object with a `null` `type` which would cause a type error since `type` is non-nullable.
765+
//
766+
// Cases where this could occur:
767+
// - non-event JS object cast to SyntheticEvent
768+
// - a mock class that hasn't mocked `type`
769+
//
770+
// We could use `getProperty(this, 'type')` to handle the JS object case, but for mock classes it would bypass the
771+
// `type` getter and not behave as expected.
772+
bool _checkEventType(List<String> types) {
773+
String? type;
774+
try {
775+
// This is typed as null statically, but if it's not, it will probably throw (depending on the compiler).
776+
type = this.type;
777+
} catch (_) {}
778+
779+
return type != null && types.any(type.contains);
780+
}
781+
768782
bool _hasProperty(String propertyName) => hasProperty(this, propertyName);
769783

770784
/// Uses Duck Typing to detect if the event instance is a [SyntheticClipboardEvent].

test/mockito.dart

+2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ library react.test.mockito_gen_entrypoint;
55

66
import 'dart:html';
77
import 'package:mockito/annotations.dart';
8+
import 'package:react/src/react_client/synthetic_event_wrappers.dart';
89

910
@GenerateNiceMocks([
1011
MockSpec<KeyboardEvent>(),
1112
MockSpec<DataTransfer>(),
1213
MockSpec<MouseEvent>(),
14+
MockSpec<SyntheticEvent>(),
1315
])
1416
main() {}

test/mockito.mocks.dart

+57
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:html' as _i2;
77
import 'dart:math' as _i3;
88

99
import 'package:mockito/mockito.dart' as _i1;
10+
import 'package:react/src/react_client/synthetic_event_wrappers.dart' as _i4;
1011

1112
// ignore_for_file: type=lint
1213
// ignore_for_file: avoid_redundant_argument_values
@@ -444,3 +445,59 @@ class MockMouseEvent extends _i1.Mock implements _i2.MouseEvent {
444445
returnValueForMissingStub: null,
445446
);
446447
}
448+
449+
/// A class which mocks [SyntheticEvent].
450+
///
451+
/// See the documentation for Mockito's code generation for more information.
452+
class MockSyntheticEvent extends _i1.Mock implements _i4.SyntheticEvent {
453+
@override
454+
bool get bubbles => (super.noSuchMethod(
455+
Invocation.getter(#bubbles),
456+
returnValue: false,
457+
returnValueForMissingStub: false,
458+
) as bool);
459+
@override
460+
bool get cancelable => (super.noSuchMethod(
461+
Invocation.getter(#cancelable),
462+
returnValue: false,
463+
returnValueForMissingStub: false,
464+
) as bool);
465+
@override
466+
bool get defaultPrevented => (super.noSuchMethod(
467+
Invocation.getter(#defaultPrevented),
468+
returnValue: false,
469+
returnValueForMissingStub: false,
470+
) as bool);
471+
@override
472+
num get eventPhase => (super.noSuchMethod(
473+
Invocation.getter(#eventPhase),
474+
returnValue: 0,
475+
returnValueForMissingStub: 0,
476+
) as num);
477+
@override
478+
bool get isTrusted => (super.noSuchMethod(
479+
Invocation.getter(#isTrusted),
480+
returnValue: false,
481+
returnValueForMissingStub: false,
482+
) as bool);
483+
@override
484+
num get timeStamp => (super.noSuchMethod(
485+
Invocation.getter(#timeStamp),
486+
returnValue: 0,
487+
returnValueForMissingStub: 0,
488+
) as num);
489+
@override
490+
String get type => (super.noSuchMethod(
491+
Invocation.getter(#type),
492+
returnValue: '',
493+
returnValueForMissingStub: '',
494+
) as String);
495+
@override
496+
void preventDefault() => super.noSuchMethod(
497+
Invocation.method(
498+
#preventDefault,
499+
[],
500+
),
501+
returnValueForMissingStub: null,
502+
);
503+
}

test/react_client/event_helpers_test.dart

+29
Original file line numberDiff line numberDiff line change
@@ -1703,6 +1703,18 @@ main() {
17031703
() {
17041704
expect(eventTypeTester(newObject() as SyntheticEvent), isFalse);
17051705
});
1706+
1707+
test('when the argument is a mocked event object with no mocked `type` property, and does not throw', () {
1708+
// Typically consumers would mock a specific SyntheticEvent subtype, but creating null-safe mocks for those
1709+
// causes property checks like `_hasProperty('button')` in helper methods to return true in DDC
1710+
// (e.g., `.isMouseEvent` for a `MockSyntheticMouseEvent` would return true).
1711+
//
1712+
// We really just want to check the `type` behavior here, especially for non-null-safe mocks, so we'll use
1713+
// the generic MockSyntheticEvent.
1714+
//
1715+
// *See other test with similar note to this one.*
1716+
expect(eventTypeTester(MockSyntheticEvent()), isFalse);
1717+
});
17061718
});
17071719
}
17081720

@@ -1990,6 +2002,23 @@ main() {
19902002
});
19912003
});
19922004
});
2005+
2006+
// Regression test for Mock class behavior consumers rely on.
2007+
//
2008+
// Typically consumers would mock a specific SyntheticEvent subtype, but creating null-safe mocks for those
2009+
// causes property checks like `_hasProperty('button')` in helper methods to return true in DDC
2010+
// (e.g., `.isMouseEvent` for a `MockSyntheticMouseEvent` would return true).
2011+
//
2012+
// We really just want to check the `type` behavior here, especially for non-null-safe mocks, so we'll use
2013+
// the generic MockSyntheticEvent.
2014+
//
2015+
// *See other test with similar note to this one.*
2016+
test('checks types correctly for Mock objects with `type` mocked', () {
2017+
final mockEvent = MockSyntheticEvent();
2018+
when(mockEvent.type).thenReturn('click');
2019+
expect(mockEvent.isMouseEvent, isTrue);
2020+
expect(mockEvent.isKeyboardEvent, false);
2021+
});
19932022
});
19942023

19952024
group('DataTransferHelper', () {

0 commit comments

Comments
 (0)