diff --git a/src/React.re b/src/React.re index 22ff09744..ae8cd326e 100644 --- a/src/React.re +++ b/src/React.re @@ -885,9 +885,11 @@ external startTransition: ([@mel.uncurry] (unit => unit)) => unit = external useDebugValue: ('value, ~format: 'value => string=?, unit) => unit = "useDebugValue"; -[@mel.module "react"] external act: (unit => unit) => unit = "act"; [@mel.module "react"] -external actAsync: (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 1c3be073e..66b4de0ee 100644 --- a/src/React.rei +++ b/src/React.rei @@ -573,9 +573,11 @@ external startTransition: ([@mel.uncurry] (unit => unit)) => unit = external useTransition: unit => (bool, callback(callback(unit, unit), unit)) = "useTransition"; -[@mel.module "react"] external act: (unit => unit) => unit = "act"; [@mel.module "react"] -external actAsync: (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/Form__test.re b/test/Form__test.re index 2269a9c0e..dbbe7eee2 100644 --- a/test/Form__test.re +++ b/test/Form__test.re @@ -136,7 +136,7 @@ describe("Form with useOptimistic", () => { let container = ReactTestingLibrary.render(); ReactTestingLibrary.actAsync(() => { - let.await _ = findByString("Hola!", container); + let.await _ = findByString({j|¡Hola!|j}, container); let.await button = findByString("Enviar", container); let.await input = findByPlaceholderText("message", container); diff --git a/test/React__test.re b/test/React__test.re index fa447654b..fbb4fe069 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,36 +252,129 @@ 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.string(string_of_int(count))} +
; }; + }; + testPromise("act", finish => { let containerRef: ref(Js.nullable(ReactTestingLibrary.renderResult)) = ref(Js.Nullable.null); - React.act(() => { - let container = ReactTestingLibrary.render(); - let button = getByRole("Increment", container); - FireEvent.click(button); - containerRef.contents = Js.Nullable.return(container); - }); + let.await _ = + React.act(() => { + let container = ReactTestingLibrary.render(); + let button = getByRole("Increment", container); + FireEvent.click(button); + containerRef.contents = Js.Nullable.return(container); + }); switch (Js.Nullable.toOption(containerRef.contents)) { | Some(container) => expect(getByRole("counter", container)->innerHTML)->toBe("1") | None => failwith("Container is null") }; + finish(); + }); + + 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", () => {