From dbe7d707273907cdd7b72faaf103631a1bffc75e Mon Sep 17 00:00:00 2001 From: evanweible-wf Date: Wed, 6 Feb 2019 16:03:01 -0700 Subject: [PATCH 1/5] Add unconvertJsProps() util to react_client.dart --- lib/react_client.dart | 97 +++++----------- .../event_prop_key_to_event_factory.dart | 68 +++++++++++ test/react_client_test.dart | 106 ++++++++++++++++++ test/react_client_test.html | 14 +++ 4 files changed, 218 insertions(+), 67 deletions(-) create mode 100644 lib/src/react_client/event_prop_key_to_event_factory.dart create mode 100644 test/react_client_test.html diff --git a/lib/react_client.dart b/lib/react_client.dart index a721f447..e5fcb04c 100644 --- a/lib/react_client.dart +++ b/lib/react_client.dart @@ -16,6 +16,7 @@ import "package:react/react_client/js_interop_helpers.dart"; import 'package:react/react_client/react_interop.dart'; import "package:react/react_dom.dart"; import "package:react/react_dom_server.dart"; +import "package:react/src/react_client/event_prop_key_to_event_factory.dart"; import "package:react/src/react_client/synthetic_event_wrappers.dart" as events; import 'package:react/src/typedefs.dart'; import 'package:react/src/ddc_emulated_function_name_bug.dart' @@ -510,6 +511,28 @@ _convertBoundValues(Map args) { /// to the original Dart functions (the input of [_convertEventHandlers]). final Expando _originalEventHandlers = new Expando(); +/// Returns the props for a [ReactElement] or composite [ReactComponent] [instance], +/// shallow-converted to a Dart Map for convenience. +/// +/// If `style` is specified in props, then it too is shallow-converted and included +/// in the returned Map. +Map unconvertJsProps(/* ReactElement|ReactComponent */ instance) { + var props = _dartifyJsMap(instance.props); + eventPropKeyToEventFactory.keys.forEach((key) { + if (props.containsKey(key)) { + props[key] = unconvertJsEventHandler(props[key]); + } + }); + + // Convert the nested style map so it can be read by Dart code. + var style = props['style']; + if (style != null) { + props['style'] = _dartifyJsMap(style); + } + + return props; +} + /// Returns the original Dart handler function that, within [_convertEventHandlers], /// was converted/wrapped into the function [jsConvertedEventHandler] to be passed to the JS. /// @@ -529,7 +552,7 @@ Function unconvertJsEventHandler(Function jsConvertedEventHandler) { _convertEventHandlers(Map args) { var zone = Zone.current; args.forEach((propKey, value) { - var eventFactory = _eventPropKeyToEventFactory[propKey]; + var eventFactory = eventPropKeyToEventFactory[propKey]; if (eventFactory != null && value != null) { // Apply allowInterop here so that the function we store in [_originalEventHandlers] // is the same one we'll retrieve from the JS props. @@ -544,72 +567,12 @@ _convertEventHandlers(Map args) { }); } -/// A mapping from event prop keys to their respective event factories. -/// -/// Used in [_convertEventHandlers] for efficient event handler conversion. -final Map _eventPropKeyToEventFactory = (() { - var eventPropKeyToEventFactory = { - // SyntheticClipboardEvent - 'onCopy': syntheticClipboardEventFactory, - 'onCut': syntheticClipboardEventFactory, - 'onPaste': syntheticClipboardEventFactory, - - // SyntheticKeyboardEvent - 'onKeyDown': syntheticKeyboardEventFactory, - 'onKeyPress': syntheticKeyboardEventFactory, - 'onKeyUp': syntheticKeyboardEventFactory, - - // SyntheticFocusEvent - 'onFocus': syntheticFocusEventFactory, - 'onBlur': syntheticFocusEventFactory, - - // SyntheticFormEvent - 'onChange': syntheticFormEventFactory, - 'onInput': syntheticFormEventFactory, - 'onSubmit': syntheticFormEventFactory, - 'onReset': syntheticFormEventFactory, - - // SyntheticMouseEvent - 'onClick': syntheticMouseEventFactory, - 'onContextMenu': syntheticMouseEventFactory, - 'onDoubleClick': syntheticMouseEventFactory, - 'onDrag': syntheticMouseEventFactory, - 'onDragEnd': syntheticMouseEventFactory, - 'onDragEnter': syntheticMouseEventFactory, - 'onDragExit': syntheticMouseEventFactory, - 'onDragLeave': syntheticMouseEventFactory, - 'onDragOver': syntheticMouseEventFactory, - 'onDragStart': syntheticMouseEventFactory, - 'onDrop': syntheticMouseEventFactory, - 'onMouseDown': syntheticMouseEventFactory, - 'onMouseEnter': syntheticMouseEventFactory, - 'onMouseLeave': syntheticMouseEventFactory, - 'onMouseMove': syntheticMouseEventFactory, - 'onMouseOut': syntheticMouseEventFactory, - 'onMouseOver': syntheticMouseEventFactory, - 'onMouseUp': syntheticMouseEventFactory, - - // SyntheticTouchEvent - 'onTouchCancel': syntheticTouchEventFactory, - 'onTouchEnd': syntheticTouchEventFactory, - 'onTouchMove': syntheticTouchEventFactory, - 'onTouchStart': syntheticTouchEventFactory, - - // SyntheticUIEvent - 'onScroll': syntheticUIEventFactory, - - // SyntheticWheelEvent - 'onWheel': syntheticWheelEventFactory, - }; - - // Add support for capturing variants; e.g., onClick/onClickCapture - for (var key in eventPropKeyToEventFactory.keys.toList()) { - eventPropKeyToEventFactory[key + 'Capture'] = - eventPropKeyToEventFactory[key]; - } - - return eventPropKeyToEventFactory; -})(); +/// Returns a Dart Map copy of the JS property key-value pairs in [jsMap]. +Map _dartifyJsMap(jsMap) { + return new Map.fromIterable(_objectKeys(jsMap), + value: (key) => getProperty(jsMap, key) + ); +} /// Wrapper for [SyntheticEvent]. SyntheticEvent syntheticEventFactory(events.SyntheticEvent e) { diff --git a/lib/src/react_client/event_prop_key_to_event_factory.dart b/lib/src/react_client/event_prop_key_to_event_factory.dart new file mode 100644 index 00000000..bd05f4e9 --- /dev/null +++ b/lib/src/react_client/event_prop_key_to_event_factory.dart @@ -0,0 +1,68 @@ +import 'package:react/react_client.dart'; + +/// A mapping from event prop keys to their respective event factories. +/// +/// Used in [_convertEventHandlers] for efficient event handler conversion. +final Map eventPropKeyToEventFactory = (() { + var _eventPropKeyToEventFactory = { + // SyntheticClipboardEvent + 'onCopy': syntheticClipboardEventFactory, + 'onCut': syntheticClipboardEventFactory, + 'onPaste': syntheticClipboardEventFactory, + + // SyntheticKeyboardEvent + 'onKeyDown': syntheticKeyboardEventFactory, + 'onKeyPress': syntheticKeyboardEventFactory, + 'onKeyUp': syntheticKeyboardEventFactory, + + // SyntheticFocusEvent + 'onFocus': syntheticFocusEventFactory, + 'onBlur': syntheticFocusEventFactory, + + // SyntheticFormEvent + 'onChange': syntheticFormEventFactory, + 'onInput': syntheticFormEventFactory, + 'onSubmit': syntheticFormEventFactory, + 'onReset': syntheticFormEventFactory, + + // SyntheticMouseEvent + 'onClick': syntheticMouseEventFactory, + 'onContextMenu': syntheticMouseEventFactory, + 'onDoubleClick': syntheticMouseEventFactory, + 'onDrag': syntheticMouseEventFactory, + 'onDragEnd': syntheticMouseEventFactory, + 'onDragEnter': syntheticMouseEventFactory, + 'onDragExit': syntheticMouseEventFactory, + 'onDragLeave': syntheticMouseEventFactory, + 'onDragOver': syntheticMouseEventFactory, + 'onDragStart': syntheticMouseEventFactory, + 'onDrop': syntheticMouseEventFactory, + 'onMouseDown': syntheticMouseEventFactory, + 'onMouseEnter': syntheticMouseEventFactory, + 'onMouseLeave': syntheticMouseEventFactory, + 'onMouseMove': syntheticMouseEventFactory, + 'onMouseOut': syntheticMouseEventFactory, + 'onMouseOver': syntheticMouseEventFactory, + 'onMouseUp': syntheticMouseEventFactory, + + // SyntheticTouchEvent + 'onTouchCancel': syntheticTouchEventFactory, + 'onTouchEnd': syntheticTouchEventFactory, + 'onTouchMove': syntheticTouchEventFactory, + 'onTouchStart': syntheticTouchEventFactory, + + // SyntheticUIEvent + 'onScroll': syntheticUIEventFactory, + + // SyntheticWheelEvent + 'onWheel': syntheticWheelEventFactory, + }; + + // Add support for capturing variants; e.g., onClick/onClickCapture + for (var key in _eventPropKeyToEventFactory.keys.toList()) { + _eventPropKeyToEventFactory[key + 'Capture'] = + _eventPropKeyToEventFactory[key]; + } + + return _eventPropKeyToEventFactory; +})(); \ No newline at end of file diff --git a/test/react_client_test.dart b/test/react_client_test.dart index 5ef52994..94144d36 100644 --- a/test/react_client_test.dart +++ b/test/react_client_test.dart @@ -1,11 +1,103 @@ @TestOn('browser') library react_test_utils_test; +import 'dart:html' show DivElement; + +import 'package:js/js.dart'; import 'package:test/test.dart'; +import 'package:react/react.dart' as react show div; import 'package:react/react_client.dart'; +import 'package:react/react_client/react_interop.dart' show React, ReactClassConfig, ReactComponent; +import 'package:react/react_client/js_interop_helpers.dart'; +import 'package:react/react_dom.dart' as react_dom; +import 'package:react/src/react_client/event_prop_key_to_event_factory.dart'; main() { + setClientConfiguration(); + + group('unconvertJsProps', () { + const List testChildren = const ['child1', 'child2']; + const Map testStyle = const {'background': 'white'}; + + test('returns props for a composite JS component ReactElement', () { + ReactElement instance = testJsComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren); + + expect(unconvertJsProps(instance), equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('returns props for a composite JS ReactComponent', () { + var mountNode = new DivElement(); + ReactComponent renderedInstance = react_dom.render(testJsComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren), mountNode); + + expect(unconvertJsProps(renderedInstance), equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren + })); + }); + + 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(unconvertJsProps(renderedInstance), equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren + })); + + renderedInstance = react_dom.render(testJsComponentFactory({ + 'jsProp': 'other js', + 'style': testStyle, + }, testChildren), mountNode); + + expect(unconvertJsProps(renderedInstance), equals({ + 'jsProp': 'other js', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('returns props for a DOM component ReactElement', () { + ReactElement instance = react.div({ + 'domProp': 'dom', + 'style': testStyle, + }, testChildren); + + expect(unconvertJsProps(instance), equals({ + 'domProp': 'dom', + 'style': testStyle, + 'children': testChildren + })); + }); + + test('should unconvert JS event handlers', () { + var genericEventHandler = (_) {}; + var props = new Map.fromIterable(eventPropKeyToEventFactory.keys, value: (key) { + return genericEventHandler; + }); + var component = react.div(props); + var jsProps = unconvertJsProps(component); + for (final key in eventPropKeyToEventFactory.keys) { + expect(identical(jsProps[key], genericEventHandler), isTrue); + } + }); + }); + group('unconvertJsEventHandler', () { test('returns null when the input is null', () { var result; @@ -17,3 +109,17 @@ main() { }); }); } + +/// A factory for a JS composite component, for use in testing. +final Function testJsComponentFactory = (() { + var componentClass = React.createClass(new ReactClassConfig( + displayName: 'testJsComponent', + render: allowInterop(() => react.div({}, 'test js component')) + )); + + var reactFactory = React.createFactory(componentClass); + + return ([props = const {}, children]) { + return reactFactory(jsify(props), listifyChildren(children)); + }; +})(); \ No newline at end of file diff --git a/test/react_client_test.html b/test/react_client_test.html new file mode 100644 index 00000000..55d37bf6 --- /dev/null +++ b/test/react_client_test.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + From 6fd505dfe06b8f6b98f38ad5249c05ca771beb44 Mon Sep 17 00:00:00 2001 From: evanweible-wf Date: Wed, 6 Feb 2019 21:29:38 -0700 Subject: [PATCH 2/5] Add dartfmt check to CI. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 3f83b83e..6ea6888f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ jobs: dart: stable script: - dartanalyzer . + - dartfmt -n --set-exit-if-changed . - pub run dependency_validator -i build_runner,build_test,build_web_compilers - pub run test -p chrome - pub run build_runner test -- -p chrome From c28328561657c2e380f7fa1210fd3e50a2116740 Mon Sep 17 00:00:00 2001 From: evanweible-wf Date: Wed, 6 Feb 2019 21:29:42 -0700 Subject: [PATCH 3/5] dartfmt --- lib/react_client.dart | 10 +- .../event_prop_key_to_event_factory.dart | 2 +- test/react_client_test.dart | 118 +++++++++++------- 3 files changed, 78 insertions(+), 52 deletions(-) diff --git a/lib/react_client.dart b/lib/react_client.dart index 589e7cf1..0c4a84a0 100644 --- a/lib/react_client.dart +++ b/lib/react_client.dart @@ -516,6 +516,10 @@ final Expando _originalEventHandlers = new Expando(); /// /// If `style` is specified in props, then it too is shallow-converted and included /// in the returned Map. +/// +/// Any JS event handlers included in the props for the given [instance] will be +/// unconverted such that the original JS handlers are returned instead of their +/// Dart synthetic counterparts. Map unconvertJsProps(/* ReactElement|ReactComponent */ instance) { var props = _dartifyJsMap(instance.props); eventPropKeyToEventFactory.keys.forEach((key) { @@ -570,8 +574,7 @@ _convertEventHandlers(Map args) { /// Returns a Dart Map copy of the JS property key-value pairs in [jsMap]. Map _dartifyJsMap(jsMap) { return new Map.fromIterable(_objectKeys(jsMap), - value: (key) => getProperty(jsMap, key) - ); + value: (key) => getProperty(jsMap, key)); } /// Wrapper for [SyntheticEvent]. @@ -675,7 +678,8 @@ SyntheticFormEvent syntheticFormEventFactory(events.SyntheticFormEvent e) { } /// Wrapper for [SyntheticDataTransfer]. -SyntheticDataTransfer syntheticDataTransferFactory(events.SyntheticDataTransfer dt) { +SyntheticDataTransfer syntheticDataTransferFactory( + events.SyntheticDataTransfer dt) { if (dt == null) return null; List files = []; if (dt.files != null) { diff --git a/lib/src/react_client/event_prop_key_to_event_factory.dart b/lib/src/react_client/event_prop_key_to_event_factory.dart index bd05f4e9..769d2ce0 100644 --- a/lib/src/react_client/event_prop_key_to_event_factory.dart +++ b/lib/src/react_client/event_prop_key_to_event_factory.dart @@ -65,4 +65,4 @@ final Map eventPropKeyToEventFactory = (() { } return _eventPropKeyToEventFactory; -})(); \ No newline at end of file +})(); diff --git a/test/react_client_test.dart b/test/react_client_test.dart index 94144d36..54340e8d 100644 --- a/test/react_client_test.dart +++ b/test/react_client_test.dart @@ -8,7 +8,8 @@ import 'package:test/test.dart'; import 'package:react/react.dart' as react show div; import 'package:react/react_client.dart'; -import 'package:react/react_client/react_interop.dart' show React, ReactClassConfig, ReactComponent; +import 'package:react/react_client/react_interop.dart' + show React, ReactClassConfig, ReactComponent; import 'package:react/react_client/js_interop_helpers.dart'; import 'package:react/react_dom.dart' as react_dom; import 'package:react/src/react_client/event_prop_key_to_event_factory.dart'; @@ -26,50 +27,69 @@ main() { 'style': testStyle, }, testChildren); - expect(unconvertJsProps(instance), equals({ - 'jsProp': 'js', - 'style': testStyle, - 'children': testChildren - })); + expect( + unconvertJsProps(instance), + equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren, + }), + ); }); test('returns props for a composite JS ReactComponent', () { var mountNode = new DivElement(); - ReactComponent renderedInstance = react_dom.render(testJsComponentFactory({ - 'jsProp': 'js', - 'style': testStyle, - }, testChildren), mountNode); - - expect(unconvertJsProps(renderedInstance), equals({ - 'jsProp': 'js', - 'style': testStyle, - 'children': testChildren - })); + ReactComponent renderedInstance = react_dom.render( + testJsComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren), + mountNode); + + expect( + unconvertJsProps(renderedInstance), + equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren, + }), + ); }); - test('returns props for a composite JS ReactComponent, even when the props change', () { + 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(unconvertJsProps(renderedInstance), equals({ - 'jsProp': 'js', - 'style': testStyle, - 'children': testChildren - })); - - renderedInstance = react_dom.render(testJsComponentFactory({ - 'jsProp': 'other js', - 'style': testStyle, - }, testChildren), mountNode); - - expect(unconvertJsProps(renderedInstance), equals({ - 'jsProp': 'other js', - 'style': testStyle, - 'children': testChildren - })); + ReactComponent renderedInstance = react_dom.render( + testJsComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren), + mountNode); + + expect( + unconvertJsProps(renderedInstance), + equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren, + }), + ); + + renderedInstance = react_dom.render( + testJsComponentFactory({ + 'jsProp': 'other js', + 'style': testStyle, + }, testChildren), + mountNode); + + expect( + unconvertJsProps(renderedInstance), + equals({ + 'jsProp': 'other js', + 'style': testStyle, + 'children': testChildren, + })); }); test('returns props for a DOM component ReactElement', () { @@ -78,16 +98,19 @@ main() { 'style': testStyle, }, testChildren); - expect(unconvertJsProps(instance), equals({ - 'domProp': 'dom', - 'style': testStyle, - 'children': testChildren - })); + expect( + unconvertJsProps(instance), + equals({ + 'domProp': 'dom', + 'style': testStyle, + 'children': testChildren, + })); }); test('should unconvert JS event handlers', () { var genericEventHandler = (_) {}; - var props = new Map.fromIterable(eventPropKeyToEventFactory.keys, value: (key) { + var props = + new Map.fromIterable(eventPropKeyToEventFactory.keys, value: (key) { return genericEventHandler; }); var component = react.div(props); @@ -113,13 +136,12 @@ main() { /// A factory for a JS composite component, for use in testing. final Function testJsComponentFactory = (() { var componentClass = React.createClass(new ReactClassConfig( - displayName: 'testJsComponent', - render: allowInterop(() => react.div({}, 'test js component')) - )); + displayName: 'testJsComponent', + render: allowInterop(() => react.div({}, 'test js component')))); var reactFactory = React.createFactory(componentClass); return ([props = const {}, children]) { return reactFactory(jsify(props), listifyChildren(children)); }; -})(); \ No newline at end of file +})(); From f8a74d78c0205d019b0ec4643f411a8fe99a9399 Mon Sep 17 00:00:00 2001 From: evanweible-wf Date: Thu, 7 Feb 2019 10:53:22 -0700 Subject: [PATCH 4/5] Handle JS composite components that may have unconverted event handlers. --- lib/react_client.dart | 2 +- test/react_client_test.dart | 49 +++++++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/lib/react_client.dart b/lib/react_client.dart index 0c4a84a0..7fbe0ebd 100644 --- a/lib/react_client.dart +++ b/lib/react_client.dart @@ -524,7 +524,7 @@ Map unconvertJsProps(/* ReactElement|ReactComponent */ instance) { var props = _dartifyJsMap(instance.props); eventPropKeyToEventFactory.keys.forEach((key) { if (props.containsKey(key)) { - props[key] = unconvertJsEventHandler(props[key]); + props[key] = unconvertJsEventHandler(props[key]) ?? props[key]; } }); diff --git a/test/react_client_test.dart b/test/react_client_test.dart index 54340e8d..abbaa236 100644 --- a/test/react_client_test.dart +++ b/test/react_client_test.dart @@ -107,17 +107,46 @@ main() { })); }); - test('should unconvert JS event handlers', () { - var genericEventHandler = (_) {}; - var props = - new Map.fromIterable(eventPropKeyToEventFactory.keys, value: (key) { - return genericEventHandler; + group('should unconvert JS event handlers', () { + Function createHandler(eventKey) => (_) { + print(eventKey); + }; + Map originalHandlers; + Map props; + + setUp(() { + originalHandlers = {}; + props = {}; + + for (final key in eventPropKeyToEventFactory.keys) { + props[key] = originalHandlers[key] = createHandler(key); + } + }); + + test('for a DOM element', () { + var component = react.div(props); + var jsProps = unconvertJsProps(component); + for (final key in eventPropKeyToEventFactory.keys) { + expect(jsProps[key], isNotNull, + reason: 'JS event handler prop should not be null'); + expect(identical(jsProps[key], originalHandlers[key]), isTrue, + reason: 'JS event handler prop was not unconverted'); + } + }); + + test( + ', except for a JS composite component (handlers should already be unconverted)', + () { + var component = testJsComponentFactory(props); + var jsProps = unconvertJsProps(component); + for (final key in eventPropKeyToEventFactory.keys) { + expect(jsProps[key], isNotNull, + reason: 'JS event handler prop should not be null'); + expect(identical(jsProps[key], allowInterop(originalHandlers[key])), + isTrue, + reason: 'JS event handler prop was unexpectedly modified'); + } }); - var component = react.div(props); - var jsProps = unconvertJsProps(component); - for (final key in eventPropKeyToEventFactory.keys) { - expect(identical(jsProps[key], genericEventHandler), isTrue); - } }); }); From 494f1985a02308f39dbb0a44e7e02575992f241f Mon Sep 17 00:00:00 2001 From: evanweible-wf Date: Thu, 7 Feb 2019 16:02:07 -0700 Subject: [PATCH 5/5] Use `same()` matcher. --- test/react_client_test.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/react_client_test.dart b/test/react_client_test.dart index abbaa236..5685536a 100644 --- a/test/react_client_test.dart +++ b/test/react_client_test.dart @@ -129,7 +129,7 @@ main() { for (final key in eventPropKeyToEventFactory.keys) { expect(jsProps[key], isNotNull, reason: 'JS event handler prop should not be null'); - expect(identical(jsProps[key], originalHandlers[key]), isTrue, + expect(jsProps[key], same(originalHandlers[key]), reason: 'JS event handler prop was not unconverted'); } }); @@ -142,8 +142,7 @@ main() { for (final key in eventPropKeyToEventFactory.keys) { expect(jsProps[key], isNotNull, reason: 'JS event handler prop should not be null'); - expect(identical(jsProps[key], allowInterop(originalHandlers[key])), - isTrue, + expect(jsProps[key], same(allowInterop(originalHandlers[key])), reason: 'JS event handler prop was unexpectedly modified'); } });