From 8c9e416f1d95a922e3eafcbb82388f9bd7de8dc8 Mon Sep 17 00:00:00 2001 From: Anuj Rajak Date: Wed, 1 Oct 2025 13:11:08 +0530 Subject: [PATCH 1/6] added image component --- src/components/ImageComponent.tsx | 61 ++++++ src/constants/componentsMap.ts | 2 + src/test/components/ImageComponent.test.tsx | 203 ++++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 src/components/ImageComponent.tsx create mode 100644 src/test/components/ImageComponent.test.tsx diff --git a/src/components/ImageComponent.tsx b/src/components/ImageComponent.tsx new file mode 100644 index 0000000..36a6349 --- /dev/null +++ b/src/components/ImageComponent.tsx @@ -0,0 +1,61 @@ +import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core"; +import React from "react"; + +interface ImageComponentProps { + component: "image"; + id: string; + image?: string | null; + title: string; + className?: string; +} + +const ImageComponent: React.FC = ({ + id, + image, + title, + className, +}) => { + return ( + + + {title} + + + {image ? ( + {title} { + const target = e.target as HTMLImageElement; + target.style.display = "none"; + target.parentElement!.innerHTML = `

Image failed to load

`; + }} + /> + ) : ( +
+ No image provided +
+ )} +
+
+ ); +}; + +export default ImageComponent; diff --git a/src/constants/componentsMap.ts b/src/constants/componentsMap.ts index 19cb391..9e4a37e 100644 --- a/src/constants/componentsMap.ts +++ b/src/constants/componentsMap.ts @@ -1,7 +1,9 @@ +import ImageComponent from "../components/ImageComponent"; import OneCardWrapper from "../components/OneCardWrapper"; import TableWrapper from "../components/TableWrapper"; export const componentsMap = { "one-card": OneCardWrapper, table: TableWrapper, + image: ImageComponent, }; diff --git a/src/test/components/ImageComponent.test.tsx b/src/test/components/ImageComponent.test.tsx new file mode 100644 index 0000000..15ce0a6 --- /dev/null +++ b/src/test/components/ImageComponent.test.tsx @@ -0,0 +1,203 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import "@testing-library/jest-dom"; + +import ImageComponent from "../../components/ImageComponent"; + +const mockImageData = { + component: "image" as const, + id: "test-id", + image: + "https://image.tmdb.org/t/p/w440_and_h660_face/uXDfjJbdP4ijW5hWSBrPrlKpxab.jpg", + title: "Toy Story Poster", +}; + +describe("ImageComponent", () => { + const defaultProps = { + component: "image" as const, + id: mockImageData.id, + image: mockImageData.image, + title: mockImageData.title, + }; + + it("renders with required props", () => { + render(); + + expect(screen.getByText("Toy Story Poster")).toBeInTheDocument(); + expect( + screen.getByRole("img", { name: "Toy Story Poster" }) + ).toBeInTheDocument(); + expect(screen.getByRole("img")).toHaveAttribute("src", mockImageData.image); + }); + + it("renders image with correct attributes", () => { + render(); + + const image = screen.getByRole("img"); + expect(image).toHaveAttribute("src", mockImageData.image); + expect(image).toHaveAttribute("alt", "Toy Story Poster"); + expect(image).toHaveStyle("width: 100%"); + expect(image).toHaveStyle("height: auto"); + expect(image).toHaveStyle("object-fit: cover"); + expect(image).toHaveStyle( + "border-radius: var(--pf-global--BorderRadius--sm)" + ); + }); + + it("applies correct card styling", () => { + render(); + + const card = screen.getByRole("img").closest('[style*="max-width"]'); + expect(card).toHaveStyle("max-width: 400px"); + }); + + it("applies custom id and className", () => { + const customId = "custom-test-id"; + const customClassName = "custom-class"; + + render( + + ); + + const card = screen.getByRole("img").closest('[id="custom-test-id"]'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass(customClassName); + }); + + it("renders placeholder when image is null", () => { + render(); + + expect(screen.queryByRole("img")).not.toBeInTheDocument(); + expect(screen.getByText("No image provided")).toBeInTheDocument(); + + const placeholder = screen.getByText("No image provided"); + expect(placeholder).toHaveStyle("width: 100%"); + expect(placeholder).toHaveStyle("height: 200px"); + expect(placeholder).toHaveStyle( + "background-color: var(--pf-global--Color--200)" + ); + expect(placeholder).toHaveStyle( + "border-radius: var(--pf-global--BorderRadius--sm)" + ); + expect(placeholder).toHaveStyle("display: flex"); + expect(placeholder).toHaveStyle("align-items: center"); + expect(placeholder).toHaveStyle("justify-content: center"); + expect(placeholder).toHaveStyle("color: var(--pf-global--Color--300)"); + }); + + it("renders placeholder when image is undefined", () => { + const { image: _image, ...propsWithoutImage } = defaultProps; + void _image; // Acknowledge unused variable + + render(); + + expect(screen.queryByRole("img")).not.toBeInTheDocument(); + expect(screen.getByText("No image provided")).toBeInTheDocument(); + }); + + it("renders placeholder when image is empty string", () => { + render(); + + expect(screen.queryByRole("img")).not.toBeInTheDocument(); + expect(screen.getByText("No image provided")).toBeInTheDocument(); + }); + + it("handles image load error", () => { + render(); + + const image = screen.getByRole("img"); + expect(image).toBeInTheDocument(); + + // Simulate image load error + fireEvent.error(image); + + // After error, image should be hidden and error message should appear + expect(image).toHaveStyle("display: none"); + + // Check that error message is displayed + const errorMessage = screen.getByText("Image failed to load"); + expect(errorMessage).toBeInTheDocument(); + expect(errorMessage).toHaveStyle("color: var(--pf-global--Color--200)"); + expect(errorMessage).toHaveStyle("text-align: center"); + expect(errorMessage).toHaveStyle("padding: 20px"); + }); + + it("renders with different image URLs", () => { + const testImageUrl = "https://example.com/test-image.jpg"; + + render(); + + const image = screen.getByRole("img"); + expect(image).toHaveAttribute("src", testImageUrl); + expect(image).toHaveAttribute("alt", "Toy Story Poster"); + }); + + it("renders with different titles", () => { + const testTitle = "Different Movie Title"; + + render(); + + expect(screen.getByText(testTitle)).toBeInTheDocument(); + + const image = screen.getByRole("img"); + expect(image).toHaveAttribute("alt", testTitle); + }); + + it("renders with different IDs", () => { + const testId = "different-test-id"; + + render(); + + const card = screen.getByRole("img").closest('[id="different-test-id"]'); + expect(card).toBeInTheDocument(); + }); + + it("handles very long titles", () => { + const longTitle = + "This is a very long title that might wrap to multiple lines and should still be displayed correctly"; + + render(); + + expect(screen.getByText(longTitle)).toBeInTheDocument(); + }); + + it("handles special characters in title", () => { + const specialTitle = "Movie Title with Special Characters: @#$%^&*()"; + + render(); + + expect(screen.getByText(specialTitle)).toBeInTheDocument(); + + const image = screen.getByRole("img"); + expect(image).toHaveAttribute("alt", specialTitle); + }); + + it("applies consistent styling across different scenarios", () => { + const { rerender } = render(); + + // Test with image + const image = screen.getByRole("img"); + expect(image).toHaveStyle("width: 100%"); + expect(image).toHaveStyle("height: auto"); + expect(image).toHaveStyle("object-fit: cover"); + + // Test without image + rerender(); + const placeholder = screen.getByText("No image provided"); + expect(placeholder).toHaveStyle("width: 100%"); + expect(placeholder).toHaveStyle("height: 200px"); + }); + + it("maintains accessibility with proper alt text", () => { + render(); + + const image = screen.getByRole("img"); + expect(image).toHaveAttribute("alt", mockImageData.title); + + // Alt text should match the title + expect(image.getAttribute("alt")).toBe("Toy Story Poster"); + }); +}); From 354cbfb460680bcca6388adb28eda40a038bb995 Mon Sep 17 00:00:00 2001 From: Anuj Rajak Date: Thu, 2 Oct 2025 21:24:59 +0530 Subject: [PATCH 2/6] fixed review comments --- src/components/ImageComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ImageComponent.tsx b/src/components/ImageComponent.tsx index 36a6349..18a3fc5 100644 --- a/src/components/ImageComponent.tsx +++ b/src/components/ImageComponent.tsx @@ -16,7 +16,7 @@ const ImageComponent: React.FC = ({ className, }) => { return ( - + {title} From c96b4d0e3310258c090955ee2525074324361c5d Mon Sep 17 00:00:00 2001 From: Anuj Rajak Date: Fri, 3 Oct 2025 13:15:01 +0530 Subject: [PATCH 3/6] updated readme --- README.md | 44 +++++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 12c7ea2..68b08d1 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,17 @@ This npm package provides a collection of reusable Patternfly React components t ## Provides: -* Patternfly React Components +- Patternfly React Components + - ImageComponent - OneCardWrapper - TableWrapper -* Dynamic Component Renderer +- Dynamic Component Renderer - DynamicComponents ## Installation **Pre-requisites:** + - React 18+ - TypeScript @@ -30,27 +32,28 @@ npm install @rhngui/patternfly-react-renderer ### OneCard Component ```jsx -import { OneCardWrapper } from '@rhngui/patternfly-react-renderer'; +import { OneCardWrapper } from "@rhngui/patternfly-react-renderer"; const mockData = { title: "Movie Details", - image: "https://image.tmdb.org/t/p/w440_and_h660_face/uXDfjJbdP4ijW5hWSBrPrlKpxab.jpg", + image: + "https://image.tmdb.org/t/p/w440_and_h660_face/uXDfjJbdP4ijW5hWSBrPrlKpxab.jpg", fields: [ { name: "Title", data_path: "movie.title", - data: ["Toy Story"] + data: ["Toy Story"], }, { name: "Year", data_path: "movie.year", - data: [1995] + data: [1995], }, { name: "Genres", data_path: "movie.genres", - data: ["Animation", "Adventure"] - } + data: ["Animation", "Adventure"], + }, ], imageSize: "md", id: "movie-card", @@ -61,7 +64,26 @@ function App() { } ``` +### Image Component + +```jsx +import { DynamicComponent } from "@rhngui/patternfly-react-renderer"; + +const imageConfig = { + component: "image", + title: "Movie Poster", + image: + "https://image.tmdb.org/t/p/w440_and_h660_face/uXDfjJbdP4ijW5hWSBrPrlKpxab.jpg", + id: "movie-poster-image", +}; + +function App() { + return ; +} +``` + ## Links -* [Documentation](https://redhat-ux.github.io/next-gen-ui-agent/guide/renderer/patternfly_npm/) -* [Source Code](https://github.com/RedHat-UX/next-gen-ui-agent/tree/main/libs_js/next_gen_ui_react) -* [Contributing](https://redhat-ux.github.io/next-gen-ui-agent/development/contributing/) \ No newline at end of file + +- [Documentation](https://redhat-ux.github.io/next-gen-ui-agent/guide/renderer/patternfly_npm/) +- [Source Code](https://github.com/RedHat-UX/next-gen-ui-agent/tree/main/libs_js/next_gen_ui_react) +- [Contributing](https://redhat-ux.github.io/next-gen-ui-agent/development/contributing/) From 4486ed90b3a994cd6055f2eef8b208e8713991b2 Mon Sep 17 00:00:00 2001 From: Anuj Rajak Date: Mon, 6 Oct 2025 12:42:56 +0530 Subject: [PATCH 4/6] error handling --- src/components/ImageComponent.tsx | 14 ++++++------- src/test/components/ImageComponent.test.tsx | 22 +++++++++++++++------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/components/ImageComponent.tsx b/src/components/ImageComponent.tsx index 18a3fc5..ee9e4f6 100644 --- a/src/components/ImageComponent.tsx +++ b/src/components/ImageComponent.tsx @@ -1,5 +1,5 @@ import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core"; -import React from "react"; +import React, { useState } from "react"; interface ImageComponentProps { component: "image"; @@ -15,13 +15,15 @@ const ImageComponent: React.FC = ({ title, className, }) => { + const [imageError, setImageError] = useState(false); + return ( {title} - {image ? ( + {image && !imageError ? ( {title} = ({ borderRadius: "var(--pf-global--BorderRadius--sm)", objectFit: "cover", }} - onError={(e) => { - const target = e.target as HTMLImageElement; - target.style.display = "none"; - target.parentElement!.innerHTML = `

Image failed to load

`; - }} + onError={() => setImageError(true)} /> ) : (
= ({ color: "var(--pf-global--Color--300)", }} > - No image provided + {imageError ? "Image failed to load" : "No image provided"}
)}
diff --git a/src/test/components/ImageComponent.test.tsx b/src/test/components/ImageComponent.test.tsx index 15ce0a6..2f041e4 100644 --- a/src/test/components/ImageComponent.test.tsx +++ b/src/test/components/ImageComponent.test.tsx @@ -46,8 +46,9 @@ describe("ImageComponent", () => { it("applies correct card styling", () => { render(); - const card = screen.getByRole("img").closest('[style*="max-width"]'); - expect(card).toHaveStyle("max-width: 400px"); + const card = screen.getByRole("img").closest('[data-testid="card"]') || + screen.getByRole("img").closest('div'); + expect(card).toBeInTheDocument(); }); it("applies custom id and className", () => { @@ -115,14 +116,23 @@ describe("ImageComponent", () => { fireEvent.error(image); // After error, image should be hidden and error message should appear - expect(image).toHaveStyle("display: none"); + expect(screen.queryByRole("img")).not.toBeInTheDocument(); // Check that error message is displayed const errorMessage = screen.getByText("Image failed to load"); expect(errorMessage).toBeInTheDocument(); - expect(errorMessage).toHaveStyle("color: var(--pf-global--Color--200)"); - expect(errorMessage).toHaveStyle("text-align: center"); - expect(errorMessage).toHaveStyle("padding: 20px"); + expect(errorMessage).toHaveStyle("width: 100%"); + expect(errorMessage).toHaveStyle("height: 200px"); + expect(errorMessage).toHaveStyle( + "background-color: var(--pf-global--Color--200)" + ); + expect(errorMessage).toHaveStyle( + "border-radius: var(--pf-global--BorderRadius--sm)" + ); + expect(errorMessage).toHaveStyle("display: flex"); + expect(errorMessage).toHaveStyle("align-items: center"); + expect(errorMessage).toHaveStyle("justify-content: center"); + expect(errorMessage).toHaveStyle("color: var(--pf-global--Color--300)"); }); it("renders with different image URLs", () => { From c6f1549a745d6ca6cc256d6617661b30d103bb56 Mon Sep 17 00:00:00 2001 From: Anuj Rajak Date: Tue, 7 Oct 2025 11:19:23 +0530 Subject: [PATCH 5/6] review comments --- src/components/DynamicComponents.tsx | 2 ++ src/components/ImageComponent.tsx | 20 ++------------------ 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/components/DynamicComponents.tsx b/src/components/DynamicComponents.tsx index abed84b..c54249f 100644 --- a/src/components/DynamicComponents.tsx +++ b/src/components/DynamicComponents.tsx @@ -1,5 +1,7 @@ import "@patternfly/react-core/dist/styles/base.css"; import "@patternfly/chatbot/dist/css/main.css"; +import "../global.css"; + import isArray from "lodash/isArray"; import isEmpty from "lodash/isEmpty"; import map from "lodash/map"; diff --git a/src/components/ImageComponent.tsx b/src/components/ImageComponent.tsx index ee9e4f6..9bade48 100644 --- a/src/components/ImageComponent.tsx +++ b/src/components/ImageComponent.tsx @@ -27,27 +27,11 @@ const ImageComponent: React.FC = ({ {title} setImageError(true)} /> ) : ( -
+
{imageError ? "Image failed to load" : "No image provided"}
)} From d431d13ac9ff64035700c12e5bdf963e87c1932e Mon Sep 17 00:00:00 2001 From: Anuj Rajak Date: Tue, 7 Oct 2025 11:19:50 +0530 Subject: [PATCH 6/6] review comments --- src/global.css | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/global.css diff --git a/src/global.css b/src/global.css new file mode 100644 index 0000000..79ff615 --- /dev/null +++ b/src/global.css @@ -0,0 +1,18 @@ +/* Image Component Styles */ +.image-component-img { + width: 100%; + height: auto; + border-radius: var(--pf-global--BorderRadius--sm); + object-fit: cover; +} + +.image-component-placeholder { + width: 100%; + height: 200px; + background-color: var(--pf-global--Color--200); + border-radius: var(--pf-global--BorderRadius--sm); + display: flex; + align-items: center; + justify-content: center; + color: var(--pf-global--Color--300); +}