Skip to content

Commit 9cc429e

Browse files
authored
Merge pull request #2443 from dxc-technology/PelayoFelgueroso/theme-generator_test
[doc] Test Theme Generator
2 parents b63aae9 + ccb9cf1 commit 9cc429e

14 files changed

Lines changed: 1027 additions & 2 deletions

apps/website/eslint.config.mjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import nextPlugin from "@next/eslint-plugin-next";
22
import nextConfig from "@dxc-technology/eslint-config/next.js";
33
import js from "@eslint/js";
4+
import jest from "eslint-plugin-jest";
5+
import globals from "globals";
46
import path from "node:path";
57
import { fileURLToPath } from "node:url";
68

@@ -19,6 +21,20 @@ export default [
1921
tsconfigRootDir: __dirname,
2022
tsconfigName: "tsconfig.lint.json",
2123
}),
24+
{
25+
files: ["**/*.test.{ts,tsx,js,jsx}"],
26+
plugins: { jest },
27+
rules: {
28+
...jest.configs.recommended.rules,
29+
"jest/no-commented-out-tests": "off",
30+
},
31+
languageOptions: {
32+
globals: {
33+
...globals.jest,
34+
...globals.node,
35+
},
36+
},
37+
},
2238
{
2339
ignores: ["out/**", ".next/**"],
2440
},

apps/website/jest.config.mjs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import nextJest from "next/jest.js";
2+
3+
const createJestConfig = nextJest({
4+
dir: "./",
5+
});
6+
7+
const customJestConfig = {
8+
testEnvironment: "jsdom",
9+
collectCoverage: true,
10+
coveragePathIgnorePatterns: [
11+
"utils.ts",
12+
"index.ts",
13+
"test/mocks",
14+
".*Context\\.tsx$",
15+
],
16+
moduleNameMapper: {
17+
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
18+
"\\.(svg)$": "<rootDir>/test/mocks/svgMock.ts",
19+
"\\.(png)$": "<rootDir>/test/mocks/pngMock.ts",
20+
"^screens/(.*)$": "<rootDir>/screens/$1",
21+
"^hooks/(.*)$": "<rootDir>/hooks/$1",
22+
"^@/(.*)$": "<rootDir>/$1",
23+
},
24+
testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)", "!**/?(*.)+(accessibility.)(spec|test).[jt]s?(x)"],
25+
};
26+
27+
export default createJestConfig(customJestConfig);

apps/website/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"dev": "next dev --webpack",
66
"build": "next build --webpack",
77
"start": "next start",
8-
"lint": "eslint . --max-warnings 0"
8+
"lint": "eslint . --max-warnings 0",
9+
"test": "jest --config=./jest.config.mjs"
910
},
1011
"dependencies": {
1112
"@adobe/leonardo-contrast-colors": "^1.0.0",
@@ -29,15 +30,20 @@
2930
"slugify": "^1.6.5"
3031
},
3132
"devDependencies": {
33+
"@testing-library/jest-dom": "^6.8.0",
34+
"@testing-library/react": "^16.3.0",
3235
"@dxc-technology/typescript-config": "*",
3336
"@eslint/js": "^9.31.1",
37+
"@types/jest": "^29.5.12",
3438
"@types/node": "^20",
3539
"@types/react": "^18",
3640
"@types/react-color": "^3.0.6",
3741
"@types/react-dom": "^18",
3842
"@types/webpack": "^5.28.5",
3943
"eslint": "^9.39.1",
4044
"eslint-config-next": "16.2.6",
45+
"jest": "^30.2.0",
46+
"jest-environment-jsdom": "^30.2.0",
4147
"typescript": "^5.6.3"
4248
}
4349
}

apps/website/screens/theme-generator/steps/ReviewDetails.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const ReviewDetails = ({ tokens, logos, themeJson }: { tokens: Tokens; logos: Lo
2323
mode="secondary"
2424
icon="content_copy"
2525
size={{ height: "medium" }}
26+
title="Copy theme"
2627
onClick={() => handleCopy(themeJson)}
2728
/>
2829
</div>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { renderHook } from "@testing-library/react";
2+
import { HalstackProvider } from "@dxc-technology/halstack-react";
3+
import useCopyToClipboard from "../../hooks/useCopyToClipboard";
4+
5+
describe("useCopyToClipboard", () => {
6+
const wrapper = ({ children }: { children: React.ReactNode }) => <HalstackProvider>{children}</HalstackProvider>;
7+
8+
let mockWriteText: jest.Mock;
9+
10+
beforeEach(() => {
11+
jest.clearAllMocks();
12+
13+
mockWriteText = jest.fn();
14+
Object.assign(navigator, {
15+
clipboard: {
16+
writeText: mockWriteText,
17+
},
18+
});
19+
});
20+
21+
afterEach(() => {
22+
jest.restoreAllMocks();
23+
});
24+
25+
it("should return a function", () => {
26+
const { result } = renderHook(() => useCopyToClipboard(), { wrapper });
27+
expect(typeof result.current).toBe("function");
28+
});
29+
30+
it("should copy text to clipboard successfully", async () => {
31+
const { result } = renderHook(() => useCopyToClipboard(), { wrapper });
32+
const testText = "Hello World";
33+
34+
mockWriteText.mockResolvedValue(undefined);
35+
36+
result.current(testText);
37+
38+
expect(mockWriteText).toHaveBeenCalledWith(testText);
39+
40+
await new Promise((resolve) => setTimeout(resolve, 0));
41+
});
42+
43+
it("should handle errors when copying to clipboard", async () => {
44+
const { result } = renderHook(() => useCopyToClipboard(), { wrapper });
45+
const testText = "Test Error";
46+
const error = new Error("Clipboard error");
47+
48+
mockWriteText.mockRejectedValue(error);
49+
50+
result.current(testText);
51+
52+
expect(mockWriteText).toHaveBeenCalledWith(testText);
53+
54+
await new Promise((resolve) => setTimeout(resolve, 0));
55+
});
56+
57+
it("should call navigator.clipboard.writeText with the correct text", () => {
58+
const { result } = renderHook(() => useCopyToClipboard(), { wrapper });
59+
const testTexts = ["Text 1", "Example code", ""];
60+
61+
mockWriteText.mockResolvedValue(undefined);
62+
63+
for (const text of testTexts) {
64+
result.current(text);
65+
expect(mockWriteText).toHaveBeenCalledWith(text);
66+
}
67+
});
68+
69+
it("should work with multiple consecutive calls", () => {
70+
const { result } = renderHook(() => useCopyToClipboard(), { wrapper });
71+
72+
mockWriteText.mockResolvedValue(undefined);
73+
74+
result.current("First copy");
75+
result.current("Second copy");
76+
77+
expect(mockWriteText).toHaveBeenCalledTimes(2);
78+
expect(mockWriteText).toHaveBeenNthCalledWith(1, "First copy");
79+
expect(mockWriteText).toHaveBeenNthCalledWith(2, "Second copy");
80+
});
81+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import "@testing-library/jest-dom";
2+
import { fireEvent, render, screen } from "@testing-library/react";
3+
import BottomButtons from "../../screens/theme-generator/components/BottomButtons";
4+
5+
describe("BottomButtons", () => {
6+
it("shows Back disabled and Next enabled on first step", () => {
7+
const onChangeStep = jest.fn();
8+
const onExport = jest.fn();
9+
10+
render(<BottomButtons currentStep={0} onChangeStep={onChangeStep} onExport={onExport} />);
11+
12+
const backButton = screen.getByRole("button", { name: "Back" });
13+
const nextButton = screen.getByRole("button", { name: "Next" });
14+
15+
expect(backButton).toBeDisabled();
16+
expect(nextButton).toBeEnabled();
17+
18+
fireEvent.click(nextButton);
19+
expect(onChangeStep).toHaveBeenCalledWith(1);
20+
});
21+
22+
it("shows Export theme on last step and triggers export", () => {
23+
const onChangeStep = jest.fn();
24+
const onExport = jest.fn();
25+
26+
render(<BottomButtons currentStep={2} onChangeStep={onChangeStep} onExport={onExport} />);
27+
28+
const backButton = screen.getByRole("button", { name: "Back" });
29+
const exportButton = screen.getByRole("button", { name: "Export theme" });
30+
31+
fireEvent.click(backButton);
32+
expect(onChangeStep).toHaveBeenCalledWith(1);
33+
34+
fireEvent.click(exportButton);
35+
expect(onExport).toHaveBeenCalledTimes(1);
36+
});
37+
});
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import "@testing-library/jest-dom";
2+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3+
import BrandingDetails from "../../screens/theme-generator/steps/BrandingDetails";
4+
import { Colors, Logos } from "../../screens/theme-generator/types";
5+
6+
// Mock ResizeObserver
7+
global.ResizeObserver = jest.fn().mockImplementation(() => ({
8+
observe: jest.fn(),
9+
unobserve: jest.fn(),
10+
disconnect: jest.fn(),
11+
}));
12+
13+
// Mock SketchColorPicker to avoid react-color issues (Canvas/Context errors)
14+
jest.mock("react-color", () => ({
15+
SketchPicker: ({ color, onChange }: { color: string; onChange: (color: { hex: string }) => void }) => (
16+
<div data-testid="mock-sketch-picker">
17+
Picker for {color}
18+
<button onClick={() => onChange({ hex: "#ffffff" })}>Select White</button>
19+
</div>
20+
),
21+
}));
22+
23+
describe("BrandingDetails", () => {
24+
beforeAll(() => {
25+
global.URL.createObjectURL = jest.fn(() => "blob:mock-url");
26+
});
27+
28+
const colors: Colors = {
29+
primary: "#5F249F",
30+
secondary: "#0067B3",
31+
tertiary: "#F7CF2B",
32+
neutral: "#999999",
33+
info: "#0067B3",
34+
success: "#59D97D",
35+
error: "#FE344F",
36+
warning: "#F59F3D",
37+
};
38+
39+
it("renders color sections with ColorCard components", () => {
40+
const onColorsChange = jest.fn();
41+
const onLogosChange = jest.fn();
42+
43+
const logos: Logos = { mainLogo: [], footerLogo: [], footerReducedLogo: [], favicon: [] };
44+
45+
render(
46+
<BrandingDetails colors={colors} onColorsChange={onColorsChange} logos={logos} onLogosChange={onLogosChange} />
47+
);
48+
49+
expect(screen.getByDisplayValue("#5F249F")).toBeInTheDocument();
50+
expect(screen.getByText("Primary")).toBeInTheDocument();
51+
});
52+
53+
it("updates colors when input value changes", () => {
54+
const onColorsChange = jest.fn();
55+
const onLogosChange = jest.fn();
56+
57+
const logos: Logos = { mainLogo: [], footerLogo: [], footerReducedLogo: [], favicon: [] };
58+
59+
render(
60+
<BrandingDetails colors={colors} onColorsChange={onColorsChange} logos={logos} onLogosChange={onLogosChange} />
61+
);
62+
63+
const primaryInput = screen.getByDisplayValue("#5F249F");
64+
fireEvent.change(primaryInput, { target: { value: "#111111" } });
65+
fireEvent.blur(primaryInput);
66+
67+
expect(onColorsChange).toHaveBeenCalledWith(
68+
expect.objectContaining({
69+
primary: "#111111",
70+
})
71+
);
72+
});
73+
74+
it("renders logo sections with FileInput components", () => {
75+
const onColorsChange = jest.fn();
76+
const onLogosChange = jest.fn();
77+
78+
const logos: Logos = {
79+
mainLogo: [],
80+
footerLogo: [{ file: new File(["f"], "footer.png", { type: "image/png" }) }],
81+
footerReducedLogo: [],
82+
favicon: [],
83+
};
84+
85+
render(
86+
<BrandingDetails colors={colors} onColorsChange={onColorsChange} logos={logos} onLogosChange={onLogosChange} />
87+
);
88+
89+
expect(screen.getByText("Main logo")).toBeInTheDocument();
90+
expect(screen.getByText("Default footer logo")).toBeInTheDocument();
91+
});
92+
93+
it("updates logos when a file is selected", async () => {
94+
const onColorsChange = jest.fn();
95+
const onLogosChange = jest.fn();
96+
97+
const logos: Logos = { mainLogo: [], footerLogo: [], footerReducedLogo: [], favicon: [] };
98+
99+
render(
100+
<BrandingDetails colors={colors} onColorsChange={onColorsChange} logos={logos} onLogosChange={onLogosChange} />
101+
);
102+
103+
const file = new File(["logo content"], "main-logo.png", { type: "image/png" });
104+
const mainLogoInput = screen.getByLabelText("Main logo");
105+
fireEvent.change(mainLogoInput, { target: { files: [file] } });
106+
107+
await waitFor(() => {
108+
expect(onLogosChange).toHaveBeenCalled();
109+
const firstCallArguments = onLogosChange.mock.calls[0] as [Logos];
110+
const updatedLogos = firstCallArguments[0];
111+
expect(updatedLogos.mainLogo).toHaveLength(1);
112+
expect(updatedLogos.mainLogo[0]?.file).toBeInstanceOf(File);
113+
expect(updatedLogos.mainLogo[0]?.file.name).toBe("main-logo.png");
114+
});
115+
});
116+
});

0 commit comments

Comments
 (0)