From 140912a5b1faccf718969d04a826f340e72edd35 Mon Sep 17 00:00:00 2001 From: Claire Sarsam Date: Fri, 6 Jan 2017 16:27:40 -0700 Subject: [PATCH] Sync changes from source repository onto over_react --- README.md | 8 +- lib/over_react.dart | 1 + lib/src/component/callback_typedefs.dart | 9 ++ lib/src/component/resize_sensor.dart | 18 +-- .../component_declaration/component_base.dart | 33 +++-- .../component_declaration/flux_component.dart | 68 +++++----- .../transformer_helpers.dart | 46 +++++-- lib/src/util/class_names.dart | 1 - lib/src/util/constants_base.dart | 11 +- lib/src/util/css_value_util.dart | 13 +- lib/src/util/document_event_helper_util.dart | 16 +++ lib/src/util/dom_util.dart | 98 +++++++++++++++ lib/src/util/handler_chain_util.dart | 1 - lib/src/util/js_util.dart | 14 +++ lib/src/util/map_util.dart | 13 +- lib/src/util/prop_key_util.dart | 61 +++++++++ lib/src/util/react_wrappers.dart | 23 +++- lib/src/util/rem_util.dart | 9 +- lib/src/util/test_mode.dart | 1 - pubspec.yaml | 2 +- .../component/dom_components_test.dart | 14 +-- .../component/resize_sensor_test.dart | 10 +- .../component_base_test.dart | 2 +- .../abstract_inheritance/extendedtype.dart | 1 - .../type_inheritance/subsubtype.dart | 1 - .../handler_precedence.dart | 4 +- .../flux_component_test/store_handlers.dart | 4 +- .../accessor_mixin_integration_test.dart | 6 +- test/over_react/util/dom_util_test.dart | 117 ++++++++++++++++++ .../util/handler_chain_util_test.dart | 100 +++++++++++++++ test/over_react/util/map_util_test.dart | 18 +++ test/over_react/util/prop_key_util_test.dart | 48 +++++++ test/over_react/util/react_wrappers_test.dart | 59 ++++++++- test/over_react/util/rem_util_test.dart | 38 ++---- test/over_react/util/test_mode_test.dart | 1 - test/over_react_test.dart | 2 + 36 files changed, 715 insertions(+), 156 deletions(-) create mode 100644 lib/src/util/document_event_helper_util.dart create mode 100644 lib/src/util/js_util.dart create mode 100644 lib/src/util/prop_key_util.dart create mode 100644 test/over_react/util/prop_key_util_test.dart diff --git a/README.md b/README.md index 5ed2ead3e..aab25605b 100644 --- a/README.md +++ b/README.md @@ -495,15 +495,15 @@ as shown in the examples above. ## Component Formatting > __A note on dart_style:__ -> +> > Currently, [dart_style (dartfmt)](https://github.com/dart-lang/dart_style) decreases the readability of components > built using [OverReact's fluent-style](#fluent-style-component-consumption). > See https://github.com/dart-lang/dart_style/issues/549 for more info. -> +> > We're exploring some different ideas to improve automated formatting, but for the time being, we __do not recommend__ using dart_style with OverReact. -> +> > However, if you do choose to use dart_style, you can greatly improve its output by using trailing commas in children argument lists: -> +> > * dart_style formatting: > ```dart > return (Button() diff --git a/lib/over_react.dart b/lib/over_react.dart index 8eb1a1b41..5e2beb3ab 100644 --- a/lib/over_react.dart +++ b/lib/over_react.dart @@ -37,6 +37,7 @@ export 'src/util/key_constants.dart'; export 'src/util/map_util.dart'; export 'src/util/pretty_print.dart'; export 'src/util/prop_errors.dart'; +export 'src/util/prop_key_util.dart'; export 'src/util/react_wrappers.dart'; export 'src/util/rem_util.dart'; export 'src/util/string_util.dart'; diff --git a/lib/src/component/callback_typedefs.dart b/lib/src/component/callback_typedefs.dart index 6ff4dd95d..2ae3a0838 100644 --- a/lib/src/component/callback_typedefs.dart +++ b/lib/src/component/callback_typedefs.dart @@ -14,6 +14,9 @@ library over_react.callback_typedefs; +import 'dart:html'; + +import 'package:over_react/over_react.dart' show ResizeSensorEvent; import 'package:react/react.dart' as react; // Callbacks for React's DOM event system @@ -29,3 +32,9 @@ typedef WheelEventCallback(react.SyntheticWheelEvent event); /// A generic callback that takes no arguments. typedef Callback(); + +// Callback for DOM elements +typedef Element ElementCallback(); + +// Callback for [ResizeSensorEvent]s +typedef void ResizeSensorHandler(ResizeSensorEvent event); diff --git a/lib/src/component/resize_sensor.dart b/lib/src/component/resize_sensor.dart index d4f76edf4..4749d3b12 100644 --- a/lib/src/component/resize_sensor.dart +++ b/lib/src/component/resize_sensor.dart @@ -15,16 +15,12 @@ /// Thanks! /// https://github.com/marcj/css-element-queries/blob/master/src/ResizeSensor.js library resize_sensor; - import 'dart:collection'; import 'dart:html'; -import 'package:browser_detect/browser_detect.dart'; -import 'package:over_react/over_react.dart'; +import 'package:platform_detect/platform_detect.dart'; import 'package:react/react.dart' as react; - -// Callback for [ResizeSensorEvent]s -typedef void ResizeSensorHandler(ResizeSensorEvent event); +import 'package:over_react/over_react.dart'; /// A wrapper component that detects when its parent is resized. /// @@ -82,6 +78,7 @@ class ResizeSensorComponent extends UiComponent { Element _expandSensorChildRef; Element _expandSensorRef; + Element _collapseSensorChildRef; Element _collapseSensorRef; @override @@ -114,7 +111,10 @@ class ResizeSensorComponent extends UiComponent { ..key = 'expandSensor' )(expandSensorChild); - var collapseSensorChild = (Dom.div()..style = _collapseSensorChildStyle)(); + var collapseSensorChild = (Dom.div() + ..ref = (ref) { _collapseSensorChildRef = ref; } + ..style = _collapseSensorChildStyle + )(); var collapseSensor = (Dom.div() ..className = 'resize-sensor-collapse' @@ -152,9 +152,9 @@ class ResizeSensorComponent extends UiComponent { }; // IE 10 and Safari 8 need 'special' value prefixes for 'display:flex'. - if (browser.isIe && browser.version <= '10') { + if (browser.isInternetExplorer && browser.version.major <= 10) { wrapperStyles['display'] = '-ms-flexbox'; - } else if (browser.isSafari && browser.version < '9') { + } else if (browser.isSafari && browser.version.major < 9) { wrapperStyles['display'] = '-webkit-flex'; } else { wrapperStyles['display'] = 'flex'; diff --git a/lib/src/component_declaration/component_base.dart b/lib/src/component_declaration/component_base.dart index 2784af621..b4458cb77 100644 --- a/lib/src/component_declaration/component_base.dart +++ b/lib/src/component_declaration/component_base.dart @@ -25,6 +25,7 @@ import 'package:over_react/over_react.dart' show prettyPrintMap, unindent, PropError; + import 'package:over_react/src/component_declaration/component_type_checking.dart'; import 'package:react/react.dart' as react; import 'package:react/react_client.dart'; @@ -85,14 +86,20 @@ typedef TProps UiFactory([Map backingProps]); /// For use as a Function variable type when the `backingProps` argument is not required. typedef TProps BuilderOnlyUiFactory(); -/// The basis for a over_react component. +typedef dynamic _RefTypedef(String ref); + +/// The basis for a over_react component, extending [react.Component]. (Successor to [BaseComponent]). /// /// Includes support for strongly-typed props and utilities for prop and CSS classname forwarding. -/// -/// Extends [react.Component]. -/// -/// Related: [UiStatefulComponent] abstract class UiComponent extends react.Component { + /// Returns the component of the specified [ref]. + /// > `react.Component` if it is a Dart component + /// > DOM node if it is a DOM component. + /// + /// Overridden for strong typing. + @override + _RefTypedef get ref => super.ref; + /// The props for the non-forwarding props defined in this component. Iterable get consumedProps => null; @@ -124,12 +131,12 @@ abstract class UiComponent extends react.Component { void validateRequiredProps(Map appliedProps) { consumedProps?.forEach((ConsumedProps consumedProps) { consumedProps.props.forEach((PropDescriptor prop) { - if (!prop.isRequired) return; - if (prop.isNullable && appliedProps.containsKey(prop.key)) return; - if (!prop.isNullable && appliedProps[prop.key] != null) return; + if (!prop.isRequired) return; + if (prop.isNullable && appliedProps.containsKey(prop.key)) return; + if (!prop.isNullable && appliedProps[prop.key] != null) return; - throw new PropError.required(prop.key, prop.errorMessage); - }); + throw new PropError.required(prop.key, prop.errorMessage); + }); }); } @@ -186,6 +193,7 @@ abstract class UiComponent extends react.Component { TProps typedPropsFactory(Map propsMap); /// Returns a typed props object backed by a new Map. + /// /// Convenient for use with [getDefaultProps]. TProps newProps() => typedPropsFactory({}); @@ -195,11 +203,11 @@ abstract class UiComponent extends react.Component { // ---------------------------------------------------------------------- } -/// The basis for a stateful over_react component. +/// /// The basis for a stateful over_react component. /// /// Includes support for strongly-typed props and state and utilities for prop and CSS classname forwarding. /// -/// Extends [react.Component]. +/// Extends [react.Component] /// /// Related: [UiComponent] abstract class UiStatefulComponent extends UiComponent { @@ -232,7 +240,6 @@ abstract class UiStatefulComponent super.state = value; /// Returns a typed state object backed by the specified [stateMap]. - /// /// Required to properly instantiate the generic [TState] class. TState typedStateFactory(Map stateMap); diff --git a/lib/src/component_declaration/flux_component.dart b/lib/src/component_declaration/flux_component.dart index 0589379b0..0bdc7a71e 100644 --- a/lib/src/component_declaration/flux_component.dart +++ b/lib/src/component_declaration/flux_component.dart @@ -21,13 +21,13 @@ abstract class FluxUiProps extends UiProps { ActionsT get actions => props[_actionsPropKey] as ActionsT; // ignore: avoid_as set actions(ActionsT value) => props[_actionsPropKey] = value; - /// The prop defined by [StoresT]. + /// The prop defined by [StoresT]. This object should either be an + /// instance of [Store] or should provide access to one or more [Store]s. /// - /// This object should either be an instance of [Store] or should provide access to one or more [Store]s. - /// - /// __Instead of storing state within this component via `setState`, it is recommended that data be - /// pulled directly from these stores.__ This ensures that the data being used is always up to date - /// and leaves the state management logic to the stores. + /// **Instead of storing state within this component via [setState], it is + /// recommended that data be pulled directly from these stores.** This ensures + /// that the data being used is always up to date and leaves the state + /// management logic to the stores. /// /// If this component only needs data from a single [Store], then [StoresT] /// should be an instance of [Store]. This allows the default implementation @@ -36,30 +36,28 @@ abstract class FluxUiProps extends UiProps { /// If this component needs data from multiple [Store] instances, then /// [StoresT] should be a class that provides access to these multiple stores. /// Then, you can explicitly select the [Store] instances that should be - /// listened to by overriding [_FluxComponentMixin.redrawOn]. + /// listened to by overriding [redrawOn]. StoresT get store => props[_storePropKey] as StoresT; // ignore: avoid_as set store(StoresT value) => props[_storePropKey] = value; } /// Builds on top of [UiComponent], adding w_flux integration, much like the [FluxComponent] in w_flux. /// -/// * Flux components are responsible for rendering application views and turning -/// user interactions and events into [Action]s. -/// * Flux components can use data from one or many [Store] instances to define -/// the resulting component. +/// Flux components are responsible for rendering application views and turning +/// user interactions and events into [Action]s. Flux components can use data +/// from one or many [Store] instances to define the resulting component. /// -/// Use with the over_react transformer via the `@Component()` ([annotations.Component]) annotation. +/// Use with the over_react transformer via the `@Component()` ([Component]) annotation. abstract class FluxUiComponent extends UiComponent with _FluxComponentMixin, BatchedRedraws {} -/// Builds on top of [UiStatefulComponent], adding `w_flux` integration, much like the [FluxComponent] in w_flux. +/// Builds on top of [StatefulUiComponent], adding w_flux integration, much like the [FluxComponent] in w_flux. /// -/// * Flux components are responsible for rendering application views and turning -/// user interactions and events into [Action]s. -/// * Flux components can use data from one or many [Store] instances to define -/// the resulting component. +/// Flux components are responsible for rendering application views and turning +/// user interactions and events into [Action]s. Flux components can use data +/// from one or many [Store] instances to define the resulting component. /// -/// Use with the over_react transformer via the `@Component()` ([annotations.Component]) annotation. +/// Use with the over_react transformer via the `@Component()` ([Component]) annotation. abstract class FluxUiStatefulComponent extends UiStatefulComponent with _FluxComponentMixin, BatchedRedraws {} @@ -70,19 +68,15 @@ abstract class FluxUiStatefulComponent implements BatchedRedraws { TProps get props; - /// List of store subscriptions created when the component mounts. - /// - /// These subscriptions are canceled when the component is unmounted. + /// List of store subscriptions created when the component mounts. These + /// subscriptions are canceled when the component is unmounted. List _subscriptions = []; - void componentWillMount() { - /// Subscribe to all applicable stores. - /// - /// [Store]s returned by [redrawOn] will have their triggers mapped directly to this components - /// redraw function. - /// - /// [Store]s included in the [getStoreHandlers] result will be listened to and wired up to their - /// respective handlers. + componentWillMount() { + // Subscribe to all applicable stores. Stores returned by `redrawOn()` will + // have their triggers mapped directly to this components redraw function. + // Stores included in the `getStoreHandlers()` result will be listened to + // and wired up to their respective handlers. Map handlers = new Map.fromIterable(redrawOn(), value: (_) => (_) => redraw())..addAll(getStoreHandlers()); @@ -92,8 +86,8 @@ abstract class _FluxComponentMixin implements Batche }); } - void componentWillUnmount() { - // Ensure that unmounted components don't batch render + componentWillUnmount() { + // ensure that unmounted components don't batch render shouldBatchRedraw = false; // Cancel all store subscriptions. @@ -105,7 +99,6 @@ abstract class _FluxComponentMixin implements Batche } /// Define the list of [Store] instances that this component should listen to. - /// /// When any of the returned [Store]s update their state, this component will /// redraw. /// @@ -128,9 +121,8 @@ abstract class _FluxComponentMixin implements Batche } /// If you need more fine-grained control over store trigger handling, - /// override this method to return a Map of stores to handlers. - /// - /// Whenever a store in the returned map triggers, the respective handler will be called. + /// override this method to return a Map of stores to handlers. Whenever a + /// store in the returned map triggers, the respective handler will be called. /// /// Handlers defined here take precedence over the [redrawOn] handling. /// If possible, however, [redrawOn] should be used instead of this in order @@ -140,9 +132,9 @@ abstract class _FluxComponentMixin implements Batche return {}; } - /// Register a [subscription] that should be canceled when the component unmounts. - /// - /// Cancellation will be handled automatically by [componentWillUnmount]. + /// Register a [subscription] that should be canceled when the component + /// unmounts. Cancellation will be handled automatically by + /// [componentWillUnmount]. void addSubscription(StreamSubscription subscription) { _subscriptions.add(subscription); } diff --git a/lib/src/component_declaration/transformer_helpers.dart b/lib/src/component_declaration/transformer_helpers.dart index 2973c62ad..4ea5159ba 100644 --- a/lib/src/component_declaration/transformer_helpers.dart +++ b/lib/src/component_declaration/transformer_helpers.dart @@ -21,6 +21,8 @@ export './annotations.dart'; export './component_base.dart' hide UiComponent, UiStatefulComponent, UiProps, UiState; +typedef dynamic _RefTypedef(String ref); + // ---------------------------------------------------------------------- // Helpers and extras consumable by generated code and consumers of // generated code. @@ -86,22 +88,32 @@ class GeneratedClass { } -/// See: [component_base.UiComponent] +/// The basis for a over_react component, extending [react.Component]. (Successor to [BaseComponent]). +/// +/// Includes support for strongly-typed props and utilities for prop and CSS classname forwarding. /// -/// Use with the over_react transformer via the `@Component()` ([annotations.Component]) annotation. +/// Use with the over_react transformer via the `@Component()` ([Component]) annotation. abstract class UiComponent extends component_base.UiComponent with GeneratedClass { /// This class should not be instantiated directly, and throws an error to indicate this. UiComponent() { _throwIfNotGenerated(); } - /// The default consumed props, taken from the keys generated in the associated @[annotations.Props] class. + /// Returns the component of the specified [ref]. + /// > `react.Component` if it is a Dart component + /// > DOM node if it is a DOM component. + /// + /// Overridden for strong typing. + @override + _RefTypedef get ref => super.ref; + + /// The default consumed props, taken from the keys generated in the associated @[Props] class. @toBeGenerated Iterable get $defaultConsumedProps => throw new UngeneratedError(member: #$defaultConsumedProps); /// The keys for the non-forwarding props defined in this component. /// - /// For generated components, this defaults to the keys generated in the associated @[annotations.Props] class + /// For generated components, this defaults to the keys generated in the associated @[Props] class /// if this getter is not overridden. @override Iterable get consumedProps => $defaultConsumedProps; @@ -114,9 +126,11 @@ abstract class UiComponent extends component_base.UiComp } -/// See: [component_base.UiStatefulComponent] +/// The basis for a stateful over_react component, extending [react.Component]. (Successor to [BaseComponentWithState]). +/// +/// Includes support for strongly-typed props and state and utilities for prop and CSS classname forwarding. /// -/// Use with the over_react transformer via the `@Component()` ([annotations.Component]) annotation. +/// Use with the over_react transformer via the `@Component()` ([Component]) annotation. abstract class UiStatefulComponent extends component_base.UiStatefulComponent with GeneratedClass { /// This class should not be instantiated directly, and throws an error to indicate this. @@ -124,26 +138,32 @@ abstract class UiStatefulComponent `react.Component` if it is a Dart component + /// > DOM node if it is a DOM component. + /// + /// Overridden for strong typing. + @override + _RefTypedef get ref => super.ref; + + /// The default consumed prop keys, taken from the keys generated in the associated @[Props] class. @toBeGenerated Iterable get $defaultConsumedProps => throw new UngeneratedError(member: #$defaultConsumedProps); /// The keys for the non-forwarding props defined in this component. /// - /// For generated components, this defaults to the keys generated in the associated @[annotations.Props] class + /// For generated components, this defaults to the keys generated in the associated @[Props] class /// if this getter is not overridden. @override Iterable get consumedProps => $defaultConsumedProps; /// Returns a typed props object backed by the specified [propsMap]. - /// /// Required to properly instantiate the generic [TProps] class. @override @toBeGenerated TProps typedPropsFactory(Map propsMap) => throw new UngeneratedError(member: #typedPropsFactory); /// Returns a typed state object backed by the specified [stateMap]. - /// /// Required to properly instantiate the generic [TState] class. @override @toBeGenerated TState typedStateFactory(Map stateMap) => throw new UngeneratedError(member: #typedStateFactory); } @@ -151,10 +171,10 @@ abstract class UiStatefulComponent '$runtimeType.$_name ($debugDescription)'; + String toString() { + var string = '$runtimeType.$_name'; + + var debugDescription = this.debugDescription; + if (debugDescription != null) { + string = '$string ($debugDescription)'; + } + + return string; + } } /// A named constant with a helpful string representation diff --git a/lib/src/util/css_value_util.dart b/lib/src/util/css_value_util.dart index 998979a6a..d6e1b04d2 100644 --- a/lib/src/util/css_value_util.dart +++ b/lib/src/util/css_value_util.dart @@ -57,15 +57,14 @@ class CssValue implements Comparable { unit = 'px'; } else { var unitMatch = new RegExp(r'(?:rem|em|ex|vh|vw|vmin|vmax|%|px|cm|mm|in|pt|pc|ch)?$').firstMatch(source.toString()); - try { - number = double.parse(unitMatch.input.substring(0, unitMatch.start)); unit = unitMatch.group(0); - if (unit == '') { - unit = 'px'; - } - } catch(e) { - error = new ArgumentError.value(source, 'value', 'Invalid number/unit for CSS value'); + if (unit == '') { + unit = 'px'; } + + number = double.parse(unitMatch.input.substring(0, unitMatch.start), (_) { + error = new ArgumentError.value(source, 'value', 'Invalid number/unit for CSS value'); + }); } if (number != null && !number.isFinite) { diff --git a/lib/src/util/document_event_helper_util.dart b/lib/src/util/document_event_helper_util.dart new file mode 100644 index 000000000..df4d1c8b6 --- /dev/null +++ b/lib/src/util/document_event_helper_util.dart @@ -0,0 +1,16 @@ +library document_event_helper; + +import 'dart:html' as html; + +/// Utility class that helps with dependency injection when mocking document events. +abstract class DocumentEventHelper { + static html.HtmlDocument _document = html.document; + /// Singleton that can be used to listen to events like you would with `document`. + /// + /// Can be mutated when testing + static html.HtmlDocument get document => _document; + static set document(html.HtmlDocument value) { + // TODO add `.fromEnvironment` check for warning when not testing. + _document = value; + } +} diff --git a/lib/src/util/dom_util.dart b/lib/src/util/dom_util.dart index ea96136fd..da2993977 100644 --- a/lib/src/util/dom_util.dart +++ b/lib/src/util/dom_util.dart @@ -16,6 +16,11 @@ library dom_util; import 'dart:html'; +import 'package:platform_detect/platform_detect.dart'; + +import './string_util.dart'; +import './validation_util.dart'; + /// Returns whether [root] is the same as or contains the [other] node. /// /// Returns false if either [root] or [other] is null. @@ -51,3 +56,96 @@ Element getActiveElement() { return activeElement; } + +/// A list of the `type` attribute values for an HTML `` element that implement [TextInputElementBase]. +/// +/// Necessary because of the circular inheritance hierarchy in Dart's [InputElement] class structure. +/// +/// See: +/// +/// Related: [isTextInputElementBase] +const List inputTypesWithSelectionRangeSupport = const [ + 'search', + 'text', + 'url', + 'tel', + 'email', + 'password', + 'number', +]; + +/// Returns whether the provided [element] supports `setSelectionRange`. +/// +/// Necessary in part because of the circular inheritance hierarchy in Dart's [InputElement] class structure, +/// and in part because the classes do not correspond to whether setSelectionRange is supported (e.g. number inputs). +/// +/// See: +bool supportsSelectionRange(InputElement element) { + // Uncomment once https://github.com/dart-lang/sdk/issues/22967 is fixed. + // if (element is TextInputElementBase) return true; + + final type = element.getAttribute('type'); + return inputTypesWithSelectionRangeSupport.contains(type); +} + +/// Custom implementation to prevent the error that [TextInputElementBase.setSelectionRange] throws when called +/// on an [EmailInputElement] or [NumberInputElement] since ONLY Chrome does not support it. +/// +/// A warning will be displayed in the console instead of an error. +/// +/// __Example that will throw an exception in Chrome:__ +/// InputElement inputNodeRef; +/// +/// // This will throw an exception in Chrome when the node is focused. +/// renderEmailInput() { +/// return (Dom.input() +/// ..type = 'email' +/// ..onFocus = (_) { +/// inputNodeRef.setSelectionRange(inputNodeRef.value.length, inputNodeRef.value.length); +/// } +/// ..ref = (instance) { inputNodeRef = instance; } +/// )(); +/// } +/// +/// __Example that will not throw:__ +/// InputElement inputNodeRef; +/// +/// // This will not throw an exception - and will work in all +/// // browsers except Chrome until +/// // https://bugs.chromium.org/p/chromium/issues/detail?id=324360 +/// // is fixed. +/// renderChromeSafeEmailInput() { +/// return (Dom.input() +/// ..type = 'email' +/// ..onFocus = (_) { +/// setSelectionRange(inputNodeRef, inputNodeRef.value.length, inputNodeRef.value.length); +/// } +/// ..ref = (instance) { inputNodeRef = instance; } +/// )(); +/// } +/// +/// See: +void setSelectionRange(/* TextInputElement | TextAreaElement */Element input, int start, int end, [String direction]) { + if (input is TextAreaElement) { + input.setSelectionRange(start, end, direction); + } else if (input is InputElement && supportsSelectionRange(input)) { + if (browser.isChrome) { + final inputType = input.getAttribute('type'); + + if (inputType == 'email' || inputType == 'number') { + assert(ValidationUtil.warn(unindent( + ''' + Google Chrome does not support `setSelectionRange` on email or number inputs. + See: https://bugs.chromium.org/p/chromium/issues/detail?id=324360 + ''' + ))); + + return; + } + } + + input.setSelectionRange(start, end, direction); + } else { + throw new ArgumentError.value(input, 'input', 'must be an instance of `TextInputElementBase`, `NumberInputElement` or `TextAreaElement`'); + } +} diff --git a/lib/src/util/handler_chain_util.dart b/lib/src/util/handler_chain_util.dart index 0a8da8cfc..c85f881e0 100644 --- a/lib/src/util/handler_chain_util.dart +++ b/lib/src/util/handler_chain_util.dart @@ -61,7 +61,6 @@ final CallbackUtil0Arg callbacks = co /// Provides chaining utilities for [ResizeSensorHandler]. final CallbackUtil1Arg resizeEventCallbacks = const CallbackUtil1Arg(); - typedef Callback0Arg(); typedef Callback1Arg(T1 arg1); typedef Callback2Arg(T1 arg1, T2 arg2); diff --git a/lib/src/util/js_util.dart b/lib/src/util/js_util.dart new file mode 100644 index 000000000..57e30f963 --- /dev/null +++ b/lib/src/util/js_util.dart @@ -0,0 +1,14 @@ +library over_react.js_util; + +import 'dart:js'; + +/// [Global JavaScript Array Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) +JsObject jsArray = context['Array']; + +/// Store a helper reference to the global [jsArray]'s [`slice` method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) +JsObject arrayProtoSlice = jsArray['prototype']['slice']; + +/// Converts the Array-like [object] to a [JsArray] using [arrayProtoSlice]. +JsArray convertToArray(JsObject object) { + return arrayProtoSlice.callMethod('apply', [object]); +} diff --git a/lib/src/util/map_util.dart b/lib/src/util/map_util.dart index b48df0989..583b37236 100644 --- a/lib/src/util/map_util.dart +++ b/lib/src/util/map_util.dart @@ -16,8 +16,9 @@ library over_react.map_util; import 'dart:collection'; -import 'package:over_react/src/component/prop_mixins.dart'; import 'package:over_react/src/component_declaration/transformer_helpers.dart'; +import 'package:over_react/src/component/dom_components.dart'; +import 'package:over_react/src/component/prop_mixins.dart'; /// Returns a copy of the specified props map, omitting reserved React props by default, /// in addition to any specified keys. @@ -60,6 +61,16 @@ Map getPropsToForward(Map props, {bool omitReactProps: true, bool onlyCopyDomPro return propsToForward; } +/// Returns a copy of the style map found in [props]. +/// +/// Returns an empty map if [props] or its style map are null. +Map newStyleFromProps(Map props) { + if (props == null) return {}; + + var existingStyle = domProps(props).style; + return existingStyle == null ? {} : new Map.from(existingStyle); +} + SplayTreeSet _validDomProps = new SplayTreeSet() ..addAll(const $PropKeys(DomPropsMixin)) ..addAll(const $PropKeys(SvgPropsMixin)); diff --git a/lib/src/util/prop_key_util.dart b/lib/src/util/prop_key_util.dart new file mode 100644 index 000000000..fde65d5ae --- /dev/null +++ b/lib/src/util/prop_key_util.dart @@ -0,0 +1,61 @@ +// Copyright 2016 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. + +library over_react.prop_key_util; + +import 'dart:collection'; + +/// Returns the string key of the [factory] prop accessed in [accessProp]. +/// +/// Example usage: +/// +/// var valuePropKey = getPropKey((props) => props.value, TextInput); +String getPropKey(void accessProp(Map keySpy), Map factory(Map props)) { + return _getKey((Map keySpy) { + return accessProp(factory(keySpy)); + }); +} + +dynamic _getKey(void accessKey(Map keySpy)) { + var keySpy = new _SingleKeyAccessMapSpy(const {}); + + accessKey(keySpy); + + return keySpy.key; +} + +/// Helper class that stores the key accessed while getting a value of a map. +class _SingleKeyAccessMapSpy extends MapView { + _SingleKeyAccessMapSpy(Map map) : super(map); + + bool _hasBeenAccessed = false; + + dynamic _key; + + dynamic get key { + if (!_hasBeenAccessed) throw new StateError('Key has not been accessed.'); + + return _key; + } + + @override + operator[](key) { + if (_hasBeenAccessed) throw new StateError('A key has already been accessed.'); + + _key = key; + _hasBeenAccessed = true; + + return null; + } +} diff --git a/lib/src/util/react_wrappers.dart b/lib/src/util/react_wrappers.dart index a6321d8a6..c3b183c82 100644 --- a/lib/src/util/react_wrappers.dart +++ b/lib/src/util/react_wrappers.dart @@ -100,6 +100,8 @@ Map getJsProps(/* ReactElement|ReactComponent */ instance) { return props; } +Expando _elementPropsCache = new Expando('_elementPropsCache'); + /// Returns an unmodifiable Map view of props for a [ReactElement] or composite [ReactComponent] [instance]. /// /// For a native Dart component, this returns its [react.Component.props] in an unmodifiable Map view. @@ -107,9 +109,20 @@ Map getJsProps(/* ReactElement|ReactComponent */ instance) { /// /// Throws if [instance] is not a valid [ReactElement] or composite [ReactComponent] . Map getProps(/* ReactElement|ReactComponent */ instance) { - if (isValidElement(instance) || _isCompositeComponent(instance)) { + var isCompositeComponent = _isCompositeComponent(instance); + + if (isValidElement(instance) || isCompositeComponent) { + if (!isCompositeComponent) { + var cachedView = _elementPropsCache[instance]; + if (cachedView != null) return cachedView; + } + var propsMap = isDartComponent(instance) ? _getExtendedProps(instance) : getJsProps(instance); - return new UnmodifiableMapView(propsMap); + var view = new UnmodifiableMapView(propsMap); + + if (!isCompositeComponent) _elementPropsCache[instance] = view; + + return view; } throw new ArgumentError.value(instance, 'instance', 'must be a valid ReactElement or composite ReactComponent'); @@ -209,6 +222,12 @@ ReactElement cloneElement(ReactElement element, [Map props, Iterable children]) } } +/// Returns whether the React [instance] is mounted. +/// +/// Deprecated: Simply call `isMounted` on the [ReactComponent] instead. +@Deprecated('2.0.0') +bool isMounted(ReactComponent instance) => instance.isMounted(); + /// Returns the native Dart component associated with a React JS component instance, or null if the component is not Dart-based. react.Component getDartComponent(/* [1] */ instance) { if (instance is Element) { diff --git a/lib/src/util/rem_util.dart b/lib/src/util/rem_util.dart index 414aca9e6..419f0ce6a 100644 --- a/lib/src/util/rem_util.dart +++ b/lib/src/util/rem_util.dart @@ -21,7 +21,6 @@ import 'dart:html'; import 'package:over_react/over_react.dart'; import 'package:over_react/src/util/css_value_util.dart'; import 'package:react/react_dom.dart' as react_dom; - double _computeRootFontSize() { return new CssValue.parse(document.documentElement.getComputedStyle().fontSize).number.toDouble(); } @@ -120,9 +119,7 @@ CssValue toRem(dynamic value, {bool treatNumAsRem: false, bool passThroughUnsupp } else if (parsedValue?.unit == 'px') { remValueNum = parsedValue.number / rootFontSize; } else { - if (passThroughUnsupportedUnits) { - return parsedValue; - } + if (passThroughUnsupportedUnits) return parsedValue; throw new ArgumentError.value(value, 'value', 'must be a px num or a String px/rem value'); } @@ -167,9 +164,7 @@ CssValue toPx(dynamic value, {bool treatNumAsPx: false, bool passThroughUnsuppor } else if (parsedValue?.unit == 'rem') { pxValueNum = parsedValue.number * rootFontSize; } else { - if (passThroughUnsupportedUnits) { - return parsedValue; - } + if (passThroughUnsupportedUnits) return parsedValue; throw new ArgumentError.value(value, 'value', 'must be a rem num or a String px/rem value'); } diff --git a/lib/src/util/test_mode.dart b/lib/src/util/test_mode.dart index 11d81ec5d..6cc25b42a 100644 --- a/lib/src/util/test_mode.dart +++ b/lib/src/util/test_mode.dart @@ -13,7 +13,6 @@ // limitations under the License. library over_react.test_mode; - import 'package:over_react/src/component_declaration/component_base.dart' as component_base; /// Enables test mode. diff --git a/pubspec.yaml b/pubspec.yaml index 00006e777..73ea252f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: source_span: "^1.2.0" transformer_utils: "^0.1.1" w_flux: "^2.5.0" - browser_detect: "^1.0.4" + platform_detect: "^1.1.1" dev_dependencies: matcher: ">=0.11.0 <0.13.0" coverage: "^0.7.2" diff --git a/test/over_react/component/dom_components_test.dart b/test/over_react/component/dom_components_test.dart index 03793f301..e7a34d33b 100644 --- a/test/over_react/component/dom_components_test.dart +++ b/test/over_react/component/dom_components_test.dart @@ -39,17 +39,9 @@ main() { for (var element in methods) { String name = MirrorSystem.getName(element.simpleName); String expectedTagName = name; - if (expectedTagName == 'variable') expectedTagName = 'var'; - if (expectedTagName == 'svgSwitch') expectedTagName = 'switch'; - if (expectedTagName == 'colorProfile') expectedTagName = 'color-profile'; - if (expectedTagName == 'fontFace') expectedTagName = 'font-face'; - if (expectedTagName == 'fontFaceFormat') expectedTagName = 'font-face-format'; - if (expectedTagName == 'fontFaceName') expectedTagName = 'font-face-name'; - if (expectedTagName == 'fontFaceSrc') expectedTagName = 'font-face-src'; - if (expectedTagName == 'fontFaceUri') expectedTagName = 'font-face-uri'; - if (expectedTagName == 'missingGlyph') expectedTagName = 'missing-glyph'; - if (expectedTagName.startsWith(new RegExp('svg.'))) expectedTagName = expectedTagName.substring(3); - + if (expectedTagName == 'variable') { + expectedTagName = 'var'; + } test('Dom.$name generates the correct type', () { DomProps builder = domClassMirror.invoke(element.simpleName, []).reflectee; ReactElement component = builder(); diff --git a/test/over_react/component/resize_sensor_test.dart b/test/over_react/component/resize_sensor_test.dart index 7539eeeb4..11dbbae49 100644 --- a/test/over_react/component/resize_sensor_test.dart +++ b/test/over_react/component/resize_sensor_test.dart @@ -18,7 +18,7 @@ library resize_sensor_test; import 'dart:async'; import 'dart:html'; -import 'package:browser_detect/browser_detect.dart'; +import 'package:platform_detect/platform_detect.dart'; import 'package:over_react/over_react.dart'; import 'package:react/react.dart' as react; import 'package:react/react_dom.dart' as react_dom; @@ -126,11 +126,11 @@ void main() { expect(renderedNode.style.display, equals('block')); var nodeStyleDecl = renderedNode.style; - if (browser.isIe && browser.version < '11') { + if (browser.isInternetExplorer && browser.version.major < 11) { expect(nodeStyleDecl.getPropertyValue('-ms-flex-positive'), '1'); expect(nodeStyleDecl.getPropertyValue('-ms-flex-negative'), '1'); expect(nodeStyleDecl.getPropertyValue('-ms-flex-preferred-size'), '0%'); - } else if (browser.isSafari && browser.version < '9') { + } else if (browser.isSafari && browser.version.major < 9) { expect(nodeStyleDecl.getPropertyValue('-webkit-flex'), '1 1 0%'); } else { expect(nodeStyleDecl.getPropertyValue('flex'), '1 1 0%'); @@ -143,12 +143,12 @@ void main() { expect(renderedNode.style.position, equals('relative')); var nodeStyleDecl = renderedNode.style; - if (browser.isIe && browser.version < '11') { + if (browser.isInternetExplorer && browser.version.major < 11) { expect(renderedNode.style.display, equals('-ms-flexbox')); expect(nodeStyleDecl.getPropertyValue('-ms-flex-positive'), '1'); expect(nodeStyleDecl.getPropertyValue('-ms-flex-negative'), '1'); expect(nodeStyleDecl.getPropertyValue('-ms-flex-preferred-size'), '0%'); - } else if (browser.isSafari && browser.version < '9') { + } else if (browser.isSafari && browser.version.major < 9) { expect(renderedNode.style.display, equals('-webkit-flex')); expect(nodeStyleDecl.getPropertyValue('-webkit-flex'), '1 1 0%'); } else { diff --git a/test/over_react/component_declaration/component_base_test.dart b/test/over_react/component_declaration/component_base_test.dart index ac1dd1182..fa74f520e 100644 --- a/test/over_react/component_declaration/component_base_test.dart +++ b/test/over_react/component_declaration/component_base_test.dart @@ -92,7 +92,7 @@ main() { test('a single child is passed in', () { var child = 'Only child'; var renderedNode = renderAndGetDom(Dom.div()(child)); - List children = renderedNode.childNodes.where((node) => node.nodeType != Node.COMMENT_NODE).toList(); + List children = renderedNode.childNodes.where((node) => node.nodeType != Node.COMMENT_NODE).toList(); expect(children.length, equals(1)); expect(children[0].data, equals(child)); diff --git a/test/over_react/component_declaration/component_type_checking_test/type_inheritance/abstract_inheritance/extendedtype.dart b/test/over_react/component_declaration/component_type_checking_test/type_inheritance/abstract_inheritance/extendedtype.dart index bf5e95fed..56159d2af 100644 --- a/test/over_react/component_declaration/component_type_checking_test/type_inheritance/abstract_inheritance/extendedtype.dart +++ b/test/over_react/component_declaration/component_type_checking_test/type_inheritance/abstract_inheritance/extendedtype.dart @@ -15,7 +15,6 @@ library test_component.type_inheritance.extendedtype; import 'package:over_react/over_react.dart'; - import './abstract.dart'; @Factory() diff --git a/test/over_react/component_declaration/component_type_checking_test/type_inheritance/subsubtype.dart b/test/over_react/component_declaration/component_type_checking_test/type_inheritance/subsubtype.dart index 2cc2dc2bb..e579c6f0f 100644 --- a/test/over_react/component_declaration/component_type_checking_test/type_inheritance/subsubtype.dart +++ b/test/over_react/component_declaration/component_type_checking_test/type_inheritance/subsubtype.dart @@ -15,7 +15,6 @@ library test_component.type_inheritance.subsubtype; import 'package:over_react/over_react.dart'; - import './subtype.dart'; @Factory() diff --git a/test/over_react/component_declaration/flux_component_test/handler_precedence.dart b/test/over_react/component_declaration/flux_component_test/handler_precedence.dart index 28b182b7a..803f63ce3 100644 --- a/test/over_react/component_declaration/flux_component_test/handler_precedence.dart +++ b/test/over_react/component_declaration/flux_component_test/handler_precedence.dart @@ -7,7 +7,7 @@ UiFactory TestHandlerPrecedence; class TestHandlerPrecedenceProps extends FluxUiProps {} @Component() -class TestHandlerPrecedenceComponent extends FluxUiComponent { +class TestHandlerPrecedenceComponent extends FluxUiComponent { int numberOfRedraws = 0; int numberOfHandlerCalls = 0; @@ -20,7 +20,7 @@ class TestHandlerPrecedenceComponent extends FluxUiComponent {props.store.store1: increment}; - increment(Store store) { + increment(_) { numberOfHandlerCalls += 1; } diff --git a/test/over_react/component_declaration/flux_component_test/store_handlers.dart b/test/over_react/component_declaration/flux_component_test/store_handlers.dart index 04a42490b..dd81431a7 100644 --- a/test/over_react/component_declaration/flux_component_test/store_handlers.dart +++ b/test/over_react/component_declaration/flux_component_test/store_handlers.dart @@ -7,7 +7,7 @@ UiFactory TestStoreHandlers; class TestStoreHandlersProps extends FluxUiProps {} @Component() -class TestStoreHandlersComponent extends FluxUiComponent { +class TestStoreHandlersComponent extends FluxUiComponent { int numberOfHandlerCalls = 0; @override @@ -16,7 +16,7 @@ class TestStoreHandlersComponent extends FluxUiComponent @override getStoreHandlers() => {props.store: increment}; - increment(Store store) { + increment(_) { numberOfHandlerCalls += 1; } } diff --git a/test/over_react/component_declaration/transformer_integration_tests/accessor_mixin_integration_test.dart b/test/over_react/component_declaration/transformer_integration_tests/accessor_mixin_integration_test.dart index 1cbda5cf7..877c8c28b 100644 --- a/test/over_react/component_declaration/transformer_integration_tests/accessor_mixin_integration_test.dart +++ b/test/over_react/component_declaration/transformer_integration_tests/accessor_mixin_integration_test.dart @@ -50,7 +50,7 @@ main() { }); }); - group('generates prop getters/setters, when there is a custom key namespace, with', () { + group('generates prop getters/setters, when there is a custom key namespace, with', () { test('the custom namespace and the prop name as the key by default', () { var mixinsTest; @@ -81,7 +81,7 @@ main() { }); }); - group('@StateMixin()', () { + group('@StateMixin()', () { group('generates state getters/setters with', () { test('the state class name as a namespace and the state name as the key by default', () { var mixinsTest; @@ -112,7 +112,7 @@ main() { }); }); - group('generates state getters/setters, when there is a custom key namespace, with', () { + group('generates state getters/setters, when there is a custom key namespace, with', () { test('the custom namespace and the state name as the key by default', () { var mixinsTest; diff --git a/test/over_react/util/dom_util_test.dart b/test/over_react/util/dom_util_test.dart index 7977caded..8d4ca0d77 100644 --- a/test/over_react/util/dom_util_test.dart +++ b/test/over_react/util/dom_util_test.dart @@ -20,6 +20,7 @@ import 'package:over_react/over_react.dart'; import 'package:test/test.dart'; import '../../test_util/test_util.dart'; +import '../../wsd_test_util/validation_util_helpers.dart'; /// Main entry point for DomUtil testing main() { @@ -152,6 +153,122 @@ main() { expect(getActiveElement(), isNull); }); }); + + group('setSelectionRange', () { + test('throws an ArgumentError if called on an unsupported Element type', () { + var invalidElement = new DivElement(); + expect(() => setSelectionRange(invalidElement, 0, 0), throwsArgumentError); + }); + + test('throws an ArgumentError if called on an unsupported InputElement type', () { + var invalidElement = new CheckboxInputElement(); + + // Note: For some unknown reason - when running the exact same expect() we use for DivElement above, + // this one fails with "Invalid Object" - the stack trace never leaves test() + // + // ¯\_(ツ)_/¯ + var error; + + try { + setSelectionRange(invalidElement, 0, 0); + } catch (err) { + error = err; + } + + expect(error, isNotNull); + }); + + group('correctly calls setSelectionRange', () { + var renderedInstance; + InputElement inputElement; + TextAreaElement textareaElement; + const String testValue = 'foo'; + + tearDown(() { + renderedInstance = null; + inputElement = null; + + tearDownAttachedNodes(); + }); + + group('on an `` of type:', () { + void sharedInputSetSelectionRangeTest(String type) { + renderedInstance = renderAttachedToDocument((Dom.input() + ..defaultValue = testValue + ..type = type + )()); + inputElement = findDomNode(renderedInstance); + setSelectionRange(inputElement, testValue.length, testValue.length); + + // setSelectionRange on number inputs shouldn't throw in other browsers, + // but it also doesn't always work. + // Don't expect that the selection actually changed. + if (type != 'number') { + expect(inputElement.selectionStart, equals(testValue.length)); + expect(inputElement.selectionEnd, equals(testValue.length)); + } + } + + for (var type in inputTypesWithSelectionRangeSupport) { + if (type == 'email' || type == 'number') { + // See: https://bugs.chromium.org/p/chromium/issues/detail?id=324360 + test(type, () { + sharedInputSetSelectionRangeTest(type); + }, testOn: 'js && !chrome'); + } else { + test(type, () { sharedInputSetSelectionRangeTest(type); }); + } + } + }); + + test('on TextAreaElement', () { + renderedInstance = renderAttachedToDocument((Dom.textarea() + ..defaultValue = testValue + )()); + textareaElement = findDomNode(renderedInstance); + setSelectionRange(textareaElement, testValue.length, testValue.length); + + expect(textareaElement.selectionStart, equals(testValue.length)); + expect(textareaElement.selectionEnd, equals(testValue.length)); + }); + + // See: https://bugs.chromium.org/p/chromium/issues/detail?id=324360 + group('without throwing an error in Google Chrome when `props.type` is', () { + void verifyLackOfException() { + expect(renderedInstance, isNotNull, reason: 'test setup sanity check'); + expect(inputElement, isNotNull, reason: 'test setup sanity check'); + + expect(() => setSelectionRange(inputElement, testValue.length, testValue.length), returnsNormally); + } + + setUp(() { + startRecordingValidationWarnings(); + }); + + tearDown(() { + stopRecordingValidationWarnings(); + }); + + test('email', () { + renderedInstance = renderAttachedToDocument((Dom.input() + ..defaultValue = testValue + ..type = 'email' + )()); + inputElement = findDomNode(renderedInstance); + verifyLackOfException(); + }); + + test('number', () { + renderedInstance = renderAttachedToDocument((Dom.input() + ..defaultValue = testValue + ..type = 'number' + )()); + inputElement = findDomNode(renderedInstance); + verifyLackOfException(); + }); + }, testOn: 'chrome'); + }); + }); } @Factory() diff --git a/test/over_react/util/handler_chain_util_test.dart b/test/over_react/util/handler_chain_util_test.dart index 6d445a809..ebb52a03a 100644 --- a/test/over_react/util/handler_chain_util_test.dart +++ b/test/over_react/util/handler_chain_util_test.dart @@ -279,6 +279,106 @@ main() { sharedTests(const CallbackUtil3Arg(), 3); }); }); + + group('React DOM event callback creation utility function', () { + callsBothFunctions(Function creator) { + bool calledA = false, calledB = false; + Function a = (event) => calledA = true; + Function b = (event) => calledB = true; + + var chainedCallback = creator(a, b); + var result = chainedCallback(null); + + expect(calledA, isTrue); + expect(calledB, isTrue); + + expect(result, isNull); + } + + callsFunctionsInOrder(Function creator) { + int counter = 1; + bool calledA = false, calledB = false; + Function a = (event) { + calledA = true; + expect(counter, equals(1)); + counter++; + }; + Function b = (event) { + calledB = true; + expect(counter, equals(2)); + }; + + var chainedCallback = creator(a, b); + chainedCallback(null); + + expect(calledA, isTrue); + expect(calledB, isTrue); + } + + returnsFalseWhenAReturnsFalse(Function creator) { + Function a = (event) => false; + Function b = (event) => true; + + var chainedCallback = creator(a, b); + var result = chainedCallback(null); + + expect(result, isFalse); + } + + returnsFalseWhenBReturnsFalse(Function creator) { + Function a = (event) => true; + Function b = (event) => false; + + var chainedCallback = creator(a, b); + var result = chainedCallback(null); + + expect(result, isFalse); + } + + returnsFalseWhenBothReturnFalse(Function creator) { + Function a = (event) => false; + Function b = (event) => false; + + var chainedCallback = creator(a, b); + var result = chainedCallback(null); + + expect(result, isFalse); + } + + returnsNullWhenNeitherReturnFalse(Function creator) { + Function a = (event) => true; + Function b = (event) => true; + + var chainedCallback = creator(a, b); + var result = chainedCallback(null); + + expect(result, isNull); + } + + handlesNullParameters(Function creator) { + bool flag = false; + Function callback = (event) => flag = true; + var chainedCallback, result; + + chainedCallback = creator(callback, null); + chainedCallback(null); + expect(flag, isTrue, reason: 'The first callback should be called when the second callback is null'); + + flag = false; + + chainedCallback = creator(null, callback); + chainedCallback(null); + expect(flag, isTrue, reason: 'The second callback should be called when the first callback is null'); + + chainedCallback = creator(null, null); + try { + result = chainedCallback(null); + expect(result, isNull); + } catch (exception) { + fail(exception); + } + } + }); }); } diff --git a/test/over_react/util/map_util_test.dart b/test/over_react/util/map_util_test.dart index adce5085d..da9524467 100644 --- a/test/over_react/util/map_util_test.dart +++ b/test/over_react/util/map_util_test.dart @@ -121,5 +121,23 @@ main() { expect(actual, equals(expected)); }); }); + + group('newStyleFromProps() returns', () { + test('a copy of the style map found in the specified props', () { + var styles = {'color': 'red', 'width': '10rem'}; + var props = domProps() + ..style = styles; + + expect(newStyleFromProps(props), equals(styles)); + }); + + test('an empty map when the specified props are null', () { + expect(newStyleFromProps(null), equals({})); + }); + + test('an empty map when the specified props have a null style map', () { + expect(newStyleFromProps(domProps()), equals({})); + }); + }); }); } diff --git a/test/over_react/util/prop_key_util_test.dart b/test/over_react/util/prop_key_util_test.dart new file mode 100644 index 000000000..ac6b19ffa --- /dev/null +++ b/test/over_react/util/prop_key_util_test.dart @@ -0,0 +1,48 @@ +library prop_key_util_test; + +import 'package:test/test.dart'; +import 'package:over_react/over_react.dart'; + +main() { + group('getPropKey', () { + test('returns the expected key', () { + var testProps = Test()..foo = 'baz'; + var fooPropKey = getPropKey((Map props) { (props as TestProps).foo; }, Test); // ignore: avoid_as + expect(testProps, equals({fooPropKey: 'baz'})); + }); + + test('throws if you don\'t access the prop', () { + expect(() => getPropKey((props) => false, Test), throwsStateError); + }); + + test('throws if you access the prop multiple times', () { + expect(() => getPropKey((Map props) { + (props as TestProps).foo; // ignore: avoid_as + (props as TestProps).foo; // ignore: avoid_as + }, Test), throwsStateError); + }); + + test('throws if you access multiple props', () { + expect(() => getPropKey((Map props) { + (props as TestProps).foo; // ignore: avoid_as + (props as TestProps).bar; // ignore: avoid_as + }, Test), throwsStateError); + }); + }); +} + +@Factory() +UiFactory Test; + +@Props() +class TestProps extends UiProps { + String foo; + + String bar; +} + +@Component() +class TestComponent extends UiComponent { + @override + render() {} +} diff --git a/test/over_react/util/react_wrappers_test.dart b/test/over_react/util/react_wrappers_test.dart index 7966ad57e..2d076d23e 100644 --- a/test/over_react/util/react_wrappers_test.dart +++ b/test/over_react/util/react_wrappers_test.dart @@ -460,10 +460,10 @@ main() { const Map testStyle = const {'background': 'white'}; test('returns props for a composite JS component ReactElement', () { - ReactElement instance = render(testJsComponentFactory({ + ReactElement instance = testJsComponentFactory({ 'jsProp': 'js', 'style': testStyle, - }, testChildren)); + }, testChildren); expect(getProps(instance), equals({ 'jsProp': 'js', @@ -485,6 +485,31 @@ main() { })); }); + test('returns props for a composite JS ReactComponent, even when the props change', () { + var mountNode = new DivElement(); + ReactComponent renderedInstance = react_dom.render(testJsComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren), mountNode); + + expect(getProps(renderedInstance), equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren + })); + + renderedInstance = react_dom.render(testJsComponentFactory({ + 'jsProp': 'other js', + 'style': testStyle, + }, testChildren), mountNode); + + expect(getProps(renderedInstance), equals({ + 'jsProp': 'other js', + 'style': testStyle, + 'children': testChildren + })); + }); + test('returns props for a DOM component ReactElement', () { ReactElement instance = (Dom.div() ..addProp('domProp', 'dom') @@ -499,10 +524,10 @@ main() { }); test('returns props for a Dart component ReactElement', () { - ReactElement instance = render(TestComponentFactory({ + ReactElement instance = TestComponentFactory({ 'dartProp': 'dart', 'style': testStyle, - }, testChildren)); + }, testChildren); expect(getProps(instance), equals({ 'dartProp': 'dart', @@ -524,6 +549,31 @@ main() { })); }); + test('returns props for a Dart component ReactComponent, even when the props change', () { + var mountNode = new DivElement(); + ReactComponent renderedInstance = react_dom.render(TestComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren), mountNode); + + expect(getProps(renderedInstance), equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren + })); + + renderedInstance = react_dom.render(TestComponentFactory({ + 'jsProp': 'other js', + 'style': testStyle, + }, testChildren), mountNode); + + expect(getProps(renderedInstance), equals({ + 'jsProp': 'other js', + 'style': testStyle, + 'children': testChildren + })); + }); + test('returns props as an unmodifiable map', () { ReactComponent renderedInstance = render(TestComponentFactory({ 'dartProp': 'dart' @@ -689,7 +739,6 @@ main() { /// Helper component for testing a Dart (react-dart) React component with cloneElement. final TestComponentFactory = react.registerComponent(() => new TestComponent()) as ReactComponentFactory; // ignore: avoid_as - class TestComponent extends react.Component { @override render() => Dom.div()(); diff --git a/test/over_react/util/rem_util_test.dart b/test/over_react/util/rem_util_test.dart index f4d02d51b..e558b0d2e 100644 --- a/test/over_react/util/rem_util_test.dart +++ b/test/over_react/util/rem_util_test.dart @@ -167,30 +167,18 @@ main() { ); }); - group('throws when passed a CSS value string with a unit other than px/rem', () { - test('', () { - expect(() => toPx('1em'), allOf( - throwsArgumentError, - throwsA(hasToStringValue(contains('must be a rem num or a String px/rem value')))) - ); - }); - - test('unless `passThroughUnsupportedUnits` is true', () { - expect(toPx('1em', passThroughUnsupportedUnits: true), new CssValue.parse('1em')); - }); + test('throws when passed a CSS value string with a unit other than px/rem', () { + expect(() => toPx('1em'), allOf( + throwsArgumentError, + throwsA(hasToStringValue(contains('must be a rem num or a String px/rem value')))) + ); }); - group('throws when passed a CssValue instance with a unit other than px/rem', () { - test('', () { - expect(() => toPx(new CssValue.parse('1em')), allOf( - throwsArgumentError, - throwsA(hasToStringValue(contains('must be a rem num or a String px/rem value')))) - ); - }); - - test('unless `passThroughUnsupportedUnits` is true', () { - expect(toPx(new CssValue.parse('1em'), passThroughUnsupportedUnits: true), new CssValue.parse('1em')); - }); + test('throws when passed a CssValue instance with a unit other than px/rem', () { + expect(() => toPx(new CssValue.parse('1em')), allOf( + throwsArgumentError, + throwsA(hasToStringValue(contains('must be a rem num or a String px/rem value')))) + ); }); }); @@ -205,7 +193,7 @@ main() { 'correctly dispatches an event in resopnse to the first change', () async { expect(querySelector('#rem_change_sensor'), isNull); - var calls = []; + var calls = []; var listener = onRemChange.listen(calls.add); expect(querySelector('#rem_change_sensor'), isNotNull); @@ -222,7 +210,7 @@ main() { }); test('does not dispatch duplicate events when there are multiple listeners', () async { - List calls = []; + var calls = []; var listener1 = onRemChange.listen((_) {}); var listener2 = onRemChange.listen(calls.add); @@ -239,7 +227,7 @@ main() { }); test('does not dispatch events when recomputeRootFontSize is called and there is no change', () async { - List calls = []; + var calls = []; var listener = onRemChange.listen(calls.add); recomputeRootFontSize(); diff --git a/test/over_react/util/test_mode_test.dart b/test/over_react/util/test_mode_test.dart index 55e752b6b..d0ac17fb5 100644 --- a/test/over_react/util/test_mode_test.dart +++ b/test/over_react/util/test_mode_test.dart @@ -18,7 +18,6 @@ import 'package:over_react/src/component_declaration/component_base.dart' as com import 'package:over_react/src/util/test_mode.dart'; import 'package:test/test.dart'; - /// Main entry point for enableTestMode and disabledTestMode testing main() { test('enableTestMode and disableTestMode set UiProps.testMode as expected', () { diff --git a/test/over_react_test.dart b/test/over_react_test.dart index 72e930630..fd9d77240 100644 --- a/test/over_react_test.dart +++ b/test/over_react_test.dart @@ -46,6 +46,7 @@ import 'over_react/util/handler_chain_util_test.dart' as handler_chain_util_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_error_test.dart' as prop_error_test; +import 'over_react/util/prop_key_util_test.dart' as prop_key_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/string_util_test.dart' as string_util_test; @@ -79,6 +80,7 @@ main() { map_util_test.main(); pretty_print_test.main(); prop_error_test.main(); + prop_key_util_test.main(); react_wrappers_test.main(); rem_util_test.main(); string_util_test.main();