From 44facccc90e031e3a2f4113b667079caf0604740 Mon Sep 17 00:00:00 2001 From: David Sancho Moreno Date: Mon, 25 Nov 2024 17:36:40 +0100 Subject: [PATCH] Test react.act and react.actasync --- src/React.re | 5 +- src/React.rei | 5 +- test/React__test.re | 158 ++++++++++++++++++++++++++++++++++++-------- 3 files changed, 138 insertions(+), 30 deletions(-) diff --git a/src/React.re b/src/React.re index b5b3b42b6..ae8cd326e 100644 --- a/src/React.re +++ b/src/React.re @@ -886,7 +886,10 @@ external useDebugValue: ('value, ~format: 'value => string=?, unit) => unit = "useDebugValue"; [@mel.module "react"] -external act: (unit => Js.Promise.t(unit)) => unit = "act"; +external act: (unit => unit) => Js.Promise.t(unit) = "act"; +[@mel.module "react"] +external actAsync: (unit => Js.Promise.t(unit)) => Js.Promise.t(unit) = + "act"; module Experimental = { /* This module is used to bind to APIs for future versions of React. There is no guarantee of backwards compatibility or stability. */ diff --git a/src/React.rei b/src/React.rei index c51cf5f3b..66b4de0ee 100644 --- a/src/React.rei +++ b/src/React.rei @@ -574,7 +574,10 @@ external useTransition: unit => (bool, callback(callback(unit, unit), unit)) = "useTransition"; [@mel.module "react"] -external act: (unit => Js.Promise.t(unit)) => unit = "act"; +external act: (unit => unit) => Js.Promise.t(unit) = "act"; +[@mel.module "react"] +external actAsync: (unit => Js.Promise.t(unit)) => Js.Promise.t(unit) = + "act"; module Experimental: { /* This module is used to bind to APIs for future versions of React. There is no guarantee of backwards compatibility or stability. */ diff --git a/test/React__test.re b/test/React__test.re index 7c193166f..184716112 100644 --- a/test/React__test.re +++ b/test/React__test.re @@ -47,6 +47,7 @@ module DummyContext = { [@mel.get] external tagName: Dom.element => string = "tagName"; [@mel.get] external innerHTML: Dom.element => string = "innerHTML"; +[@mel.set] external setInnerHTML: (Dom.element, string) => unit = "innerHTML"; let getByRole = (role, container) => { ReactTestingLibrary.getByRole(~matcher=`Str(role), container); @@ -62,6 +63,24 @@ let getByTag = (tag, container) => { [@mel.send] external getAttribute: (Dom.element, string) => option(string) = "getAttribute"; +[@mel.set] external setTitle: (Dom.element, string) => unit = "title"; +[@mel.get] external getTitle: Dom.element => string = "title"; + +let (let.await) = (p, f) => Js.Promise.then_(f, p); + +external createElement: string => Dom.element = "document.createElement"; +[@mel.send] +external appendChild: (Dom.element, Dom.element) => unit = "appendChild"; +external document: Dom.element = "document"; +external body: Dom.element = "document.body"; +external querySelector: (string, Dom.element) => option(Dom.element) = + "document.querySelector"; + +[@mel.new] +external mouseEvent: (string, Js.t('a)) => Dom.event = "MouseEvent"; + +[@mel.send] +external dispatchEvent: (Dom.element, Dom.event) => unit = "dispatchEvent"; describe("React", () => { test("can render DOM elements", () => { @@ -233,37 +252,120 @@ describe("React", () => { expect(image->getAttribute("src"))->toEqual(Some("https://foo.png")); }); - test("React.act", () => { - module Counter = { - [@react.component] - let make = () => { - let (count, setCount) = React.useState(() => 0); - -
- - {React.string(string_of_int(count))} -
; - }; + module Counter = { + [@react.component] + let make = () => { + let (count, setCount) = React.Uncurried.useState(() => 0); + + React.useEffect1( + () => { + document->setTitle( + "You clicked " ++ Int.to_string(count) ++ " times", + ); + None; + }, + [|count|], + ); + +
+ + {React.string(string_of_int(count))} +
; }; + }; - let containerRef: ref(option(ReactTestingLibrary.renderResult)) = - ref(None); - - React.act(() => { - let container = ReactTestingLibrary.render(); - let button = getByRole("Increment", container); - FireEvent.click(button); - containerRef.contents = Some(container); - Js.Promise.resolve(); - }); - - switch (containerRef.contents) { - | Some(container) => - expect(getByRole("counter", container)->innerHTML)->toBe("1") - | None => failwith("Container is null") + testPromise("act", finish => { + /* This test doesn't use ReactTestingLibrary to test the act API, and the code comes from + https://react.dev/reference/react/act example */ + + let container: Dom.element = createElement("div"); + body->appendChild(container); + + let.await () = + React.act(() => { + let root = ReactDOM.Client.createRoot(container); + ReactDOM.Client.render(root, ); + }); + + let valueElement = querySelector(".Value", container); + switch (valueElement) { + | Some(value) => expect(value->innerHTML)->toBe("0") + | None => failwith("Can't find 'Value' element") + }; + + let title = getTitle(document); + expect(title)->toBe("You clicked 0 times"); + + let.await () = + React.act(() => { + let buttonElement = querySelector(".Increment", container); + switch (buttonElement) { + | Some(button) => + dispatchEvent(button, mouseEvent("click", {"bubbles": true})) + | None => failwith("Can't find 'Increment' button") + }; + }); + + let valueElement = querySelector(".Value", container); + switch (valueElement) { + | Some(value) => expect(value->innerHTML)->toBe("1") + | None => failwith("Can't find 'Value' element") + }; + + let title = getTitle(document); + expect(title)->toBe("You clicked 1 times"); + + finish(); + }); + + testPromise("actAsync", finish => { + /* This test doesn't use ReactTestingLibrary to test the act API, and the code comes from + https://react.dev/reference/react/act example */ + + body->setInnerHTML(""); + let container: Dom.element = createElement("div"); + body->appendChild(container); + + let.await () = + React.actAsync(() => { + let root = ReactDOM.Client.createRoot(container); + ReactDOM.Client.render(root, ); + Js.Promise.resolve(); + }); + + let valueElement = querySelector(".Value", container); + switch (valueElement) { + | Some(value) => expect(value->innerHTML)->toBe("0") + | None => failwith("Can't find 'Value' element") }; + + let title = getTitle(document); + expect(title)->toBe("You clicked 0 times"); + + let.await () = + React.actAsync(() => { + let buttonElement = querySelector(".Increment", container); + switch (buttonElement) { + | Some(button) => + dispatchEvent(button, mouseEvent("click", {"bubbles": true})) + | None => failwith("Can't find 'Increment' button") + }; + Js.Promise.resolve(); + }); + + let valueElement = querySelector(".Value", container); + switch (valueElement) { + | Some(value) => expect(value->innerHTML)->toBe("1") + | None => failwith("Can't find 'Value' element") + }; + + let title = getTitle(document); + expect(title)->toBe("You clicked 1 times"); + + finish(); }); test("ErrorBoundary + Suspense", () => {