diff --git a/.travis.yml b/.travis.yml index d8d932e81..6b54daa39 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: dart dart: - - "1.19.1" + - "1.21.1" with_content_shell: true before_install: - export DISPLAY=:99.0 diff --git a/lib/src/util/react_wrappers.dart b/lib/src/util/react_wrappers.dart index 6d3fe162c..bb81b3669 100644 --- a/lib/src/util/react_wrappers.dart +++ b/lib/src/util/react_wrappers.dart @@ -19,6 +19,7 @@ import 'dart:collection'; import 'dart:html'; import 'package:js/js.dart'; +import 'package:over_react/src/component_declaration/component_type_checking.dart'; import 'package:react/react.dart' as react; import 'package:react/react_client.dart'; import 'package:react/react_client/js_interop_helpers.dart'; @@ -107,11 +108,38 @@ Expando _elementPropsCache = new Expando('_elementPropsCach /// For a native Dart component, this returns its [react.Component.props] in an unmodifiable Map view. /// For a JS component, this returns the result of [getJsProps] in an unmodifiable Map view. /// +/// If [traverseWrappers] is `true` then it will return an unmodifiable Map view of props of the first non-"Wrapper" +/// instance. +/// /// Throws if [instance] is not a valid [ReactElement] or composite [ReactComponent] . -Map getProps(/* ReactElement|ReactComponent */ instance) { +Map getProps(/* ReactElement|ReactComponent */ instance, {bool traverseWrappers: false}) { var isCompositeComponent = _isCompositeComponent(instance); if (isValidElement(instance) || isCompositeComponent) { + if (traverseWrappers) { + ComponentTypeMeta instanceTypeMeta; + + if (isCompositeComponent && isDartComponent(instance)) { + ReactClass type = getProperty(getDartComponent(instance).jsThis, 'constructor'); + instanceTypeMeta = getComponentTypeMeta(type); + } else if (isValidElement(instance)) { + instanceTypeMeta = getComponentTypeMeta(instance.type); + } else { + throw new ArgumentError.value(instance, 'instance', + 'must either be a Dart component ReactComponent or ReactElement when traverseWrappers is true.'); + } + + if (instanceTypeMeta.isWrapper) { + assert(isDartComponent(instance) && 'Non-dart components should not be wrappers' is String); + + List children = getProps(instance)['children']; + + if (children != null && children.isNotEmpty && isValidElement(children.first)) { + return getProps(children.first, traverseWrappers: true); + } + } + } + if (!isCompositeComponent) { var cachedView = _elementPropsCache[instance]; if (cachedView != null) return cachedView; diff --git a/pubspec.yaml b/pubspec.yaml index a8f8c4da5..42d59f08c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://github.com/Workiva/over_react/ authors: - Workiva UI Platform Team environment: - sdk: ">=1.19.1" + sdk: ">=1.21.1" dependencies: analyzer: ">=0.26.1+3 <0.30.0" barback: "^0.15.0" diff --git a/smithy.yaml b/smithy.yaml index ac9b6981e..609bfa172 100644 --- a/smithy.yaml +++ b/smithy.yaml @@ -1,8 +1,8 @@ project: dart language: dart -# dart 1.19.1, built from https://github.com/Workiva/smithy-runner-dart/tree/0.0.4 -runner_image: drydock-prod.workiva.org/workiva/smithy-runner-dart:74173 +# dart 1.21.1, built from https://github.com/Workiva/smithy-runner-dart/tree/0.0.4 +runner_image: drydock-prod.workiva.net/workiva/smithy-runner-dart:119662 script: - pub get diff --git a/test/over_react/component_declaration/component_type_checking_test.dart b/test/over_react/component_declaration/component_type_checking_test.dart index cdad09777..393f88a82 100644 --- a/test/over_react/component_declaration/component_type_checking_test.dart +++ b/test/over_react/component_declaration/component_type_checking_test.dart @@ -21,10 +21,10 @@ import 'package:react/react_client.dart'; import 'package:react/react_client/react_interop.dart'; import 'package:test/test.dart'; -import 'component_type_checking_test/one_level_wrapper.dart'; +import '../../test_util/one_level_wrapper.dart'; +import '../../test_util/two_level_wrapper.dart'; import 'component_type_checking_test/test_a.dart'; import 'component_type_checking_test/test_b.dart'; -import 'component_type_checking_test/two_level_wrapper.dart'; import 'component_type_checking_test/type_inheritance/abstract_inheritance/abstract.dart'; import 'component_type_checking_test/type_inheritance/abstract_inheritance/extendedtype.dart'; import 'component_type_checking_test/type_inheritance/parent.dart'; diff --git a/test/over_react/component_declaration/transformer_helpers_test.dart b/test/over_react/component_declaration/transformer_helpers_test.dart index ebab02e08..6de8109c4 100644 --- a/test/over_react/component_declaration/transformer_helpers_test.dart +++ b/test/over_react/component_declaration/transformer_helpers_test.dart @@ -140,7 +140,7 @@ main() { group('\$PropKeys (ungenerated)', () { setUpAll(() { - expect(() => const $PropKeys(Null), isNot(throwsNoSuchMethodError), + expect(() => const $PropKeys(Null), returnsNormally, reason: 'Instanitating a const \$PropKeys should not have thrown an error. ' 'Ensure that the over_react transformer is NOT running for this test file.' ); @@ -164,7 +164,7 @@ main() { group('\$Props (ungenerated)', () { setUpAll(() { - expect(() => const $Props(Null), isNot(throwsNoSuchMethodError), + expect(() => const $Props(Null), returnsNormally, reason: 'Instanitating a const \$Props should not have thrown an error. ' 'Ensure that the over_react transformer is NOT running for this test file.' ); diff --git a/test/over_react/util/react_wrappers_test.dart b/test/over_react/util/react_wrappers_test.dart index 2d076d23e..8dfd9ea6a 100644 --- a/test/over_react/util/react_wrappers_test.dart +++ b/test/over_react/util/react_wrappers_test.dart @@ -27,7 +27,9 @@ import 'package:react/react_dom.dart' as react_dom; import 'package:react/react_test_utils.dart' as react_test_utils; import 'package:test/test.dart'; +import '../../test_util/one_level_wrapper.dart'; import '../../test_util/test_util.dart'; +import '../../test_util/two_level_wrapper.dart'; import '../../wsd_test_util/test_js_component.dart'; /// Main entry point for react wrappers testing @@ -574,6 +576,345 @@ main() { })); }); + group('traverses children of Wrapper components', () { + group('and returns props for a', () { + group('composite JS ReactElement', () { + test('', () { + ReactElement instance = OneLevelWrapper()( + testJsComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren) + ); + + expect(getProps(instance, traverseWrappers: true), equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('even when there are multiple levels of wrappers', () { + ReactElement instance = TwoLevelWrapper()( + OneLevelWrapper()( + testJsComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren) + ) + ); + + expect(getProps(instance, traverseWrappers: true), equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('except when the top level component is not a wrapper', () { + ReactElement instance = testJsComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren); + + expect(getProps(instance, traverseWrappers: true), equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('except when traverseWrappers is false', () { + ReactElement instance = OneLevelWrapper()( + testJsComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren) + ); + + expect(getProps(instance), equals({'children': anything})); + }); + }); + + group('composite JS ReactComponent', () { + test('', () { + ReactComponent renderedInstance = render(OneLevelWrapper()( + testJsComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren) + )); + + expect(getProps(renderedInstance, traverseWrappers: true), equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('even when there are multiple levels of wrappers', () { + ReactComponent renderedInstance = render(TwoLevelWrapper()( + OneLevelWrapper()( + testJsComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren) + ) + )); + + expect(getProps(renderedInstance, traverseWrappers: true), equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('even when props change', () { + var mountNode = new DivElement(); + ReactComponent renderedInstance = react_dom.render(OneLevelWrapper()( + testJsComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren) + ), mountNode); + + expect(getProps(renderedInstance, traverseWrappers: true), equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren + })); + + renderedInstance = react_dom.render(OneLevelWrapper()( + testJsComponentFactory({ + 'jsProp': 'other js', + 'style': testStyle, + }, testChildren) + ), mountNode); + + expect(getProps(renderedInstance, traverseWrappers: true), equals({ + 'jsProp': 'other js', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('except when traverseWrappers is false', () { + ReactComponent renderedInstance = render(OneLevelWrapper()( + testJsComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren) + )); + + expect(getProps(renderedInstance), equals({'children': anything})); + }); + }); + + group('DOM component ReactElement', () { + test('', () { + ReactElement instance = OneLevelWrapper()( + (Dom.div() + ..addProp('domProp', 'dom') + ..style = testStyle + )(testChildren) + ); + + expect(getProps(instance, traverseWrappers: true), equals({ + 'domProp': 'dom', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('even when there are multiple levels of wrappers', () { + ReactElement instance = TwoLevelWrapper()( + OneLevelWrapper()( + (Dom.div() + ..addProp('domProp', 'dom') + ..style = testStyle + )(testChildren) + ) + ); + + expect(getProps(instance, traverseWrappers: true), equals({ + 'domProp': 'dom', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('except when the top level component is not a wrapper', () { + ReactElement instance = (Dom.div() + ..addProp('domProp', 'dom') + ..style = testStyle + )(testChildren); + + expect(getProps(instance, traverseWrappers: true), equals({ + 'domProp': 'dom', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('except when traverseWrappers is false', () { + ReactElement instance = OneLevelWrapper()( + (Dom.div() + ..addProp('domProp', 'dom') + ..style = testStyle + )(testChildren) + ); + + expect(getProps(instance), equals({'children': anything})); + }); + }); + + group('Dart component ReactElement', () { + test('', () { + ReactElement instance = OneLevelWrapper()( + TestComponentFactory({ + 'dartProp': 'dart', + 'style': testStyle, + }, testChildren) + ); + + expect(getProps(instance, traverseWrappers: true), equals({ + 'dartProp': 'dart', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('even when there are multiple levels of wrappers', () { + ReactElement instance = TwoLevelWrapper()( + OneLevelWrapper()( + TestComponentFactory({ + 'dartProp': 'dart', + 'style': testStyle, + }, testChildren) + ) + ); + + expect(getProps(instance, traverseWrappers: true), equals({ + 'dartProp': 'dart', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('except when the top level component is not a wrapper', () { + ReactElement instance = TestComponentFactory({ + 'dartProp': 'dart', + 'style': testStyle, + }, testChildren); + + expect(getProps(instance, traverseWrappers: true), equals({ + 'dartProp': 'dart', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('except when traverseWrappers is false', () { + ReactElement instance = OneLevelWrapper()( + TestComponentFactory({ + 'dartProp': 'dart', + 'style': testStyle, + }, testChildren) + ); + + expect(getProps(instance), equals({'children': anything})); + }); + }); + + group('Dart component ReactComponent', () { + test('', () { + ReactComponent renderedInstance = render(OneLevelWrapper()( + TestComponentFactory({ + 'dartProp': 'dart', + 'style': testStyle, + }, testChildren) + )); + + expect(getProps(renderedInstance, traverseWrappers: true), equals({ + 'dartProp': 'dart', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('even when there are multiple levels of wrappers', () { + ReactComponent renderedInstance = render(TwoLevelWrapper()( + OneLevelWrapper()( + TestComponentFactory({ + 'dartProp': 'dart', + 'style': testStyle, + }, testChildren) + ) + )); + + expect(getProps(renderedInstance, traverseWrappers: true), equals({ + 'dartProp': 'dart', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('even when props change', () { + var mountNode = new DivElement(); + ReactComponent renderedInstance = react_dom.render(OneLevelWrapper()( + TestComponentFactory({ + 'dartProp': 'dart', + 'style': testStyle, + }, testChildren) + ), mountNode); + + expect(getProps(renderedInstance, traverseWrappers: true), equals({ + 'dartProp': 'dart', + 'style': testStyle, + 'children': testChildren + })); + + renderedInstance = react_dom.render(OneLevelWrapper()( + TestComponentFactory({ + 'dartProp': 'other dart', + 'style': testStyle, + }, testChildren) + ), mountNode); + + expect(getProps(renderedInstance, traverseWrappers: true), equals({ + 'dartProp': 'other dart', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('expect when the top level component is not a wrapper', () { + ReactComponent renderedInstance = render(TestComponentFactory({ + 'dartProp': 'dart', + 'style': testStyle, + }, testChildren)); + + expect(getProps(renderedInstance, traverseWrappers: true), equals({ + 'dartProp': 'dart', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('except when traverseWrappers is false', () { + ReactComponent renderedInstance = render(OneLevelWrapper()( + TestComponentFactory({ + 'dartProp': 'dart', + 'style': testStyle, + }, testChildren) + )); + + expect(getProps(renderedInstance), equals({'children': anything})); + }); + }); + }); + }); + test('returns props as an unmodifiable map', () { ReactComponent renderedInstance = render(TestComponentFactory({ 'dartProp': 'dart' @@ -583,6 +924,11 @@ main() { }); group('throws when passed', () { + test('a JS ReactComponent and traverseWrappers is true', () { + var renderedInstance = render(testJsComponentFactory({})); + expect(() => getProps(renderedInstance, traverseWrappers: true), throwsArgumentError); + }); + test('a DOM ReactComponent (Element)', () { var renderedInstance = render(Dom.div()); expect(() => getProps(renderedInstance), throwsArgumentError); diff --git a/test/over_react/component_declaration/component_type_checking_test/one_level_wrapper.dart b/test/test_util/one_level_wrapper.dart similarity index 95% rename from test/over_react/component_declaration/component_type_checking_test/one_level_wrapper.dart rename to test/test_util/one_level_wrapper.dart index cafdf5ecf..1d8b65fc4 100644 --- a/test/over_react/component_declaration/component_type_checking_test/one_level_wrapper.dart +++ b/test/test_util/one_level_wrapper.dart @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -library test_components.one_level_wrapper; +library test_util.one_level_wrapper; import 'package:over_react/over_react.dart'; diff --git a/test/over_react/component_declaration/component_type_checking_test/two_level_wrapper.dart b/test/test_util/two_level_wrapper.dart similarity index 95% rename from test/over_react/component_declaration/component_type_checking_test/two_level_wrapper.dart rename to test/test_util/two_level_wrapper.dart index 183dd21e5..5dce08ced 100644 --- a/test/over_react/component_declaration/component_type_checking_test/two_level_wrapper.dart +++ b/test/test_util/two_level_wrapper.dart @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -library test_components.two_level_wrapper; +library test_util.two_level_wrapper; import 'package:over_react/over_react.dart'; diff --git a/test/test_util_tests/custom_matchers_test.dart b/test/test_util_tests/custom_matchers_test.dart index 38724ba89..de04e1d73 100644 --- a/test/test_util_tests/custom_matchers_test.dart +++ b/test/test_util_tests/custom_matchers_test.dart @@ -241,7 +241,7 @@ main() { }); group('fails when the props are not present in a', () { - final failMessagePattern = new RegExp(r"Which: has props/attributes map with value .* which doesn't contain key 'id'"); + final failMessagePattern = new RegExp(r"Which: has props/attributes map with value .* which doesn't contain key 'id'"); group('ReactElement', () { test('(DOM)', () { @@ -450,7 +450,7 @@ void shouldFail(value, Matcher matcher, expected) { if (expected is String) { expect(_errorString, equalsIgnoringWhitespace(expected)); } else { - expect(_errorString.replaceAll('\n', ' '), expected); + expect(_errorString.replaceAll(new RegExp(r'[\s\n]+'), ' '), expected); } } diff --git a/test/vm_tests/transformer/impl_generation_test.dart b/test/vm_tests/transformer/impl_generation_test.dart index e352f4a26..c86bb5cff 100644 --- a/test/vm_tests/transformer/impl_generation_test.dart +++ b/test/vm_tests/transformer/impl_generation_test.dart @@ -72,7 +72,7 @@ main() { expect(() { parseCompilationUnit(transformedSource); - }, isNot(throws), reason: 'transformed source should parse without errors:\n$transformedSource'); + }, returnsNormally, reason: 'transformed source should parse without errors:\n$transformedSource'); } group('generates an implementation that parses correctly, preserving line numbers', () { @@ -266,7 +266,6 @@ main() { class FooComponent {} '''); - print(transformedFile.getTransformedText()); expect(transformedFile.getTransformedText(), contains('parentType: \$BarComponentFactory')); }); @@ -282,7 +281,6 @@ main() { class FooComponent {} '''); - print(transformedFile.getTransformedText()); expect(transformedFile.getTransformedText(), contains('parentType: baz.\$BarComponentFactory')); }); }); diff --git a/test/wsd_test_util/zone.dart b/test/wsd_test_util/zone.dart index 5f9c2fcff..079982611 100644 --- a/test/wsd_test_util/zone.dart +++ b/test/wsd_test_util/zone.dart @@ -41,5 +41,7 @@ void zonedExpect(actual, matcher, {String reason, bool verbose: false, ErrorFormatter formatter}) { validateZone(); - return _zone.run(() => expect(actual, matcher, verbose: verbose, formatter: formatter)); + return _zone.run(() { + expect(actual, matcher, verbose: verbose, formatter: formatter); + }); }