Sfoglia il codice sorgente

adding unit test for Input number

namnguyen 10 mesi fa
parent
commit
1b5ac9cc2f

+ 82 - 41
frontend/taipy-gui/src/components/Taipy/Input.spec.tsx

@@ -12,54 +12,54 @@
  */
 
 import React from "react";
-import { render, waitFor } from "@testing-library/react";
+import {render, waitFor} from "@testing-library/react";
 import "@testing-library/jest-dom";
 import userEvent from "@testing-library/user-event";
 
 import Input from "./Input";
-import { TaipyContext } from "../../context/taipyContext";
-import { TaipyState, INITIAL_STATE } from "../../context/taipyReducers";
+import {TaipyContext} from "../../context/taipyContext";
+import {TaipyState, INITIAL_STATE} from "../../context/taipyReducers";
 
 describe("Input Component", () => {
     it("renders", async () => {
-        const { getByDisplayValue } = render(<Input type="text" value="toto" />);
+        const {getByDisplayValue} = render(<Input type="text" value="toto"/>);
         const elt = getByDisplayValue("toto");
         expect(elt.tagName).toBe("INPUT");
     });
     it("displays the right info for string", async () => {
-        const { getByDisplayValue } = render(
-            <Input value="toto" type="text" defaultValue="titi" className="taipy-input" />
+        const {getByDisplayValue} = render(
+            <Input value="toto" type="text" defaultValue="titi" className="taipy-input"/>
         );
         const elt = getByDisplayValue("toto");
         expect(elt.parentElement?.parentElement).toHaveClass("taipy-input");
     });
     it("displays the default value", async () => {
-        const { getByDisplayValue } = render(
-            <Input defaultValue="titi" value={undefined as unknown as string} type="text" />
+        const {getByDisplayValue} = render(
+            <Input defaultValue="titi" value={undefined as unknown as string} type="text"/>
         );
         getByDisplayValue("titi");
     });
     it("is disabled", async () => {
-        const { getByDisplayValue } = render(<Input value="val" type="text" active={false} />);
+        const {getByDisplayValue} = render(<Input value="val" type="text" active={false}/>);
         const elt = getByDisplayValue("val");
         expect(elt).toBeDisabled();
     });
     it("is enabled by default", async () => {
-        const { getByDisplayValue } = render(<Input value="val" type="text" />);
+        const {getByDisplayValue} = render(<Input value="val" type="text"/>);
         const elt = getByDisplayValue("val");
         expect(elt).not.toBeDisabled();
     });
     it("is enabled by active", async () => {
-        const { getByDisplayValue } = render(<Input value="val" type="text" active={true} />);
+        const {getByDisplayValue} = render(<Input value="val" type="text" active={true}/>);
         const elt = getByDisplayValue("val");
         expect(elt).not.toBeDisabled();
     });
     it("dispatch a well formed message", async () => {
         const dispatch = jest.fn();
         const state: TaipyState = INITIAL_STATE;
-        const { getByDisplayValue } = render(
-            <TaipyContext.Provider value={{ state, dispatch }}>
-                <Input value="Val" type="text" updateVarName="varname" />
+        const {getByDisplayValue} = render(
+            <TaipyContext.Provider value={{state, dispatch}}>
+                <Input value="Val" type="text" updateVarName="varname"/>
             </TaipyContext.Provider>
         );
         const elt = getByDisplayValue("Val");
@@ -67,7 +67,7 @@ describe("Input Component", () => {
         await waitFor(() => expect(dispatch).toHaveBeenCalled());
         expect(dispatch).toHaveBeenLastCalledWith({
             name: "varname",
-            payload: { value: "" },
+            payload: {value: ""},
             propagate: true,
             type: "SEND_UPDATE_ACTION",
         });
@@ -75,9 +75,9 @@ describe("Input Component", () => {
     it("dispatch a well formed message on enter", async () => {
         const dispatch = jest.fn();
         const state: TaipyState = INITIAL_STATE;
-        const { getByDisplayValue } = render(
-            <TaipyContext.Provider value={{ state, dispatch }}>
-                <Input value="Val" type="text" updateVarName="varname" onAction="on_action" />
+        const {getByDisplayValue} = render(
+            <TaipyContext.Provider value={{state, dispatch}}>
+                <Input value="Val" type="text" updateVarName="varname" onAction="on_action"/>
             </TaipyContext.Provider>
         );
         const elt = getByDisplayValue("Val");
@@ -86,16 +86,16 @@ describe("Input Component", () => {
         await waitFor(() => expect(dispatch).toHaveBeenCalled());
         expect(dispatch).toHaveBeenLastCalledWith({
             name: "",
-            payload: { action: "on_action", args: ["Enter", "varname", "Valdata"] },
+            payload: {action: "on_action", args: ["Enter", "varname", "Valdata"]},
             type: "SEND_ACTION_ACTION",
         });
     });
     it("dispatch a well formed update message with change_delay=-1", async () => {
         const dispatch = jest.fn();
         const state: TaipyState = INITIAL_STATE;
-        const { getByDisplayValue } = render(
-            <TaipyContext.Provider value={{ state, dispatch }}>
-                <Input value="Val" type="text" updateVarName="varname" changeDelay={-1} />
+        const {getByDisplayValue} = render(
+            <TaipyContext.Provider value={{state, dispatch}}>
+                <Input value="Val" type="text" updateVarName="varname" changeDelay={-1}/>
             </TaipyContext.Provider>
         );
         const elt = getByDisplayValue("Val");
@@ -104,7 +104,7 @@ describe("Input Component", () => {
         await waitFor(() => expect(dispatch).toHaveBeenCalled());
         expect(dispatch).toHaveBeenLastCalledWith({
             name: "varname",
-            payload: { value: "Valdata" },
+            payload: {value: "Valdata"},
             propagate: true,
             type: "SEND_UPDATE_ACTION",
         });
@@ -112,9 +112,9 @@ describe("Input Component", () => {
     it("dispatch a no action message on unsupported key", async () => {
         const dispatch = jest.fn();
         const state: TaipyState = INITIAL_STATE;
-        const { getByDisplayValue } = render(
-            <TaipyContext.Provider value={{ state, dispatch }}>
-                <Input value="Val" type="text" updateVarName="varname" onAction="on_action" />
+        const {getByDisplayValue} = render(
+            <TaipyContext.Provider value={{state, dispatch}}>
+                <Input value="Val" type="text" updateVarName="varname" onAction="on_action"/>
             </TaipyContext.Provider>
         );
         const elt = getByDisplayValue("Val");
@@ -123,7 +123,7 @@ describe("Input Component", () => {
         await waitFor(() => expect(dispatch).toHaveBeenCalled());
         expect(dispatch).toHaveBeenLastCalledWith({
             name: "varname",
-            payload: { value: "Valdata" },
+            payload: {value: "Valdata"},
             propagate: true,
             type: "SEND_UPDATE_ACTION",
         });
@@ -132,44 +132,44 @@ describe("Input Component", () => {
 
 describe("Number Component", () => {
     it("renders", async () => {
-        const { getByDisplayValue } = render(<Input type="number" value="12" />);
+        const {getByDisplayValue} = render(<Input type="number" value="12"/>);
         const elt = getByDisplayValue("12");
         expect(elt.tagName).toBe("INPUT");
     });
     it("displays the right info for string", async () => {
-        const { getByDisplayValue } = render(
-            <Input value="12" type="number" defaultValue="1" className="taipy-number" />
+        const {getByDisplayValue} = render(
+            <Input value="12" type="number" defaultValue="1" className="taipy-number"/>
         );
         const elt = getByDisplayValue(12);
         expect(elt.parentElement?.parentElement).toHaveClass("taipy-number");
     });
     it("displays the default value", async () => {
-        const { getByDisplayValue } = render(
-            <Input defaultValue="1" value={undefined as unknown as string} type="number" />
+        const {getByDisplayValue} = render(
+            <Input defaultValue="1" value={undefined as unknown as string} type="number"/>
         );
         getByDisplayValue("1");
     });
     it("is disabled", async () => {
-        const { getByDisplayValue } = render(<Input value={"33"} type="number" active={false} />);
+        const {getByDisplayValue} = render(<Input value={"33"} type="number" active={false}/>);
         const elt = getByDisplayValue("33");
         expect(elt).toBeDisabled();
     });
     it("is enabled by default", async () => {
-        const { getByDisplayValue } = render(<Input value={"33"} type="number" />);
+        const {getByDisplayValue} = render(<Input value={"33"} type="number"/>);
         const elt = getByDisplayValue("33");
         expect(elt).not.toBeDisabled();
     });
     it("is enabled by active", async () => {
-        const { getByDisplayValue } = render(<Input value={"33"} type="number" active={true} />);
+        const {getByDisplayValue} = render(<Input value={"33"} type="number" active={true}/>);
         const elt = getByDisplayValue("33");
         expect(elt).not.toBeDisabled();
     });
     it("dispatch a well formed message", async () => {
         const dispatch = jest.fn();
         const state: TaipyState = INITIAL_STATE;
-        const { getByDisplayValue } = render(
-            <TaipyContext.Provider value={{ state, dispatch }}>
-                <Input value={"33"} type="number" updateVarName="varname" />
+        const {getByDisplayValue} = render(
+            <TaipyContext.Provider value={{state, dispatch}}>
+                <Input value={"33"} type="number" updateVarName="varname"/>
             </TaipyContext.Provider>
         );
         const elt = getByDisplayValue("33");
@@ -178,14 +178,14 @@ describe("Number Component", () => {
         await waitFor(() => expect(dispatch).toHaveBeenCalled());
         expect(dispatch).toHaveBeenLastCalledWith({
             name: "varname",
-            payload: { value: "666" },
+            payload: {value: "666"},
             propagate: true,
             type: "SEND_UPDATE_ACTION",
         });
     });
     xit("shows 0", async () => {
-    //not working cf. https://github.com/testing-library/user-event/issues/1066
-    const { getByDisplayValue, rerender } = render(<Input value={"0"} type="number" />);
+        //not working cf. https://github.com/testing-library/user-event/issues/1066
+        const {getByDisplayValue, rerender} = render(<Input value={"0"} type="number"/>);
         const elt = getByDisplayValue("0") as HTMLInputElement;
         expect(elt).toBeInTheDocument();
         await userEvent.type(elt, "{ArrowUp}");
@@ -193,4 +193,45 @@ describe("Number Component", () => {
         await userEvent.type(elt, "{ArrowDown}");
         expect(elt.value).toBe("0");
     });
+    it("Validates increment by step value on up click", async () => {
+        const {getByDisplayValue, getByTestId} = render(<Input value={"0"} type="number" step={2}/>);
+        const upSpinner = getByTestId("stepper-up-spinner");
+        const elt = getByDisplayValue("0") as HTMLInputElement;
+        await userEvent.click(upSpinner);
+        expect(elt.value).toBe("2");
+    })
+    it("Validates decrement by step value on down click", async () => {
+        const {getByDisplayValue, getByTestId} = render(<Input value={"0"} type="number" step={2}/>);
+        const downSpinner = getByTestId("stepper-down-spinner");
+        const elt = getByDisplayValue("0") as HTMLInputElement;
+        await userEvent.click(downSpinner);
+        expect(elt.value).toBe("-2");
+    })
+    it("Validates increment when holding shift key and clicking up", async () => {
+        const user = userEvent.setup();
+        const {getByDisplayValue, getByTestId} = render(<Input value={"0"} type="number" step={2}/>);
+        const upSpinner = getByTestId("stepper-up-spinner");
+        const elt = getByDisplayValue("0") as HTMLInputElement;
+        await user.keyboard('[ShiftLeft>]');
+        await user.click(upSpinner);
+        expect(elt.value).toBe("20");
+    })
+    it("Validates decrement when holding shift key and clicking down", async () => {
+        const user = userEvent.setup();
+        const {getByDisplayValue, getByTestId} = render(<Input value={"0"} type="number" step={2}/>);
+        const downSpinner = getByTestId("stepper-down-spinner");
+        const elt = getByDisplayValue("0") as HTMLInputElement;
+        await user.keyboard('[ShiftLeft>]');
+        await user.click(downSpinner);
+        expect(elt.value).toBe("-20");
+    })
+    it("Validate increment when holding shift key and arrow up", async () => {
+        const user = userEvent.setup();
+        const {getByDisplayValue} = render(<Input value={"0"} type="number" step={2}/>);
+        const elt = getByDisplayValue("0") as HTMLInputElement;
+        await user.click(elt);
+        await user.keyboard('[ShiftLeft>]');
+        await user.keyboard('[ArrowUp]');
+        expect(elt.value).toBe("20");
+    })
 });

+ 37 - 23
frontend/taipy-gui/src/components/Taipy/Input.tsx

@@ -14,7 +14,7 @@
 import React, { useState, useEffect, useCallback, useRef, KeyboardEvent, useMemo } from "react";
 import TextField from "@mui/material/TextField";
 import Tooltip from "@mui/material/Tooltip";
-import { styled } from "@mui/material/styles";
+import Stack from "@mui/material/Stack";
 import IconButton from "@mui/material/IconButton";
 import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp";
 import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
@@ -25,15 +25,6 @@ import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../..
 
 const AUTHORIZED_KEYS = ["Enter", "Escape", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"];
 
-const StyledTextField = styled(TextField)({
-    "& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button": {
-        display: "none",
-    },
-    "& input[type=number]": {
-        MozAppearance: "textfield",
-    },
-});
-
 const getActionKeys = (keys?: string): string[] => {
     const ak = (
         keys
@@ -100,21 +91,21 @@ const Input = (props: TaipyInputProps) => {
                 if (evt.key === "ArrowUp") {
                     let val =
                         Number(evt.currentTarget.querySelector("input")?.value || 0) +
-                        (step || 1) * (stepMultiplier || 10) -
-                        (step || 1);
+                        (step || 1) * (stepMultiplier || 10);
                     if (max !== undefined && val > max) {
-                        val = max - (step || 1);
+                        val = max;
                     }
                     setValue(val.toString());
+                    evt.preventDefault();
                 } else if (evt.key === "ArrowDown") {
                     let val =
                         Number(evt.currentTarget.querySelector("input")?.value || 0) -
-                        (step || 1) * (stepMultiplier || 10) +
-                        (step || 1);
+                        (step || 1) * (stepMultiplier || 10);
                     if (min !== undefined && val < min) {
-                        val = min + (step || 1);
+                        val = min;
                     }
                     setValue(val.toString());
+                    evt.preventDefault();
                 }
             } else if (!evt.shiftKey && !evt.ctrlKey && !evt.altKey && actionKeys.includes(evt.key)) {
                 const val = evt.currentTarget.querySelector("input")?.value;
@@ -206,7 +197,16 @@ const Input = (props: TaipyInputProps) => {
 
     return (
         <Tooltip title={hover || ""}>
-            <StyledTextField
+            <TextField
+                sx={{
+                    "& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button":
+                        {
+                            display: "none",
+                        },
+                    "& input[type=number]": {
+                        MozAppearance: "textfield",
+                    },
+                }}
                 margin="dense"
                 hiddenLabel
                 value={value ?? ""}
@@ -226,14 +226,28 @@ const Input = (props: TaipyInputProps) => {
                     type !== "text"
                         ? {
                               endAdornment: (
-                                  <>
-                                      <IconButton size="small" onMouseDown={handleUpStepperMouseDown}>
-                                          <ArrowDropUpIcon />
+                                  <div
+                                      style={{
+                                          display: "flex",
+                                          flexDirection: "column",
+                                          gap: 0,
+                                      }}
+                                  >
+                                      <IconButton
+                                          data-testid="stepper-up-spinner"
+                                          size="small"
+                                          onMouseDown={handleUpStepperMouseDown}
+                                      >
+                                          <ArrowDropUpIcon fontSize="inherit" />
                                       </IconButton>
-                                      <IconButton size="small" onMouseDown={handleDownStepperMouseDown}>
-                                          <ArrowDropDownIcon />
+                                      <IconButton
+                                          data-testid="stepper-down-spinner"
+                                          size="small"
+                                          onMouseDown={handleDownStepperMouseDown}
+                                      >
+                                          <ArrowDropDownIcon fontSize="inherit" />
                                       </IconButton>
-                                  </>
+                                  </div>
                               ),
                           }
                         : {}