Skip to content

Commit 44faccc

Browse files
committed
Test react.act and react.actasync
1 parent 2f5ae8d commit 44faccc

File tree

3 files changed

+138
-30
lines changed

3 files changed

+138
-30
lines changed

src/React.re

+4-1
Original file line numberDiff line numberDiff line change
@@ -886,7 +886,10 @@ external useDebugValue: ('value, ~format: 'value => string=?, unit) => unit =
886886
"useDebugValue";
887887

888888
[@mel.module "react"]
889-
external act: (unit => Js.Promise.t(unit)) => unit = "act";
889+
external act: (unit => unit) => Js.Promise.t(unit) = "act";
890+
[@mel.module "react"]
891+
external actAsync: (unit => Js.Promise.t(unit)) => Js.Promise.t(unit) =
892+
"act";
890893

891894
module Experimental = {
892895
/* This module is used to bind to APIs for future versions of React. There is no guarantee of backwards compatibility or stability. */

src/React.rei

+4-1
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,10 @@ external useTransition: unit => (bool, callback(callback(unit, unit), unit)) =
574574
"useTransition";
575575

576576
[@mel.module "react"]
577-
external act: (unit => Js.Promise.t(unit)) => unit = "act";
577+
external act: (unit => unit) => Js.Promise.t(unit) = "act";
578+
[@mel.module "react"]
579+
external actAsync: (unit => Js.Promise.t(unit)) => Js.Promise.t(unit) =
580+
"act";
578581

579582
module Experimental: {
580583
/* This module is used to bind to APIs for future versions of React. There is no guarantee of backwards compatibility or stability. */

test/React__test.re

+130-28
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ module DummyContext = {
4747

4848
[@mel.get] external tagName: Dom.element => string = "tagName";
4949
[@mel.get] external innerHTML: Dom.element => string = "innerHTML";
50+
[@mel.set] external setInnerHTML: (Dom.element, string) => unit = "innerHTML";
5051

5152
let getByRole = (role, container) => {
5253
ReactTestingLibrary.getByRole(~matcher=`Str(role), container);
@@ -62,6 +63,24 @@ let getByTag = (tag, container) => {
6263
[@mel.send]
6364
external getAttribute: (Dom.element, string) => option(string) =
6465
"getAttribute";
66+
[@mel.set] external setTitle: (Dom.element, string) => unit = "title";
67+
[@mel.get] external getTitle: Dom.element => string = "title";
68+
69+
let (let.await) = (p, f) => Js.Promise.then_(f, p);
70+
71+
external createElement: string => Dom.element = "document.createElement";
72+
[@mel.send]
73+
external appendChild: (Dom.element, Dom.element) => unit = "appendChild";
74+
external document: Dom.element = "document";
75+
external body: Dom.element = "document.body";
76+
external querySelector: (string, Dom.element) => option(Dom.element) =
77+
"document.querySelector";
78+
79+
[@mel.new]
80+
external mouseEvent: (string, Js.t('a)) => Dom.event = "MouseEvent";
81+
82+
[@mel.send]
83+
external dispatchEvent: (Dom.element, Dom.event) => unit = "dispatchEvent";
6584

6685
describe("React", () => {
6786
test("can render DOM elements", () => {
@@ -233,37 +252,120 @@ describe("React", () => {
233252
expect(image->getAttribute("src"))->toEqual(Some("https://foo.png"));
234253
});
235254

236-
test("React.act", () => {
237-
module Counter = {
238-
[@react.component]
239-
let make = () => {
240-
let (count, setCount) = React.useState(() => 0);
241-
242-
<div>
243-
<button role="Increment" onClick={_ => setCount(prev => prev + 1)}>
244-
{React.string("Increment")}
245-
</button>
246-
<span role="counter"> {React.string(string_of_int(count))} </span>
247-
</div>;
248-
};
255+
module Counter = {
256+
[@react.component]
257+
let make = () => {
258+
let (count, setCount) = React.Uncurried.useState(() => 0);
259+
260+
React.useEffect1(
261+
() => {
262+
document->setTitle(
263+
"You clicked " ++ Int.to_string(count) ++ " times",
264+
);
265+
None;
266+
},
267+
[|count|],
268+
);
269+
270+
<div>
271+
<button
272+
className="Increment" onClick={_ => setCount(. prev => prev + 1)}>
273+
{React.string("Increment")}
274+
</button>
275+
<span className="Value"> {React.string(string_of_int(count))} </span>
276+
</div>;
249277
};
278+
};
250279

251-
let containerRef: ref(option(ReactTestingLibrary.renderResult)) =
252-
ref(None);
253-
254-
React.act(() => {
255-
let container = ReactTestingLibrary.render(<Counter />);
256-
let button = getByRole("Increment", container);
257-
FireEvent.click(button);
258-
containerRef.contents = Some(container);
259-
Js.Promise.resolve();
260-
});
261-
262-
switch (containerRef.contents) {
263-
| Some(container) =>
264-
expect(getByRole("counter", container)->innerHTML)->toBe("1")
265-
| None => failwith("Container is null")
280+
testPromise("act", finish => {
281+
/* This test doesn't use ReactTestingLibrary to test the act API, and the code comes from
282+
https://react.dev/reference/react/act example */
283+
284+
let container: Dom.element = createElement("div");
285+
body->appendChild(container);
286+
287+
let.await () =
288+
React.act(() => {
289+
let root = ReactDOM.Client.createRoot(container);
290+
ReactDOM.Client.render(root, <Counter />);
291+
});
292+
293+
let valueElement = querySelector(".Value", container);
294+
switch (valueElement) {
295+
| Some(value) => expect(value->innerHTML)->toBe("0")
296+
| None => failwith("Can't find 'Value' element")
297+
};
298+
299+
let title = getTitle(document);
300+
expect(title)->toBe("You clicked 0 times");
301+
302+
let.await () =
303+
React.act(() => {
304+
let buttonElement = querySelector(".Increment", container);
305+
switch (buttonElement) {
306+
| Some(button) =>
307+
dispatchEvent(button, mouseEvent("click", {"bubbles": true}))
308+
| None => failwith("Can't find 'Increment' button")
309+
};
310+
});
311+
312+
let valueElement = querySelector(".Value", container);
313+
switch (valueElement) {
314+
| Some(value) => expect(value->innerHTML)->toBe("1")
315+
| None => failwith("Can't find 'Value' element")
316+
};
317+
318+
let title = getTitle(document);
319+
expect(title)->toBe("You clicked 1 times");
320+
321+
finish();
322+
});
323+
324+
testPromise("actAsync", finish => {
325+
/* This test doesn't use ReactTestingLibrary to test the act API, and the code comes from
326+
https://react.dev/reference/react/act example */
327+
328+
body->setInnerHTML("");
329+
let container: Dom.element = createElement("div");
330+
body->appendChild(container);
331+
332+
let.await () =
333+
React.actAsync(() => {
334+
let root = ReactDOM.Client.createRoot(container);
335+
ReactDOM.Client.render(root, <Counter />);
336+
Js.Promise.resolve();
337+
});
338+
339+
let valueElement = querySelector(".Value", container);
340+
switch (valueElement) {
341+
| Some(value) => expect(value->innerHTML)->toBe("0")
342+
| None => failwith("Can't find 'Value' element")
266343
};
344+
345+
let title = getTitle(document);
346+
expect(title)->toBe("You clicked 0 times");
347+
348+
let.await () =
349+
React.actAsync(() => {
350+
let buttonElement = querySelector(".Increment", container);
351+
switch (buttonElement) {
352+
| Some(button) =>
353+
dispatchEvent(button, mouseEvent("click", {"bubbles": true}))
354+
| None => failwith("Can't find 'Increment' button")
355+
};
356+
Js.Promise.resolve();
357+
});
358+
359+
let valueElement = querySelector(".Value", container);
360+
switch (valueElement) {
361+
| Some(value) => expect(value->innerHTML)->toBe("1")
362+
| None => failwith("Can't find 'Value' element")
363+
};
364+
365+
let title = getTitle(document);
366+
expect(title)->toBe("You clicked 1 times");
367+
368+
finish();
267369
});
268370

269371
test("ErrorBoundary + Suspense", () => {

0 commit comments

Comments
 (0)