Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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",
Expand All @@ -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 <DynamicComponent config={imageConfig} />;
}
```

## 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/)

- [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/)
2 changes: 2 additions & 0 deletions src/components/DynamicComponents.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
43 changes: 43 additions & 0 deletions src/components/ImageComponent.tsx
Original file line number Diff line number Diff line change
@@ -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<ImageComponentProps> = ({
id,
image,
title,
className,
}) => {
const [imageError, setImageError] = useState(false);

return (
<Card id={id} className={className}>
<CardHeader>
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardBody>
{image && !imageError ? (
<img
src={image}
alt={title}
className="image-component-img"
onError={() => setImageError(true)}
/>
) : (
<div className="image-component-placeholder">
{imageError ? "Image failed to load" : "No image provided"}
</div>
)}
</CardBody>
</Card>
);
};

export default ImageComponent;
2 changes: 2 additions & 0 deletions src/constants/componentsMap.ts
Original file line number Diff line number Diff line change
@@ -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,
};
18 changes: 18 additions & 0 deletions src/global.css
Original file line number Diff line number Diff line change
@@ -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);
}
213 changes: 213 additions & 0 deletions src/test/components/ImageComponent.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<ImageComponent {...defaultProps} />);

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(<ImageComponent {...defaultProps} />);

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(<ImageComponent {...defaultProps} />);

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(
<ImageComponent
{...defaultProps}
id={customId}
className={customClassName}
/>
);

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(<ImageComponent {...defaultProps} image={null} />);

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(<ImageComponent {...propsWithoutImage} />);

expect(screen.queryByRole("img")).not.toBeInTheDocument();
expect(screen.getByText("No image provided")).toBeInTheDocument();
});

it("renders placeholder when image is empty string", () => {
render(<ImageComponent {...defaultProps} image="" />);

expect(screen.queryByRole("img")).not.toBeInTheDocument();
expect(screen.getByText("No image provided")).toBeInTheDocument();
});

it("handles image load error", () => {
render(<ImageComponent {...defaultProps} />);

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(<ImageComponent {...defaultProps} image={testImageUrl} />);

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(<ImageComponent {...defaultProps} title={testTitle} />);

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(<ImageComponent {...defaultProps} id={testId} />);

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(<ImageComponent {...defaultProps} title={longTitle} />);

expect(screen.getByText(longTitle)).toBeInTheDocument();
});

it("handles special characters in title", () => {
const specialTitle = "Movie Title with Special Characters: @#$%^&*()";

render(<ImageComponent {...defaultProps} title={specialTitle} />);

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(<ImageComponent {...defaultProps} />);

// 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(<ImageComponent {...defaultProps} image={null} />);
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(<ImageComponent {...defaultProps} />);

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");
});
});