");
+ expect(evaluations.contents)->toBe(3);
+ });
+
+ test("evaluates optional prop expressions once", () => {
+ let render = present => {
+ let evaluations = ref(0);
+ let next = value => {
+ incr(evaluations);
+ present ? Some(value) : None;
+ };
+ let element =
+ ;
+ expect(evaluations.contents)->toBe(2);
+ let html = ReactDOMServer.renderToStaticMarkup(element);
+ expect(evaluations.contents)->toBe(2);
+ html;
+ };
+ expect(render(false))->toBe("");
+ expect(render(true))
+ ->toBe("");
+ });
+
+ test("evaluates the final unit argument once", () => {
+ let evaluations = ref(0);
+ let nextUnit = () => incr(evaluations);
+ let element = [@JSX] div(~dataFoo="value", nextUnit());
+ expect(evaluations.contents)->toBe(1);
+ expect(ReactDOMServer.renderToStaticMarkup(element))
+ ->toBe("");
+ expect(evaluations.contents)->toBe(1);
+ });
+});
diff --git a/test/blackbox-tests/data-attributes.t/input.re.in b/test/blackbox-tests/data-attributes.t/input.re.in
new file mode 100644
index 000000000..b6ca5ff00
--- /dev/null
+++ b/test/blackbox-tests/data-attributes.t/input.re.in
@@ -0,0 +1,72 @@
+let first = ;
+let second = ;
+let onlyData = ;
+let svg = ;
+let ordinaryData = ;
+
+let optional = (~className=?, ~dataFoo=?, ()) => ;
+
+let renamed =
+ ;
+
+let withChildren =
+
+
+
+
;
+
+let withRefAndEvent = (elementRef, handleClick) =>
+ ;
+
+let optionalKey = (~key=?, ()) => ;
+
+module Component = {
+ [@react.component]
+ let make = (~dataFoo) => ;
+};
+
+let component = ;
+
+external next: string => string = "next";
+let effects = ;
+
+let ordinary = ;
+
+let names =
+ ;
+
+module ShadowedTypes = {
+ type string = int;
+ type unit = int;
+
+ let element = ;
+ let optional = (~dataFoo=?, ()) => ;
+};
+
+module IntegerComponent = {
+ [@react.component]
+ let make = (~dataFoo: int) => ;
+};
+
+let integerComponent = ;
+
+external nextUnit: unit => unit = "nextUnit";
+let effectfulUnit = [@JSX] div(~dataFoo="unit", nextUnit());
diff --git a/test/blackbox-tests/data-attributes.t/run.t b/test/blackbox-tests/data-attributes.t/run.t
new file mode 100644
index 000000000..1fa76d256
--- /dev/null
+++ b/test/blackbox-tests/data-attributes.t/run.t
@@ -0,0 +1,327 @@
+DOM data attributes use a constructor for each JSX occurrence, with ordinary
+props checked against ReactDOM.domProps and no runtime validation callback.
+
+ $ cat > dune-project < (lang dune 3.8)
+ > (using melange 0.1)
+ > EOF
+
+ $ cat > dune < (melange.emit
+ > (target output)
+ > (alias mel)
+ > (emit_stdlib false)
+ > (libraries reason-react)
+ > (preprocess (pps melange.ppx reason-react-ppx)))
+ > EOF
+
+ $ cp input.re.in input.re
+ $ chmod u+w input.re
+ $ dune build @mel
+
+ $ cat _build/default/output/input.js
+ // Generated by Melange
+ 'use strict';
+
+ const Caml_option = require("melange.js/caml_option.js");
+ const JsxRuntime = require("react/jsx-runtime");
+
+ const first = JsxRuntime.jsx("div", {
+ className: "first",
+ "data-foo": "one"
+ });
+
+ const second = JsxRuntime.jsx("div", {
+ id: "second",
+ "data-bar": "two",
+ "data-test-id": "three"
+ });
+
+ const onlyData = JsxRuntime.jsx("div", {
+ "data-foo": "one"
+ });
+
+ const svg = JsxRuntime.jsx("svg", {
+ "data-foo": "value"
+ });
+
+ const ordinaryData = JsxRuntime.jsx("object", {
+ data: "file",
+ datatype: "text",
+ "data-test-id": "object"
+ });
+
+ function optional(className, dataFoo, param) {
+ let tmp = {};
+ if (className !== undefined) {
+ tmp.className = Caml_option.valFromOption(className);
+ }
+ if (dataFoo !== undefined) {
+ tmp["data-foo"] = Caml_option.valFromOption(dataFoo);
+ }
+ return JsxRuntime.jsx("div", tmp);
+ }
+
+ const renamed = JsxRuntime.jsx("div", {
+ "data-test-id": "aliases",
+ "aria-label": "label",
+ "aria-activedescendant": "descendant",
+ as: "button",
+ begin: "0",
+ end: "1",
+ in: "source",
+ open: true,
+ to: "target",
+ type: "button"
+ });
+
+ const withChildren = JsxRuntime.jsxs("div", {
+ children: [
+ JsxRuntime.jsx("span", {
+ "data-foo": "child"
+ }),
+ JsxRuntime.jsx("span", {})
+ ],
+ "data-test-id": "parent"
+ }, "parent");
+
+ function withRefAndEvent(elementRef, handleClick) {
+ return JsxRuntime.jsx("button", {
+ ref: elementRef,
+ onClick: handleClick,
+ "data-test-id": "button"
+ });
+ }
+
+ function optionalKey(key, param) {
+ return JsxRuntime.jsx("div", {
+ "data-foo": "keyed"
+ }, key !== undefined ? Caml_option.valFromOption(key) : undefined);
+ }
+
+ function Input$Component(Props) {
+ let dataFoo = Props.dataFoo;
+ return JsxRuntime.jsx("div", {
+ "data-foo": dataFoo
+ });
+ }
+
+ const Component = {
+ make: Input$Component
+ };
+
+ const component = JsxRuntime.jsx(Input$Component, {
+ dataFoo: "component"
+ });
+
+ const effects = JsxRuntime.jsx("div", {
+ className: next("class"),
+ "data-foo": next("data")
+ });
+
+ const ordinary = JsxRuntime.jsx("div", {
+ className: "unchanged"
+ });
+
+ const names = JsxRuntime.jsx("div", {
+ "data-foo": "plain",
+ "data-foo__bar": "double",
+ "data-foo___": "trailing",
+ "data-foo_bar": "single",
+ "data-u-r-l": "acronym",
+ "data-version2": "digit"
+ });
+
+ const element = JsxRuntime.jsx("div", {
+ "data-foo": "value"
+ });
+
+ function optional$1(dataFoo, param) {
+ let tmp = {};
+ if (dataFoo !== undefined) {
+ tmp["data-foo"] = Caml_option.valFromOption(dataFoo);
+ }
+ return JsxRuntime.jsx("div", tmp);
+ }
+
+ const ShadowedTypes = {
+ element,
+ optional: optional$1
+ };
+
+ function Input$IntegerComponent(Props) {
+ let dataFoo = Props.dataFoo;
+ return JsxRuntime.jsx("div", {
+ "data-foo": String(dataFoo)
+ });
+ }
+
+ const IntegerComponent = {
+ make: Input$IntegerComponent
+ };
+
+ const integerComponent = JsxRuntime.jsx(Input$IntegerComponent, {
+ dataFoo: 42
+ });
+
+ const effectfulUnit = JsxRuntime.jsx("div", (nextUnit(), {
+ "data-foo": "unit"
+ }));
+
+ module.exports = {
+ first,
+ second,
+ onlyData,
+ svg,
+ ordinaryData,
+ optional,
+ renamed,
+ withChildren,
+ withRefAndEvent,
+ optionalKey,
+ Component,
+ component,
+ effects,
+ ordinary,
+ names,
+ ShadowedTypes,
+ IntegerComponent,
+ integerComponent,
+ effectfulUnit,
+ }
+ /* first Not a pure module */
+
+Ordinary prop types are still checked.
+
+ $ cat > input.re < let element = ;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 1, characters 29-31:
+ 1 | let element = ;
+ ^^
+ Error: The constant 42 has type int but an expression was expected of type
+ string
+ [1]
+
+Data attribute values must be strings.
+
+ $ cat > input.re < let element = ;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 1, characters 27-29:
+ 1 | let element = ;
+ ^^
+ Error: The constant 42 has type int but an expression was expected of type
+ string
+ [1]
+
+Optional data attribute values are checked too.
+
+ $ cat > input.re < let element = ;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 1, characters 33-35:
+ 1 | let element = ;
+ ^^
+ Error: The constant 42 has type int but an expression was expected of type
+ string
+ [1]
+
+Renamed ordinary props retain their original types.
+
+ $ cat > input.re < let element = ;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 1, characters 25-30:
+ 1 | let element = ;
+ ^^^^^
+ Error: This constant has type string but an expression was expected of type
+ bool
+ [1]
+
+Unknown ordinary props still fail. Only show the final diagnostic, since the
+constructor's list of valid props is long.
+
+ $ cat > input.re < let element = ;
+ > EOF
+ $ dune build @mel > error.log 2>&1
+ [1]
+ $ tail -n 1 error.log
+ This argument cannot be applied with label ~clasName
+
+Data attribute names must be unique, including optional arguments.
+
+ $ cat > input.re < let element = ;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 1, characters 43-51:
+ 1 | let element = ;
+ ^^^^^^^^
+ Error: JSX: duplicate data attribute "data-foo"
+ [1]
+
+ $ cat > input.re < let render = (~dataFoo=?, ()) => ;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 1, characters 55-63:
+ 1 | let render = (~dataFoo=?, ()) => ;
+ ^^^^^^^^
+ Error: JSX: duplicate data attribute "data-foo"
+ [1]
+
+Shadowing string must not change the type of a data attribute.
+
+ $ cat > input.re < type string = int;
+ > let element = ;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 2, characters 27-29:
+ 2 | let element = ;
+ ^^
+ Error: The constant 42 has type string but an expression was expected of type
+ string
+ [1]
+
+Optional ordinary props, event handlers, and refs retain their types.
+
+ $ cat > input.re < let element = ;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 1, characters 35-37:
+ 1 | let element = ;
+ ^^
+ Error: The constant 42 has type int but an expression was expected of type
+ string
+ [1]
+
+ $ cat > input.re < let element =
event + 1} dataFoo="x" />;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 1, characters 37-42:
+ 1 | let element =
event + 1} dataFoo="x" />;
+ ^^^^^
+ Error: The value event has type
+ React.Event.Mouse.t = React.Event.Mouse.tag React.Event.synthetic
+ but an expression was expected of type int
+ [1]
+
+ $ cat > input.re < let element = ;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 1, characters 23-25:
+ 1 | let element = ;
+ ^^
+ Error: The constant 42 has type int but an expression was expected of type
+ ReactDOM.domRef
+ [1]
diff --git a/test/blackbox-tests/dom-props.t/input.re.in b/test/blackbox-tests/dom-props.t/input.re.in
new file mode 100644
index 000000000..d0ddab268
--- /dev/null
+++ b/test/blackbox-tests/dom-props.t/input.re.in
@@ -0,0 +1,45 @@
+let first = ;
+let second = ;
+let ordinaryData = ;
+
+let optional = (~className=?, ()) => ;
+
+let renamed =
+ ;
+
+let withChildren =
;
+};
+
+let integerComponent = ;
+
+external next: string => string = "next";
+let effects = ;
+
+external nextUnit: unit => unit = "nextUnit";
+let effectfulUnit = [@JSX] div(~className="unit", nextUnit());
diff --git a/test/blackbox-tests/dom-props.t/run.t b/test/blackbox-tests/dom-props.t/run.t
new file mode 100644
index 000000000..2aadfa23d
--- /dev/null
+++ b/test/blackbox-tests/dom-props.t/run.t
@@ -0,0 +1,227 @@
+DOM props retain their types and JavaScript names. Component props named
+dataFoo are ordinary component props, including when their type is int.
+
+ $ cat > dune-project < (lang dune 3.8)
+ > (using melange 0.1)
+ > EOF
+
+ $ cat > dune < (melange.emit
+ > (target output)
+ > (alias mel)
+ > (emit_stdlib false)
+ > (libraries reason-react)
+ > (preprocess (pps melange.ppx reason-react-ppx)))
+ > EOF
+
+ $ cp input.re.in input.re
+ $ chmod u+w input.re
+ $ dune build @mel
+ $ cat _build/default/output/input.js
+ // Generated by Melange
+ 'use strict';
+
+ const Caml_option = require("melange.js/caml_option.js");
+ const JsxRuntime = require("react/jsx-runtime");
+
+ const first = JsxRuntime.jsx("div", {
+ className: "first"
+ });
+
+ const second = JsxRuntime.jsx("div", {
+ id: "second"
+ });
+
+ const ordinaryData = JsxRuntime.jsx("object", {
+ data: "file",
+ datatype: "text"
+ });
+
+ function optional(className, param) {
+ let tmp = {};
+ if (className !== undefined) {
+ tmp.className = Caml_option.valFromOption(className);
+ }
+ return JsxRuntime.jsx("div", tmp);
+ }
+
+ const renamed = JsxRuntime.jsx("div", {
+ "aria-activedescendant": "descendant",
+ "aria-label": "label",
+ as: "button",
+ open: true,
+ type: "button",
+ begin: "0",
+ end: "1",
+ in: "source",
+ to: "target"
+ });
+
+ const withChildren = JsxRuntime.jsxs("div", {
+ children: [
+ JsxRuntime.jsx("span", {}),
+ JsxRuntime.jsx("span", {})
+ ]
+ }, "parent");
+
+ function withRefAndEvent(elementRef, handleClick) {
+ return JsxRuntime.jsx("button", {
+ ref: elementRef,
+ onClick: handleClick
+ });
+ }
+
+ function optionalKey(key, param) {
+ return JsxRuntime.jsx("div", {}, key !== undefined ? Caml_option.valFromOption(key) : undefined);
+ }
+
+ function Input$Component(Props) {
+ let dataFoo = Props.dataFoo;
+ return JsxRuntime.jsx("div", {
+ children: dataFoo
+ });
+ }
+
+ const Component = {
+ make: Input$Component
+ };
+
+ const component = JsxRuntime.jsx(Input$Component, {
+ dataFoo: "component"
+ });
+
+ function Input$IntegerComponent(Props) {
+ let dataFoo = Props.dataFoo;
+ return JsxRuntime.jsx("div", {
+ children: dataFoo
+ });
+ }
+
+ const IntegerComponent = {
+ make: Input$IntegerComponent
+ };
+
+ const integerComponent = JsxRuntime.jsx(Input$IntegerComponent, {
+ dataFoo: 42
+ });
+
+ const effects = JsxRuntime.jsx("div", {
+ className: next("class"),
+ id: next("id")
+ });
+
+ const effectfulUnit = JsxRuntime.jsx("div", (nextUnit(), {
+ className: "unit"
+ }));
+
+ module.exports = {
+ first,
+ second,
+ ordinaryData,
+ optional,
+ renamed,
+ withChildren,
+ withRefAndEvent,
+ optionalKey,
+ Component,
+ component,
+ IntegerComponent,
+ integerComponent,
+ effects,
+ effectfulUnit,
+ }
+ /* first Not a pure module */
+
+Ordinary prop types are checked.
+
+ $ cat > input.re < let element = ;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 1, characters 29-31:
+ 1 | let element = ;
+ ^^
+ Error: The constant 42 has type int but an expression was expected of type
+ string
+ [1]
+
+Optional ordinary prop types are checked too.
+
+ $ cat > input.re < let element = ;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 1, characters 35-37:
+ 1 | let element = ;
+ ^^
+ Error: The constant 42 has type int but an expression was expected of type
+ string
+ [1]
+
+Renamed props retain their original types.
+
+ $ cat > input.re < let element = ;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 1, characters 25-30:
+ 1 | let element = ;
+ ^^^^^
+ Error: This constant has type string but an expression was expected of type
+ bool
+ [1]
+
+Unknown props fail. Show only the final diagnostic because the constructor's
+list of valid props is long, while checking the compiler's exit status separately.
+
+ $ cat > input.re < let element = ;
+ > EOF
+ $ dune build @mel > error.log 2>&1
+ [1]
+ $ tail -n 1 error.log
+ This argument cannot be applied with label ~clasName
+
+Components must declare their dataFoo prop.
+
+ $ cat > input.re < module Component = {
+ > [@react.component]
+ > let make = () => React.null;
+ > };
+ > let element = ;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 5, characters 33-36:
+ 5 | let element = ;
+ ^^^
+ Error: The function applied to this argument has type
+ ?key:string -> < > Js.t
+ This argument cannot be applied with label ~dataFoo
+ [1]
+
+Event handlers and refs retain their types.
+
+ $ cat > input.re < let element =
event + 1} />;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 1, characters 37-42:
+ 1 | let element =
event + 1} />;
+ ^^^^^
+ Error: The value event has type
+ React.Event.Mouse.t = React.Event.Mouse.tag React.Event.synthetic
+ but an expression was expected of type int
+ [1]
+
+ $ cat > input.re < let element = ;
+ > EOF
+ $ dune build @mel
+ File "input.re", line 1, characters 23-25:
+ 1 | let element = ;
+ ^^
+ Error: The constant 42 has type int but an expression was expected of type
+ ReactDOM.domRef
+ [1]
diff --git a/test/dune b/test/dune
index 0dde05c81..be549402e 100644
--- a/test/dune
+++ b/test/dune
@@ -1,6 +1,7 @@
(melange.emit
(alias runtest)
(target test)
+ (package reason-react)
(module_systems
(commonjs bs.js))
(libraries