diff --git a/lib/js_component.dart b/lib/js_component.dart new file mode 100644 index 000000000..1292951a3 --- /dev/null +++ b/lib/js_component.dart @@ -0,0 +1,16 @@ +// Copyright 2022 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'src/util/js_component.dart'; +export 'src/util/prop_conversion.dart'; diff --git a/lib/src/util/js_component.dart b/lib/src/util/js_component.dart new file mode 100644 index 000000000..33622c5a5 --- /dev/null +++ b/lib/src/util/js_component.dart @@ -0,0 +1,48 @@ +import 'package:over_react/over_react.dart'; +import 'package:react/react_client/component_factory.dart'; + +/// Creates a Dart component factory that wraps a ReactJS [factoryProxy]. +/// +/// More in-depth documentation for wrapping JS components is coming soon. +/// +/// Example: +/// ```dart +/// UiFactory Button = uiJsComponent( +/// ReactJsComponentFactoryProxy(MaterialUI.Button), +/// _$ButtonConfig, // ignore: undefined_identifier +/// ); +/// +/// @Props(keyNamespace: '') +/// mixin ButtonProps on UiProps {} +/// ``` +UiFactory uiJsComponent( + ReactJsComponentFactoryProxy factoryProxy, + dynamic _config, +) { + ArgumentError.checkNotNull(_config, '_config'); + + if (_config is! UiFactoryConfig) { + throw ArgumentError( + '_config should be a UiFactory. Make sure you are ' + r'using either the generated factory config (i.e. _$FooConfig) or manually ' + 'declaring your config correctly.'); + } + + // ignore: invalid_use_of_protected_member + final propsFactory = (_config as UiFactoryConfig).propsFactory; + ArgumentError.checkNotNull(_config, '_config'); + + TProps _uiFactory([Map backingMap]) { + TProps builder; + if (backingMap == null) { + builder = propsFactory.jsMap(JsBackedMap()); + } else if (backingMap is JsBackedMap) { + builder = propsFactory.jsMap(backingMap); + } else { + builder = propsFactory.map(backingMap); + } + return builder..componentFactory = factoryProxy; + } + + return _uiFactory; +} diff --git a/lib/src/util/prop_conversion.dart b/lib/src/util/prop_conversion.dart new file mode 100644 index 000000000..7646ec84a --- /dev/null +++ b/lib/src/util/prop_conversion.dart @@ -0,0 +1,127 @@ +import 'package:js/js.dart'; +import 'package:js/js_util.dart'; +import 'package:meta/meta.dart'; +import 'package:over_react/over_react.dart' show Ref; +import 'package:over_react/src/util/weak_map.dart'; +import 'package:react/react_client/js_backed_map.dart'; +import 'package:react/react_client/component_factory.dart'; +import 'package:react/react_client/react_interop.dart' show JsRef; + +// Export JsMap since props that utilize jsifyMapProp/unjsifyMapProp +// via custom getters/setters will need JsMap to avoid implicit cast errors. +export 'package:react/react_client/js_backed_map.dart' show JsMap; + +/// Returns a JS-deep-converted version of [value] for storage in the props map, or null if [value] is null. +/// +/// For use in JS component prop setters where the component expects a JS object, but typing the getter/setter +/// as a Dart Map is more convenient to the consumer reading/writing the props. +/// +/// Should be used alongside [unjsifyMapProp]. +/// +/// For more information on why this conversion is done in the getter/setter, +/// see documentation around Dart wrapper component prop conversion issues. +/// FIXME CPLAT-15060 add reference/link to documentation +JsMap jsifyMapProp(Map value) { + if (value == null) return null; + // Use generateJsProps so that, in addition to deeply converting props, we also properly convert the `ref` prop. + // Dart props get deep converted (just like they would when invoking the ReactJsComponentFactoryProxy anyways), + // but that's a tradeoff we're willing to make, given that there's no perfect solution, + // and shouldn't happen often since we shouldn't be passing Dart values to JS components. + // As a workaround, consumers can wrap any Dart values in an opaque Dart object (similar to DartValueWrapper). + return generateJsProps(value); +} + +/// Returns [value] converted to a Dart map and [Map.cast]ed to the provided generics, or null if [value] is null. +/// +/// For use in JS component prop getters where the component expects a JS object, but typing the getter/setter +/// as a Dart Map is more convenient to the consumer reading/writing the props. +/// +/// Should be used alongside [jsifyMapProp]. +/// +/// For more information on why this conversion is done in the getter/setter, +/// see documentation around Dart wrapper component prop conversion issues. +/// FIXME CPLAT-15060 add reference/link to documentation +Map unjsifyMapProp(JsMap value) { + if (value == null) return null; + // Don't deep unconvert so that JS values don't get converted incorrectly to Maps. See jsifyMapProp for more information. + return JsBackedMap.backedBy(value).cast(); +} + +/// Returns [value] converted to its JS ref representation for storage in a props map, or null of the [value] is null. +/// +/// For use in JS component prop getters where the component expects a JS ref, but accepting Dart refs +/// is more convenient to the consumer reading/writing the props. +/// +/// Should be used alongside [unjsifyRefProp]. +/// +/// Normally ref props get converted in [ReactJsComponentFactoryProxy.build] and [jsifyMapProp], +/// but that same conversion for props not under the default `'ref'` key doesn't occur automatically, +/// which is where this function comes in. +dynamic jsifyRefProp(dynamic value) { + // Case 1: null + if (value == null) return null; + + // Case 2a: Dart callback refs + // Case 2b: JS callback refs + // allowInterop is technically redundant, since that will get done in ReactJsComponentFactoryProxy.build + // (or jsifyMapProp for props that accept props maps, like ButtonProps.TouchRippleProps), + // but we'll do it here anyways since we know it'll be needed. + if (value is Function) return allowInterop(value); + + // Case 2: Dart ref objects + if (value is Ref) { + // Store the original Dart ref so we can retrieve it later in unjsifyRefProp. + // See _dartRefForJsRef comment for more info. + _dartRefForJsRef.set(value.jsRef, value); + return value.jsRef; + } + + // Case 3: JS ref objects + return value; +} + +/// Returns a [JsRef] object converted back into its Dart [Ref] object (if it was converted via [jsifyRefProp], +/// or if [value] is not a [JsRef], passes through [value] (including null). +/// +/// For use in JS component prop getters where the component expects a JS ref, but accepting Dart refs +/// is more convenient to the consumer reading/writing the props. +/// +/// Should be used alongside [unjsifyRefProp]. +/// +/// Note that Dart refs currently lose their reified types when jsified/unjsified. +dynamic unjsifyRefProp(dynamic value, + {@visibleForTesting bool throwOnUnhandled = false}) { + // Case 1: null + if (value == null) return null; + + // Case 2: JS callback refs + if (value is Function) return value; + + // Case 2: JS ref objects + if (value is! Ref && value is JsRef && hasProperty(value, 'current')) { + // Return the original Dart ref is there is one, otherwise return the JsRef itself. + // See _dartRefForJsRef comment for more info. + return _dartRefForJsRef.get(value) ?? value; + } + + // Case 3: unreachable? + // We shouldn't ever get here, but just pass through the value in case there's + // a case we didn't handle properly above (or throw for testing purposes). + if (throwOnUnhandled) { + throw ArgumentError.value(value, 'value', 'Unhandled case'); + } + return value; +} + +/// A weak mapping from JsRef objects to the original Dart Refs they back. +/// +/// Useful for +/// 1. Preserving the reified type of the Dart ref when it gets jsified/unjsified +/// 2. Telling whether the ref was originally a Dart or JS ref +/// +/// We're using WeakMap here so that we don't have a global object strongly referencing and thus retaining refs, +/// and not because strong references from the JS ref to the Dart ref would be problematic. +/// +/// We also have to use a WeakMap instead of a JS property (or an Expando, whose DDC implementation uses JS properties), +/// since those can't be used with sealed JS objects (like React.createRef() objects in development builds, and potentially other cases). +final _dartRefForJsRef = WeakMap(); diff --git a/lib/src/util/weak_map.dart b/lib/src/util/weak_map.dart new file mode 100644 index 000000000..18c3352ef --- /dev/null +++ b/lib/src/util/weak_map.dart @@ -0,0 +1,31 @@ +@JS() +library react_material_ui.weak_map; + +import 'package:js/js.dart'; + +/// Bindings for the JS WeakMap class, a collection of key/value pairs in which the keys are weakly referenced. +/// +/// For more info, see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap +/// +/// This API is supported in IE11 and all modern browsers. +/// +/// This class has no type parameters since they cause issues in dart2js: https://github.com/dart-lang/sdk/issues/43373 +@JS() +class WeakMap { + external WeakMap(); + + /// Removes any value associated to the [key]. `has(key)` will return false afterwards. + /// + /// Returns whether a value was associated with the [key]. + external bool delete(Object key); + + /// Returns the value associated to the [key], or null if there is none. + external Object get(Object key); + + /// Returns whether a value has been associated to the key. + external bool has(Object key); + + /// Sets the value for the [key]. + // void since IE11 returns undefined, and you can cascade on the object instead of returning it in Dart. + external void set(Object key, Object value); +} diff --git a/pubspec.yaml b/pubspec.yaml index c28a74326..ebe4c8a7f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,7 @@ dev_dependencies: glob: ^1.2.0 io: ^0.3.2+1 mockito: ^4.1.1 + react_testing_library: ^2.1.0 over_react_test: ^2.10.2 pedantic: ^1.8.0 test: ^1.15.7 diff --git a/test/over_react/component_declaration/redux_component_test/test_reducer.g.dart b/test/over_react/component_declaration/redux_component_test/test_reducer.g.dart index b4adebfa4..8afa9e060 100644 --- a/test/over_react/component_declaration/redux_component_test/test_reducer.g.dart +++ b/test/over_react/component_declaration/redux_component_test/test_reducer.g.dart @@ -44,12 +44,8 @@ class _$BaseState extends BaseState { (new BaseStateBuilder()..update(updates)).build(); _$BaseState._({this.count1, this.count2}) : super._() { - if (count1 == null) { - throw new BuiltValueNullFieldError('BaseState', 'count1'); - } - if (count2 == null) { - throw new BuiltValueNullFieldError('BaseState', 'count2'); - } + BuiltValueNullFieldError.checkNotNull(count1, 'BaseState', 'count1'); + BuiltValueNullFieldError.checkNotNull(count2, 'BaseState', 'count2'); } @override @@ -95,9 +91,10 @@ class BaseStateBuilder implements Builder { BaseStateBuilder(); BaseStateBuilder get _$this { - if (_$v != null) { - _count1 = _$v.count1; - _count2 = _$v.count2; + final $v = _$v; + if ($v != null) { + _count1 = $v.count1; + _count2 = $v.count2; _$v = null; } return this; @@ -105,9 +102,7 @@ class BaseStateBuilder implements Builder { @override void replace(BaseState other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$BaseState; } @@ -118,7 +113,12 @@ class BaseStateBuilder implements Builder { @override _$BaseState build() { - final _$result = _$v ?? new _$BaseState._(count1: count1, count2: count2); + final _$result = _$v ?? + new _$BaseState._( + count1: BuiltValueNullFieldError.checkNotNull( + count1, 'BaseState', 'count1'), + count2: BuiltValueNullFieldError.checkNotNull( + count2, 'BaseState', 'count2')); replace(_$result); return _$result; } diff --git a/test/over_react/util/js_component_test.dart b/test/over_react/util/js_component_test.dart new file mode 100644 index 000000000..2cc4e6462 --- /dev/null +++ b/test/over_react/util/js_component_test.dart @@ -0,0 +1,237 @@ +@TestOn('browser') +@JS() +library react_material_ui.test.js_component_test; + +import 'dart:html'; + +import 'package:js/js.dart'; +import 'package:over_react/over_react.dart'; +import 'package:over_react/src/util/js_component.dart'; +import 'package:react_testing_library/matchers.dart'; +import 'package:react_testing_library/react_testing_library.dart' as rtl; +import 'package:react/react_client.dart'; +import 'package:react/react_client/react_interop.dart'; +import 'package:test/test.dart'; + +part 'js_component_test.over_react.g.dart'; + +main() { + enableTestMode(); + + group('uiJsComponent', () { + group('with generated props config', () { + jsComponentTestHelper(Test); + }); + + group('with custom PropsFactory', () { + jsComponentTestHelper(TestCustom); + }); + + group('with no left hand typing', () { + jsComponentTestHelper(NoLHSTest); + }); + + group('with private prefix', () { + jsComponentTestHelper(_Test); + }); + + group('throws an error when', () { + test('config is null', () { + expect( + () => uiJsComponent( + ReactJsComponentFactoryProxy(_TestJsComponent), + null, + ), + throwsArgumentError); + }); + + test('props factory is not provided when using custom props class', () { + expect( + () => uiJsComponent( + ReactJsComponentFactoryProxy(_TestJsComponent), + UiFactoryConfig(displayName: 'Foo'), + ), + throwsArgumentError); + }); + + test('config not the correct type', () { + expect( + () => uiJsComponent( + ReactJsComponentFactoryProxy(_TestJsComponent), + 'foo', + ), + throwsArgumentError); + }); + }); + }); +} + +void jsComponentTestHelper(UiFactory factory) { + test( + 'renders a component from end to end, successfully reading props via typed getters', + () { + var view = rtl.render( + (factory()..addTestId('testId'))(), + ); + var node = view.getByTestId('testId'); + + // Sanity check for values with no props set. + expect(node, isA()); + expect(node, hasTextContent('')); + + view.rerender( + (factory() + ..addTestId('testId') + ..component = 'div')('rendered content'), + ); + node = view.getByTestId('testId'); + + // Verify the expected outcome of each prop. + expect(node, isA()); + expect(node, hasTextContent('rendered content')); + }); + + group('initializes the factory variable with a function', () { + test('that returns a new props class implementation instance', () { + final instance = factory(); + expect(instance, isA()); + expect(instance, isA()); + }); + + test( + 'that returns a new props class implementation instance backed by an existing map', + () { + Map existingMap = {'stringProp': 'test'}; + final props = factory(existingMap); + + expect(props.stringProp, equals('test')); + + props.stringProp = 'modified'; + expect(props.stringProp, equals('modified')); + expect(existingMap['stringProp'], equals('modified')); + }); + }); + + test('generates prop getters/setters with no namespace', () { + expect(factory()..stringProp = 'test', containsPair('stringProp', 'test')); + + expect(factory()..dynamicProp = 2, containsPair('dynamicProp', 2)); + + expect(factory()..untypedProp = false, containsPair('untypedProp', false)); + }); + + group('can pass along unconsumed props', () { + const stringProp = 'a string'; + const anotherProp = 'this should be filtered'; + const className = 'aClassName'; + + group('using `addUnconsumedProps`', () { + TestProps initialProps; + TestProps secondProps; + + setUp(() { + initialProps = (factory() + ..stringProp = stringProp + ..anotherProp = anotherProp); + + secondProps = factory(); + + expect(secondProps.stringProp, isNull, + reason: 'Test setup sanity check'); + expect(secondProps.anotherProp, isNull, + reason: 'Test setup sanity check'); + }); + + test('', () { + secondProps.addUnconsumedProps(initialProps, []); + expect(secondProps.anotherProp, anotherProp); + expect(secondProps.stringProp, stringProp); + }); + + test('and consumed props are correctly filtered', () { + final consumedProps = + initialProps.staticMeta.forMixins({TestPropsMixin}); + secondProps.addUnconsumedProps(initialProps, consumedProps); + expect(secondProps.stringProp, isNull); + expect(secondProps.anotherProp, anotherProp); + }); + }); + + group('using `addUnconsumedDomProps`', () { + TestProps initialProps; + TestProps secondProps; + + setUp(() { + initialProps = (factory() + ..stringProp = stringProp + ..anotherProp = anotherProp + ..className = className); + + secondProps = factory(); + + expect(secondProps.className, isNull, + reason: 'Test setup sanity check'); + }); + + test('', () { + secondProps.addUnconsumedDomProps(initialProps, []); + expect(secondProps.stringProp, isNull); + expect(secondProps.anotherProp, isNull); + expect(secondProps.className, className); + }); + + test('and consumed props are correctly filtered', () { + expect(initialProps.className, isNotNull, + reason: 'Test setup sanity check'); + secondProps.addUnconsumedDomProps( + initialProps, [PropsMeta.forSimpleKey('className')]); + expect(secondProps.stringProp, isNull); + expect(secondProps.anotherProp, isNull); + expect(secondProps.className, isNull); + }); + }); + }); +} + +@JS('TestJsComponent') +external ReactClass get _TestJsComponent; + +UiFactory Test = uiJsComponent( + ReactJsComponentFactoryProxy(_TestJsComponent), + _$TestConfig, // ignore: undefined_identifier +); + +UiFactory TestCustom = uiJsComponent( + ReactJsComponentFactoryProxy(_TestJsComponent), + UiFactoryConfig( + propsFactory: PropsFactory.fromUiFactory(Test), + ), +); + +final NoLHSTest = uiJsComponent( + ReactJsComponentFactoryProxy(_TestJsComponent), + _$NoLHSTestConfig, // ignore: undefined_identifier +); + +final _Test = uiJsComponent( + ReactJsComponentFactoryProxy(_TestJsComponent), + _$_TestConfig, // ignore: undefined_identifier +); + +@Props(keyNamespace: '') +mixin TestPropsMixin on UiProps { + String size; + dynamic component; + + String stringProp; + dynamic dynamicProp; + var untypedProp; // ignore: prefer_typing_uninitialized_variables +} + +@Props(keyNamespace: '') +mixin ASecondPropsMixin on UiProps { + bool disabled; + String anotherProp; +} + +class TestProps = UiProps with TestPropsMixin, ASecondPropsMixin; diff --git a/test/over_react/util/js_component_test.over_react.g.dart b/test/over_react/util/js_component_test.over_react.g.dart new file mode 100644 index 000000000..32cfe167f --- /dev/null +++ b/test/over_react/util/js_component_test.over_react.g.dart @@ -0,0 +1,235 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: deprecated_member_use_from_same_package, unnecessary_null_in_if_null_operators, prefer_null_aware_operators +part of 'js_component_test.dart'; + +// ************************************************************************** +// OverReactBuilder (package:over_react/src/builder.dart) +// ************************************************************************** + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.' + ' EXCEPTION: this may be used in legacy boilerplate until' + ' it is transitioned to the new mixin-based boilerplate.') +mixin $TestPropsMixin on TestPropsMixin { + static const PropsMeta meta = _$metaForTestPropsMixin; + @override + String get size => (props[_$key__size__TestPropsMixin] ?? null) as String; + @override + set size(String value) => props[_$key__size__TestPropsMixin] = value; + @override + dynamic get component => + (props[_$key__component__TestPropsMixin] ?? null) as dynamic; + @override + set component(dynamic value) => + props[_$key__component__TestPropsMixin] = value; + @override + String get stringProp => + (props[_$key__stringProp__TestPropsMixin] ?? null) as String; + @override + set stringProp(String value) => + props[_$key__stringProp__TestPropsMixin] = value; + @override + dynamic get dynamicProp => + (props[_$key__dynamicProp__TestPropsMixin] ?? null) as dynamic; + @override + set dynamicProp(dynamic value) => + props[_$key__dynamicProp__TestPropsMixin] = value; + @override + get untypedProp => props[_$key__untypedProp__TestPropsMixin] ?? null; + @override + set untypedProp(value) => props[_$key__untypedProp__TestPropsMixin] = value; + /* GENERATED CONSTANTS */ + static const PropDescriptor _$prop__size__TestPropsMixin = + PropDescriptor(_$key__size__TestPropsMixin); + static const PropDescriptor _$prop__component__TestPropsMixin = + PropDescriptor(_$key__component__TestPropsMixin); + static const PropDescriptor _$prop__stringProp__TestPropsMixin = + PropDescriptor(_$key__stringProp__TestPropsMixin); + static const PropDescriptor _$prop__dynamicProp__TestPropsMixin = + PropDescriptor(_$key__dynamicProp__TestPropsMixin); + static const PropDescriptor _$prop__untypedProp__TestPropsMixin = + PropDescriptor(_$key__untypedProp__TestPropsMixin); + static const String _$key__size__TestPropsMixin = 'size'; + static const String _$key__component__TestPropsMixin = 'component'; + static const String _$key__stringProp__TestPropsMixin = 'stringProp'; + static const String _$key__dynamicProp__TestPropsMixin = 'dynamicProp'; + static const String _$key__untypedProp__TestPropsMixin = 'untypedProp'; + + static const List $props = [ + _$prop__size__TestPropsMixin, + _$prop__component__TestPropsMixin, + _$prop__stringProp__TestPropsMixin, + _$prop__dynamicProp__TestPropsMixin, + _$prop__untypedProp__TestPropsMixin + ]; + static const List $propKeys = [ + _$key__size__TestPropsMixin, + _$key__component__TestPropsMixin, + _$key__stringProp__TestPropsMixin, + _$key__dynamicProp__TestPropsMixin, + _$key__untypedProp__TestPropsMixin + ]; +} + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +const PropsMeta _$metaForTestPropsMixin = PropsMeta( + fields: $TestPropsMixin.$props, + keys: $TestPropsMixin.$propKeys, +); + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.' + ' EXCEPTION: this may be used in legacy boilerplate until' + ' it is transitioned to the new mixin-based boilerplate.') +mixin $ASecondPropsMixin on ASecondPropsMixin { + static const PropsMeta meta = _$metaForASecondPropsMixin; + @override + bool get disabled => + (props[_$key__disabled__ASecondPropsMixin] ?? null) as bool; + @override + set disabled(bool value) => props[_$key__disabled__ASecondPropsMixin] = value; + @override + String get anotherProp => + (props[_$key__anotherProp__ASecondPropsMixin] ?? null) as String; + @override + set anotherProp(String value) => + props[_$key__anotherProp__ASecondPropsMixin] = value; + /* GENERATED CONSTANTS */ + static const PropDescriptor _$prop__disabled__ASecondPropsMixin = + PropDescriptor(_$key__disabled__ASecondPropsMixin); + static const PropDescriptor _$prop__anotherProp__ASecondPropsMixin = + PropDescriptor(_$key__anotherProp__ASecondPropsMixin); + static const String _$key__disabled__ASecondPropsMixin = 'disabled'; + static const String _$key__anotherProp__ASecondPropsMixin = 'anotherProp'; + + static const List $props = [ + _$prop__disabled__ASecondPropsMixin, + _$prop__anotherProp__ASecondPropsMixin + ]; + static const List $propKeys = [ + _$key__disabled__ASecondPropsMixin, + _$key__anotherProp__ASecondPropsMixin + ]; +} + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +const PropsMeta _$metaForASecondPropsMixin = PropsMeta( + fields: $ASecondPropsMixin.$props, + keys: $ASecondPropsMixin.$propKeys, +); + +final UiFactoryConfig<_$$TestProps> _$TestConfig = UiFactoryConfig( + propsFactory: PropsFactory( + map: (map) => _$$TestProps(map), + jsMap: (map) => _$$TestProps$JsMap(map), + ), + displayName: 'Test'); + +@Deprecated(r'Use the private variable, _$TestConfig, instead ' + 'and update the `over_react` lower bound to version 4.1.0. ' + 'For information on why this is deprecated, see https://github.com/Workiva/over_react/pull/650') +final UiFactoryConfig<_$$TestProps> $TestConfig = _$TestConfig; + +final UiFactoryConfig<_$$TestProps> _$NoLHSTestConfig = UiFactoryConfig( + propsFactory: PropsFactory( + map: (map) => _$$TestProps(map), + jsMap: (map) => _$$TestProps$JsMap(map), + ), + displayName: 'NoLHSTest'); + +@Deprecated(r'Use the private variable, _$NoLHSTestConfig, instead ' + 'and update the `over_react` lower bound to version 4.1.0. ' + 'For information on why this is deprecated, see https://github.com/Workiva/over_react/pull/650') +final UiFactoryConfig<_$$TestProps> $NoLHSTestConfig = _$NoLHSTestConfig; + +final UiFactoryConfig<_$$TestProps> _$_TestConfig = UiFactoryConfig( + propsFactory: PropsFactory( + map: (map) => _$$TestProps(map), + jsMap: (map) => _$$TestProps$JsMap(map), + ), + displayName: '_Test'); + +@Deprecated(r'Use the private variable, _$_TestConfig, instead ' + 'and update the `over_react` lower bound to version 4.1.0. ' + 'For information on why this is deprecated, see https://github.com/Workiva/over_react/pull/650') +final UiFactoryConfig<_$$TestProps> $_TestConfig = _$_TestConfig; + +// Concrete props implementation. +// +// Implements constructor and backing map, and links up to generated component factory. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +abstract class _$$TestProps extends UiProps + with + TestPropsMixin, + $TestPropsMixin, // If this generated mixin is undefined, it's likely because TestPropsMixin is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of TestPropsMixin, and check that $TestPropsMixin is exported/imported properly. + ASecondPropsMixin, + $ASecondPropsMixin // If this generated mixin is undefined, it's likely because ASecondPropsMixin is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of ASecondPropsMixin, and check that $ASecondPropsMixin is exported/imported properly. + implements + TestProps { + _$$TestProps._(); + + factory _$$TestProps(Map backingMap) { + if (backingMap == null || backingMap is JsBackedMap) { + return _$$TestProps$JsMap(backingMap as JsBackedMap); + } else { + return _$$TestProps$PlainMap(backingMap); + } + } + + /// Let `UiProps` internals know that this class has been generated. + @override + bool get $isClassGenerated => true; + + /// The default namespace for the prop getters/setters generated for this class. + @override + String get propKeyNamespace => ''; + + @override + PropsMetaCollection get staticMeta => const PropsMetaCollection({ + // If this generated mixin is undefined, it's likely because TestPropsMixin is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of TestPropsMixin, and check that $TestPropsMixin is exported/imported properly. + TestPropsMixin: $TestPropsMixin.meta, + // If this generated mixin is undefined, it's likely because ASecondPropsMixin is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of ASecondPropsMixin, and check that $ASecondPropsMixin is exported/imported properly. + ASecondPropsMixin: $ASecondPropsMixin.meta, + }); +} + +// Concrete props implementation that can be backed by any [Map]. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$TestProps$PlainMap extends _$$TestProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$TestProps$PlainMap(Map backingMap) + : this._props = {}, + super._() { + this._props = backingMap ?? {}; + } + + /// The backing props map proxied by this class. + @override + Map get props => _props; + Map _props; +} + +// Concrete props implementation that can only be backed by [JsMap], +// allowing dart2js to compile more optimal code for key-value pair reads/writes. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$TestProps$JsMap extends _$$TestProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$TestProps$JsMap(JsBackedMap backingMap) + : this._props = JsBackedMap(), + super._() { + this._props = backingMap ?? JsBackedMap(); + } + + /// The backing props map proxied by this class. + @override + JsBackedMap get props => _props; + JsBackedMap _props; +} diff --git a/test/over_react/util/prop_conversion_test.dart b/test/over_react/util/prop_conversion_test.dart new file mode 100644 index 000000000..fdcfdcb0e --- /dev/null +++ b/test/over_react/util/prop_conversion_test.dart @@ -0,0 +1,1065 @@ +@TestOn('browser') +@JS() +library rmui.test.unit.util.prop_conversion_test; + +import 'dart:convert'; +import 'dart:html'; +import 'dart:js_util'; + +import 'package:js/js.dart'; +import 'package:over_react/over_react.dart'; +import 'package:over_react/components.dart' as components; +import 'package:over_react/src/util/js_component.dart'; +import 'package:over_react/src/util/prop_conversion.dart'; +import 'package:react/react_client/component_factory.dart'; +import 'package:react/react_client/react_interop.dart' + show React, ReactClass, ReactComponent; +import 'package:react_testing_library/matchers.dart'; +import 'package:react_testing_library/react_testing_library.dart'; +import 'package:react_testing_library/user_event.dart'; +import 'package:test/test.dart'; + +import 'ref_test_cases.dart'; +part 'prop_conversion_test.over_react.g.dart'; + +main() { + enableTestMode(); + + group('prop conversion', () { + group('utilities:', () { + group('jsifyMapProp', () { + test('passes through null values', () { + expect(jsifyMapProp(null), null); + }); + + Matcher hasJsBackedMapValue(dynamic matcher) => isA().having( + (jsMap) => JsBackedMap.backedBy(jsMap), + 'JsBackedMap.backedBy', + matcher); + + test('converts maps to JS objects', () { + expect(jsifyMapProp({'foo': 'bar'}), + hasJsBackedMapValue({'foo': 'bar'})); + }); + + test('converts nested maps deep conversion of JS objects and functions', + () { + dartFunction() {} + + expect( + jsifyMapProp({ + 'foo': { + 'bar': dartFunction, + } + }), + hasJsBackedMapValue({ + 'foo': hasJsBackedMapValue({ + 'bar': allowInterop(dartFunction), + }) + })); + }); + + // This function is tested more thoroughly and functionally in the "Map props using (un)jsifyMapProp" group below. + }); + + group('unjsifyMapProp', () { + test('passes through null values', () { + expect(unjsifyMapProp(null), null); + }); + + group('converts JS objects to maps', () { + test('with the correct contents', () { + expect(unjsifyMapProp(jsify({'foo': 'bar'}) as JsMap), + equals({'foo': 'bar'})); + }); + + test('casting the returned map to the correct type', () { + expect( + unjsifyMapProp(jsify({'foo': 'bar'}) as JsMap), + isA>()); + expect(unjsifyMapProp(jsify({'foo': 1}) as JsMap), + isA>()); + }); + }); + // This function is tested more thoroughly and functionally in the "Map props using (un)jsifyMapProp" group below. + }); + + group('jsifyRefProp', () { + test('passes through null', () { + expect(jsifyRefProp(null), null); + }); + + test('passes through Dart callback refs', () { + void dartCallbackRef(dynamic ref) {} + expect(jsifyRefProp(dartCallbackRef), + sameOrSameAllowInterop(dartCallbackRef)); + }); + + test('converts Dart ref objects', () { + final dartRef = createRef(); + expect(jsifyRefProp(dartRef), same(dartRef.jsRef)); + }); + + test('passes through JS refs', () { + final jsRef = createRef().jsRef; + expect(jsifyRefProp(jsRef), same(jsRef)); + }); + + test('passes through JS callback refs', () { + final jsCallbackRef = allowInterop((dynamic ref) {}); + expect(jsifyRefProp(jsCallbackRef), same(jsCallbackRef)); + }); + + test('passes through arbitrary Dart objects', () { + final object = Object(); + expect(jsifyRefProp(object), same(object)); + }); + + test('passes through arbitrary JS objects', () { + final object = newObject(); + expect(jsifyRefProp(object), same(object)); + }); + }); + + group('unjsifyRefProp', () { + test('passes through null', () { + expect(unjsifyRefProp(null), null); + }); + + test('passes through Dart callback refs', () { + void dartCallbackRef(dynamic ref) {} + expect(unjsifyRefProp(dartCallbackRef), same(dartCallbackRef)); + }); + + test('passes through Dart ref objects', () { + final dartRef = createRef(); + expect(unjsifyRefProp(dartRef), same(dartRef)); + }); + + test( + 'passes through JS ref objects' + ' (that had not previously been converted with jsifyRefProp)', () { + final jsRef = createRef().jsRef; + expect(unjsifyRefProp(jsRef), same(jsRef)); + }); + + test('passes through JS callback refs', () { + final jsCallbackRef = allowInterop((dynamic ref) {}); + expect(unjsifyRefProp(jsCallbackRef), same(jsCallbackRef)); + }); + + test('passes through arbitrary Dart objects', () { + final object = Object(); + expect(unjsifyRefProp(object), same(object)); + expect(() => unjsifyRefProp(object, throwOnUnhandled: true), + throwsArgumentError, + reason: + 'this object type should be considered an "unhandled" case' + ' and not get treated like another ref type'); + }); + + test('passes through arbitrary JS objects', () { + final object = newObject(); + expect(unjsifyRefProp(object), same(object)); + expect(() => unjsifyRefProp(object, throwOnUnhandled: true), + throwsArgumentError, + reason: + 'this object type should be considered an "unhandled" case' + ' and not get treated like another ref type'); + }); + }); + + group( + 'unjsifyRefProp(jsifyRefProp(object)) returns a value equivalent to `object` when passed', + () { + dynamic jsifyAndUnjsify(dynamic value) => + unjsifyRefProp(jsifyRefProp(value)); + + test('null', () { + expect(jsifyAndUnjsify(null), null); + }); + + test('Dart callback refs', () { + void dartCallbackRef(dynamic ref) {} + // Unwrapping from allowInterop isn't trivial to do, and in this case that's unnecessary, + // since the allowInterop'd function can be called the same as the original. + expect(jsifyAndUnjsify(dartCallbackRef), + same(allowInterop(dartCallbackRef))); + }); + + // E.g., this unsound but seemingly safe case + // + // final builder = Foo(); + // builder.barRef = createRef(); + // (If we don't reconvert it) Runtime type error expected `Ref`, got `NativeJavaScriptObject`. + // builder.barRef as Ref; + test('Dart ref objects (with same reified generic type)', () { + final dartRef = createRef(); + expect(jsifyAndUnjsify(dartRef), same(dartRef)); + + // These expectations are redundant due to the `same` expectation above, + // but this is really the behavior we want to test. + expect(jsifyAndUnjsify(dartRef), + isA().havingJsRef(same(dartRef.jsRef)), + reason: 'should be backed by the same JS object'); + expect(jsifyAndUnjsify(dartRef), isA>(), + reason: 'should have the same reified type'); + }); + + test('JS ref objects', () { + final jsRef = createRef().jsRef; + expect(jsifyAndUnjsify(jsRef), same(jsRef)); + }); + + test('JS callback refs', () { + final jsCallbackRef = allowInterop((dynamic ref) {}); + expect(jsifyAndUnjsify(jsCallbackRef), same(jsCallbackRef)); + }); + + test('arbitrary Dart objects', () { + final object = Object(); + expect(jsifyAndUnjsify(object), same(object)); + }); + + test('arbitrary JS objects', () { + final object = newObject(); + expect(jsifyAndUnjsify(object), same(object)); + }); + }); + }); + + group( + 'works as expected when using converting prop getters/setters with JS components:', + () { + group('Map props using (un)jsifyMapProp', () { + group('get converted to JS objects', () { + group('in the setter, and gets unconverted in getter', () { + // This case is a little redundant with the (un)jsifyMapProp tests above, but include it for completeness. + test('when set to a Map', () { + final builder = TestJs()..buttonProps = {'data-foo': 'bar'}; + + final propKey = TestJs.getPropKey((p) => p.buttonProps); + expect(builder, {propKey: isA()}, + reason: + 'test setup: should have converted to a JS object for storage in props map' + ' (we want to ensure this happens before it gets to the ReactComponentFactoryProxy)'); + expect(builder.buttonProps, isA(), + reason: + 'should have unconverted JsMap to a Map in the typed getter'); + }); + + // This case is a little redundant with the (un)jsifyMapProp tests above, but include it for completeness. + test('when null', () { + final builder = TestJs(); + + expect(builder, {}, reason: 'test setup check'); + expect(builder.buttonProps, isNull); + + builder.buttonProps = null; + final propKey = TestJs.getPropKey((p) => p.buttonProps); + expect(builder, {propKey: null}); + expect(builder.buttonProps, isNull); + }); + + group('and allows pattern of setting a map prop in a builder', () { + test('and then mutating the value read from the builder', () { + final builder = TestJs(); + + builder.buttonProps = {}; + expect(builder.buttonProps, {}); + + final propKey = TestJs.getPropKey((p) => p.buttonProps); + expect(builder, {propKey: isA()}, + reason: + 'test setup: should have converted to a JS object for storage in props map'); + + // For this test, it's important to mutate the the value returned from the `builder.buttonProps` getter, + // and not the original Map passed into `builder.buttonProps`. + builder.buttonProps['data-foo'] = 'bar'; + expect(builder.buttonProps, {'data-foo': 'bar'}); + }); + + group('and then reading a nested value that gets unconverted:', + () { + group('A nested map prop', () { + test('using the typed props map', () { + final builder = TestJs() + ..buttonProps = (TestJs()..buttonProps = {'foo': 'bar'}); + expect(TestJs(builder.buttonProps).buttonProps, + allOf(isA(), {'foo': 'bar'})); + }); + + test('not using the typed props map', () { + final builder = TestJs() + ..buttonProps = (TestJs()..buttonProps = {'foo': 'bar'}); + final propKey = TestJs.getPropKey((p) => p.buttonProps); + expect(builder.buttonProps, + containsPair(propKey, isA()), + reason: 'not a Dart map due to tradeoffs'); + }, tags: 'js-interop-tradeoff'); + }); + + // This is a special case since style gets unconverted conditionally in UiProps + group('special-case: style map', () { + test('using the typed props map', () { + final builder = TestJs() + ..buttonProps = { + 'style': { + 'color': 'blue', + }, + }; + expect(domProps(builder.buttonProps).style, isA()); + }); + + test('not using the typed props map', () { + final builder = TestJs() + ..buttonProps = { + 'style': { + 'color': 'blue', + }, + }; + expect(builder.buttonProps, + containsPair('style', isA()), + reason: 'not a Dart map due to tradeoffs'); + }, tags: 'js-interop-tradeoff'); + }); + }); + + test('but not then mutating the original value', () { + final buttonProps = {}; + + final builder = TestJs(); + + builder.buttonProps = buttonProps; + expect(builder.buttonProps, {}); + + final propKey = TestJs.getPropKey((p) => p.buttonProps); + expect(builder, {propKey: isA()}, + reason: + 'test setup: should have converted to a JS object for storage in props map'); + + // For this test, it's important to mutate the original `buttonProps` Map passed into `builder.buttonProps`, + // and not the value returned from the `builder.buttonProps` getter. + buttonProps['data-foo'] = 'bar'; + expect(builder.buttonProps, isEmpty); + // This is what we'd expect if this case worked. + // expect(builder.buttonProps, {'data-foo': 'bar'}); + }, tags: 'js-interop-tradeoff'); + }); + }); + + test('and can be read properly by the JS component', () { + final view = + render((TestJs()..buttonProps = {'data-foo': 'bar'})()); + final button = view.getByRole('button'); + expect(button, hasAttribute('data-foo', 'bar'), + reason: 'buttonProps should have been readable by JS component' + ' and properly passed to the rendered button'); + }); + }); + + // We're testing this since ReactJsComponentFactoryProxy doesn't convert values nested within JS objects; + // it only converts values nested within Dart Maps/Lists. + + group( + 'work with props that would normally get converted in ReactJsComponentFactoryProxy:', + () { + test('Dart Maps and Functions', () { + final onClickCalls = []; + final view = render((TestJs() + ..buttonProps = (domProps() + ..style = {'color': 'blue'} + ..onClick = (event) { + onClickCalls.add(event); + }))()); + + final button = view.getByRole('button'); + expect(button, hasStyles({'color': 'blue'})); + + UserEvent.click(button); + expect(onClickCalls, [ + isA(), + ]); + }); + + group('refs under the "ref" key', () { + test('JS callback ref', () { + dynamic buttonRef; + final view = render((TestJs() + ..buttonProps = (domProps() + ..ref = allowInterop((ref) { + buttonRef = ref; + })))()); + expect(buttonRef, view.getByRole('button')); + }); + + group('Dart callback ref', () { + test('(untyped)', () { + dynamic buttonRef; + final view = render((TestJs() + ..buttonProps = (domProps() + ..ref = (ref) { + buttonRef = ref; + }))()); + expect(buttonRef, view.getByRole('button')); + }); + + test('(typed)', () { + ButtonElement buttonRef; + final view = render((TestJs() + ..buttonProps = (domProps() + ..ref = (ButtonElement ref) { + buttonRef = ref; + }))()); + expect(buttonRef, view.getByRole('button')); + }); + }); + + // This test only fails in Dart2js when conversion is missing, since the JS class that DDC compiles the + // Dart `Ref` class to has a `current` getter/setter that are compatible with the JS API, and happens to work. + test('Dart ref object', () { + final buttonRef = createRef(); + final view = render( + (TestJs()..buttonProps = (domProps()..ref = buttonRef))()); + expect(buttonRef.current, view.getByRole('button')); + }, tags: 'ddc-false-positive'); + + test('JS ref object', () { + final buttonRef = createRef().jsRef; + final view = render( + (TestJs()..buttonProps = (domProps()..ref = buttonRef))()); + expect(buttonRef.current, view.getByRole('button')); + }); + }); + }); + }); + + group('custom ref props using (un)jsifyRefProp', () { + group( + 'convert the ref properly for consumption by the JS component when the ref is a', + () { + test('JS callback ref', () { + dynamic inputRef; + final view = render((TestJs() + ..inputRef = allowInterop((ref) { + inputRef = ref; + }))()); + expect(inputRef, view.getByRole('textbox')); + }); + + group('Dart callback ref', () { + test('(untyped)', () { + dynamic inputRef; + final view = render((TestJs() + ..inputRef = (ref) { + inputRef = ref; + })()); + expect(inputRef, view.getByRole('textbox')); + }); + + test('(typed)', () { + InputElement inputRef; + final view = render((TestJs() + ..inputRef = (InputElement ref) { + inputRef = ref; + })()); + expect(inputRef, view.getByRole('textbox')); + }); + }); + + test('Dart ref object', () { + final inputRef = createRef(); + final view = render((TestJs()..inputRef = inputRef)()); + expect(inputRef.current, view.getByRole('textbox')); + }); + + test('JS ref object', () { + final inputRef = createRef().jsRef; + final view = render((TestJs()..inputRef = inputRef)()); + expect(inputRef.current, view.getByRole('textbox')); + }); + }); + + group('get converted to JS objects', () { + group('in the setter, and gets unconverted in getter', () { + // This case is a little redundant with the (un)jsifyRefProp tests above, but include it for completeness. + test('when set to a ref that gets converted', () { + final dartRef = createRef(); + final builder = TestJs()..inputRef = dartRef; + + final propKey = TestJs.getPropKey((p) => p.inputRef); + expect(builder, {propKey: same(dartRef.jsRef)}, + reason: + 'test setup: should have converted to a JS ref for storage in props map' + ' (we want to ensure this happens before it gets to the ReactComponentFactoryProxy)'); + expect(builder.inputRef, isA(), + reason: + 'should have unconverted JsRef to a Ref in the typed getter'); + }); + + // This case is a little redundant with the (un)jsifyRefProp tests above, but include it for completeness. + test('when null', () { + final builder = TestJs(); + + expect(builder, {}, reason: 'test setup check'); + expect(builder.inputRef, isNull); + + builder.inputRef = null; + final propKey = TestJs.getPropKey((p) => p.inputRef); + expect(builder, {propKey: null}); + expect(builder.inputRef, isNull); + }); + }); + }); + }); + }); + + group('works as expected in advanced cases:', () { + group('Dart component wrapper around JS component', () { + // This is a regression test for the original case described in the original Dart wrapper props conversion issues + // ticket (CPLAT-11941), and it works as expected due to unconverting props in the getter. + test( + 'when a JS component clones the element with JS props that normally get converted before passing them in,' + ' and the Dart component receives those JS props and attempts to read them', + () { + final propKey = TestJs.getPropKey((p) => p.buttonProps); + + DartTestJsWrapperProps capturedProps; + final view = render(React.cloneElement( + (DartTestJsWrapper() + ..onRender = expectAsync1((props) { + capturedProps = props; + }))(), + jsify({ + propKey: { + 'style': {'color': 'blue'} + } + }) as JsMap, + )); + + expect(() => capturedProps.buttonProps, returnsNormally); + expect(capturedProps.buttonProps, isA()); + + final node = view.getByRole('button'); + expect(node, hasStyles({'color': 'blue'})); + }); + + group('that renders a Dart component expecting a Dart style map', () { + test('when passed styles from a Dart component', () { + final view = render((DartTestJsWrapper() + ..component = ExpectsDartStyleProp.elementType + ..style = {'color': 'blue'} + ..addTestId('componentRoot'))()); + final node = view.getByTestId('componentRoot'); + expect(node, hasStyles({'color': 'blue'})); + }); + + test('when a JS component clones the element with JS styles', () { + final view = render(React.cloneElement( + (DartTestJsWrapper() + ..component = ExpectsDartStyleProp.elementType + ..addTestId('componentRoot'))(), + jsify({ + 'style': {'color': 'blue'} + }) as JsMap, + )); + final node = view.getByTestId('componentRoot'); + expect(node, hasStyles({'color': 'blue'})); + }); + }); + }); + + group('Dart component used as `component` prop', () { + test('expecting a Dart style map', () { + final view = render((TestJs() + ..component = ExpectsDartStyleProp.elementType + ..style = {'color': 'blue'} + ..addTestId('componentRoot'))()); + final node = view.getByTestId('componentRoot'); + expect(node, hasStyles({'color': 'blue'})); + }); + + test('expecting a custom Dart Map prop', () { + final renderErrors = []; + + // Use an ErrorBoundary to detect errors on render, since otherwise + // React will just unmount the tree without throwing. + render((components.ErrorBoundary() + ..onComponentDidCatch = ((error, _) => renderErrors.add(error)) + ..shouldLogErrors = false + ..fallbackUIRenderer = + ((_, __) => Dom.span()('An error occurred during render')))( + (TestJs() + ..component = ExpectsDartMapProp.elementType + ..addProps(ExpectsDartMapProp()..dartMapProp = {'foo': 'bar'}) + ..addTestId('componentRoot'))(), + )); + + expect(renderErrors, [ + isA().havingToStringValue(anyOf( + // DDC error message + matches(RegExp( + r"Expected a value of type 'Map[^']*', but got one of type 'NativeJavaScriptObject'")), + // dart2js error message + matches(RegExp( + r"type 'UnknownJavaScriptObject' is not a subtype of type 'Map[^']*'")), + )), + ]); + + // These are the expectations we'd use if this case didn't error: + // final node = view.getByTestId('componentRoot'); + // expect(node, hasAttribute('data-dart-map-prop', jsonEncode({'foo': 'bar'}))); + }, tags: ['js-interop-tradeoff']); + + // This case can't use MUI button because it always passes children as lists instead of passing them through. + group('expecting a custom Dart children prop', () { + ReactElement expectNonListChildren(ReactElement el) { + expect( + JsBackedMap.backedBy(el.props)['children'], isNot(isA()), + reason: + 'test setup check; children should not be a List in order to test how' + ' the Dart `component` behaves when the `children` prop is not a List'); + return el; + } + + // Using UiProps/ReactComponentFactoryProxy might not be the most stable way to pass in children that aren't + // lists (though they do conditionally pass in non-Lists for JS components), but it gets the job done, and also + // emulates real-world cases of passing children from Dart consumption + // (though not when the JS component is providing the children; but that may not be the case for real world uses of MUI). + // + // So, we'll use expectNonListChildren to ensure these are testing non-list children properly. + test('(no children)', () { + render(expectNonListChildren((TestJs() + ..component = ExpectsListChildrenProp.elementType + ..addTestId('componentRoot'))())); + }); + + test('(one child)', () { + final view = render(expectNonListChildren((TestJs() + ..component = ExpectsListChildrenProp.elementType + ..addTestId('componentRoot'))('single-child'))); + final node = view.getByTestId('componentRoot'); + expect(node, hasTextContent('single-child')); + }); + + test('(multiple children)', () { + final view = render((TestJs() + ..component = ExpectsListChildrenProp.elementType + ..addTestId('componentRoot'))('child-1', 'child-2')); + final node = view.getByTestId('componentRoot'); + expect(node, hasTextContent('child-1' 'child-2')); + }); + }); + + group('and a ref is set on that component', () { + group('which eventually ends up on a non-Dart component', () { + final testCaseCollection = RefTestCaseCollection(); + + group('and is passed in via a forwarded "ref" prop as a', () { + for (final testCaseName in testCaseCollection.allTestCaseNames) { + test(testCaseName, () { + final testCase = + testCaseCollection.createCaseByName(testCaseName); + + render((TestJs() + ..component = BasicForwardRef.elementType + ..ref = testCase.ref)()); + + expect(testCase.getCurrent(), isA()); + expect(testCase.getCurrent(), + hasAttribute(basicForwardRefAttribute), + reason: + 'test setup: ref should have been forwarded to the element we expect'); + }); + } + }); + + group( + 'and is passed in via a custom "ref" prop (with conversion) as a', + () { + for (final testCaseName in testCaseCollection.allTestCaseNames) { + test(testCaseName, () { + final testCase = + testCaseCollection.createCaseByName(testCaseName); + + render((TestJs() + ..inputComponent = BasicForwardRef.elementType + ..inputRef = testCase.ref)()); + + expect(testCase.getCurrent(), isA()); + expect(testCase.getCurrent(), + hasAttribute(basicForwardRefAttribute), + reason: + 'test setup: ref should have been forwarded to the element we expect'); + }); + } + }); + + group( + 'and is passed in via a "ref" prop in a nested props Map (with conversion) as a', + () { + for (final testCaseName in testCaseCollection.allTestCaseNames) { + test(testCaseName, () { + final testCase = + testCaseCollection.createCaseByName(testCaseName); + + render((TestJs() + ..buttonComponent = BasicForwardRef.elementType + ..buttonProps = (domProps()..ref = testCase.ref))()); + + expect(testCase.getCurrent(), isA()); + expect(testCase.getCurrent(), + hasAttribute(basicForwardRefAttribute), + reason: + 'test setup: ref should have been forwarded to the element we expect'); + }); + } + }); + }); + + group('which eventually ends up on a Dart class component', () { + final testCaseCollection = + RefTestCaseCollection(); + + setUpAll(() { + // Make sure the special cases we're concerned about are included in this set of test cases: + void assertSingleMetaWhere(bool Function(RefTestCaseMeta) test) { + expect(testCaseCollection.allTestCaseMetas.where(test).toList(), + hasLength(1)); + } + + assertSingleMetaWhere( + (m) => !m.isJs && m.kind.isCallback && m.isStronglyTyped); + assertSingleMetaWhere( + (m) => !m.isJs && m.kind.isCallback && !m.isStronglyTyped); + assertSingleMetaWhere( + (m) => !m.isJs && m.kind.isObject && m.isStronglyTyped); + assertSingleMetaWhere( + (m) => !m.isJs && m.kind.isObject && !m.isStronglyTyped); + }); + + void forEachTestCase( + void callback(String testCaseName, bool isDartCallbackCase, + bool isDartRefObjectCase, bool isTyped)) { + for (final testCaseName in testCaseCollection.allTestCaseNames) { + final meta = + testCaseCollection.testCaseMetaByName(testCaseName); + final isDartCallbackCase = !meta.isJs && meta.kind.isCallback; + final isDartRefObjectCase = !meta.isJs && meta.kind.isObject; + final isTyped = meta.isStronglyTyped; + + callback(testCaseName, isDartCallbackCase, isDartRefObjectCase, + isTyped); + } + } + + group('and is passed in via a forwarded "ref" prop as a', () { + forEachTestCase((testCaseName, isDartCallbackCase, + isDartRefObjectCase, isTyped) { + if (isDartCallbackCase && isTyped) { + // Bail since we know typed Dart callback refs will fail with a runtime type error since the JS ReactComponent + // will be passed in instead of the Dart instance; this behavior will be tested in the untyped Dart callback ref test case. + return; + } + + test(testCaseName, () { + final testCase = + testCaseCollection.createCaseByName(testCaseName); + + render((TestJs() + ..component = ClassComponent.elementType + ..ref = testCase.ref)()); + + if (isDartRefObjectCase) { + expect( + testCase.getCurrent(), isA()); + } else { + expect( + testCase.getCurrent(), + isA().havingDartComponent( + isA())); + } + }, tags: [ + if (isDartCallbackCase) 'js-interop-tradeoff', + ]); + }); + }); + + group( + 'and is passed in via a custom "ref" prop (with conversion) as a', + () { + forEachTestCase((testCaseName, isDartCallbackCase, + isDartRefObjectCase, isTyped) { + if (isDartCallbackCase && isTyped) { + // Bail since we know typed Dart callback refs will fail with a runtime type error since the JS ReactComponent + // will be passed in instead of the Dart instance; this behavior will be tested in the untyped Dart callback ref test case. + return; + } + + test(testCaseName, () { + final testCase = + testCaseCollection.createCaseByName(testCaseName); + + render((TestJs() + ..inputComponent = ClassComponent.elementType + ..inputRef = testCase.ref)()); + + if (isDartRefObjectCase) { + expect( + testCase.getCurrent(), isA()); + } else { + expect( + testCase.getCurrent(), + isA().havingDartComponent( + isA())); + } + }, tags: [ + if (isDartCallbackCase) 'js-interop-tradeoff', + ]); + }); + }); + + // This is the one case where Dart (and unfortunately, JS) callback refs actually get the class component + group( + 'and is passed in via a "ref" prop in a nested props Map (with conversion) as a', + () { + forEachTestCase((testCaseName, isDartCallbackCase, + isDartRefObjectCase, isTyped) { + final isJsCallbackCase = + testCaseName == RefTestCaseCollection.jsCallbackRefCaseName; + + test(testCaseName, () { + final testCase = + testCaseCollection.createCaseByName(testCaseName); + + render((TestJs() + ..buttonComponent = ClassComponent.elementType + ..buttonProps = (domProps()..ref = testCase.ref))()); + + // JS callbacks look the same to generateJsProps as Dart callback refs do, + // so they get the Dart component as well. // TODO investigate, decide how concerned we should be about this + if (isDartRefObjectCase || + isDartCallbackCase || + isJsCallbackCase) { + expect( + testCase.getCurrent(), isA()); + } else { + expect( + testCase.getCurrent(), + isA().havingDartComponent( + isA())); + } + }, tags: [ + if (isJsCallbackCase) 'js-interop-tradeoff', + ]); + }); + }); + }); + }); + }); + }); + }); +} + +// +// Misc convenience functions +// + +const _getPropKey = getPropKey; + +extension on UiFactory { + String getPropKey(void Function(T) accessProp) { + return _getPropKey(accessProp, this); + } +} + +// +// Matcher convenience functions +// + +Matcher sameOrSameAllowInterop(Function f) => + anyOf(same(allowInterop(f)), same(f)); + +extension on TypeMatcher { + Matcher havingJsRef(dynamic matcher) => + having((ref) => ref.jsRef, 'jsRef', matcher); +} + +extension on TypeMatcher { + Matcher havingToStringValue(dynamic matcher) => + having((o) => o.toString(), 'toString() value', matcher); +} + +extension on TypeMatcher { + Matcher havingDartComponent(dynamic matcher) => + having((ref) => ref.dartComponent, 'dartComponent', matcher); +} + +// +// Runtime type verification +// + +/// Verifies the runtime type of object, throwing a cast error if it does not match. +/// +/// Even though [object] typed as [T], we'll verify it explicitly in case +/// the type-check at the function call site gets compiled out. +void verifyType(T object) { + if (T == Object || T == dynamic || T == Null) { + throw ArgumentError.value(T, 'T', 'must be more specific'); + } + + if (object is! T) { + // Try to get Dart to throw a real type error by explicitly casting. + // ignore: unnecessary_statements, unnecessary_cast + object as T; + + // The above `as` should always fail, but may not if the cast gets compiled out, + // or if there are edge cases in the type system cases where this seemingly-incorrect cast works. + // + // Explicitly throw an error so that this case doesn't go unnoticed. + throw UnexpectedSuccessfulCastError( + 'Casting ${Error.safeToString(object)} to type $T should have failed, but did not for some reason.' + ' Check the implementation of verifyType.'); + } +} + +class UnexpectedSuccessfulCastError extends Error { + final String message; + + UnexpectedSuccessfulCastError(this.message); + + @override + String toString() => message; +} + +// +// Test components for use as JS `component` prop +// + +mixin ExpectsDartMapPropProps on UiProps { + Map dartMapProp; +} + +UiFactory ExpectsDartMapProp = uiForwardRef( + (props, ref) { + verifyType(props.dartMapProp); + + return (Dom.div() + ..addProps(props) + // Use the prop in a way that would fail if it wasn't the correct type + ..['data-dart-map-prop'] = jsonEncode(props.dartMapProp) + ..ref = ref)(props.children); + }, + _$ExpectsDartMapPropConfig, // ignore: undefined_identifier +); + +mixin ExpectsDartStylePropProps on UiProps {} + +UiFactory ExpectsDartStyleProp = uiForwardRef( + (props, ref) { + verifyType>(props.style); + + return (Dom.div() + ..addProps(props) + ..style = { + // Use the prop in a way that would fail if it wasn't the correct type + ...props.style, + })(props.children); + }, + _$ExpectsDartStylePropConfig, // ignore: undefined_identifier +); + +mixin ExpectsListChildrenPropProps on UiProps {} + +UiFactory ExpectsListChildrenProp = uiForwardRef( + (props, ref) { + verifyType(props.children); + + return (Dom.div()..addProps(props))( + // Use the prop in a way that would fail if it wasn't the correct type + props.children.map((child) => child), + ); + }, + _$ExpectsListChildrenPropConfig, // ignore: undefined_identifier +); + +// +// Other test components +// + +UiFactory ClassComponent = + castUiFactory(_$ClassComponent); // ignore: undefined_identifier + +mixin ClassComponentProps on UiProps {} + +class ClassComponentComponent extends UiComponent2 { + @override + render() { + return (Dom.div()..addProps(props))(props.children); + } +} + +mixin BasicForwardRefProps on UiProps {} + +const basicForwardRefAttribute = 'data-basic-forward-ref'; +UiFactory BasicForwardRef = uiForwardRef( + (props, ref) { + return (Dom.span() + ..addProps(props) + ..[basicForwardRefAttribute] = '' + ..ref = ref)(props.children); + }, + _$BasicForwardRefConfig, // ignore: undefined_identifier +); + +mixin DartTestJsWrapperPropsMixin on UiProps { + void Function(DartTestJsWrapperProps props) onRender; +} + +class DartTestJsWrapperProps = UiProps + with TestJsProps, DartTestJsWrapperPropsMixin; + +UiFactory DartTestJsWrapper = uiForwardRef( + (props, ref) { + props.onRender?.call(props); + + final consumedProps = + props.staticMeta.forMixins({DartTestJsWrapperPropsMixin}); + return (TestJs() + ..addUnconsumedProps(props, consumedProps) + ..ref = ref)(props.children); + }, + _$DartTestJsWrapperConfig, // ignore: undefined_identifier +); + +// +// OverReact bindings for a JS test component +// to assert correct behavior in more functional-style cases. +// + +@Props(keyNamespace: '') +mixin TestJsProps on UiProps { + @Accessor(key: 'buttonProps') + JsMap _$raw$buttonProps; + + Map get buttonProps => unjsifyMapProp(_$raw$buttonProps); + set buttonProps(Map value) => _$raw$buttonProps = jsifyMapProp(value); + + @Accessor(key: 'inputRef') + dynamic _$raw$inputRef; + + dynamic get inputRef => unjsifyRefProp(_$raw$inputRef); + set inputRef(dynamic value) => _$raw$inputRef = jsifyRefProp(value); + + dynamic /*ElementType*/ component; + dynamic /*ElementType*/ inputComponent; + dynamic /*ElementType*/ buttonComponent; +} + +UiFactory TestJs = uiJsComponent( + ReactJsComponentFactoryProxy(_TestJs), + _$TestJsConfig, // ignore: undefined_identifier +); + +@JS('TestJsComponent') +external ReactClass get _TestJs; diff --git a/test/over_react/util/prop_conversion_test.over_react.g.dart b/test/over_react/util/prop_conversion_test.over_react.g.dart new file mode 100644 index 000000000..8d77fcbda --- /dev/null +++ b/test/over_react/util/prop_conversion_test.over_react.g.dart @@ -0,0 +1,899 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: deprecated_member_use_from_same_package, unnecessary_null_in_if_null_operators, prefer_null_aware_operators +part of 'prop_conversion_test.dart'; + +// ************************************************************************** +// OverReactBuilder (package:over_react/src/builder.dart) +// ************************************************************************** + +// React component factory implementation. +// +// Registers component implementation and links type meta to builder factory. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +final $ClassComponentComponentFactory = registerComponent2( + () => _$ClassComponentComponent(), + builderFactory: _$ClassComponent, + componentClass: ClassComponentComponent, + isWrapper: false, + parentType: null, + displayName: 'ClassComponent', +); + +_$$ClassComponentProps _$ClassComponent([Map backingProps]) => + backingProps == null + ? _$$ClassComponentProps$JsMap(JsBackedMap()) + : _$$ClassComponentProps(backingProps); + +// Concrete props implementation. +// +// Implements constructor and backing map, and links up to generated component factory. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +abstract class _$$ClassComponentProps extends UiProps + with + ClassComponentProps, + $ClassComponentProps // If this generated mixin is undefined, it's likely because ClassComponentProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of ClassComponentProps, and check that $ClassComponentProps is exported/imported properly. +{ + _$$ClassComponentProps._(); + + factory _$$ClassComponentProps(Map backingMap) { + if (backingMap == null || backingMap is JsBackedMap) { + return _$$ClassComponentProps$JsMap(backingMap as JsBackedMap); + } else { + return _$$ClassComponentProps$PlainMap(backingMap); + } + } + + /// Let `UiProps` internals know that this class has been generated. + @override + bool get $isClassGenerated => true; + + /// The `ReactComponentFactory` associated with the component built by this class. + @override + ReactComponentFactoryProxy get componentFactory => + super.componentFactory ?? $ClassComponentComponentFactory; + + /// The default namespace for the prop getters/setters generated for this class. + @override + String get propKeyNamespace => ''; + + @override + PropsMetaCollection get staticMeta => const PropsMetaCollection({ + // If this generated mixin is undefined, it's likely because ClassComponentProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of ClassComponentProps, and check that $ClassComponentProps is exported/imported properly. + ClassComponentProps: $ClassComponentProps.meta, + }); +} + +// Concrete props implementation that can be backed by any [Map]. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$ClassComponentProps$PlainMap extends _$$ClassComponentProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$ClassComponentProps$PlainMap(Map backingMap) + : this._props = {}, + super._() { + this._props = backingMap ?? {}; + } + + /// The backing props map proxied by this class. + @override + Map get props => _props; + Map _props; +} + +// Concrete props implementation that can only be backed by [JsMap], +// allowing dart2js to compile more optimal code for key-value pair reads/writes. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$ClassComponentProps$JsMap extends _$$ClassComponentProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$ClassComponentProps$JsMap(JsBackedMap backingMap) + : this._props = JsBackedMap(), + super._() { + this._props = backingMap ?? JsBackedMap(); + } + + /// The backing props map proxied by this class. + @override + JsBackedMap get props => _props; + JsBackedMap _props; +} + +// Concrete component implementation mixin. +// +// Implements typed props/state factories, defaults `consumedPropKeys` to the keys +// generated for the associated props class. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$ClassComponentComponent extends ClassComponentComponent { + _$$ClassComponentProps$JsMap _cachedTypedProps; + + @override + _$$ClassComponentProps$JsMap get props => _cachedTypedProps; + + @override + set props(Map value) { + assert( + getBackingMap(value) is JsBackedMap, + 'Component2.props should never be set directly in ' + 'production. If this is required for testing, the ' + 'component should be rendered within the test. If ' + 'that does not have the necessary result, the last ' + 'resort is to use typedPropsFactoryJs.'); + super.props = value; + _cachedTypedProps = + typedPropsFactoryJs(getBackingMap(value) as JsBackedMap); + } + + @override + _$$ClassComponentProps$JsMap typedPropsFactoryJs(JsBackedMap backingMap) => + _$$ClassComponentProps$JsMap(backingMap); + + @override + _$$ClassComponentProps typedPropsFactory(Map backingMap) => + _$$ClassComponentProps(backingMap); + + /// Let `UiComponent` internals know that this class has been generated. + @override + bool get $isClassGenerated => true; + + /// The default consumed props, comprising all props mixins used by ClassComponentProps. + /// Used in `*ConsumedProps` methods if [consumedProps] is not overridden. + @override + get $defaultConsumedProps => propsMeta.all; + + @override + PropsMetaCollection get propsMeta => const PropsMetaCollection({ + // If this generated mixin is undefined, it's likely because ClassComponentProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of ClassComponentProps, and check that $ClassComponentProps is exported/imported properly. + ClassComponentProps: $ClassComponentProps.meta, + }); +} + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.' + ' EXCEPTION: this may be used in legacy boilerplate until' + ' it is transitioned to the new mixin-based boilerplate.') +mixin $ExpectsDartMapPropProps on ExpectsDartMapPropProps { + static const PropsMeta meta = _$metaForExpectsDartMapPropProps; + @override + Map get dartMapProp => + (props[_$key__dartMapProp__ExpectsDartMapPropProps] ?? null) as Map; + @override + set dartMapProp(Map value) => + props[_$key__dartMapProp__ExpectsDartMapPropProps] = value; + /* GENERATED CONSTANTS */ + static const PropDescriptor _$prop__dartMapProp__ExpectsDartMapPropProps = + PropDescriptor(_$key__dartMapProp__ExpectsDartMapPropProps); + static const String _$key__dartMapProp__ExpectsDartMapPropProps = + 'ExpectsDartMapPropProps.dartMapProp'; + + static const List $props = [ + _$prop__dartMapProp__ExpectsDartMapPropProps + ]; + static const List $propKeys = [ + _$key__dartMapProp__ExpectsDartMapPropProps + ]; +} + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +const PropsMeta _$metaForExpectsDartMapPropProps = PropsMeta( + fields: $ExpectsDartMapPropProps.$props, + keys: $ExpectsDartMapPropProps.$propKeys, +); + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.' + ' EXCEPTION: this may be used in legacy boilerplate until' + ' it is transitioned to the new mixin-based boilerplate.') +mixin $ExpectsDartStylePropProps on ExpectsDartStylePropProps { + static const PropsMeta meta = _$metaForExpectsDartStylePropProps; + /* GENERATED CONSTANTS */ + + static const List $props = []; + static const List $propKeys = []; +} + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +const PropsMeta _$metaForExpectsDartStylePropProps = PropsMeta( + fields: $ExpectsDartStylePropProps.$props, + keys: $ExpectsDartStylePropProps.$propKeys, +); + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.' + ' EXCEPTION: this may be used in legacy boilerplate until' + ' it is transitioned to the new mixin-based boilerplate.') +mixin $ExpectsListChildrenPropProps on ExpectsListChildrenPropProps { + static const PropsMeta meta = _$metaForExpectsListChildrenPropProps; + /* GENERATED CONSTANTS */ + + static const List $props = []; + static const List $propKeys = []; +} + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +const PropsMeta _$metaForExpectsListChildrenPropProps = PropsMeta( + fields: $ExpectsListChildrenPropProps.$props, + keys: $ExpectsListChildrenPropProps.$propKeys, +); + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.' + ' EXCEPTION: this may be used in legacy boilerplate until' + ' it is transitioned to the new mixin-based boilerplate.') +mixin $ClassComponentProps on ClassComponentProps { + static const PropsMeta meta = _$metaForClassComponentProps; + /* GENERATED CONSTANTS */ + + static const List $props = []; + static const List $propKeys = []; +} + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +const PropsMeta _$metaForClassComponentProps = PropsMeta( + fields: $ClassComponentProps.$props, + keys: $ClassComponentProps.$propKeys, +); + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.' + ' EXCEPTION: this may be used in legacy boilerplate until' + ' it is transitioned to the new mixin-based boilerplate.') +mixin $BasicForwardRefProps on BasicForwardRefProps { + static const PropsMeta meta = _$metaForBasicForwardRefProps; + /* GENERATED CONSTANTS */ + + static const List $props = []; + static const List $propKeys = []; +} + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +const PropsMeta _$metaForBasicForwardRefProps = PropsMeta( + fields: $BasicForwardRefProps.$props, + keys: $BasicForwardRefProps.$propKeys, +); + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.' + ' EXCEPTION: this may be used in legacy boilerplate until' + ' it is transitioned to the new mixin-based boilerplate.') +mixin $DartTestJsWrapperPropsMixin on DartTestJsWrapperPropsMixin { + static const PropsMeta meta = _$metaForDartTestJsWrapperPropsMixin; + @override + void Function(DartTestJsWrapperProps props) get onRender => + (props[_$key__onRender__DartTestJsWrapperPropsMixin] ?? null) as void + Function(DartTestJsWrapperProps props); + @override + set onRender(void Function(DartTestJsWrapperProps props) value) => + props[_$key__onRender__DartTestJsWrapperPropsMixin] = value; + /* GENERATED CONSTANTS */ + static const PropDescriptor _$prop__onRender__DartTestJsWrapperPropsMixin = + PropDescriptor(_$key__onRender__DartTestJsWrapperPropsMixin); + static const String _$key__onRender__DartTestJsWrapperPropsMixin = + 'DartTestJsWrapperPropsMixin.onRender'; + + static const List $props = [ + _$prop__onRender__DartTestJsWrapperPropsMixin + ]; + static const List $propKeys = [ + _$key__onRender__DartTestJsWrapperPropsMixin + ]; +} + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +const PropsMeta _$metaForDartTestJsWrapperPropsMixin = PropsMeta( + fields: $DartTestJsWrapperPropsMixin.$props, + keys: $DartTestJsWrapperPropsMixin.$propKeys, +); + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.' + ' EXCEPTION: this may be used in legacy boilerplate until' + ' it is transitioned to the new mixin-based boilerplate.') +mixin $TestJsProps on TestJsProps { + static const PropsMeta meta = _$metaForTestJsProps; + @override + @Accessor(key: 'buttonProps') + JsMap get _$raw$buttonProps => + (props[_$key___$raw$buttonProps__TestJsProps] ?? null) as JsMap; + @override + @Accessor(key: 'buttonProps') + set _$raw$buttonProps(JsMap value) => + props[_$key___$raw$buttonProps__TestJsProps] = value; + @override + @Accessor(key: 'inputRef') + dynamic get _$raw$inputRef => + (props[_$key___$raw$inputRef__TestJsProps] ?? null) as dynamic; + @override + @Accessor(key: 'inputRef') + set _$raw$inputRef(dynamic value) => + props[_$key___$raw$inputRef__TestJsProps] = value; + @override + dynamic get component => + (props[_$key__component__TestJsProps] ?? null) as dynamic; + @override + set component(dynamic value) => props[_$key__component__TestJsProps] = value; + @override + dynamic get inputComponent => + (props[_$key__inputComponent__TestJsProps] ?? null) as dynamic; + @override + set inputComponent(dynamic value) => + props[_$key__inputComponent__TestJsProps] = value; + @override + dynamic get buttonComponent => + (props[_$key__buttonComponent__TestJsProps] ?? null) as dynamic; + @override + set buttonComponent(dynamic value) => + props[_$key__buttonComponent__TestJsProps] = value; + /* GENERATED CONSTANTS */ + static const PropDescriptor _$prop___$raw$buttonProps__TestJsProps = + PropDescriptor(_$key___$raw$buttonProps__TestJsProps); + static const PropDescriptor _$prop___$raw$inputRef__TestJsProps = + PropDescriptor(_$key___$raw$inputRef__TestJsProps); + static const PropDescriptor _$prop__component__TestJsProps = + PropDescriptor(_$key__component__TestJsProps); + static const PropDescriptor _$prop__inputComponent__TestJsProps = + PropDescriptor(_$key__inputComponent__TestJsProps); + static const PropDescriptor _$prop__buttonComponent__TestJsProps = + PropDescriptor(_$key__buttonComponent__TestJsProps); + static const String _$key___$raw$buttonProps__TestJsProps = 'buttonProps'; + static const String _$key___$raw$inputRef__TestJsProps = 'inputRef'; + static const String _$key__component__TestJsProps = 'component'; + static const String _$key__inputComponent__TestJsProps = 'inputComponent'; + static const String _$key__buttonComponent__TestJsProps = 'buttonComponent'; + + static const List $props = [ + _$prop___$raw$buttonProps__TestJsProps, + _$prop___$raw$inputRef__TestJsProps, + _$prop__component__TestJsProps, + _$prop__inputComponent__TestJsProps, + _$prop__buttonComponent__TestJsProps + ]; + static const List $propKeys = [ + _$key___$raw$buttonProps__TestJsProps, + _$key___$raw$inputRef__TestJsProps, + _$key__component__TestJsProps, + _$key__inputComponent__TestJsProps, + _$key__buttonComponent__TestJsProps + ]; +} + +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +const PropsMeta _$metaForTestJsProps = PropsMeta( + fields: $TestJsProps.$props, + keys: $TestJsProps.$propKeys, +); + +final UiFactoryConfig<_$$ExpectsDartMapPropProps> _$ExpectsDartMapPropConfig = + UiFactoryConfig( + propsFactory: PropsFactory( + map: (map) => _$$ExpectsDartMapPropProps(map), + jsMap: (map) => _$$ExpectsDartMapPropProps$JsMap(map), + ), + displayName: 'ExpectsDartMapProp'); + +@Deprecated(r'Use the private variable, _$ExpectsDartMapPropConfig, instead ' + 'and update the `over_react` lower bound to version 4.1.0. ' + 'For information on why this is deprecated, see https://github.com/Workiva/over_react/pull/650') +final UiFactoryConfig<_$$ExpectsDartMapPropProps> $ExpectsDartMapPropConfig = + _$ExpectsDartMapPropConfig; + +// Concrete props implementation. +// +// Implements constructor and backing map, and links up to generated component factory. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +abstract class _$$ExpectsDartMapPropProps extends UiProps + with + ExpectsDartMapPropProps, + $ExpectsDartMapPropProps // If this generated mixin is undefined, it's likely because ExpectsDartMapPropProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of ExpectsDartMapPropProps, and check that $ExpectsDartMapPropProps is exported/imported properly. +{ + _$$ExpectsDartMapPropProps._(); + + factory _$$ExpectsDartMapPropProps(Map backingMap) { + if (backingMap == null || backingMap is JsBackedMap) { + return _$$ExpectsDartMapPropProps$JsMap(backingMap as JsBackedMap); + } else { + return _$$ExpectsDartMapPropProps$PlainMap(backingMap); + } + } + + /// Let `UiProps` internals know that this class has been generated. + @override + bool get $isClassGenerated => true; + + /// The default namespace for the prop getters/setters generated for this class. + @override + String get propKeyNamespace => ''; + + @override + PropsMetaCollection get staticMeta => const PropsMetaCollection({ + // If this generated mixin is undefined, it's likely because ExpectsDartMapPropProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of ExpectsDartMapPropProps, and check that $ExpectsDartMapPropProps is exported/imported properly. + ExpectsDartMapPropProps: $ExpectsDartMapPropProps.meta, + }); +} + +// Concrete props implementation that can be backed by any [Map]. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$ExpectsDartMapPropProps$PlainMap extends _$$ExpectsDartMapPropProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$ExpectsDartMapPropProps$PlainMap(Map backingMap) + : this._props = {}, + super._() { + this._props = backingMap ?? {}; + } + + /// The backing props map proxied by this class. + @override + Map get props => _props; + Map _props; +} + +// Concrete props implementation that can only be backed by [JsMap], +// allowing dart2js to compile more optimal code for key-value pair reads/writes. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$ExpectsDartMapPropProps$JsMap extends _$$ExpectsDartMapPropProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$ExpectsDartMapPropProps$JsMap(JsBackedMap backingMap) + : this._props = JsBackedMap(), + super._() { + this._props = backingMap ?? JsBackedMap(); + } + + /// The backing props map proxied by this class. + @override + JsBackedMap get props => _props; + JsBackedMap _props; +} + +final UiFactoryConfig<_$$ExpectsDartStylePropProps> + _$ExpectsDartStylePropConfig = UiFactoryConfig( + propsFactory: PropsFactory( + map: (map) => _$$ExpectsDartStylePropProps(map), + jsMap: (map) => _$$ExpectsDartStylePropProps$JsMap(map), + ), + displayName: 'ExpectsDartStyleProp'); + +@Deprecated(r'Use the private variable, _$ExpectsDartStylePropConfig, instead ' + 'and update the `over_react` lower bound to version 4.1.0. ' + 'For information on why this is deprecated, see https://github.com/Workiva/over_react/pull/650') +final UiFactoryConfig<_$$ExpectsDartStylePropProps> + $ExpectsDartStylePropConfig = _$ExpectsDartStylePropConfig; + +// Concrete props implementation. +// +// Implements constructor and backing map, and links up to generated component factory. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +abstract class _$$ExpectsDartStylePropProps extends UiProps + with + ExpectsDartStylePropProps, + $ExpectsDartStylePropProps // If this generated mixin is undefined, it's likely because ExpectsDartStylePropProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of ExpectsDartStylePropProps, and check that $ExpectsDartStylePropProps is exported/imported properly. +{ + _$$ExpectsDartStylePropProps._(); + + factory _$$ExpectsDartStylePropProps(Map backingMap) { + if (backingMap == null || backingMap is JsBackedMap) { + return _$$ExpectsDartStylePropProps$JsMap(backingMap as JsBackedMap); + } else { + return _$$ExpectsDartStylePropProps$PlainMap(backingMap); + } + } + + /// Let `UiProps` internals know that this class has been generated. + @override + bool get $isClassGenerated => true; + + /// The default namespace for the prop getters/setters generated for this class. + @override + String get propKeyNamespace => ''; + + @override + PropsMetaCollection get staticMeta => const PropsMetaCollection({ + // If this generated mixin is undefined, it's likely because ExpectsDartStylePropProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of ExpectsDartStylePropProps, and check that $ExpectsDartStylePropProps is exported/imported properly. + ExpectsDartStylePropProps: $ExpectsDartStylePropProps.meta, + }); +} + +// Concrete props implementation that can be backed by any [Map]. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$ExpectsDartStylePropProps$PlainMap + extends _$$ExpectsDartStylePropProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$ExpectsDartStylePropProps$PlainMap(Map backingMap) + : this._props = {}, + super._() { + this._props = backingMap ?? {}; + } + + /// The backing props map proxied by this class. + @override + Map get props => _props; + Map _props; +} + +// Concrete props implementation that can only be backed by [JsMap], +// allowing dart2js to compile more optimal code for key-value pair reads/writes. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$ExpectsDartStylePropProps$JsMap extends _$$ExpectsDartStylePropProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$ExpectsDartStylePropProps$JsMap(JsBackedMap backingMap) + : this._props = JsBackedMap(), + super._() { + this._props = backingMap ?? JsBackedMap(); + } + + /// The backing props map proxied by this class. + @override + JsBackedMap get props => _props; + JsBackedMap _props; +} + +final UiFactoryConfig<_$$ExpectsListChildrenPropProps> + _$ExpectsListChildrenPropConfig = UiFactoryConfig( + propsFactory: PropsFactory( + map: (map) => _$$ExpectsListChildrenPropProps(map), + jsMap: (map) => _$$ExpectsListChildrenPropProps$JsMap(map), + ), + displayName: 'ExpectsListChildrenProp'); + +@Deprecated( + r'Use the private variable, _$ExpectsListChildrenPropConfig, instead ' + 'and update the `over_react` lower bound to version 4.1.0. ' + 'For information on why this is deprecated, see https://github.com/Workiva/over_react/pull/650') +final UiFactoryConfig<_$$ExpectsListChildrenPropProps> + $ExpectsListChildrenPropConfig = _$ExpectsListChildrenPropConfig; + +// Concrete props implementation. +// +// Implements constructor and backing map, and links up to generated component factory. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +abstract class _$$ExpectsListChildrenPropProps extends UiProps + with + ExpectsListChildrenPropProps, + $ExpectsListChildrenPropProps // If this generated mixin is undefined, it's likely because ExpectsListChildrenPropProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of ExpectsListChildrenPropProps, and check that $ExpectsListChildrenPropProps is exported/imported properly. +{ + _$$ExpectsListChildrenPropProps._(); + + factory _$$ExpectsListChildrenPropProps(Map backingMap) { + if (backingMap == null || backingMap is JsBackedMap) { + return _$$ExpectsListChildrenPropProps$JsMap(backingMap as JsBackedMap); + } else { + return _$$ExpectsListChildrenPropProps$PlainMap(backingMap); + } + } + + /// Let `UiProps` internals know that this class has been generated. + @override + bool get $isClassGenerated => true; + + /// The default namespace for the prop getters/setters generated for this class. + @override + String get propKeyNamespace => ''; + + @override + PropsMetaCollection get staticMeta => const PropsMetaCollection({ + // If this generated mixin is undefined, it's likely because ExpectsListChildrenPropProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of ExpectsListChildrenPropProps, and check that $ExpectsListChildrenPropProps is exported/imported properly. + ExpectsListChildrenPropProps: $ExpectsListChildrenPropProps.meta, + }); +} + +// Concrete props implementation that can be backed by any [Map]. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$ExpectsListChildrenPropProps$PlainMap + extends _$$ExpectsListChildrenPropProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$ExpectsListChildrenPropProps$PlainMap(Map backingMap) + : this._props = {}, + super._() { + this._props = backingMap ?? {}; + } + + /// The backing props map proxied by this class. + @override + Map get props => _props; + Map _props; +} + +// Concrete props implementation that can only be backed by [JsMap], +// allowing dart2js to compile more optimal code for key-value pair reads/writes. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$ExpectsListChildrenPropProps$JsMap + extends _$$ExpectsListChildrenPropProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$ExpectsListChildrenPropProps$JsMap(JsBackedMap backingMap) + : this._props = JsBackedMap(), + super._() { + this._props = backingMap ?? JsBackedMap(); + } + + /// The backing props map proxied by this class. + @override + JsBackedMap get props => _props; + JsBackedMap _props; +} + +final UiFactoryConfig<_$$BasicForwardRefProps> _$BasicForwardRefConfig = + UiFactoryConfig( + propsFactory: PropsFactory( + map: (map) => _$$BasicForwardRefProps(map), + jsMap: (map) => _$$BasicForwardRefProps$JsMap(map), + ), + displayName: 'BasicForwardRef'); + +@Deprecated(r'Use the private variable, _$BasicForwardRefConfig, instead ' + 'and update the `over_react` lower bound to version 4.1.0. ' + 'For information on why this is deprecated, see https://github.com/Workiva/over_react/pull/650') +final UiFactoryConfig<_$$BasicForwardRefProps> $BasicForwardRefConfig = + _$BasicForwardRefConfig; + +// Concrete props implementation. +// +// Implements constructor and backing map, and links up to generated component factory. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +abstract class _$$BasicForwardRefProps extends UiProps + with + BasicForwardRefProps, + $BasicForwardRefProps // If this generated mixin is undefined, it's likely because BasicForwardRefProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of BasicForwardRefProps, and check that $BasicForwardRefProps is exported/imported properly. +{ + _$$BasicForwardRefProps._(); + + factory _$$BasicForwardRefProps(Map backingMap) { + if (backingMap == null || backingMap is JsBackedMap) { + return _$$BasicForwardRefProps$JsMap(backingMap as JsBackedMap); + } else { + return _$$BasicForwardRefProps$PlainMap(backingMap); + } + } + + /// Let `UiProps` internals know that this class has been generated. + @override + bool get $isClassGenerated => true; + + /// The default namespace for the prop getters/setters generated for this class. + @override + String get propKeyNamespace => ''; + + @override + PropsMetaCollection get staticMeta => const PropsMetaCollection({ + // If this generated mixin is undefined, it's likely because BasicForwardRefProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of BasicForwardRefProps, and check that $BasicForwardRefProps is exported/imported properly. + BasicForwardRefProps: $BasicForwardRefProps.meta, + }); +} + +// Concrete props implementation that can be backed by any [Map]. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$BasicForwardRefProps$PlainMap extends _$$BasicForwardRefProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$BasicForwardRefProps$PlainMap(Map backingMap) + : this._props = {}, + super._() { + this._props = backingMap ?? {}; + } + + /// The backing props map proxied by this class. + @override + Map get props => _props; + Map _props; +} + +// Concrete props implementation that can only be backed by [JsMap], +// allowing dart2js to compile more optimal code for key-value pair reads/writes. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$BasicForwardRefProps$JsMap extends _$$BasicForwardRefProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$BasicForwardRefProps$JsMap(JsBackedMap backingMap) + : this._props = JsBackedMap(), + super._() { + this._props = backingMap ?? JsBackedMap(); + } + + /// The backing props map proxied by this class. + @override + JsBackedMap get props => _props; + JsBackedMap _props; +} + +final UiFactoryConfig<_$$DartTestJsWrapperProps> _$DartTestJsWrapperConfig = + UiFactoryConfig( + propsFactory: PropsFactory( + map: (map) => _$$DartTestJsWrapperProps(map), + jsMap: (map) => _$$DartTestJsWrapperProps$JsMap(map), + ), + displayName: 'DartTestJsWrapper'); + +@Deprecated(r'Use the private variable, _$DartTestJsWrapperConfig, instead ' + 'and update the `over_react` lower bound to version 4.1.0. ' + 'For information on why this is deprecated, see https://github.com/Workiva/over_react/pull/650') +final UiFactoryConfig<_$$DartTestJsWrapperProps> $DartTestJsWrapperConfig = + _$DartTestJsWrapperConfig; + +// Concrete props implementation. +// +// Implements constructor and backing map, and links up to generated component factory. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +abstract class _$$DartTestJsWrapperProps extends UiProps + with + TestJsProps, + $TestJsProps, // If this generated mixin is undefined, it's likely because TestJsProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of TestJsProps, and check that $TestJsProps is exported/imported properly. + DartTestJsWrapperPropsMixin, + $DartTestJsWrapperPropsMixin // If this generated mixin is undefined, it's likely because DartTestJsWrapperPropsMixin is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of DartTestJsWrapperPropsMixin, and check that $DartTestJsWrapperPropsMixin is exported/imported properly. + implements + DartTestJsWrapperProps { + _$$DartTestJsWrapperProps._(); + + factory _$$DartTestJsWrapperProps(Map backingMap) { + if (backingMap == null || backingMap is JsBackedMap) { + return _$$DartTestJsWrapperProps$JsMap(backingMap as JsBackedMap); + } else { + return _$$DartTestJsWrapperProps$PlainMap(backingMap); + } + } + + /// Let `UiProps` internals know that this class has been generated. + @override + bool get $isClassGenerated => true; + + /// The default namespace for the prop getters/setters generated for this class. + @override + String get propKeyNamespace => ''; + + @override + PropsMetaCollection get staticMeta => const PropsMetaCollection({ + // If this generated mixin is undefined, it's likely because TestJsProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of TestJsProps, and check that $TestJsProps is exported/imported properly. + TestJsProps: $TestJsProps.meta, + // If this generated mixin is undefined, it's likely because DartTestJsWrapperPropsMixin is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of DartTestJsWrapperPropsMixin, and check that $DartTestJsWrapperPropsMixin is exported/imported properly. + DartTestJsWrapperPropsMixin: $DartTestJsWrapperPropsMixin.meta, + }); +} + +// Concrete props implementation that can be backed by any [Map]. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$DartTestJsWrapperProps$PlainMap extends _$$DartTestJsWrapperProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$DartTestJsWrapperProps$PlainMap(Map backingMap) + : this._props = {}, + super._() { + this._props = backingMap ?? {}; + } + + /// The backing props map proxied by this class. + @override + Map get props => _props; + Map _props; +} + +// Concrete props implementation that can only be backed by [JsMap], +// allowing dart2js to compile more optimal code for key-value pair reads/writes. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$DartTestJsWrapperProps$JsMap extends _$$DartTestJsWrapperProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$DartTestJsWrapperProps$JsMap(JsBackedMap backingMap) + : this._props = JsBackedMap(), + super._() { + this._props = backingMap ?? JsBackedMap(); + } + + /// The backing props map proxied by this class. + @override + JsBackedMap get props => _props; + JsBackedMap _props; +} + +final UiFactoryConfig<_$$TestJsProps> _$TestJsConfig = UiFactoryConfig( + propsFactory: PropsFactory( + map: (map) => _$$TestJsProps(map), + jsMap: (map) => _$$TestJsProps$JsMap(map), + ), + displayName: 'TestJs'); + +@Deprecated(r'Use the private variable, _$TestJsConfig, instead ' + 'and update the `over_react` lower bound to version 4.1.0. ' + 'For information on why this is deprecated, see https://github.com/Workiva/over_react/pull/650') +final UiFactoryConfig<_$$TestJsProps> $TestJsConfig = _$TestJsConfig; + +// Concrete props implementation. +// +// Implements constructor and backing map, and links up to generated component factory. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +abstract class _$$TestJsProps extends UiProps + with + TestJsProps, + $TestJsProps // If this generated mixin is undefined, it's likely because TestJsProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of TestJsProps, and check that $TestJsProps is exported/imported properly. +{ + _$$TestJsProps._(); + + factory _$$TestJsProps(Map backingMap) { + if (backingMap == null || backingMap is JsBackedMap) { + return _$$TestJsProps$JsMap(backingMap as JsBackedMap); + } else { + return _$$TestJsProps$PlainMap(backingMap); + } + } + + /// Let `UiProps` internals know that this class has been generated. + @override + bool get $isClassGenerated => true; + + /// The default namespace for the prop getters/setters generated for this class. + @override + String get propKeyNamespace => ''; + + @override + PropsMetaCollection get staticMeta => const PropsMetaCollection({ + // If this generated mixin is undefined, it's likely because TestJsProps is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not imported. Check the declaration of TestJsProps, and check that $TestJsProps is exported/imported properly. + TestJsProps: $TestJsProps.meta, + }); +} + +// Concrete props implementation that can be backed by any [Map]. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$TestJsProps$PlainMap extends _$$TestJsProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$TestJsProps$PlainMap(Map backingMap) + : this._props = {}, + super._() { + this._props = backingMap ?? {}; + } + + /// The backing props map proxied by this class. + @override + Map get props => _props; + Map _props; +} + +// Concrete props implementation that can only be backed by [JsMap], +// allowing dart2js to compile more optimal code for key-value pair reads/writes. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$TestJsProps$JsMap extends _$$TestJsProps { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$TestJsProps$JsMap(JsBackedMap backingMap) + : this._props = JsBackedMap(), + super._() { + this._props = backingMap ?? JsBackedMap(); + } + + /// The backing props map proxied by this class. + @override + JsBackedMap get props => _props; + JsBackedMap _props; +} diff --git a/test/over_react/util/ref_test_cases.dart b/test/over_react/util/ref_test_cases.dart new file mode 100644 index 000000000..6eeb2c20f --- /dev/null +++ b/test/over_react/util/ref_test_cases.dart @@ -0,0 +1,184 @@ +import 'package:js/js.dart'; +import 'package:meta/meta.dart'; +import 'package:react/react_client/react_interop.dart'; +import 'package:test/test.dart'; + +// Adapted from react-dart's RefTestCase: https://github.com/Workiva/react-dart/blob/e7a2fef0b50033c51e8e0177439a573d3e7fc254/test/util.dart + +/// A test case that can be used for consuming a specific kind of ref and verifying +/// it was updated properly when rendered. +/// +/// Test cases should not be reused within a test or across multiple tests, to avoid +/// the [ref] from being used by multiple components and its value being polluted. +class RefTestCase { + /// The ref to be passed into a component. + final dynamic ref; + + /// Verifies (usually via `expect`) that the ref was updated exactly once with [actualValue]. + final Function(dynamic actualValue) verifyRefWasUpdated; + + /// Returns the current value of the ref. + final dynamic Function() getCurrent; + + final RefTestCaseMeta meta; + + RefTestCase({ + @required this.ref, + @required this.verifyRefWasUpdated, + @required this.getCurrent, + @required this.meta, + }); +} + +/// A collection of methods that create [RefTestCase]s, combined into a class so that they can easily share a +/// generic parameter [T] (the type of the Dart ref value). +class RefTestCaseCollection { + final bool includeJsCallbackRefCase; + + RefTestCaseCollection({this.includeJsCallbackRefCase = true}) { + if (T == dynamic) { + throw ArgumentError('Generic parameter T must be specified'); + } + } + + static const untypedCallbackRefCaseName = 'untyped callback ref'; + RefTestCase createUntypedCallbackRefCase() { + const name = untypedCallbackRefCaseName; + final calls = []; + return RefTestCase( + ref: (value) => calls.add(value), + verifyRefWasUpdated: (actualValue) => + expect(calls, [same(actualValue)], reason: _reasonMessage(name)), + getCurrent: () => calls.single, + meta: RefTestCaseMeta(name, RefKind.callback, + isJs: false, isStronglyTyped: false), + ); + } + + static const typedCallbackRefCaseName = 'typed callback ref'; + RefTestCase createTypedCallbackRefCase() { + const name = typedCallbackRefCaseName; + final calls = []; + return RefTestCase( + ref: (T value) => calls.add(value), + verifyRefWasUpdated: (actualValue) => + expect(calls, [same(actualValue)], reason: _reasonMessage(name)), + getCurrent: () => calls.single, + meta: RefTestCaseMeta(name, RefKind.callback, + isJs: false, isStronglyTyped: true), + ); + } + + static const untypedRefObjectCaseName = 'untyped ref object'; + RefTestCase createUntypedRefObjectCase() { + const name = untypedRefObjectCaseName; + final ref = createRef(); + return RefTestCase( + ref: ref, + verifyRefWasUpdated: (actualValue) => + expect(ref.current, same(actualValue), reason: _reasonMessage(name)), + getCurrent: () => ref.current, + meta: RefTestCaseMeta(name, RefKind.object, + isJs: false, isStronglyTyped: false), + ); + } + + static const refObjectCaseName = 'ref object'; + RefTestCase createRefObjectCase() { + const name = refObjectCaseName; + final ref = createRef(); + return RefTestCase( + ref: ref, + verifyRefWasUpdated: (actualValue) => + expect(ref.current, same(actualValue), reason: _reasonMessage(name)), + getCurrent: () => ref.current, + meta: RefTestCaseMeta(name, RefKind.object, + isJs: false, isStronglyTyped: true), + ); + } + + static const jsCallbackRefCaseName = 'JS callback ref'; + RefTestCase createJsCallbackRefCase() { + const name = jsCallbackRefCaseName; + final calls = []; + return RefTestCase( + ref: allowInterop((value) => calls.add(value)), + verifyRefWasUpdated: (actualValue) => + expect(calls, [same(actualValue)], reason: _reasonMessage(name)), + getCurrent: () => calls.single, + meta: RefTestCaseMeta(name, RefKind.callback, + isJs: true, isStronglyTyped: false), + ); + } + + static const jsRefObjectCaseName = 'JS ref object'; + RefTestCase createJsRefObjectCase() { + const name = jsRefObjectCaseName; + final ref = React.createRef(); + return RefTestCase( + ref: ref, + verifyRefWasUpdated: (actualValue) => + expect(ref.current, same(actualValue), reason: _reasonMessage(name)), + getCurrent: () => ref.current, + meta: RefTestCaseMeta(name, RefKind.object, + isJs: true, isStronglyTyped: false), + ); + } + + static String _reasonMessage(String name) => '$name should have been updated'; + + /// Creates test cases for all of the valid, chainable ref types: + /// + /// 1. callback ref with untyped argument + /// 2. callback ref with typed argument + /// 3. createRef (Dart wrapper) + /// 4. createRef (JS object) + List createAllCases() => [ + createUntypedCallbackRefCase(), + createTypedCallbackRefCase(), + createUntypedRefObjectCase(), + createRefObjectCase(), + if (includeJsCallbackRefCase) createJsCallbackRefCase(), + createJsRefObjectCase(), + ]; + + RefTestCase createCaseByName(String name) => + createAllCases().singleWhere((c) => c.meta.name == name); + + RefTestCaseMeta testCaseMetaByName(String name) => + createCaseByName(name).meta; + + List get allTestCaseNames => + allTestCaseMetas.map((m) => m.name).toList(); + List get allTestCaseMetas => + createAllCases().map((c) => c.meta).toList(); +} + +class RefTestCaseMeta { + final String name; + + final RefKind kind; + + /// Whether the ref is a non-Dart object, such as a ref originating from outside of Dart code + /// or a JS-converted Dart ref. + final bool isJs; + + final bool isStronglyTyped; + + const RefTestCaseMeta(this.name, this.kind, + {this.isJs = false, this.isStronglyTyped = false}); + + @override + String toString() => + '$name ($kind, isJs: $isJs, isStronglyTyped: $isStronglyTyped)'; +} + +enum RefKind { + object, + callback, +} + +extension RefKindBooleans on RefKind { + bool get isObject => this == RefKind.object; + bool get isCallback => this == RefKind.callback; +} diff --git a/test/over_react_util_test.dart b/test/over_react_util_test.dart index 84bea7808..3c0a12a39 100644 --- a/test/over_react_util_test.dart +++ b/test/over_react_util_test.dart @@ -24,24 +24,31 @@ import 'package:test/test.dart'; import 'over_react/util/cast_ui_factory_test.dart' as cast_ui_factory_test; import 'over_react/util/class_names_test.dart' as class_names_test; -import 'over_react/util/component_debug_name_test.dart' as component_debug_name_test; +import 'over_react/util/component_debug_name_test.dart' + as component_debug_name_test; import 'over_react/util/constants_base_test.dart' as constants_base_test; import 'over_react/util/css_value_util_test.dart' as css_value_util_test; import 'over_react/util/dom_util_test.dart' as dom_util_test; import 'over_react/util/equality_test.dart' as equality_test; import 'over_react/util/guid_util_test.dart' as guid_util_test; -import 'over_react/util/handler_chain_util_test.dart' as handler_chain_util_test; +import 'over_react/util/handler_chain_util_test.dart' + as handler_chain_util_test; import 'over_react/util/hoc_test.dart' as hoc_test; import 'over_react/util/map_util_test.dart' as map_util_test; import 'over_react/util/pretty_print_test.dart' as pretty_print_test; -import 'over_react/util/prop_key_util_test_dart2.dart' as prop_key_util_test_dart2; +import 'over_react/util/prop_key_util_test_dart2.dart' + as prop_key_util_test_dart2; import 'over_react/util/react_util_test.dart' as react_util_test; import 'over_react/util/react_wrappers_test.dart' as react_wrappers_test; import 'over_react/util/rem_util_test.dart' as rem_util_test; -import 'over_react/util/safe_render_manager/safe_render_manager_test.dart' as safe_render_manager_test; -import 'over_react/util/safe_render_manager/safe_render_manager_helper_test.dart' as safe_render_manager_helper_test; +import 'over_react/util/safe_render_manager/safe_render_manager_test.dart' + as safe_render_manager_test; +import 'over_react/util/safe_render_manager/safe_render_manager_helper_test.dart' + as safe_render_manager_helper_test; import 'over_react/util/string_util_test.dart' as string_util_test; import 'over_react/util/test_mode_test.dart' as test_mode_test; +import 'over_react/util/js_component_test.dart' as js_component_test; +import 'over_react/util/prop_conversion_test.dart' as prop_conversion_test; void main() { enableTestMode(); @@ -66,4 +73,6 @@ void main() { safe_render_manager_helper_test.main(); string_util_test.main(); test_mode_test.main(); + js_component_test.main(); + prop_conversion_test.main(); } diff --git a/test/over_react_util_test.html b/test/over_react_util_test.html index cec7ccaa4..04baf4528 100644 --- a/test/over_react_util_test.html +++ b/test/over_react_util_test.html @@ -21,6 +21,26 @@ + + +