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/) 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 new file mode 100644 index 0000000..9bade48 --- /dev/null +++ b/src/components/ImageComponent.tsx @@ -0,0 +1,43 @@ +import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core"; +import React, { useState } from "react"; + +interface ImageComponentProps { + component: "image"; + id: string; + image?: string | null; + title: string; + className?: string; +} + +const ImageComponent: React.FC = ({ + id, + image, + title, + className, +}) => { + const [imageError, setImageError] = useState(false); + + return ( + + + {title} + + + {image && !imageError ? ( + {title} setImageError(true)} + /> + ) : ( +
+ {imageError ? "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/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); +} diff --git a/src/test/components/ImageComponent.test.tsx b/src/test/components/ImageComponent.test.tsx new file mode 100644 index 0000000..2f041e4 --- /dev/null +++ b/src/test/components/ImageComponent.test.tsx @@ -0,0 +1,213 @@ +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('[data-testid="card"]') || + screen.getByRole("img").closest('div'); + expect(card).toBeInTheDocument(); + }); + + 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(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("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", () => { + 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"); + }); +});