Skip to content
Closed
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
16 changes: 16 additions & 0 deletions apps/website/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import nextPlugin from "@next/eslint-plugin-next";
import nextConfig from "@dxc-technology/eslint-config/next.js";
import js from "@eslint/js";
import jest from "eslint-plugin-jest";
import globals from "globals";
import path from "node:path";
import { fileURLToPath } from "node:url";

Expand All @@ -19,6 +21,20 @@ export default [
tsconfigRootDir: __dirname,
tsconfigName: "tsconfig.lint.json",
}),
{
files: ["**/*.test.{ts,tsx,js,jsx}"],
plugins: { jest },
rules: {
...jest.configs.recommended.rules,
"jest/no-commented-out-tests": "off",
},
languageOptions: {
globals: {
...globals.jest,
...globals.node,
},
},
},
{
ignores: ["out/**", ".next/**"],
},
Expand Down
27 changes: 27 additions & 0 deletions apps/website/jest.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import nextJest from "next/jest.js";

const createJestConfig = nextJest({
dir: "./",
});

const customJestConfig = {
testEnvironment: "jsdom",
collectCoverage: true,
coveragePathIgnorePatterns: [
"utils.ts",
"index.ts",
"test/mocks",
".*Context\\.tsx$",
],
moduleNameMapper: {
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
"\\.(svg)$": "<rootDir>/test/mocks/svgMock.ts",
"\\.(png)$": "<rootDir>/test/mocks/pngMock.ts",
"^screens/(.*)$": "<rootDir>/screens/$1",
"^hooks/(.*)$": "<rootDir>/hooks/$1",
"^@/(.*)$": "<rootDir>/$1",
},
testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)", "!**/?(*.)+(accessibility.)(spec|test).[jt]s?(x)"],
};

export default createJestConfig(customJestConfig);
8 changes: 7 additions & 1 deletion apps/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"dev": "next dev --webpack",
"build": "next build --webpack",
"start": "next start",
"lint": "eslint . --max-warnings 0"
"lint": "eslint . --max-warnings 0",
"test": "jest --config=./jest.config.mjs"
},
"dependencies": {
"@adobe/leonardo-contrast-colors": "^1.0.0",
Expand All @@ -29,14 +30,19 @@
"slugify": "^1.6.5"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@dxc-technology/typescript-config": "*",
"@eslint/js": "^9.31.1",
"@types/jest": "^29.5.12",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-color": "^3.0.6",
"@types/react-dom": "^18",
"eslint": "^9.39.1",
"eslint-config-next": "16.0.8",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"typescript": "^5.6.3"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { DxcContainer, DxcFlex, DxcWizard } from "@dxc-technology/halstack-react
import StepHeading from "./components/StepHeading";
import BottomButtons from "./components/BottomButtons";
import ThemeGeneratorPreviewPage from "./ThemeGeneratorPreviewPage";
// import { FileData } from "../../../../packages/lib/src/file-input/types";

import { BrandingDetails } from "./steps/BrandingDetails";
import { generateTokens, handleExport } from "./utils";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const ReviewDetails = ({ tokens, logos, themeJson }: { tokens: Tokens; logos: Lo
mode="secondary"
icon="content_copy"
size={{ height: "medium" }}
title="Copy theme"
onClick={() => handleCopy(themeJson)}
/>
</DxcFlex>
Expand Down
81 changes: 81 additions & 0 deletions apps/website/test/hooks/useCopyToClipboard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { renderHook } from "@testing-library/react";
import { HalstackProvider } from "@dxc-technology/halstack-react";
import useCopyToClipboard from "../../hooks/useCopyToClipboard";

describe("useCopyToClipboard", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => <HalstackProvider>{children}</HalstackProvider>;

let mockWriteText: jest.Mock;

beforeEach(() => {
jest.clearAllMocks();

mockWriteText = jest.fn();
Object.assign(navigator, {
clipboard: {
writeText: mockWriteText,
},
});
});

afterEach(() => {
jest.restoreAllMocks();
});

it("should return a function", () => {
const { result } = renderHook(() => useCopyToClipboard(), { wrapper });
expect(typeof result.current).toBe("function");
});

it("should copy text to clipboard successfully", async () => {
const { result } = renderHook(() => useCopyToClipboard(), { wrapper });
const testText = "Hello World";

mockWriteText.mockResolvedValue(undefined);

result.current(testText);

expect(mockWriteText).toHaveBeenCalledWith(testText);

await new Promise((resolve) => setTimeout(resolve, 0));
});

it("should handle errors when copying to clipboard", async () => {
const { result } = renderHook(() => useCopyToClipboard(), { wrapper });
const testText = "Test Error";
const error = new Error("Clipboard error");

mockWriteText.mockRejectedValue(error);

result.current(testText);

expect(mockWriteText).toHaveBeenCalledWith(testText);

await new Promise((resolve) => setTimeout(resolve, 0));
});

it("should call navigator.clipboard.writeText with the correct text", () => {
const { result } = renderHook(() => useCopyToClipboard(), { wrapper });
const testTexts = ["Text 1", "Example code", ""];

mockWriteText.mockResolvedValue(undefined);

for (const text of testTexts) {
result.current(text);
expect(mockWriteText).toHaveBeenCalledWith(text);
}
});

it("should work with multiple consecutive calls", () => {
const { result } = renderHook(() => useCopyToClipboard(), { wrapper });

mockWriteText.mockResolvedValue(undefined);

result.current("First copy");
result.current("Second copy");

expect(mockWriteText).toHaveBeenCalledTimes(2);
expect(mockWriteText).toHaveBeenNthCalledWith(1, "First copy");
expect(mockWriteText).toHaveBeenNthCalledWith(2, "Second copy");
});
});
37 changes: 37 additions & 0 deletions apps/website/test/theme-generator/BottomButtons.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import BottomButtons from "../../screens/theme-generator/components/BottomButtons";

describe("BottomButtons", () => {
it("shows Back disabled and Next enabled on first step", () => {
const onChangeStep = jest.fn();
const onExport = jest.fn();

render(<BottomButtons currentStep={0} onChangeStep={onChangeStep} onExport={onExport} />);

const backButton = screen.getByRole("button", { name: "Back" });
const nextButton = screen.getByRole("button", { name: "Next" });

expect(backButton).toBeDisabled();
expect(nextButton).toBeEnabled();

fireEvent.click(nextButton);
expect(onChangeStep).toHaveBeenCalledWith(1);
});

it("shows Export theme on last step and triggers export", () => {
const onChangeStep = jest.fn();
const onExport = jest.fn();

render(<BottomButtons currentStep={2} onChangeStep={onChangeStep} onExport={onExport} />);

const backButton = screen.getByRole("button", { name: "Back" });
const exportButton = screen.getByRole("button", { name: "Export theme" });

fireEvent.click(backButton);
expect(onChangeStep).toHaveBeenCalledWith(1);

fireEvent.click(exportButton);
expect(onExport).toHaveBeenCalledTimes(1);
});
});
114 changes: 114 additions & 0 deletions apps/website/test/theme-generator/BrandingDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { BrandingDetails } from "../../screens/theme-generator/steps/BrandingDetails";
import { Colors, Logos } from "../../screens/theme-generator/types";
import { CssColor } from "@adobe/leonardo-contrast-colors";

// Mock ResizeObserver
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));

// Mock SketchColorPicker to avoid react-color issues (Canvas/Context errors)
jest.mock("react-color", () => ({
SketchPicker: ({ color, onChange }: { color: string; onChange: (color: { hex: string }) => void }) => (
<div data-testid="mock-sketch-picker">
Picker for {color}
<button onClick={() => onChange({ hex: "#ffffff" })}>Select White</button>
</div>
),
}));

describe("BrandingDetails", () => {
it("renders color sections with ColorCard components", () => {
const onColorsChange = jest.fn();
const onLogosChange = jest.fn();

const colors: Colors = {
primary: "#5F249F" as CssColor,
secondary: "#0067B3" as CssColor,
tertiary: "#F7CF2B" as CssColor,
neutral: "#999999" as CssColor,
info: "#0067B3" as CssColor,
success: "#59D97D" as CssColor,
error: "#FE344F" as CssColor,
warning: "#F59F3D" as CssColor,
};

const logos: Logos = { mainLogo: [], footerLogo: [], footerReducedLogo: [], favicon: [] };

render(
<BrandingDetails colors={colors} onColorsChange={onColorsChange} logos={logos} onLogosChange={onLogosChange} />
);

expect(screen.getByDisplayValue("#5F249F")).toBeInTheDocument();
expect(screen.getByText("Primary")).toBeInTheDocument();
});

it("updates colors when input value changes", async () => {
const onColorsChange = jest.fn();
const onLogosChange = jest.fn();

const colors: Colors = {
primary: "#5F249F" as CssColor,
secondary: "#0067B3" as CssColor,
tertiary: "#F7CF2B" as CssColor,
neutral: "#999999" as CssColor,
info: "#0067B3" as CssColor,
success: "#59D97D" as CssColor,
error: "#FE344F" as CssColor,
warning: "#F59F3D" as CssColor,
};

const logos: Logos = { mainLogo: [], footerLogo: [], footerReducedLogo: [], favicon: [] };

render(
<BrandingDetails colors={colors} onColorsChange={onColorsChange} logos={logos} onLogosChange={onLogosChange} />
);

const primaryInput = screen.getByDisplayValue("#5F249F");
fireEvent.change(primaryInput, { target: { value: "#111111" } });
fireEvent.blur(primaryInput);

await waitFor(() => {
expect(onColorsChange).toHaveBeenCalledWith(
expect.objectContaining({
primary: "#111111",
secondary: "#0067B3",
})
);
});
});

it("renders logo sections with FileInput components", () => {
const onColorsChange = jest.fn();
const onLogosChange = jest.fn();

const colors: Colors = {
primary: "#5F249F" as CssColor,
secondary: "#0067B3" as CssColor,
tertiary: "#F7CF2B" as CssColor,
neutral: "#999999" as CssColor,
info: "#0067B3" as CssColor,
success: "#59D97D" as CssColor,
error: "#FE344F" as CssColor,
warning: "#F59F3D" as CssColor,
};

const logos: Logos = {
mainLogo: [],
footerLogo: [{ file: new File(["f"], "footer.png", { type: "image/png" }) }],
footerReducedLogo: [],
favicon: [],
};

render(
<BrandingDetails colors={colors} onColorsChange={onColorsChange} logos={logos} onLogosChange={onLogosChange} />
);

expect(screen.getByText("Main logo")).toBeInTheDocument();
expect(screen.getByText("Default footer logo")).toBeInTheDocument();
});
});
Loading
Loading