Ver Fonte

Merge branch 'develop' into test/Alert

Nam Nguyen há 10 meses atrás
pai
commit
fee08b5e88

+ 5 - 4
frontend/taipy-gui/src/components/Taipy/FileSelector.spec.tsx

@@ -34,7 +34,7 @@ describe("FileSelector Component", () => {
     it("displays the right info for string", async () => {
         const { getByText } = render(<FileSelector label="toto" defaultLabel="titi" className="taipy-file-selector" />);
         const elt = getByText("toto");
-        expect(elt.parentElement).toHaveClass("taipy-file-selector");
+        expect(elt.parentElement?.parentElement).toHaveClass("taipy-file-selector");
     });
     it("displays the default value", async () => {
         const { getByText } = render(<FileSelector defaultLabel="titi" label={undefined as unknown as string} />);
@@ -44,6 +44,7 @@ describe("FileSelector Component", () => {
         const { getByText } = render(<FileSelector label="val" active={false} />);
         const elt = getByText("val");
         expect(elt).toHaveClass("Mui-disabled");
+        expect(elt.parentElement?.parentElement?.querySelector("input")).toBeDisabled();
     });
     it("is enabled by default", async () => {
         const { getByText } = render(<FileSelector label="val" />);
@@ -66,7 +67,7 @@ describe("FileSelector Component", () => {
             </TaipyContext.Provider>,
         );
         const elt = getByText("FileSelector");
-        const inputElt = elt.parentElement?.querySelector("input");
+        const inputElt = elt.parentElement?.parentElement?.querySelector("input");
         expect(inputElt).toBeInTheDocument();
         inputElt && (await userEvent.upload(inputElt, file));
         expect(dispatch).toHaveBeenCalledWith({
@@ -79,7 +80,7 @@ describe("FileSelector Component", () => {
         const file = new File(["(⌐□_□)"], "chucknorris2.png", { type: "image/png" });
         const { getByRole, getByText } = render(<FileSelector label="FileSelectorDrop" />);
         const elt = getByRole("button");
-        const inputElt = elt.parentElement?.querySelector("input");
+        const inputElt = elt.parentElement?.parentElement?.querySelector("input");
         expect(inputElt).toBeInTheDocument();
         waitFor(() => getByText("Drop here to Upload"));
         inputElt &&
@@ -95,7 +96,7 @@ describe("FileSelector Component", () => {
             <FileSelector label="FileSelectorDrop" dropMessage="drop here those files" />,
         );
         const elt = getByRole("button");
-        const inputElt = elt.parentElement?.querySelector("input");
+        const inputElt = elt.parentElement?.parentElement?.querySelector("input");
         expect(inputElt).toBeInTheDocument();
         waitFor(() => getByText("drop here those files"));
         inputElt &&

+ 22 - 19
frontend/taipy-gui/src/components/Taipy/FileSelector.tsx

@@ -78,25 +78,25 @@ const FileSelector = (props: FileSelectorProps) => {
                         onAction && dispatch(createSendActionNameAction(id, module, onAction));
                         notify &&
                             dispatch(
-                                createAlertAction({ atype: "success", message: value, system: false, duration: 3000 }),
+                                createAlertAction({ atype: "success", message: value, system: false, duration: 3000 })
                             );
                     },
                     (reason) => {
                         setUpload(false);
                         notify &&
                             dispatch(
-                                createAlertAction({ atype: "error", message: reason, system: false, duration: 3000 }),
+                                createAlertAction({ atype: "error", message: reason, system: false, duration: 3000 })
                             );
-                    },
+                    }
                 );
             }
         },
-        [state.id, id, onAction, notify, updateVarName, dispatch, module],
+        [state.id, id, onAction, notify, updateVarName, dispatch, module]
     );
 
     const handleChange = useCallback(
         (e: ChangeEvent<HTMLInputElement>) => handleFiles(e.target.files, e),
-        [handleFiles],
+        [handleFiles]
     );
 
     const handleDrop = useCallback(
@@ -105,7 +105,7 @@ const FileSelector = (props: FileSelectorProps) => {
             setDropSx(defaultSx);
             handleFiles(e.dataTransfer?.files, e);
         },
-        [handleFiles],
+        [handleFiles]
     );
 
     const handleDragLeave = useCallback(() => {
@@ -118,12 +118,12 @@ const FileSelector = (props: FileSelectorProps) => {
             console.log(evt);
             const target = evt.currentTarget as HTMLElement;
             setDropSx((sx) =>
-                sx.minWidth === defaultSx.minWidth && target ? { minWidth: target.clientWidth + "px" } : sx,
+                sx.minWidth === defaultSx.minWidth && target ? { minWidth: target.clientWidth + "px" } : sx
             );
             setDropLabel(dropMessage);
             handleDragOver(evt);
         },
-        [dropMessage],
+        [dropMessage]
     );
 
     useEffect(() => {
@@ -153,19 +153,22 @@ const FileSelector = (props: FileSelectorProps) => {
                 accept={extensions}
                 multiple={multiple}
                 onChange={handleChange}
+                disabled={!active || upload}
             />
             <Tooltip title={hover || ""}>
-                <Button
-                    id={id}
-                    component="span"
-                    aria-label="upload"
-                    variant="outlined"
-                    disabled={!active || upload}
-                    sx={dropSx}
-                    ref={butRef}
-                >
-                    <UploadFile /> {dropLabel || label || defaultLabel}
-                </Button>
+                <span>
+                    <Button
+                        id={id}
+                        component="span"
+                        aria-label="upload"
+                        variant="outlined"
+                        disabled={!active || upload}
+                        sx={dropSx}
+                        ref={butRef}
+                    >
+                        <UploadFile /> {dropLabel || label || defaultLabel}
+                    </Button>
+                </span>
             </Tooltip>
             {upload ? <LinearProgress value={progress} /> : null}
         </label>

+ 73 - 2
frontend/taipy-gui/src/components/Taipy/Image.spec.tsx

@@ -19,6 +19,9 @@ import userEvent from "@testing-library/user-event";
 import Image from "./Image";
 import { TaipyContext } from "../../context/taipyContext";
 import { TaipyState, INITIAL_STATE } from "../../context/taipyReducers";
+import axios from "axios";
+
+jest.mock("axios");
 
 describe("Image Component", () => {
     it("renders", async () => {
@@ -39,7 +42,7 @@ describe("Image Component", () => {
     });
     it("displays the default label", async () => {
         const { getByAltText } = render(
-            <Image defaultContent="/url/toto.png" defaultLabel="titi" label={undefined as unknown as string} />
+            <Image defaultContent="/url/toto.png" defaultLabel="titi" label={undefined as unknown as string} />,
         );
         getByAltText("titi");
     });
@@ -69,7 +72,7 @@ describe("Image Component", () => {
         const { getByRole } = render(
             <TaipyContext.Provider value={{ state, dispatch }}>
                 <Image defaultContent="/url/toto.png" onAction="on_action" />
-            </TaipyContext.Provider>
+            </TaipyContext.Provider>,
         );
         const elt = getByRole("button");
         await userEvent.click(elt);
@@ -79,4 +82,72 @@ describe("Image Component", () => {
             type: "SEND_ACTION_ACTION",
         });
     });
+    it("URL used when content prop is not provided", () => {
+        const { getByRole } = render(<Image defaultContent="/url/to/default/image.png" />);
+        const img = getByRole("img") as HTMLImageElement;
+        expect(img.src).toBe("http://localhost/url/to/default/image.png");
+    });
+    it("URL replaced when content prop is an empty string", () => {
+        const { getByRole } = render(<Image defaultContent="/url/to/default/image.png" content="" />);
+        const img = getByRole("img") as HTMLImageElement;
+        expect(img.src).toBe("http://localhost/");
+    });
+    it("URL replaced when content prop is a string of length less than 4", () => {
+        const { getByRole } = render(<Image defaultContent="/url/to/default/image.png" content="abc" />);
+        const img = getByRole("img") as HTMLImageElement;
+        expect(img.src).toBe("http://localhost/abc");
+    });
+    it("should return the content prop when it is a SVG file", async () => {
+        const svgContent = '<svg xmlns="http://www.w3.org/2000/svg"></svg>';
+        (axios.get as jest.Mock).mockResolvedValue({ data: svgContent });
+
+        const { container } = render(
+            <Image defaultContent="/url/to/default/image.png" content="/url/to/content/image.svg" />,
+        );
+
+        // Wait for useEffect to complete
+        await new Promise((resolve) => setTimeout(resolve, 0));
+
+        const svg = container.querySelector("svg");
+        expect(svg?.outerHTML.replace(/"/g, "'")).toBe(svgContent.replace(/"/g, "'"));
+    });
+    it("should return the content prop when it is inline SVG", () => {
+        const content = "<svg xmlns='http://www.w3.org/2000/svg'></svg>";
+        const { container } = render(<Image defaultContent="/url/to/default/image.png" content={content} />);
+        const svg = container.querySelector("svg");
+        expect(svg?.outerHTML.replace(/"/g, "'")).toBe(content);
+    });
+    it("should return the content prop in the general case", () => {
+        const { getByRole } = render(
+            <Image defaultContent="/url/to/default/image.png" content="/url/to/content/image.png" />,
+        );
+        const img = getByRole("img") as HTMLImageElement;
+        expect(img.src).toBe("http://localhost/url/to/content/image.png");
+    });
+    it("should render a div when content prop is a SVG file", async () => {
+        const svgContent = '<svg xmlns="http://www.w3.org/2000/svg"></svg>';
+        (axios.get as jest.Mock).mockResolvedValue({ data: svgContent });
+
+        const { container } = render(
+            <Image defaultContent="/url/to/default/image.png" content="/url/to/content/image.svg" />,
+        );
+        // Wait for useEffect to complete
+        await new Promise((resolve) => setTimeout(resolve, 0));
+
+        const div = container.querySelector("div");
+        expect(div).toBeInTheDocument();
+    });
+    it("should render a div when content prop is inline SVG", () => {
+        const content = "<svg xmlns='http://www.w3.org/2000/svg'></svg>";
+        const { container } = render(<Image defaultContent="/url/to/default/image.png" content={content} />);
+        const div = container.querySelector("div");
+        expect(div).toBeInTheDocument();
+    });
+    it("should render an img when content prop is not SVG", () => {
+        const { getByRole } = render(
+            <Image defaultContent="/url/to/default/image.png" content="/url/to/content/image.png" />,
+        );
+        const img = getByRole("img") as HTMLImageElement;
+        expect(img).toBeInTheDocument();
+    });
 });

+ 26 - 21
frontend/taipy-gui/src/components/Taipy/Image.tsx

@@ -58,12 +58,15 @@ const Image = (props: ImageProps) => {
         return [undefined, undefined, false];
     }, [content]);
 
-    const style = useMemo(() => ({
-        width: width,
-        height: height,
-        display: inlineSvg ? "inline-flex" : undefined,
-        verticalAlign: inlineSvg ? "middle" : undefined
-    }), [width, height, inlineSvg]);
+    const style = useMemo(
+        () => ({
+            width: width,
+            height: height,
+            display: inlineSvg ? "inline-flex" : undefined,
+            verticalAlign: inlineSvg ? "middle" : undefined,
+        }),
+        [width, height, inlineSvg],
+    );
 
     useEffect(() => {
         if (svg) {
@@ -76,21 +79,23 @@ const Image = (props: ImageProps) => {
     return (
         <Tooltip title={hover || label}>
             {onAction ? (
-                <Button
-                    id={id}
-                    className={className}
-                    onClick={handleClick}
-                    aria-label={label}
-                    variant="outlined"
-                    disabled={!active}
-                    title={label}
-                >
-                    {inlineSvg ? (
-                        <div ref={divRef} style={style} />
-                    ) : (
-                        <img src={content} style={style} alt={label} />
-                    )}
-                </Button>
+                <span>
+                    <Button
+                        id={id}
+                        className={className}
+                        onClick={handleClick}
+                        aria-label={label}
+                        variant="outlined"
+                        disabled={!active}
+                        title={label}
+                    >
+                        {inlineSvg ? (
+                            <div ref={divRef} style={style} />
+                        ) : (
+                            <img src={content} style={style} alt={label} />
+                        )}
+                    </Button>
+                </span>
             ) : inlineSvg ? (
                 <div id={id} className={className} style={style} ref={divRef} title={label}></div>
             ) : (