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 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 new file mode 100644 index 00000000..dbed2dd3 --- /dev/null +++ b/lib/src/react/act.dart @@ -0,0 +1,58 @@ +// 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; + +import 'dart:async'; +import 'dart:html'; + +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 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 +/// 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 new file mode 100644 index 00000000..51c9dbc4 --- /dev/null +++ b/test/unit/react/act_test.dart @@ -0,0 +1,87 @@ +// 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'; + +import '../util/exception.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); + + 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); + }); + + 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())); + }); + }); +} + +var useEffectCalls = 0; + +ReactDartFunctionComponentFactoryProxy ActTest = react.registerFunctionComponent((props) { + final text = useState('123'); + useEffect(() => useEffectCalls++); + return react.button({ + 'onClick': (e) { + text.set('abc'); + }, + }, [ + 'Click me!' + ]); +});