From 9a7727b7fe3372eecf494e341bf80e46f3fae820 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Wed, 4 Jun 2025 14:46:50 -0700 Subject: [PATCH 1/7] Add act interop from ticket --- lib/src/react/act.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 lib/src/react/act.dart diff --git a/lib/src/react/act.dart b/lib/src/react/act.dart new file mode 100644 index 00000000..12bc74e2 --- /dev/null +++ b/lib/src/react/act.dart @@ -0,0 +1,15 @@ +@JS() +library; + +import 'dart:async'; +import 'dart:html'; + +import 'package:js/js.dart'; + +@JS('rtl.act') +external dynamic _act(void Function([dynamic, dynamic, dynamic]) callback); + +Future act(FutureOr Function() callback) async { + final promise = _act(allowInterop(([_, __, ___]) => callback())) as Object; + await promiseToFuture(promise); +} \ No newline at end of file From 6c68538919257a1c9783bd51dd7494235c853cfc Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Wed, 4 Jun 2025 15:10:47 -0700 Subject: [PATCH 2/7] Add a test --- lib/react/react.dart | 1 + lib/src/react/act.dart | 14 ++++++++ test/unit/react/act_test.dart | 62 +++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 test/unit/react/act_test.dart diff --git a/lib/react/react.dart b/lib/react/react.dart index f0f920b6..9a91bedc 100644 --- a/lib/react/react.dart +++ b/lib/react/react.dart @@ -16,3 +16,4 @@ library rtl.react; export '../src/react/render/render.dart' show render, RenderResult; +export '../src/react/act.dart' show act; diff --git a/lib/src/react/act.dart b/lib/src/react/act.dart index 12bc74e2..14ebefeb 100644 --- a/lib/src/react/act.dart +++ b/lib/src/react/act.dart @@ -1,3 +1,17 @@ +// Copyright 2025 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. + @JS() library; diff --git a/test/unit/react/act_test.dart b/test/unit/react/act_test.dart new file mode 100644 index 00000000..69f37c9a --- /dev/null +++ b/test/unit/react/act_test.dart @@ -0,0 +1,62 @@ +// Copyright 2025 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. + +import 'dart:html'; + +import 'package:react/hooks.dart'; +import 'package:react/react.dart' as react; +import 'package:react/react_client.dart'; +import 'package:react_testing_library/react_testing_library.dart' as rtl; +import 'package:test/test.dart'; + +void main() { + group('act', () { + setUp(() => useEffectCalls = 0); + + test('useEffect call is missed when act is not used', () { + expect(useEffectCalls, 0); + final view = rtl.render(ActTest({})); + expect(useEffectCalls, 1); + + final button = view.getByRole('button') as ButtonElement; + button.click(); + expect(useEffectCalls, 1); + }); + + test('useEffect call is caught when act is used', () async { + expect(useEffectCalls, 0); + final view = rtl.render(ActTest({})); + expect(useEffectCalls, 1); + + final button = view.getByRole('button') as ButtonElement; + await rtl.act(() => button.click()); + expect(useEffectCalls, 2); + }); + }); +} + +var useEffectCalls = 0; + +ReactDartFunctionComponentFactoryProxy ActTest = react.registerFunctionComponent((props) { + final text = useState('123'); + useEffect(() => useEffectCalls++); + return + react.button({ + 'onClick': (e) { + text.set('abc'); + }, + }, [ + 'Click me!' + ]); +}); From 3822e04cf4248ac57cf2699ece4b58922e8dd1f0 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Wed, 4 Jun 2025 15:26:00 -0700 Subject: [PATCH 3/7] Check async return type --- lib/src/react/act.dart | 24 ++++++++++++++++++++++-- test/unit/react/act_test.dart | 20 ++++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/lib/src/react/act.dart b/lib/src/react/act.dart index 14ebefeb..af77a9c0 100644 --- a/lib/src/react/act.dart +++ b/lib/src/react/act.dart @@ -24,6 +24,26 @@ import 'package:js/js.dart'; external dynamic _act(void Function([dynamic, dynamic, dynamic]) callback); Future act(FutureOr Function() callback) async { - final promise = _act(allowInterop(([_, __, ___]) => callback())) as Object; + final callbackReturnValue = callback(); + final promise = _act(allowInterop(([_, __, ___]) => callbackReturnValue is Future ? futureToPromise(callbackReturnValue) : callbackReturnValue)) as Object; await promiseToFuture(promise); -} \ No newline at end of file +} + +// copied from react-dart https://github.com/Workiva/react-dart/blob/489d86fa72ab9a4ff60972180cf9a46c6ca2cffd/lib/src/js_interop_util.dart#L24-L40 +/// Creates JS `Promise` which is resolved when [future] completes. +/// +/// See also: +/// - [promiseToFuture] +Promise futureToPromise(Future future) { + return Promise(allowInterop((resolve, reject) { + future.then((result) => resolve(result), onError: reject); + })); +} + +@JS() +abstract class Promise { + external factory Promise( + Function(dynamic Function(dynamic value) resolve, dynamic Function(dynamic error) reject) executor); + + external Promise then(dynamic Function(dynamic value) onFulfilled, [dynamic Function(dynamic error) onRejected]); +} diff --git a/test/unit/react/act_test.dart b/test/unit/react/act_test.dart index 69f37c9a..6e25279c 100644 --- a/test/unit/react/act_test.dart +++ b/test/unit/react/act_test.dart @@ -39,8 +39,24 @@ void main() { final view = rtl.render(ActTest({})); expect(useEffectCalls, 1); - final button = view.getByRole('button') as ButtonElement; - await rtl.act(() => button.click()); + await rtl.act(() { + final button = view.getByRole('button') as ButtonElement; + button.click(); + }); + expect(useEffectCalls, 2); + }); + + test('also works with an async callback', () async { + expect(useEffectCalls, 0); + final view = rtl.render(ActTest({})); + expect(useEffectCalls, 1); + + Future asyncCallback() async { + final button = await view.findByRole('button') as ButtonElement; + button.click(); + } + + await rtl.act(asyncCallback); expect(useEffectCalls, 2); }); }); From cba8c7697a6431a9678d081e830d7949c3fd12e1 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Wed, 4 Jun 2025 15:32:28 -0700 Subject: [PATCH 4/7] Fix to work for async return types --- lib/src/react/act.dart | 13 +++++++++++-- test/unit/react/act_test.dart | 15 +++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/src/react/act.dart b/lib/src/react/act.dart index af77a9c0..dbed2dd3 100644 --- a/lib/src/react/act.dart +++ b/lib/src/react/act.dart @@ -23,13 +23,22 @@ import 'package:js/js.dart'; @JS('rtl.act') external dynamic _act(void Function([dynamic, dynamic, dynamic]) callback); +/// A test helper to apply pending React updates before making assertions. +/// +/// This is RTL's version of React.act for convenience because it handles setting `IS_REACT_ACT_ENVIRONMENT` +/// to avoid https://react.dev/reference/react/act#error-the-current-testing-environment-is-not-configured-to-support-act. +/// +/// See RTL docs: https://testing-library.com/docs/react-testing-library/api/#act +/// See React docs: https://react.dev/reference/react/act Future act(FutureOr Function() callback) async { final callbackReturnValue = callback(); - final promise = _act(allowInterop(([_, __, ___]) => callbackReturnValue is Future ? futureToPromise(callbackReturnValue) : callbackReturnValue)) as Object; + final jsCallback = + ([_, __, ___]) => callbackReturnValue is Future ? futureToPromise(callbackReturnValue) : callbackReturnValue; + final promise = _act(allowInterop(jsCallback)) as Object; await promiseToFuture(promise); } -// copied from react-dart https://github.com/Workiva/react-dart/blob/489d86fa72ab9a4ff60972180cf9a46c6ca2cffd/lib/src/js_interop_util.dart#L24-L40 +// Copied from react-dart https://github.com/Workiva/react-dart/blob/489d86fa72ab9a4ff60972180cf9a46c6ca2cffd/lib/src/js_interop_util.dart#L24-L40 /// Creates JS `Promise` which is resolved when [future] completes. /// /// See also: diff --git a/test/unit/react/act_test.dart b/test/unit/react/act_test.dart index 6e25279c..d5ad679b 100644 --- a/test/unit/react/act_test.dart +++ b/test/unit/react/act_test.dart @@ -67,12 +67,11 @@ var useEffectCalls = 0; ReactDartFunctionComponentFactoryProxy ActTest = react.registerFunctionComponent((props) { final text = useState('123'); useEffect(() => useEffectCalls++); - return - react.button({ - 'onClick': (e) { - text.set('abc'); - }, - }, [ - 'Click me!' - ]); + return react.button({ + 'onClick': (e) { + text.set('abc'); + }, + }, [ + 'Click me!' + ]); }); From fa4412c320fd471feec7d0ce11a205955d38f1cf Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Wed, 4 Jun 2025 16:46:46 -0700 Subject: [PATCH 5/7] Add error throwing tests --- test/unit/react/act_test.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/unit/react/act_test.dart b/test/unit/react/act_test.dart index d5ad679b..26e619f0 100644 --- a/test/unit/react/act_test.dart +++ b/test/unit/react/act_test.dart @@ -20,6 +20,8 @@ import 'package:react/react_client.dart'; import 'package:react_testing_library/react_testing_library.dart' as rtl; import 'package:test/test.dart'; +import '../util/exception.dart'; + void main() { group('act', () { setUp(() => useEffectCalls = 0); @@ -59,6 +61,16 @@ void main() { await rtl.act(asyncCallback); expect(useEffectCalls, 2); }); + + test('errors thrown in sync callback propagate', () { + expect(() async => await rtl.act(() => throw ExceptionForTesting()), + throwsA(isA())); + }); + + test('errors thrown in async callback propagate', () { + expect(() async => await rtl.act(() async => throw ExceptionForTesting()), + throwsA(isA())); + }); }); } From b8d2b64d530f1fe02e2e1035feaa258bf85943c8 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Wed, 4 Jun 2025 16:47:32 -0700 Subject: [PATCH 6/7] Format --- test/unit/react/act_test.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/unit/react/act_test.dart b/test/unit/react/act_test.dart index 26e619f0..51c9dbc4 100644 --- a/test/unit/react/act_test.dart +++ b/test/unit/react/act_test.dart @@ -63,13 +63,11 @@ void main() { }); test('errors thrown in sync callback propagate', () { - expect(() async => await rtl.act(() => throw ExceptionForTesting()), - throwsA(isA())); + expect(() async => await rtl.act(() => throw ExceptionForTesting()), throwsA(isA())); }); test('errors thrown in async callback propagate', () { - expect(() async => await rtl.act(() async => throw ExceptionForTesting()), - throwsA(isA())); + expect(() async => await rtl.act(() async => throw ExceptionForTesting()), throwsA(isA())); }); }); } From 5b1726ee55d80f90ed01874ef85efb100ddb9868 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Thu, 5 Jun 2025 09:10:44 -0700 Subject: [PATCH 7/7] Update changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7d252b0..7b96e551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # React Testing Library Changelog +## 3.1.0 +* [#86](https://github.com/Workiva/react_testing_library/pull/86) Add Dart bindings for React `act` + +## 3.0.3 +* [#84](https://github.com/Workiva/react_testing_library/pull/84) React 18 dual support + +## 3.0.2 +* [#76](https://github.com/Workiva/react_testing_library/pull/76) GHA OSS changes +* [#77](https://github.com/Workiva/react_testing_library/pull/77) Workiva analysis options v2 + ## 3.0.1 * [#75](https://github.com/Workiva/react_testing_library/pull/75) Update changelog for 3.0.0 Release