From a6397dd84e5752ba031ca3c7bb4052ba35c2d43f Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso Date: Mon, 16 Mar 2026 17:27:16 +0100 Subject: [PATCH 1/6] Add tests to theme generator --- apps/website/jest.config.mjs | 27 +++ apps/website/package.json | 8 +- .../ThemeGeneratorConfigPage.tsx | 3 +- .../components/BottomButtons.tsx | 1 + .../components/StepHeading.tsx | 1 + .../theme-generator/BottomButtons.test.tsx | 49 +++++ .../theme-generator/BrandingDetails.test.tsx | 126 ++++++++++++ .../theme-generator/ReviewDetails.test.tsx | 80 ++++++++ .../test/theme-generator/StepHeading.test.tsx | 25 +++ .../ThemeGeneratorConfigPage.test.tsx | 192 ++++++++++++++++++ .../ThemeGeneratorPreviewPage.test.tsx | 110 ++++++++++ .../test/theme-generator/utils.test.ts | 146 +++++++++++++ package-lock.json | 5 + 13 files changed, 770 insertions(+), 3 deletions(-) create mode 100644 apps/website/jest.config.mjs create mode 100644 apps/website/test/theme-generator/BottomButtons.test.tsx create mode 100644 apps/website/test/theme-generator/BrandingDetails.test.tsx create mode 100644 apps/website/test/theme-generator/ReviewDetails.test.tsx create mode 100644 apps/website/test/theme-generator/StepHeading.test.tsx create mode 100644 apps/website/test/theme-generator/ThemeGeneratorConfigPage.test.tsx create mode 100644 apps/website/test/theme-generator/ThemeGeneratorPreviewPage.test.tsx create mode 100644 apps/website/test/theme-generator/utils.test.ts diff --git a/apps/website/jest.config.mjs b/apps/website/jest.config.mjs new file mode 100644 index 000000000..ebfaa4fd7 --- /dev/null +++ b/apps/website/jest.config.mjs @@ -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)$": "/test/mocks/svgMock.ts", + "\\.(png)$": "/test/mocks/pngMock.ts", + "^screens/(.*)$": "/screens/$1", + "^hooks/(.*)$": "/hooks/$1", + "^@/(.*)$": "/$1", + }, + testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)", "!**/?(*.)+(accessibility.)(spec|test).[jt]s?(x)"], +}; + +export default createJestConfig(customJestConfig); diff --git a/apps/website/package.json b/apps/website/package.json index fb434a125..2887e022c 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -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", @@ -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" } } diff --git a/apps/website/screens/theme-generator/ThemeGeneratorConfigPage.tsx b/apps/website/screens/theme-generator/ThemeGeneratorConfigPage.tsx index 28342182c..de963f699 100644 --- a/apps/website/screens/theme-generator/ThemeGeneratorConfigPage.tsx +++ b/apps/website/screens/theme-generator/ThemeGeneratorConfigPage.tsx @@ -1,9 +1,8 @@ -import { useMemo, useRef, useState } from "react"; +import React, { useMemo, useRef, useState } from "react"; 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"; diff --git a/apps/website/screens/theme-generator/components/BottomButtons.tsx b/apps/website/screens/theme-generator/components/BottomButtons.tsx index 7a2974d19..b427ba40c 100644 --- a/apps/website/screens/theme-generator/components/BottomButtons.tsx +++ b/apps/website/screens/theme-generator/components/BottomButtons.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { DxcButton, DxcContainer, DxcFlex } from "@dxc-technology/halstack-react"; import { Step } from "../types"; diff --git a/apps/website/screens/theme-generator/components/StepHeading.tsx b/apps/website/screens/theme-generator/components/StepHeading.tsx index e99a3d799..60f8ced12 100644 --- a/apps/website/screens/theme-generator/components/StepHeading.tsx +++ b/apps/website/screens/theme-generator/components/StepHeading.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { DxcFlex, DxcHeading, DxcContainer, DxcTypography } from "@dxc-technology/halstack-react"; const StepHeading = ({ title, subtitle }: { title: string; subtitle: string }) => ( diff --git a/apps/website/test/theme-generator/BottomButtons.test.tsx b/apps/website/test/theme-generator/BottomButtons.test.tsx new file mode 100644 index 000000000..518ea0baf --- /dev/null +++ b/apps/website/test/theme-generator/BottomButtons.test.tsx @@ -0,0 +1,49 @@ +import "@testing-library/jest-dom/jest-globals"; +import React from "react"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, jest } from "@jest/globals"; +import BottomButtons from "../../screens/theme-generator/components/BottomButtons"; + +jest.mock("@dxc-technology/halstack-react", () => ({ + DxcContainer: ({ children }: { children: React.ReactNode }) =>
{children}
, + DxcFlex: ({ children }: { children: React.ReactNode }) =>
{children}
, + DxcButton: ({ label, onClick, disabled }: { label: string; onClick?: () => void; disabled?: boolean }) => ( + + ), +})); + +describe("BottomButtons", () => { + it("shows Back disabled and Next enabled on first step", () => { + const onChangeStep = jest.fn(); + const onExport = jest.fn(); + + render(); + + 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(); + + 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); + }); +}); diff --git a/apps/website/test/theme-generator/BrandingDetails.test.tsx b/apps/website/test/theme-generator/BrandingDetails.test.tsx new file mode 100644 index 000000000..49b2b75dc --- /dev/null +++ b/apps/website/test/theme-generator/BrandingDetails.test.tsx @@ -0,0 +1,126 @@ +import React from "react"; +import { afterEach, beforeAll, describe, expect, it, jest } from "@jest/globals"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; + +let BrandingDetails: (props: { + colors: Record; + onColorsChange: (colors: Record) => void; + logos: Record; + onLogosChange: (logos: Record) => void; +}) => React.JSX.Element; + +jest.mock("@dxc-technology/halstack-react", () => ({ + DxcContainer: ({ children }: { children: React.ReactNode }) =>
{children}
, + DxcFlex: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +jest.mock("../../screens/theme-generator/components/branding/BrandingColorGrid", () => ({ + __esModule: true, + default: ({ + section, + onColorChange, + }: { + section: { id: string }; + onColorChange: (id: string) => (value: string) => void; + }) => ( +
+ {section.id} + + +
+ ), +})); + +jest.mock("../../screens/theme-generator/components/branding/BrandingLogoGrid", () => ({ + __esModule: true, + default: ({ onLogoChange }: { onLogoChange: (logoType: string, files: File[]) => void }) => ( + + ), +})); + +beforeAll(async () => { + BrandingDetails = (await import("../../screens/theme-generator/steps/BrandingDetails")).BrandingDetails; +}); + +afterEach(() => { + cleanup(); +}); + +describe("BrandingDetails", () => { + it("updates colors preserving existing values", () => { + const onColorsChange = jest.fn(); + const onLogosChange = jest.fn(); + + const colors = { + primary: "#5F249F", + secondary: "#0067B3", + tertiary: "#F7CF2B", + neutral: "#999999", + info: "#0067B3", + success: "#59D97D", + error: "#FE344F", + warning: "#F59F3D", + }; + + const logos = { mainLogo: [], footerLogo: [], footerReducedLogo: [], favicon: [] }; + + render( + + ); + + fireEvent.click(screen.getAllByRole("button", { name: "Change primary" })[0]); + + expect(onColorsChange).toHaveBeenCalledWith( + expect.objectContaining({ + primary: "#111111", + secondary: "#0067B3", + }) + ); + }); + + it("updates logos preserving other logo values", () => { + const onColorsChange = jest.fn(); + const onLogosChange = jest.fn(); + + const colors = { + primary: "#5F249F", + secondary: "#0067B3", + tertiary: "#F7CF2B", + neutral: "#999999", + info: "#0067B3", + success: "#59D97D", + error: "#FE344F", + warning: "#F59F3D", + }; + + const logos = { + mainLogo: [], + footerLogo: [new File(["f"], "footer.png", { type: "image/png" })], + footerReducedLogo: [], + favicon: [], + }; + + render( + + ); + + fireEvent.click(screen.getByRole("button", { name: "Change main logo" })); + + expect(onLogosChange).toHaveBeenCalledTimes(1); + expect(onLogosChange).toHaveBeenCalledWith( + expect.objectContaining({ + mainLogo: expect.any(Array), + footerLogo: logos.footerLogo, + }) + ); + }); +}); diff --git a/apps/website/test/theme-generator/ReviewDetails.test.tsx b/apps/website/test/theme-generator/ReviewDetails.test.tsx new file mode 100644 index 000000000..d06fa1972 --- /dev/null +++ b/apps/website/test/theme-generator/ReviewDetails.test.tsx @@ -0,0 +1,80 @@ +import "@testing-library/jest-dom/jest-globals"; +import React from "react"; +import { afterEach, beforeAll, describe, expect, it, jest } from "@jest/globals"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; + +const mockHandleCopy = jest.fn(); +let ReviewDetails: (props: { + tokens: Record; + logos: Record; + themeJson: string; +}) => React.JSX.Element; + +jest.mock("hooks/useCopyToClipboard", () => ({ + __esModule: true, + default: () => mockHandleCopy, +})); + +jest.mock("@dxc-technology/halstack-react", () => ({ + DxcButton: ({ onClick }: { onClick?: () => void }) => ( + + ), + DxcFlex: ({ children }: { children: React.ReactNode }) =>
{children}
, + DxcGrid: ({ children }: { children: React.ReactNode }) =>
{children}
, + DxcTypography: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +jest.mock("../../screens/theme-generator/components/review/ReviewSectionContainer", () => ({ + __esModule: true, + default: ({ title, children }: { title: React.ReactNode; children: React.ReactNode }) => ( +
+
{title}
+
{children}
+
+ ), +})); + +jest.mock("../../screens/theme-generator/components/review/ReviewTokensGrid", () => ({ + __esModule: true, + default: ({ tokens }: { tokens: Record }) =>
{tokens["--color-primary-500"]}
, +})); + +jest.mock("../../screens/theme-generator/components/review/ReviewTokensList", () => ({ + __esModule: true, + default: ({ themeJson }: { themeJson: string }) =>
{themeJson}
, +})); + +jest.mock("../../screens/theme-generator/components/review/ReviewBrandingAssets", () => ({ + __esModule: true, + default: ({ logos }: { logos: { mainLogo: Array } }) =>
{`logos:${logos.mainLogo.length}`}
, +})); + +beforeAll(async () => { + ReviewDetails = (await import("../../screens/theme-generator/steps/ReviewDetails")).default; +}); + +afterEach(() => { + cleanup(); + mockHandleCopy.mockClear(); +}); + +describe("ReviewDetails", () => { + it("renders review sections and copies theme json", () => { + const tokens = { "--color-primary-500": "#101010" }; + const logos = { mainLogo: [], footerLogo: [], footerReducedLogo: [], favicon: [] }; + const themeJson = '{"tokens":{"--color-primary-500":"#101010"}}'; + + render(); + + expect(screen.getByText("Color palette & theme")).toBeInTheDocument(); + expect(screen.getByText("Branding assets")).toBeInTheDocument(); + expect(screen.getByText("#101010")).toBeInTheDocument(); + expect(screen.getByText(themeJson)).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: "Copy theme" })); + + expect(mockHandleCopy).toHaveBeenCalledWith(themeJson); + }); +}); diff --git a/apps/website/test/theme-generator/StepHeading.test.tsx b/apps/website/test/theme-generator/StepHeading.test.tsx new file mode 100644 index 000000000..d10cc6b45 --- /dev/null +++ b/apps/website/test/theme-generator/StepHeading.test.tsx @@ -0,0 +1,25 @@ +import "@testing-library/jest-dom/jest-globals"; +import React from "react"; +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, jest } from "@jest/globals"; +import StepHeading from "../../screens/theme-generator/components/StepHeading"; + +jest.mock("@dxc-technology/halstack-react", () => ({ + DxcContainer: ({ children }: { children: React.ReactNode }) =>
{children}
, + DxcFlex: ({ children }: { children: React.ReactNode }) =>
{children}
, + DxcHeading: ({ text }: { text: string }) =>

{text}

, + DxcTypography: ({ children }: { children: React.ReactNode }) =>

{children}

, +})); + +afterEach(() => { + cleanup(); +}); + +describe("StepHeading", () => { + it("renders title and subtitle", () => { + render(); + + expect(screen.getByRole("heading", { name: "Step title" })).toBeInTheDocument(); + expect(screen.getByText("Step subtitle")).toBeInTheDocument(); + }); +}); diff --git a/apps/website/test/theme-generator/ThemeGeneratorConfigPage.test.tsx b/apps/website/test/theme-generator/ThemeGeneratorConfigPage.test.tsx new file mode 100644 index 000000000..b31439b6e --- /dev/null +++ b/apps/website/test/theme-generator/ThemeGeneratorConfigPage.test.tsx @@ -0,0 +1,192 @@ +import "@testing-library/jest-dom/jest-globals"; +import React from "react"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals"; + +const mockGenerateTokens = jest.fn((_: Record) => ({ "--color-primary-500": "#101010" })); +const mockHandleExport = jest.fn((_: string) => undefined); +let ThemeGeneratorConfigPage: (props: Record) => React.JSX.Element; + +jest.mock("../../screens/theme-generator/utils", () => ({ + generateTokens: (baseColors: Record) => mockGenerateTokens(baseColors), + handleExport: (themeJson: string) => mockHandleExport(themeJson), +})); + +jest.mock("@dxc-technology/halstack-react", () => ({ + DxcContainer: ({ children }: { children: React.ReactNode }) =>
{children}
, + DxcFlex: ({ children }: { children: React.ReactNode }) =>
{children}
, + DxcWizard: ({ currentStep, onStepClick }: { currentStep: number; onStepClick: (index: number) => void }) => ( +
+ {currentStep} + + + +
+ ), +})); + +jest.mock("../../screens/theme-generator/components/StepHeading", () => ({ + __esModule: true, + default: ({ title, subtitle }: { title: string; subtitle: string }) => ( +
+

{title}

+

{subtitle}

+
+ ), +})); + +jest.mock("../../screens/theme-generator/steps/BrandingDetails", () => ({ + BrandingDetails: ({ + colors, + onColorsChange, + }: { + colors: Record; + onColorsChange: (value: Record) => void; + }) => ( +
+ {colors.primary} + +
+ ), +})); + +jest.mock("../../screens/utilities/theme-generator/componentsRegistry", () => ({ + componentsRegistry: {}, + examplesRegistry: {}, +})); + +jest.mock("../../screens/theme-generator/ThemeGeneratorPreviewPage", () => ({ + __esModule: true, + default: ({ tokens }: { tokens: Record }) => ( +
{JSON.stringify(tokens)}
+ ), +})); + +jest.mock("../../screens/theme-generator/steps/ReviewDetails", () => ({ + __esModule: true, + default: ({ themeJson }: { themeJson: string }) =>
{themeJson}
, +})); + +jest.mock("../../screens/theme-generator/components/BottomButtons", () => ({ + __esModule: true, + default: ({ + currentStep, + onChangeStep, + onExport, + }: { + currentStep: number; + onChangeStep: (step: 0 | 1 | 2) => void; + onExport: () => void; + }) => ( +
+ {currentStep} + + + +
+ ), +})); + +beforeAll(async () => { + ThemeGeneratorConfigPage = (await import("../../screens/theme-generator/ThemeGeneratorConfigPage")).default; +}); + +afterEach(() => { + cleanup(); +}); + +describe("ThemeGeneratorConfigPage", () => { + beforeEach(() => { + mockGenerateTokens.mockClear(); + mockHandleExport.mockClear(); + }); + + it("shows initial step content and heading", () => { + render(); + + expect(screen.getByRole("heading", { name: "Add your theme specifics" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Change primary color" })).toBeInTheDocument(); + expect(screen.getByTestId("current-step")).toHaveTextContent("0"); + }); + + it("generates tokens when leaving step 0", () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: "Go step 1" })); + + expect(mockGenerateTokens).toHaveBeenCalledTimes(1); + expect(mockGenerateTokens).toHaveBeenCalledWith({ + primary: "#5F249F", + secondary: "#0067B3", + tertiary: "#F7CF2B", + semantic01: "#0067B3", + semantic02: "#59D97D", + semantic03: "#F59F3D", + semantic04: "#FE344F", + neutral: "#999999", + }); + expect(screen.getByTestId("current-step")).toHaveTextContent("1"); + }); + + it("does not regenerate tokens when moving from step 1 to step 2 with unchanged colors", () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: "Go step 1" })); + fireEvent.click(screen.getByRole("button", { name: "Go step 2" })); + + expect(mockGenerateTokens).toHaveBeenCalledTimes(1); + expect(screen.getByTestId("current-step")).toHaveTextContent("2"); + }); + + it("regenerates tokens with updated colors", () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: "Change primary color" })); + fireEvent.click(screen.getByRole("button", { name: "Go step 1" })); + + expect(mockGenerateTokens).toHaveBeenCalledTimes(1); + expect(mockGenerateTokens).toHaveBeenCalledWith( + expect.objectContaining({ + primary: "#123456", + }) + ); + }); + + it("exports the generated theme json", () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: "Go step 2" })); + fireEvent.click(screen.getByRole("button", { name: "Export" })); + + expect(mockHandleExport).toHaveBeenCalledTimes(1); + const exportedTheme = mockHandleExport.mock.calls.at(0)?.[0]; + expect(exportedTheme).toBeDefined(); + if (!exportedTheme) return; + + expect(exportedTheme).toContain('"tokens": {'); + expect(exportedTheme).toContain('"--color-primary-500": "#101010"'); + expect(exportedTheme).toContain('"logos": {'); + }); +}); diff --git a/apps/website/test/theme-generator/ThemeGeneratorPreviewPage.test.tsx b/apps/website/test/theme-generator/ThemeGeneratorPreviewPage.test.tsx new file mode 100644 index 000000000..c8feac60e --- /dev/null +++ b/apps/website/test/theme-generator/ThemeGeneratorPreviewPage.test.tsx @@ -0,0 +1,110 @@ +import "@testing-library/jest-dom/jest-globals"; +import React from "react"; +import { afterEach, beforeAll, describe, expect, it, jest } from "@jest/globals"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; + +let ThemeGeneratorPreviewPage: (props: { + tokens: Record; + logos: { + mainLogo: Array<{ file: File; preview?: string }>; + footerLogo: Array; + footerReducedLogo: Array; + favicon: Array; + }; +}) => React.JSX.Element; + +jest.mock("../../screens/common/componentsList.json", () => [ + { + label: "General", + links: [{ label: "Button", path: "/components/button", icon: "click" }], + }, +]); + +jest.mock("screens/utilities/theme-generator/componentsRegistry", () => ({ + componentsRegistry: { + "/components/button": () =>
Button Preview
, + }, + examplesRegistry: { + "/examples/application": ({ logos }: { logos: { mainLogo?: Array<{ preview?: string }> } }) => ( +
{`Example with ${logos.mainLogo?.[0]?.preview ?? "no-logo"}`}
+ ), + }, +})); + +jest.mock("@dxc-technology/halstack-react", () => ({ + DxcButton: ({ title, onClick }: { title?: string; onClick?: () => void }) => ( + + ), + DxcContainer: ({ children }: { children: React.ReactNode }) =>
{children}
, + DxcFlex: ({ children }: { children: React.ReactNode }) =>
{children}
, + DxcSelect: ({ + placeholder, + multiple, + onChange, + }: { + placeholder: string; + multiple?: boolean; + onChange: (v: { value: string | string[] }) => void; + }) => ( + + ), + DxcToggleGroup: ({ onChange }: { onChange: (value: number) => void }) => ( +
+ + +
+ ), + DxcTypography: ({ children }: { children: React.ReactNode }) =>
{children}
, + HalstackProvider: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +beforeAll(async () => { + ThemeGeneratorPreviewPage = (await import("../../screens/theme-generator/ThemeGeneratorPreviewPage")).default; +}); + +afterEach(() => { + cleanup(); +}); + +describe("ThemeGeneratorPreviewPage", () => { + it("shows empty state and renders component preview after selection", () => { + const logos = { mainLogo: [], footerLogo: [], footerReducedLogo: [], favicon: [] }; + + render(); + + expect(screen.getByText("Select a component to preview")).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: "Select components" })); + + expect(screen.getByText("Button")).toBeInTheDocument(); + expect(screen.getByText("Button Preview")).toBeInTheDocument(); + }); + + it("switches to examples mode and renders selected example", () => { + const logos = { + mainLogo: [{ file: new File(["x"], "main.png"), preview: "main-preview" }], + footerLogo: [], + footerReducedLogo: [], + favicon: [], + }; + + render(); + + fireEvent.click(screen.getByRole("button", { name: "Examples mode" })); + fireEvent.click(screen.getByRole("button", { name: "Select examples" })); + + expect(screen.getByText(/Some components are presentational examples\./)).toBeInTheDocument(); + expect(screen.getByText("Example with main-preview")).toBeInTheDocument(); + }); +}); diff --git a/apps/website/test/theme-generator/utils.test.ts b/apps/website/test/theme-generator/utils.test.ts new file mode 100644 index 000000000..59bd289bb --- /dev/null +++ b/apps/website/test/theme-generator/utils.test.ts @@ -0,0 +1,146 @@ +import "@testing-library/jest-dom/jest-globals"; +import { describe, expect, it, jest, beforeEach } from "@jest/globals"; + +const mockValues = [ + { value: "#101010" }, + { value: "#202020" }, + { value: "#303030" }, + { value: "#404040" }, + { value: "#505050" }, + { value: "#606060" }, + { value: "#707070" }, + { value: "#808080" }, + { value: "#909090" }, + { value: "#a0a0a0" }, +]; + +jest.mock("@adobe/leonardo-contrast-colors", () => ({ + BackgroundColor: class { + name: string; + colorKeys: string[]; + ratios: number[]; + + constructor({ name, colorKeys, ratios }: { name: string; colorKeys: string[]; ratios: number[] }) { + this.name = name; + this.colorKeys = colorKeys; + this.ratios = ratios; + } + }, + Color: class { + name: string; + colorKeys: string[]; + ratios: number[]; + colorSpace: string; + smooth: boolean; + + constructor({ + name, + colorKeys, + ratios, + colorSpace, + smooth, + }: { + name: string; + colorKeys: string[]; + ratios: number[]; + colorSpace: string; + smooth: boolean; + }) { + this.name = name; + this.colorKeys = colorKeys; + this.ratios = ratios; + this.colorSpace = colorSpace; + this.smooth = smooth; + } + }, + Theme: class { + contrastColors: Array<{ values: Array<{ value: string }> }>; + + constructor({ colors }: { colors: Array<{ colorKeys: string[] }> }) { + if (colors[0]?.colorKeys?.[0] === "throw") { + throw new Error("Palette error"); + } + this.contrastColors = [{ values: [] }, { values: mockValues }]; + } + }, +})); + +let generatePalette: (hex: string) => string[]; +let generateTokens: (baseColors: Record) => Record; +let handleExport: (themeJson: string) => void; +let divideColorTokens: (tokens: Record) => Record; + +beforeEach(async () => { + jest.resetModules(); + const utils = await import("../../screens/theme-generator/utils"); + generatePalette = utils.generatePalette; + generateTokens = utils.generateTokens; + handleExport = utils.handleExport; + divideColorTokens = utils.divideColorTokens; +}); + +describe("theme-generator utils", () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + + it("generatePalette returns generated palette values", () => { + const palette = generatePalette("#5F249F"); + + expect(palette).toHaveLength(10); + expect(palette[0]).toBe("#101010"); + expect(palette[9]).toBe("#a0a0a0"); + }); + + it("generatePalette handles errors and returns empty array", () => { + const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => undefined); + + const palette = generatePalette("throw"); + + expect(palette).toEqual([]); + expect(consoleSpy).toHaveBeenCalled(); + }); + + it("generateTokens includes base, alpha and absolute tokens", () => { + const tokens = generateTokens({ + primary: "#111111", + secondary: "#222222", + neutral: "#333333", + }); + + expect(tokens["--color-primary-50"]).toBe("#101010"); + expect(tokens["--color-secondary-900"]).toBe("#a0a0a0"); + expect(tokens["--color-alpha-100-a"]).toBe("#2020201a"); + expect(tokens["--color-alpha-900-a"]).toBe("#a0a0a0e5"); + expect(tokens["--color-absolutes-black"]).toBe("#000000"); + expect(tokens["--color-absolutes-white"]).toBe("#ffffff"); + }); + + it("handleExport creates and clicks a download link", () => { + const anchor = document.createElement("a"); + const clickSpy = jest.spyOn(anchor, "click").mockImplementation(() => undefined); + const createElementSpy = jest.spyOn(document, "createElement").mockReturnValue(anchor); + + handleExport('{"tokens":{}}'); + + expect(createElementSpy).toHaveBeenCalledWith("a"); + expect(anchor.getAttribute("download")).toBe("halstack-theme-tokens.json"); + expect(anchor.getAttribute("href")).toContain("data:text/json;charset=utf-8,"); + expect(clickSpy).toHaveBeenCalledTimes(1); + }); + + it("divideColorTokens groups only matching color token keys", () => { + const grouped = divideColorTokens({ + "--color-primary-500": "#111111", + "--color-semantic01-500": "#222222", + "--color-alpha-100-a": "#333333", + "--color-neutral-300": "#444444", + "--spacing-padding-s": "8px", + }); + + expect(grouped.primary).toEqual(["#111111"]); + expect(grouped.semantic01).toEqual(["#222222"]); + expect(grouped.alpha).toEqual(["#333333"]); + expect(grouped.neutral).toEqual(["#444444"]); + }); +}); diff --git a/package-lock.json b/package-lock.json index 1090b96f4..346d65c80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,12 +51,17 @@ "devDependencies": { "@dxc-technology/typescript-config": "*", "@eslint/js": "^9.31.1", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@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" } }, From 4a51663d9eeeeaedfd6694a35952025a36f57eaa Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso Date: Wed, 18 Mar 2026 07:45:45 +0100 Subject: [PATCH 2/6] Add tests --- apps/website/eslint.config.mjs | 16 + .../ThemeGeneratorConfigPage.tsx | 2 +- .../theme-generator/steps/ReviewDetails.tsx | 1 + .../theme-generator/BottomButtons.test.tsx | 14 +- .../theme-generator/BrandingDetails.test.tsx | 169 ++++--- .../test/theme-generator/ColorCard.test.tsx | 315 +++++++++++++ .../theme-generator/ReviewDetails.test.tsx | 90 ++-- .../test/theme-generator/StepHeading.test.tsx | 25 -- .../ThemeGeneratorConfigPage.test.tsx | 205 ++++----- .../ThemeGeneratorPreviewPage.test.tsx | 32 +- .../test/theme-generator/utils.test.ts | 420 +++++++++++++----- apps/website/tsconfig.json | 2 +- 12 files changed, 844 insertions(+), 447 deletions(-) create mode 100644 apps/website/test/theme-generator/ColorCard.test.tsx delete mode 100644 apps/website/test/theme-generator/StepHeading.test.tsx diff --git a/apps/website/eslint.config.mjs b/apps/website/eslint.config.mjs index d83a5e66d..05c9fb7eb 100644 --- a/apps/website/eslint.config.mjs +++ b/apps/website/eslint.config.mjs @@ -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"; @@ -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/**"], }, diff --git a/apps/website/screens/theme-generator/ThemeGeneratorConfigPage.tsx b/apps/website/screens/theme-generator/ThemeGeneratorConfigPage.tsx index de963f699..800784be8 100644 --- a/apps/website/screens/theme-generator/ThemeGeneratorConfigPage.tsx +++ b/apps/website/screens/theme-generator/ThemeGeneratorConfigPage.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { DxcContainer, DxcFlex, DxcWizard } from "@dxc-technology/halstack-react"; import StepHeading from "./components/StepHeading"; import BottomButtons from "./components/BottomButtons"; diff --git a/apps/website/screens/theme-generator/steps/ReviewDetails.tsx b/apps/website/screens/theme-generator/steps/ReviewDetails.tsx index 2fa934186..43fa76bd8 100644 --- a/apps/website/screens/theme-generator/steps/ReviewDetails.tsx +++ b/apps/website/screens/theme-generator/steps/ReviewDetails.tsx @@ -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)} /> diff --git a/apps/website/test/theme-generator/BottomButtons.test.tsx b/apps/website/test/theme-generator/BottomButtons.test.tsx index 518ea0baf..b87784f49 100644 --- a/apps/website/test/theme-generator/BottomButtons.test.tsx +++ b/apps/website/test/theme-generator/BottomButtons.test.tsx @@ -1,19 +1,7 @@ -import "@testing-library/jest-dom/jest-globals"; -import React from "react"; +import "@testing-library/jest-dom"; import { fireEvent, render, screen } from "@testing-library/react"; -import { describe, expect, it, jest } from "@jest/globals"; import BottomButtons from "../../screens/theme-generator/components/BottomButtons"; -jest.mock("@dxc-technology/halstack-react", () => ({ - DxcContainer: ({ children }: { children: React.ReactNode }) =>
{children}
, - DxcFlex: ({ children }: { children: React.ReactNode }) =>
{children}
, - DxcButton: ({ label, onClick, disabled }: { label: string; onClick?: () => void; disabled?: boolean }) => ( - - ), -})); - describe("BottomButtons", () => { it("shows Back disabled and Next enabled on first step", () => { const onChangeStep = jest.fn(); diff --git a/apps/website/test/theme-generator/BrandingDetails.test.tsx b/apps/website/test/theme-generator/BrandingDetails.test.tsx index 49b2b75dc..a7ba7b1d9 100644 --- a/apps/website/test/theme-generator/BrandingDetails.test.tsx +++ b/apps/website/test/theme-generator/BrandingDetails.test.tsx @@ -1,110 +1,107 @@ -import React from "react"; -import { afterEach, beforeAll, describe, expect, it, jest } from "@jest/globals"; -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; - -let BrandingDetails: (props: { - colors: Record; - onColorsChange: (colors: Record) => void; - logos: Record; - onLogosChange: (logos: Record) => void; -}) => React.JSX.Element; - -jest.mock("@dxc-technology/halstack-react", () => ({ - DxcContainer: ({ children }: { children: React.ReactNode }) =>
{children}
, - DxcFlex: ({ children }: { children: React.ReactNode }) =>
{children}
, +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(), })); -jest.mock("../../screens/theme-generator/components/branding/BrandingColorGrid", () => ({ - __esModule: true, - default: ({ - section, - onColorChange, - }: { - section: { id: string }; - onColorChange: (id: string) => (value: string) => void; - }) => ( -
- {section.id} - - +// Mock SketchColorPicker to avoid react-color issues (Canvas/Context errors) +jest.mock("react-color", () => ({ + SketchPicker: ({ color, onChange }: { color: string; onChange: (color: { hex: string }) => void }) => ( +
+ Picker for {color} +
), })); -jest.mock("../../screens/theme-generator/components/branding/BrandingLogoGrid", () => ({ - __esModule: true, - default: ({ onLogoChange }: { onLogoChange: (logoType: string, files: File[]) => void }) => ( - - ), -})); - -beforeAll(async () => { - BrandingDetails = (await import("../../screens/theme-generator/steps/BrandingDetails")).BrandingDetails; -}); - -afterEach(() => { - cleanup(); -}); - describe("BrandingDetails", () => { - it("updates colors preserving existing values", () => { + it("renders color sections with ColorCard components", () => { const onColorsChange = jest.fn(); const onLogosChange = jest.fn(); - const colors = { - primary: "#5F249F", - secondary: "#0067B3", - tertiary: "#F7CF2B", - neutral: "#999999", - info: "#0067B3", - success: "#59D97D", - error: "#FE344F", - warning: "#F59F3D", + 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 = { mainLogo: [], footerLogo: [], footerReducedLogo: [], favicon: [] }; + const logos: Logos = { mainLogo: [], footerLogo: [], footerReducedLogo: [], favicon: [] }; render( ); - fireEvent.click(screen.getAllByRole("button", { name: "Change primary" })[0]); + // Verify that primary color input is rendered with correct value + expect(screen.getByDisplayValue("#5F249F")).toBeInTheDocument(); + expect(screen.getByText("Primary")).toBeInTheDocument(); + }); - expect(onColorsChange).toHaveBeenCalledWith( - expect.objectContaining({ - primary: "#111111", - secondary: "#0067B3", - }) + 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( + ); + + // Find the primary color input and change it + 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("updates logos preserving other logo values", () => { + it("renders logo sections with FileInput components", () => { const onColorsChange = jest.fn(); const onLogosChange = jest.fn(); - const colors = { - primary: "#5F249F", - secondary: "#0067B3", - tertiary: "#F7CF2B", - neutral: "#999999", - info: "#0067B3", - success: "#59D97D", - error: "#FE344F", - warning: "#F59F3D", + 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 = { + const logos: Logos = { mainLogo: [], - footerLogo: [new File(["f"], "footer.png", { type: "image/png" })], + footerLogo: [{ file: new File(["f"], "footer.png", { type: "image/png" }) }], footerReducedLogo: [], favicon: [], }; @@ -113,14 +110,8 @@ describe("BrandingDetails", () => { ); - fireEvent.click(screen.getByRole("button", { name: "Change main logo" })); - - expect(onLogosChange).toHaveBeenCalledTimes(1); - expect(onLogosChange).toHaveBeenCalledWith( - expect.objectContaining({ - mainLogo: expect.any(Array), - footerLogo: logos.footerLogo, - }) - ); + // Verify logo sections are rendered + expect(screen.getByText("Main logo")).toBeInTheDocument(); + expect(screen.getByText("Default footer logo")).toBeInTheDocument(); }); }); diff --git a/apps/website/test/theme-generator/ColorCard.test.tsx b/apps/website/test/theme-generator/ColorCard.test.tsx new file mode 100644 index 000000000..f723d0d7e --- /dev/null +++ b/apps/website/test/theme-generator/ColorCard.test.tsx @@ -0,0 +1,315 @@ +import "@testing-library/jest-dom"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { ColorCard } from "../../screens/theme-generator/components/branding/ColorCard"; + +// Mock ResizeObserver +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); + +const mockHandleCopy = jest.fn(); + +jest.mock("hooks/useCopyToClipboard", () => ({ + __esModule: true, + default: () => mockHandleCopy, +})); + +// Mock SketchColorPicker to avoid react-color issues (Canvas/Context errors) +jest.mock("react-color", () => ({ + SketchPicker: ({ + color, + onChange, + onErrorChange, + }: { + color: string; + onChange: (color: { hex: string }) => void; + onErrorChange?: (error: boolean) => void; + }) => ( +
+ Picker for {color} + + + +
+ ), +})); + +describe("ColorCard", () => { + const defaultProps = { + label: "Primary Color", + helperText: "Main brand color", + color: "#5F249F", + onChange: jest.fn(), + }; + + describe("Rendering", () => { + it("should render with initial color and label", () => { + render(); + + expect(screen.getByText("Primary Color")).toBeInTheDocument(); + expect(screen.getByText("Main brand color")).toBeInTheDocument(); + }); + + it("should render color box button", () => { + render(); + + const colorBox = screen.getByLabelText("Open color picker for Primary Color"); + expect(colorBox).toBeInTheDocument(); + expect(colorBox).toHaveAttribute("tabIndex", "0"); + }); + + it("should render with initial color value in input", () => { + render(); + + const input = screen.getByDisplayValue("#5F249F"); + expect(input).toBeInTheDocument(); + }); + }); + + describe("Color Picker Popover", () => { + it("should open sketch picker when clicking color box", async () => { + render(); + + const colorBox = screen.getByLabelText("Open color picker for Primary Color"); + fireEvent.click(colorBox); + + await waitFor(() => { + expect(screen.getByTestId("mock-sketch-picker")).toBeInTheDocument(); + }); + }); + + it("should call onChange when selecting color from picker", async () => { + const onChange = jest.fn(); + render(); + + const colorBox = screen.getByLabelText("Open color picker for Primary Color"); + fireEvent.click(colorBox); + + await waitFor(() => { + expect(screen.getByTestId("mock-sketch-picker")).toBeInTheDocument(); + }); + + const selectWhiteButton = screen.getByText("Select White"); + fireEvent.click(selectWhiteButton); + + expect(onChange).toHaveBeenCalledWith("#ffffff"); + }); + + it("should update input value when changing color from picker", async () => { + render(); + + const colorBox = screen.getByLabelText("Open color picker for Primary Color"); + fireEvent.click(colorBox); + + await waitFor(() => { + expect(screen.getByTestId("mock-sketch-picker")).toBeInTheDocument(); + }); + + const selectWhiteButton = screen.getByText("Select White"); + fireEvent.click(selectWhiteButton); + + await waitFor(() => { + expect(screen.getByDisplayValue("#ffffff")).toBeInTheDocument(); + }); + }); + }); + + describe("Text Input", () => { + it("should update input value when typing", () => { + render(); + + const input = screen.getByDisplayValue("#5F249F"); + fireEvent.change(input, { target: { value: "#123456" } }); + + expect(screen.getByDisplayValue("#123456")).toBeInTheDocument(); + }); + + it("should call onChange with valid hex color on blur", async () => { + const onChange = jest.fn(); + render(); + + const input = screen.getByDisplayValue("#5F249F"); + fireEvent.change(input, { target: { value: "#123456" } }); + fireEvent.blur(input); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith("#123456"); + }); + }); + + it("should accept 3-character hex colors", async () => { + const onChange = jest.fn(); + render(); + + const input = screen.getByDisplayValue("#5F249F"); + fireEvent.change(input, { target: { value: "#fff" } }); + fireEvent.blur(input); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith("#fff"); + }); + }); + + it("should accept 6-character hex colors", async () => { + const onChange = jest.fn(); + render(); + + const input = screen.getByDisplayValue("#5F249F"); + fireEvent.change(input, { target: { value: "#ffffff" } }); + fireEvent.blur(input); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith("#ffffff"); + }); + }); + + it("should show error for invalid hex format", async () => { + const onChange = jest.fn(); + render(); + + const input = screen.getByDisplayValue("#5F249F"); + fireEvent.change(input, { target: { value: "invalid" } }); + fireEvent.blur(input); + + await waitFor(() => { + expect(screen.getByText("Please match the format requested.")).toBeInTheDocument(); + }); + expect(onChange).not.toHaveBeenCalled(); + }); + + it("should show error for hex without # prefix", async () => { + const onChange = jest.fn(); + render(); + + const input = screen.getByDisplayValue("#5F249F"); + fireEvent.change(input, { target: { value: "123456" } }); + fireEvent.blur(input); + + await waitFor(() => { + expect(screen.getByText("Please match the format requested.")).toBeInTheDocument(); + }); + expect(onChange).not.toHaveBeenCalled(); + }); + + it("should show error for hex with wrong length", async () => { + const onChange = jest.fn(); + render(); + + const input = screen.getByDisplayValue("#5F249F"); + fireEvent.change(input, { target: { value: "#12345" } }); + fireEvent.blur(input); + + await waitFor(() => { + expect(screen.getByText("Please match the format requested.")).toBeInTheDocument(); + }); + expect(onChange).not.toHaveBeenCalled(); + }); + }); + + describe("Copy to Clipboard", () => { + it("should copy current input value when clicking copy button", () => { + render(); + + const copyButton = screen.getByLabelText("Copy the hex value"); + fireEvent.click(copyButton); + + expect(mockHandleCopy).toHaveBeenCalledWith("#5F249F"); + }); + + it("should copy updated value after input change", () => { + render(); + + const input = screen.getByDisplayValue("#5F249F"); + fireEvent.change(input, { target: { value: "#ABCDEF" } }); + + const copyButton = screen.getByLabelText("Copy the hex value"); + fireEvent.click(copyButton); + + expect(mockHandleCopy).toHaveBeenCalledWith("#ABCDEF"); + }); + + it("should copy value from color picker change", async () => { + render(); + + const colorBox = screen.getByLabelText("Open color picker for Primary Color"); + fireEvent.click(colorBox); + + await waitFor(() => { + expect(screen.getByTestId("mock-sketch-picker")).toBeInTheDocument(); + }); + + const selectWhiteButton = screen.getByText("Select White"); + fireEvent.click(selectWhiteButton); + + const copyButton = screen.getByLabelText("Copy the hex value"); + fireEvent.click(copyButton); + + expect(mockHandleCopy).toHaveBeenCalledWith("#ffffff"); + }); + }); + + describe("Accessibility", () => { + it("should have proper aria attributes on color box", () => { + render(); + + const colorBox = screen.getByLabelText("Open color picker for Primary Color"); + expect(colorBox).toHaveAttribute("aria-label", "Open color picker for Primary Color"); + expect(colorBox).toHaveAttribute("aria-haspopup", "dialog"); + }); + + it("should have accessible label for input", () => { + render(); + + expect(screen.getByText("Primary Color")).toBeInTheDocument(); + }); + + it("should have helper text", () => { + render(); + + expect(screen.getByText("Main brand color")).toBeInTheDocument(); + }); + }); + + describe("Edge Cases", () => { + it("should handle uppercase hex colors", async () => { + const onChange = jest.fn(); + render(); + + const input = screen.getByDisplayValue("#5F249F"); + fireEvent.change(input, { target: { value: "#FFFFFF" } }); + fireEvent.blur(input); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith("#FFFFFF"); + }); + }); + + it("should handle lowercase hex colors", async () => { + const onChange = jest.fn(); + render(); + + const input = screen.getByDisplayValue("#5F249F"); + fireEvent.change(input, { target: { value: "#ffffff" } }); + fireEvent.blur(input); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith("#ffffff"); + }); + }); + + it("should handle mixed case hex colors", async () => { + const onChange = jest.fn(); + render(); + + const input = screen.getByDisplayValue("#5F249F"); + fireEvent.change(input, { target: { value: "#FfFfFf" } }); + fireEvent.blur(input); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith("#FfFfFf"); + }); + }); + }); +}); diff --git a/apps/website/test/theme-generator/ReviewDetails.test.tsx b/apps/website/test/theme-generator/ReviewDetails.test.tsx index d06fa1972..6b9295ff6 100644 --- a/apps/website/test/theme-generator/ReviewDetails.test.tsx +++ b/apps/website/test/theme-generator/ReviewDetails.test.tsx @@ -1,67 +1,34 @@ -import "@testing-library/jest-dom/jest-globals"; -import React from "react"; -import { afterEach, beforeAll, describe, expect, it, jest } from "@jest/globals"; -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; +import ReviewDetails from "../../screens/theme-generator/steps/ReviewDetails"; + +// Mock @adobe/leonardo-contrast-colors (ESM module) +jest.mock("@adobe/leonardo-contrast-colors", () => { + return { + Color: jest.fn(), + }; +}); + +// Mock ResizeObserver +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const mockHandleCopy = jest.fn(); -let ReviewDetails: (props: { - tokens: Record; - logos: Record; - themeJson: string; -}) => React.JSX.Element; jest.mock("hooks/useCopyToClipboard", () => ({ __esModule: true, default: () => mockHandleCopy, })); -jest.mock("@dxc-technology/halstack-react", () => ({ - DxcButton: ({ onClick }: { onClick?: () => void }) => ( - - ), - DxcFlex: ({ children }: { children: React.ReactNode }) =>
{children}
, - DxcGrid: ({ children }: { children: React.ReactNode }) =>
{children}
, - DxcTypography: ({ children }: { children: React.ReactNode }) =>
{children}
, -})); - -jest.mock("../../screens/theme-generator/components/review/ReviewSectionContainer", () => ({ - __esModule: true, - default: ({ title, children }: { title: React.ReactNode; children: React.ReactNode }) => ( -
-
{title}
-
{children}
-
- ), -})); - -jest.mock("../../screens/theme-generator/components/review/ReviewTokensGrid", () => ({ - __esModule: true, - default: ({ tokens }: { tokens: Record }) =>
{tokens["--color-primary-500"]}
, -})); - -jest.mock("../../screens/theme-generator/components/review/ReviewTokensList", () => ({ - __esModule: true, - default: ({ themeJson }: { themeJson: string }) =>
{themeJson}
, -})); - -jest.mock("../../screens/theme-generator/components/review/ReviewBrandingAssets", () => ({ - __esModule: true, - default: ({ logos }: { logos: { mainLogo: Array } }) =>
{`logos:${logos.mainLogo.length}`}
, -})); - -beforeAll(async () => { - ReviewDetails = (await import("../../screens/theme-generator/steps/ReviewDetails")).default; -}); - -afterEach(() => { - cleanup(); - mockHandleCopy.mockClear(); -}); - describe("ReviewDetails", () => { - it("renders review sections and copies theme json", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("renders review sections with real components", () => { const tokens = { "--color-primary-500": "#101010" }; const logos = { mainLogo: [], footerLogo: [], footerReducedLogo: [], favicon: [] }; const themeJson = '{"tokens":{"--color-primary-500":"#101010"}}'; @@ -70,10 +37,17 @@ describe("ReviewDetails", () => { expect(screen.getByText("Color palette & theme")).toBeInTheDocument(); expect(screen.getByText("Branding assets")).toBeInTheDocument(); - expect(screen.getByText("#101010")).toBeInTheDocument(); - expect(screen.getByText(themeJson)).toBeInTheDocument(); + }); + + it("copies theme json when clicking copy button", () => { + const tokens = { "--color-primary-500": "#101010" }; + const logos = { mainLogo: [], footerLogo: [], footerReducedLogo: [], favicon: [] }; + const themeJson = '{"tokens":{"--color-primary-500":"#101010"}}'; + + render(); - fireEvent.click(screen.getByRole("button", { name: "Copy theme" })); + const copyButton = screen.getByLabelText("Copy theme"); + fireEvent.click(copyButton); expect(mockHandleCopy).toHaveBeenCalledWith(themeJson); }); diff --git a/apps/website/test/theme-generator/StepHeading.test.tsx b/apps/website/test/theme-generator/StepHeading.test.tsx deleted file mode 100644 index d10cc6b45..000000000 --- a/apps/website/test/theme-generator/StepHeading.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import "@testing-library/jest-dom/jest-globals"; -import React from "react"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, it, jest } from "@jest/globals"; -import StepHeading from "../../screens/theme-generator/components/StepHeading"; - -jest.mock("@dxc-technology/halstack-react", () => ({ - DxcContainer: ({ children }: { children: React.ReactNode }) =>
{children}
, - DxcFlex: ({ children }: { children: React.ReactNode }) =>
{children}
, - DxcHeading: ({ text }: { text: string }) =>

{text}

, - DxcTypography: ({ children }: { children: React.ReactNode }) =>

{children}

, -})); - -afterEach(() => { - cleanup(); -}); - -describe("StepHeading", () => { - it("renders title and subtitle", () => { - render(); - - expect(screen.getByRole("heading", { name: "Step title" })).toBeInTheDocument(); - expect(screen.getByText("Step subtitle")).toBeInTheDocument(); - }); -}); diff --git a/apps/website/test/theme-generator/ThemeGeneratorConfigPage.test.tsx b/apps/website/test/theme-generator/ThemeGeneratorConfigPage.test.tsx index b31439b6e..3ee8a29aa 100644 --- a/apps/website/test/theme-generator/ThemeGeneratorConfigPage.test.tsx +++ b/apps/website/test/theme-generator/ThemeGeneratorConfigPage.test.tsx @@ -1,122 +1,57 @@ -import "@testing-library/jest-dom/jest-globals"; -import React from "react"; -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals"; +import "@testing-library/jest-dom"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import ThemeGeneratorConfigPage from "../../screens/theme-generator/ThemeGeneratorConfigPage"; const mockGenerateTokens = jest.fn((_: Record) => ({ "--color-primary-500": "#101010" })); const mockHandleExport = jest.fn((_: string) => undefined); -let ThemeGeneratorConfigPage: (props: Record) => React.JSX.Element; jest.mock("../../screens/theme-generator/utils", () => ({ generateTokens: (baseColors: Record) => mockGenerateTokens(baseColors), handleExport: (themeJson: string) => mockHandleExport(themeJson), + divideColorTokens: (tokens: Record) => ({ + primary: [tokens["--color-primary-500"]], + secondary: [], + tertiary: [], + semantic01: [], + semantic02: [], + semantic03: [], + semantic04: [], + neutral: [], + alpha: [], + }), + SHADE_VALUES: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + CONTRAST_RATIOS: [1.03, 1.18, 1.34, 1.52, 2.04, 2.79, 4.3, 6.7, 9, 12.46], })); -jest.mock("@dxc-technology/halstack-react", () => ({ - DxcContainer: ({ children }: { children: React.ReactNode }) =>
{children}
, - DxcFlex: ({ children }: { children: React.ReactNode }) =>
{children}
, - DxcWizard: ({ currentStep, onStepClick }: { currentStep: number; onStepClick: (index: number) => void }) => ( -
- {currentStep} - - - -
- ), +// Mock @adobe/leonardo-contrast-colors (ESM module) +jest.mock("@adobe/leonardo-contrast-colors", () => ({ + Color: jest.fn(), })); -jest.mock("../../screens/theme-generator/components/StepHeading", () => ({ +// Mock ResizeObserver +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); + +// Mock useCopyToClipboard +jest.mock("hooks/useCopyToClipboard", () => ({ __esModule: true, - default: ({ title, subtitle }: { title: string; subtitle: string }) => ( -
-

{title}

-

{subtitle}

-
- ), + default: () => jest.fn(), })); -jest.mock("../../screens/theme-generator/steps/BrandingDetails", () => ({ - BrandingDetails: ({ - colors, - onColorsChange, - }: { - colors: Record; - onColorsChange: (value: Record) => void; - }) => ( -
- {colors.primary} - -
- ), +// Mock react-color SketchPicker +jest.mock("react-color", () => ({ + SketchPicker: () =>
Color Picker
, })); +// Mock componentsRegistry and examplesRegistry jest.mock("../../screens/utilities/theme-generator/componentsRegistry", () => ({ componentsRegistry: {}, examplesRegistry: {}, })); -jest.mock("../../screens/theme-generator/ThemeGeneratorPreviewPage", () => ({ - __esModule: true, - default: ({ tokens }: { tokens: Record }) => ( -
{JSON.stringify(tokens)}
- ), -})); - -jest.mock("../../screens/theme-generator/steps/ReviewDetails", () => ({ - __esModule: true, - default: ({ themeJson }: { themeJson: string }) =>
{themeJson}
, -})); - -jest.mock("../../screens/theme-generator/components/BottomButtons", () => ({ - __esModule: true, - default: ({ - currentStep, - onChangeStep, - onExport, - }: { - currentStep: number; - onChangeStep: (step: 0 | 1 | 2) => void; - onExport: () => void; - }) => ( -
- {currentStep} - - - -
- ), -})); - -beforeAll(async () => { - ThemeGeneratorConfigPage = (await import("../../screens/theme-generator/ThemeGeneratorConfigPage")).default; -}); - -afterEach(() => { - cleanup(); -}); - describe("ThemeGeneratorConfigPage", () => { beforeEach(() => { mockGenerateTokens.mockClear(); @@ -126,17 +61,22 @@ describe("ThemeGeneratorConfigPage", () => { it("shows initial step content and heading", () => { render(); - expect(screen.getByRole("heading", { name: "Add your theme specifics" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Change primary color" })).toBeInTheDocument(); - expect(screen.getByTestId("current-step")).toHaveTextContent("0"); + expect(screen.getByText("Add your theme specifics")).toBeInTheDocument(); + expect(screen.getByText("Core colors")).toBeInTheDocument(); + expect(screen.getByText("Semantic colors")).toBeInTheDocument(); + expect(screen.getByLabelText("Primary")).toBeInTheDocument(); }); - it("generates tokens when leaving step 0", () => { + it("generates tokens when leaving step 0", async () => { render(); - fireEvent.click(screen.getByRole("button", { name: "Go step 1" })); + const nextButton = screen.getByRole("button", { name: "Next" }); + fireEvent.click(nextButton); + + await waitFor(() => { + expect(mockGenerateTokens).toHaveBeenCalledTimes(1); + }); - expect(mockGenerateTokens).toHaveBeenCalledTimes(1); expect(mockGenerateTokens).toHaveBeenCalledWith({ primary: "#5F249F", secondary: "#0067B3", @@ -147,26 +87,44 @@ describe("ThemeGeneratorConfigPage", () => { semantic04: "#FE344F", neutral: "#999999", }); - expect(screen.getByTestId("current-step")).toHaveTextContent("1"); + + expect(screen.getByText("Preview how your theme applies")).toBeInTheDocument(); }); - it("does not regenerate tokens when moving from step 1 to step 2 with unchanged colors", () => { + it("does not regenerate tokens when moving from step 1 to step 2 with unchanged colors", async () => { render(); - fireEvent.click(screen.getByRole("button", { name: "Go step 1" })); - fireEvent.click(screen.getByRole("button", { name: "Go step 2" })); + const nextButton = screen.getByRole("button", { name: "Next" }); + fireEvent.click(nextButton); + + await waitFor(() => { + expect(mockGenerateTokens).toHaveBeenCalledTimes(1); + }); + + fireEvent.click(nextButton); + + await waitFor(() => { + expect(screen.getByText("Review and export your theme")).toBeInTheDocument(); + }); expect(mockGenerateTokens).toHaveBeenCalledTimes(1); - expect(screen.getByTestId("current-step")).toHaveTextContent("2"); }); - it("regenerates tokens with updated colors", () => { + it("regenerates tokens with updated colors", async () => { render(); - fireEvent.click(screen.getByRole("button", { name: "Change primary color" })); - fireEvent.click(screen.getByRole("button", { name: "Go step 1" })); + // Change primary color by finding the primary color input and changing it + const primaryInput = screen.getByLabelText("Primary"); + fireEvent.change(primaryInput, { target: { value: "#123456" } }); + fireEvent.blur(primaryInput); + + const nextButton = screen.getByRole("button", { name: "Next" }); + fireEvent.click(nextButton); + + await waitFor(() => { + expect(mockGenerateTokens).toHaveBeenCalledTimes(1); + }); - expect(mockGenerateTokens).toHaveBeenCalledTimes(1); expect(mockGenerateTokens).toHaveBeenCalledWith( expect.objectContaining({ primary: "#123456", @@ -174,11 +132,26 @@ describe("ThemeGeneratorConfigPage", () => { ); }); - it("exports the generated theme json", () => { + it("exports the generated theme json", async () => { render(); - fireEvent.click(screen.getByRole("button", { name: "Go step 2" })); - fireEvent.click(screen.getByRole("button", { name: "Export" })); + // Navigate to step 2 + const nextButton = screen.getByRole("button", { name: "Next" }); + fireEvent.click(nextButton); + + await waitFor(() => { + expect(mockGenerateTokens).toHaveBeenCalledTimes(1); + }); + + fireEvent.click(nextButton); + + await waitFor(() => { + expect(screen.getByText("Review and export your theme")).toBeInTheDocument(); + }); + + // Click export button + const exportButton = screen.getByRole("button", { name: "Export theme" }); + fireEvent.click(exportButton); expect(mockHandleExport).toHaveBeenCalledTimes(1); const exportedTheme = mockHandleExport.mock.calls.at(0)?.[0]; diff --git a/apps/website/test/theme-generator/ThemeGeneratorPreviewPage.test.tsx b/apps/website/test/theme-generator/ThemeGeneratorPreviewPage.test.tsx index c8feac60e..53ab546de 100644 --- a/apps/website/test/theme-generator/ThemeGeneratorPreviewPage.test.tsx +++ b/apps/website/test/theme-generator/ThemeGeneratorPreviewPage.test.tsx @@ -1,24 +1,6 @@ -import "@testing-library/jest-dom/jest-globals"; -import React from "react"; -import { afterEach, beforeAll, describe, expect, it, jest } from "@jest/globals"; -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; - -let ThemeGeneratorPreviewPage: (props: { - tokens: Record; - logos: { - mainLogo: Array<{ file: File; preview?: string }>; - footerLogo: Array; - footerReducedLogo: Array; - favicon: Array; - }; -}) => React.JSX.Element; - -jest.mock("../../screens/common/componentsList.json", () => [ - { - label: "General", - links: [{ label: "Button", path: "/components/button", icon: "click" }], - }, -]); +import "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; +import ThemeGeneratorPreviewPage from "../../screens/theme-generator/ThemeGeneratorPreviewPage"; jest.mock("screens/utilities/theme-generator/componentsRegistry", () => ({ componentsRegistry: { @@ -69,14 +51,6 @@ jest.mock("@dxc-technology/halstack-react", () => ({ HalstackProvider: ({ children }: { children: React.ReactNode }) =>
{children}
, })); -beforeAll(async () => { - ThemeGeneratorPreviewPage = (await import("../../screens/theme-generator/ThemeGeneratorPreviewPage")).default; -}); - -afterEach(() => { - cleanup(); -}); - describe("ThemeGeneratorPreviewPage", () => { it("shows empty state and renders component preview after selection", () => { const logos = { mainLogo: [], footerLogo: [], footerReducedLogo: [], favicon: [] }; diff --git a/apps/website/test/theme-generator/utils.test.ts b/apps/website/test/theme-generator/utils.test.ts index 59bd289bb..aad381d37 100644 --- a/apps/website/test/theme-generator/utils.test.ts +++ b/apps/website/test/theme-generator/utils.test.ts @@ -1,146 +1,336 @@ -import "@testing-library/jest-dom/jest-globals"; -import { describe, expect, it, jest, beforeEach } from "@jest/globals"; +import "@testing-library/jest-dom"; +import { CssColor } from "@adobe/leonardo-contrast-colors"; +import { + CONTRAST_RATIOS, + SHADE_VALUES, + generatePalette, + generateTokens, + handleExport, + divideColorTokens, +} from "../../screens/theme-generator/utils"; const mockValues = [ - { value: "#101010" }, - { value: "#202020" }, - { value: "#303030" }, - { value: "#404040" }, - { value: "#505050" }, - { value: "#606060" }, - { value: "#707070" }, - { value: "#808080" }, - { value: "#909090" }, - { value: "#a0a0a0" }, + { value: "#fff5f5" }, + { value: "#ffe5e5" }, + { value: "#ffd5d5" }, + { value: "#ffc5c5" }, + { value: "#ffb5b5" }, + { value: "#ffa5a5" }, + { value: "#ff9595" }, + { value: "#ff8585" }, + { value: "#ff7575" }, + { value: "#ff6565" }, ]; jest.mock("@adobe/leonardo-contrast-colors", () => ({ - BackgroundColor: class { - name: string; - colorKeys: string[]; - ratios: number[]; - - constructor({ name, colorKeys, ratios }: { name: string; colorKeys: string[]; ratios: number[] }) { - this.name = name; - this.colorKeys = colorKeys; - this.ratios = ratios; + Color: jest.fn().mockImplementation(({ colorKeys }: { colorKeys: string[] }) => { + if (colorKeys[0] === "throw") { + throw new Error("Invalid color"); } - }, - Color: class { - name: string; - colorKeys: string[]; - ratios: number[]; - colorSpace: string; - smooth: boolean; - - constructor({ - name, - colorKeys, - ratios, - colorSpace, - smooth, - }: { - name: string; - colorKeys: string[]; - ratios: number[]; - colorSpace: string; - smooth: boolean; - }) { - this.name = name; - this.colorKeys = colorKeys; - this.ratios = ratios; - this.colorSpace = colorSpace; - this.smooth = smooth; - } - }, - Theme: class { - contrastColors: Array<{ values: Array<{ value: string }> }>; - - constructor({ colors }: { colors: Array<{ colorKeys: string[] }> }) { - if (colors[0]?.colorKeys?.[0] === "throw") { - throw new Error("Palette error"); - } - this.contrastColors = [{ values: [] }, { values: mockValues }]; - } - }, + return {}; + }), + BackgroundColor: jest.fn().mockImplementation(() => ({})), + Theme: jest.fn().mockImplementation(() => ({ + contrastColors: [null, { values: mockValues }], + })), })); -let generatePalette: (hex: string) => string[]; -let generateTokens: (baseColors: Record) => Record; -let handleExport: (themeJson: string) => void; -let divideColorTokens: (tokens: Record) => Record; - -beforeEach(async () => { - jest.resetModules(); - const utils = await import("../../screens/theme-generator/utils"); - generatePalette = utils.generatePalette; - generateTokens = utils.generateTokens; - handleExport = utils.handleExport; - divideColorTokens = utils.divideColorTokens; -}); - describe("theme-generator utils", () => { beforeEach(() => { - jest.restoreAllMocks(); + jest.clearAllMocks(); }); - it("generatePalette returns generated palette values", () => { - const palette = generatePalette("#5F249F"); + describe("Constants", () => { + it("should export CONTRAST_RATIOS with correct length", () => { + expect(CONTRAST_RATIOS).toHaveLength(10); + expect(CONTRAST_RATIOS[0]).toBe(1.03); + expect(CONTRAST_RATIOS[9]).toBe(12.46); + }); - expect(palette).toHaveLength(10); - expect(palette[0]).toBe("#101010"); - expect(palette[9]).toBe("#a0a0a0"); + it("should export SHADE_VALUES with correct values", () => { + expect(SHADE_VALUES).toEqual([50, 100, 200, 300, 400, 500, 600, 700, 800, 900]); + }); }); - it("generatePalette handles errors and returns empty array", () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => undefined); + describe("generatePalette", () => { + it("should generate palette with 10 colors", () => { + const palette = generatePalette("#FF5733" as CssColor); + expect(palette).toHaveLength(10); + }); + + it("should return array of hex colors", () => { + const palette = generatePalette("#FF5733" as CssColor); + palette.forEach((color) => { + expect(color).toMatch(/^#[0-9a-fA-F]{6}$/); + }); + }); + + it("should handle errors and return empty array", () => { + const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => undefined); - const palette = generatePalette("throw"); + const palette = generatePalette("throw" as CssColor); - expect(palette).toEqual([]); - expect(consoleSpy).toHaveBeenCalled(); + expect(palette).toEqual([]); + expect(consoleSpy).toHaveBeenCalled(); + }); + + it("should return consistent palette structure", () => { + const palette = generatePalette("#5F249F" as CssColor); + expect(palette).toHaveLength(10); + expect(Array.isArray(palette)).toBe(true); + }); + + it("should return correct values from mocked Leonardo", () => { + const palette = generatePalette("#5F249F" as CssColor); + expect(palette[0]).toBe("#fff5f5"); + expect(palette[9]).toBe("#ff6565"); + }); }); - it("generateTokens includes base, alpha and absolute tokens", () => { - const tokens = generateTokens({ - primary: "#111111", - secondary: "#222222", - neutral: "#333333", + describe("generateTokens", () => { + it("should include base color tokens for all provided colors", () => { + const tokens = generateTokens({ + primary: "#111111" as CssColor, + secondary: "#222222" as CssColor, + neutral: "#333333" as CssColor, + }); + + expect(tokens["--color-primary-50"]).toBeDefined(); + expect(tokens["--color-primary-900"]).toBeDefined(); + expect(tokens["--color-secondary-50"]).toBeDefined(); + expect(tokens["--color-secondary-900"]).toBeDefined(); + expect(tokens["--color-neutral-50"]).toBeDefined(); + expect(tokens["--color-neutral-900"]).toBeDefined(); + }); + + it("should include alpha tokens", () => { + const tokens = generateTokens({ + primary: "#111111" as CssColor, + neutral: "#333333" as CssColor, + }); + + expect(tokens["--color-alpha-100-a"]).toBeDefined(); + expect(tokens["--color-alpha-200-a"]).toBeDefined(); + expect(tokens["--color-alpha-900-a"]).toBeDefined(); }); - expect(tokens["--color-primary-50"]).toBe("#101010"); - expect(tokens["--color-secondary-900"]).toBe("#a0a0a0"); - expect(tokens["--color-alpha-100-a"]).toBe("#2020201a"); - expect(tokens["--color-alpha-900-a"]).toBe("#a0a0a0e5"); - expect(tokens["--color-absolutes-black"]).toBe("#000000"); - expect(tokens["--color-absolutes-white"]).toBe("#ffffff"); + it("should include absolute color tokens", () => { + const tokens = generateTokens({ + primary: "#111111" as CssColor, + }); + + expect(tokens["--color-absolutes-black"]).toBe("#000000"); + expect(tokens["--color-absolutes-white"]).toBe("#ffffff"); + }); + + it("should generate correct alpha values with opacity", () => { + const tokens = generateTokens({ + neutral: "#999999" as CssColor, + }); + + expect(tokens["--color-alpha-100-a"]).toMatch(/^#[0-9a-fA-F]{6}1a$/); + expect(tokens["--color-alpha-900-a"]).toMatch(/^#[0-9a-fA-F]{6}e5$/); + }); + + it("should handle single color input", () => { + const tokens = generateTokens({ + primary: "#FF5733" as CssColor, + }); + + expect(Object.keys(tokens).length).toBeGreaterThan(0); + expect(tokens["--color-primary-500"]).toBeDefined(); + }); + + it("should convert color names to lowercase in tokens", () => { + const tokens = generateTokens({ + Primary: "#FF5733" as CssColor, + }); + + expect(tokens["--color-primary-50"]).toBeDefined(); + }); + + it("should use default neutral color if not provided", () => { + const tokens = generateTokens({ + primary: "#FF5733" as CssColor, + }); + + // Alpha tokens should still be generated with default neutral + expect(tokens["--color-alpha-100-a"]).toBeDefined(); + }); }); - it("handleExport creates and clicks a download link", () => { - const anchor = document.createElement("a"); - const clickSpy = jest.spyOn(anchor, "click").mockImplementation(() => undefined); - const createElementSpy = jest.spyOn(document, "createElement").mockReturnValue(anchor); + describe("handleExport", () => { + let anchor: HTMLAnchorElement; + let clickSpy: jest.SpyInstance; + let createElementSpy: jest.SpyInstance; + let appendChildSpy: jest.SpyInstance; + let removeSpy: jest.SpyInstance; + + beforeEach(() => { + anchor = document.createElement("a"); + clickSpy = jest.spyOn(anchor, "click").mockImplementation(() => undefined); + removeSpy = jest.spyOn(anchor, "remove").mockImplementation(() => undefined); + createElementSpy = jest.spyOn(document, "createElement").mockReturnValue(anchor); + appendChildSpy = jest.spyOn(document.body, "appendChild").mockImplementation(() => anchor); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("should create an anchor element", () => { + handleExport('{"tokens":{}}'); + expect(createElementSpy).toHaveBeenCalledWith("a"); + }); + + it("should set correct download attribute", () => { + handleExport('{"tokens":{}}'); + expect(anchor.getAttribute("download")).toBe("halstack-theme-tokens.json"); + }); + + it("should set href with encoded JSON data", () => { + handleExport('{"tokens":{}}'); + expect(anchor.getAttribute("href")).toContain("data:text/json;charset=utf-8,"); + expect(anchor.getAttribute("href")).toContain(encodeURIComponent('{"tokens":{}}')); + }); - handleExport('{"tokens":{}}'); + it("should append anchor to body", () => { + handleExport('{"tokens":{}}'); + expect(appendChildSpy).toHaveBeenCalledWith(anchor); + }); + + it("should click the anchor element", () => { + handleExport('{"tokens":{}}'); + expect(clickSpy).toHaveBeenCalledTimes(1); + }); + + it("should remove anchor after click", () => { + handleExport('{"tokens":{}}'); + expect(removeSpy).toHaveBeenCalledTimes(1); + }); - expect(createElementSpy).toHaveBeenCalledWith("a"); - expect(anchor.getAttribute("download")).toBe("halstack-theme-tokens.json"); - expect(anchor.getAttribute("href")).toContain("data:text/json;charset=utf-8,"); - expect(clickSpy).toHaveBeenCalledTimes(1); + it("should handle complex JSON data", () => { + const complexJson = JSON.stringify({ + tokens: { + "--color-primary-500": "#FF5733", + "--color-secondary-500": "#33FF57", + }, + metadata: { version: "1.0" }, + }); + + handleExport(complexJson); + expect(anchor.getAttribute("href")).toContain(encodeURIComponent(complexJson)); + }); + + it("should handle empty JSON", () => { + handleExport("{}"); + expect(clickSpy).toHaveBeenCalled(); + }); }); - it("divideColorTokens groups only matching color token keys", () => { - const grouped = divideColorTokens({ - "--color-primary-500": "#111111", - "--color-semantic01-500": "#222222", - "--color-alpha-100-a": "#333333", - "--color-neutral-300": "#444444", - "--spacing-padding-s": "8px", + describe("divideColorTokens", () => { + it("should group tokens by color type", () => { + const grouped = divideColorTokens({ + "--color-primary-500": "#111111", + "--color-secondary-500": "#222222", + "--color-tertiary-500": "#333333", + }); + + expect(grouped.primary).toEqual(["#111111"]); + expect(grouped.secondary).toEqual(["#222222"]); + expect(grouped.tertiary).toEqual(["#333333"]); + }); + + it("should group semantic color tokens", () => { + const grouped = divideColorTokens({ + "--color-semantic01-500": "#111111", + "--color-semantic02-600": "#222222", + "--color-semantic03-700": "#333333", + "--color-semantic04-800": "#444444", + }); + + expect(grouped.semantic01).toEqual(["#111111"]); + expect(grouped.semantic02).toEqual(["#222222"]); + expect(grouped.semantic03).toEqual(["#333333"]); + expect(grouped.semantic04).toEqual(["#444444"]); + }); + + it("should group alpha tokens", () => { + const grouped = divideColorTokens({ + "--color-alpha-100-a": "#33333333", + "--color-alpha-200-a": "#33333366", + }); + + expect(grouped.alpha).toEqual(["#33333333", "#33333366"]); + }); + + it("should group neutral tokens", () => { + const grouped = divideColorTokens({ + "--color-neutral-300": "#999999", + "--color-neutral-500": "#666666", + }); + + expect(grouped.neutral).toEqual(["#999999", "#666666"]); }); - expect(grouped.primary).toEqual(["#111111"]); - expect(grouped.semantic01).toEqual(["#222222"]); - expect(grouped.alpha).toEqual(["#333333"]); - expect(grouped.neutral).toEqual(["#444444"]); + it("should ignore non-color tokens", () => { + const grouped = divideColorTokens({ + "--color-primary-500": "#111111", + "--spacing-padding-s": "8px", + "--font-size-large": "16px", + "--border-radius": "4px", + }); + + expect(grouped.primary).toEqual(["#111111"]); + expect(Object.values(grouped).flat()).toHaveLength(1); + }); + + it("should handle multiple values for same color group", () => { + const grouped = divideColorTokens({ + "--color-primary-50": "#fff5f5", + "--color-primary-100": "#ffe5e5", + "--color-primary-200": "#ffd5d5", + "--color-primary-500": "#ff5733", + }); + + expect(grouped.primary).toHaveLength(4); + expect(grouped.primary).toContain("#fff5f5"); + expect(grouped.primary).toContain("#ff5733"); + }); + + it("should return empty arrays for unused color groups", () => { + const grouped = divideColorTokens({ + "--color-primary-500": "#111111", + }); + + expect(grouped.secondary).toEqual([]); + expect(grouped.tertiary).toEqual([]); + expect(grouped.neutral).toEqual([]); + expect(grouped.alpha).toEqual([]); + }); + + it("should handle empty input", () => { + const grouped = divideColorTokens({}); + + expect(grouped.primary).toEqual([]); + expect(grouped.secondary).toEqual([]); + expect(grouped.tertiary).toEqual([]); + expect(grouped.semantic01).toEqual([]); + expect(grouped.semantic02).toEqual([]); + expect(grouped.semantic03).toEqual([]); + expect(grouped.semantic04).toEqual([]); + expect(grouped.neutral).toEqual([]); + expect(grouped.alpha).toEqual([]); + }); + + it("should preserve order of tokens within each group", () => { + const grouped = divideColorTokens({ + "--color-primary-900": "#333333", + "--color-primary-50": "#111111", + "--color-primary-500": "#222222", + }); + + expect(grouped.primary).toEqual(["#333333", "#111111", "#222222"]); + }); }); }); diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json index aa9c8211b..ba2885ac0 100644 --- a/apps/website/tsconfig.json +++ b/apps/website/tsconfig.json @@ -9,6 +9,6 @@ "@/common/*": ["screens/common/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "next.config.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "next.config.ts", "test"], "exclude": ["node_modules", "out", ".turbo", ".next"] } From 2ffc7b018482db73e29245ee1c3970726778e397 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso Date: Wed, 18 Mar 2026 12:58:47 +0100 Subject: [PATCH 3/6] Add tests to useCopyToClipboard hook --- .../test/hooks/useCopyToClipboard.test.tsx | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 apps/website/test/hooks/useCopyToClipboard.test.tsx diff --git a/apps/website/test/hooks/useCopyToClipboard.test.tsx b/apps/website/test/hooks/useCopyToClipboard.test.tsx new file mode 100644 index 000000000..f6ab11b83 --- /dev/null +++ b/apps/website/test/hooks/useCopyToClipboard.test.tsx @@ -0,0 +1,83 @@ +import { renderHook } from "@testing-library/react"; +import { HalstackProvider } from "@dxc-technology/halstack-react"; +import useCopyToClipboard from "../../hooks/useCopyToClipboard"; +import React from "react"; + +describe("useCopyToClipboard", () => { + const wrapper = ({ children }: { children: React.ReactNode }) => {children}; + + 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); + + // Wait for the rejected promise to resolve + 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"); + }); +}); From a780707a1c3ff9bea29b4c106863c25f9ba9a91c Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso Date: Tue, 24 Mar 2026 10:03:15 +0100 Subject: [PATCH 4/6] Update tests --- .../ThemeGeneratorPreviewPage.tsx | 22 +++--- .../components/BottomButtons.tsx | 1 - .../components/StepHeading.tsx | 1 - .../test/hooks/useCopyToClipboard.test.tsx | 2 - .../theme-generator/BrandingDetails.test.tsx | 3 - .../ThemeGeneratorConfigPage.test.tsx | 4 +- .../ThemeGeneratorPreviewPage.test.tsx | 76 +++++++------------ .../test/theme-generator/utils.test.ts | 2 +- 8 files changed, 39 insertions(+), 72 deletions(-) diff --git a/apps/website/screens/theme-generator/ThemeGeneratorPreviewPage.tsx b/apps/website/screens/theme-generator/ThemeGeneratorPreviewPage.tsx index 00a9c1961..d1ea73b71 100644 --- a/apps/website/screens/theme-generator/ThemeGeneratorPreviewPage.tsx +++ b/apps/website/screens/theme-generator/ThemeGeneratorPreviewPage.tsx @@ -45,7 +45,7 @@ const informationIcon = ( ); -const exampleOptions = [ +export const exampleOptions = [ { label: "Application example", value: "/examples/application", @@ -63,7 +63,7 @@ const exampleOptions = [ }, ]; -const componentsExceptions = [ +export const componentsExceptions = [ "/components/application-layout", "/components/bleed", "/components/bulleted-list", @@ -83,7 +83,7 @@ const componentsExceptions = [ "/components/typography", ]; -const mapToSelectGroups = (data: ComponentItem[]) => { +export const mapToSelectGroups = (data: ComponentItem[]) => { const collectOptions = (items: ComponentItem[]): ListOptionType[] => { return items.flatMap((item) => { const current: ListOptionType[] = @@ -109,6 +109,13 @@ const mapToSelectGroups = (data: ComponentItem[]) => { })); }; +export const processLogos = (logos: Logos) => ({ + mainLogo: logos.mainLogo?.[0]?.preview, + footerLogo: logos.footerLogo?.[0]?.preview, + footerReducedLogo: logos.footerReducedLogo?.[0]?.preview, + favicon: logos.favicon?.[0]?.preview, +}); + const ThemeGeneratorPreviewPage = ({ tokens, logos }: { tokens: Record; logos: Logos }) => { const [mode, setMode] = useState<"components" | "examples">("components"); @@ -150,14 +157,7 @@ const ThemeGeneratorPreviewPage = ({ tokens, logos }: { tokens: Record { - return { - mainLogo: logos.mainLogo?.[0]?.preview, - footerLogo: logos.footerLogo?.[0]?.preview, - footerReducedLogo: logos.footerReducedLogo?.[0]?.preview, - favicon: logos.favicon?.[0]?.preview, - }; - }, [logos]); + const processedLogos = useMemo(() => processLogos(logos), [logos]); return ( diff --git a/apps/website/screens/theme-generator/components/BottomButtons.tsx b/apps/website/screens/theme-generator/components/BottomButtons.tsx index b427ba40c..7a2974d19 100644 --- a/apps/website/screens/theme-generator/components/BottomButtons.tsx +++ b/apps/website/screens/theme-generator/components/BottomButtons.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { DxcButton, DxcContainer, DxcFlex } from "@dxc-technology/halstack-react"; import { Step } from "../types"; diff --git a/apps/website/screens/theme-generator/components/StepHeading.tsx b/apps/website/screens/theme-generator/components/StepHeading.tsx index 60f8ced12..e99a3d799 100644 --- a/apps/website/screens/theme-generator/components/StepHeading.tsx +++ b/apps/website/screens/theme-generator/components/StepHeading.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { DxcFlex, DxcHeading, DxcContainer, DxcTypography } from "@dxc-technology/halstack-react"; const StepHeading = ({ title, subtitle }: { title: string; subtitle: string }) => ( diff --git a/apps/website/test/hooks/useCopyToClipboard.test.tsx b/apps/website/test/hooks/useCopyToClipboard.test.tsx index f6ab11b83..13183e86f 100644 --- a/apps/website/test/hooks/useCopyToClipboard.test.tsx +++ b/apps/website/test/hooks/useCopyToClipboard.test.tsx @@ -1,7 +1,6 @@ import { renderHook } from "@testing-library/react"; import { HalstackProvider } from "@dxc-technology/halstack-react"; import useCopyToClipboard from "../../hooks/useCopyToClipboard"; -import React from "react"; describe("useCopyToClipboard", () => { const wrapper = ({ children }: { children: React.ReactNode }) => {children}; @@ -52,7 +51,6 @@ describe("useCopyToClipboard", () => { expect(mockWriteText).toHaveBeenCalledWith(testText); - // Wait for the rejected promise to resolve await new Promise((resolve) => setTimeout(resolve, 0)); }); diff --git a/apps/website/test/theme-generator/BrandingDetails.test.tsx b/apps/website/test/theme-generator/BrandingDetails.test.tsx index a7ba7b1d9..022169e54 100644 --- a/apps/website/test/theme-generator/BrandingDetails.test.tsx +++ b/apps/website/test/theme-generator/BrandingDetails.test.tsx @@ -43,7 +43,6 @@ describe("BrandingDetails", () => { ); - // Verify that primary color input is rendered with correct value expect(screen.getByDisplayValue("#5F249F")).toBeInTheDocument(); expect(screen.getByText("Primary")).toBeInTheDocument(); }); @@ -69,7 +68,6 @@ describe("BrandingDetails", () => { ); - // Find the primary color input and change it const primaryInput = screen.getByDisplayValue("#5F249F"); fireEvent.change(primaryInput, { target: { value: "#111111" } }); fireEvent.blur(primaryInput); @@ -110,7 +108,6 @@ describe("BrandingDetails", () => { ); - // Verify logo sections are rendered expect(screen.getByText("Main logo")).toBeInTheDocument(); expect(screen.getByText("Default footer logo")).toBeInTheDocument(); }); diff --git a/apps/website/test/theme-generator/ThemeGeneratorConfigPage.test.tsx b/apps/website/test/theme-generator/ThemeGeneratorConfigPage.test.tsx index 3ee8a29aa..edf2951cd 100644 --- a/apps/website/test/theme-generator/ThemeGeneratorConfigPage.test.tsx +++ b/apps/website/test/theme-generator/ThemeGeneratorConfigPage.test.tsx @@ -47,7 +47,7 @@ jest.mock("react-color", () => ({ })); // Mock componentsRegistry and examplesRegistry -jest.mock("../../screens/utilities/theme-generator/componentsRegistry", () => ({ +jest.mock("../../screens/theme-generator/componentsRegistry", () => ({ componentsRegistry: {}, examplesRegistry: {}, })); @@ -113,7 +113,6 @@ describe("ThemeGeneratorConfigPage", () => { it("regenerates tokens with updated colors", async () => { render(); - // Change primary color by finding the primary color input and changing it const primaryInput = screen.getByLabelText("Primary"); fireEvent.change(primaryInput, { target: { value: "#123456" } }); fireEvent.blur(primaryInput); @@ -149,7 +148,6 @@ describe("ThemeGeneratorConfigPage", () => { expect(screen.getByText("Review and export your theme")).toBeInTheDocument(); }); - // Click export button const exportButton = screen.getByRole("button", { name: "Export theme" }); fireEvent.click(exportButton); diff --git a/apps/website/test/theme-generator/ThemeGeneratorPreviewPage.test.tsx b/apps/website/test/theme-generator/ThemeGeneratorPreviewPage.test.tsx index 53ab546de..79a1bab72 100644 --- a/apps/website/test/theme-generator/ThemeGeneratorPreviewPage.test.tsx +++ b/apps/website/test/theme-generator/ThemeGeneratorPreviewPage.test.tsx @@ -1,56 +1,27 @@ import "@testing-library/jest-dom"; import { fireEvent, render, screen } from "@testing-library/react"; import ThemeGeneratorPreviewPage from "../../screens/theme-generator/ThemeGeneratorPreviewPage"; +import { Logos } from "../../screens/theme-generator/types"; -jest.mock("screens/utilities/theme-generator/componentsRegistry", () => ({ +// Mock ResizeObserver +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); + +jest.mock("screens/theme-generator/componentsRegistry", () => ({ componentsRegistry: { - "/components/button": () =>
Button Preview
, + "/components/button": () =>
Button Component
, }, examplesRegistry: { - "/examples/application": ({ logos }: { logos: { mainLogo?: Array<{ preview?: string }> } }) => ( -
{`Example with ${logos.mainLogo?.[0]?.preview ?? "no-logo"}`}
- ), + "/examples/application": ({ logos }: { logos: Logos }) => { + const mainLogoPreview = logos.mainLogo[0]?.preview ?? "no-logo"; + return
Application Example {mainLogoPreview}
; + }, }, })); -jest.mock("@dxc-technology/halstack-react", () => ({ - DxcButton: ({ title, onClick }: { title?: string; onClick?: () => void }) => ( - - ), - DxcContainer: ({ children }: { children: React.ReactNode }) =>
{children}
, - DxcFlex: ({ children }: { children: React.ReactNode }) =>
{children}
, - DxcSelect: ({ - placeholder, - multiple, - onChange, - }: { - placeholder: string; - multiple?: boolean; - onChange: (v: { value: string | string[] }) => void; - }) => ( - - ), - DxcToggleGroup: ({ onChange }: { onChange: (value: number) => void }) => ( -
- - -
- ), - DxcTypography: ({ children }: { children: React.ReactNode }) =>
{children}
, - HalstackProvider: ({ children }: { children: React.ReactNode }) =>
{children}
, -})); - describe("ThemeGeneratorPreviewPage", () => { it("shows empty state and renders component preview after selection", () => { const logos = { mainLogo: [], footerLogo: [], footerReducedLogo: [], favicon: [] }; @@ -59,10 +30,12 @@ describe("ThemeGeneratorPreviewPage", () => { expect(screen.getByText("Select a component to preview")).toBeInTheDocument(); - fireEvent.click(screen.getByRole("button", { name: "Select components" })); + fireEvent.click(screen.getByRole("combobox", { name: "Select" })); - expect(screen.getByText("Button")).toBeInTheDocument(); - expect(screen.getByText("Button Preview")).toBeInTheDocument(); + const buttonOption = screen.getByText("Button"); + fireEvent.click(buttonOption); + + expect(screen.getByText("Button Component")).toBeInTheDocument(); }); it("switches to examples mode and renders selected example", () => { @@ -75,10 +48,13 @@ describe("ThemeGeneratorPreviewPage", () => { render(); - fireEvent.click(screen.getByRole("button", { name: "Examples mode" })); - fireEvent.click(screen.getByRole("button", { name: "Select examples" })); + fireEvent.click(screen.getByRole("button", { name: "Layout examples" })); + + fireEvent.click(screen.getByRole("combobox", { name: "Select" })); + + const appExample = screen.getByText("Application example"); + fireEvent.click(appExample); - expect(screen.getByText(/Some components are presentational examples\./)).toBeInTheDocument(); - expect(screen.getByText("Example with main-preview")).toBeInTheDocument(); + expect(screen.getByText("Application Example main-preview")).toBeInTheDocument(); }); }); diff --git a/apps/website/test/theme-generator/utils.test.ts b/apps/website/test/theme-generator/utils.test.ts index aad381d37..39798ea69 100644 --- a/apps/website/test/theme-generator/utils.test.ts +++ b/apps/website/test/theme-generator/utils.test.ts @@ -185,7 +185,7 @@ describe("theme-generator utils", () => { it("should set correct download attribute", () => { handleExport('{"tokens":{}}'); - expect(anchor.getAttribute("download")).toBe("halstack-theme-tokens.json"); + expect(anchor.getAttribute("download")).toBe("halstack-theme.json"); }); it("should set href with encoded JSON data", () => { From 9f2f70d9a4172cb29208ee17ca4136c9d0bcde49 Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso Date: Wed, 25 Mar 2026 08:43:08 +0100 Subject: [PATCH 5/6] Remove unnecesary change --- .../ThemeGeneratorPreviewPage.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/website/screens/theme-generator/ThemeGeneratorPreviewPage.tsx b/apps/website/screens/theme-generator/ThemeGeneratorPreviewPage.tsx index d1ea73b71..00a9c1961 100644 --- a/apps/website/screens/theme-generator/ThemeGeneratorPreviewPage.tsx +++ b/apps/website/screens/theme-generator/ThemeGeneratorPreviewPage.tsx @@ -45,7 +45,7 @@ const informationIcon = ( ); -export const exampleOptions = [ +const exampleOptions = [ { label: "Application example", value: "/examples/application", @@ -63,7 +63,7 @@ export const exampleOptions = [ }, ]; -export const componentsExceptions = [ +const componentsExceptions = [ "/components/application-layout", "/components/bleed", "/components/bulleted-list", @@ -83,7 +83,7 @@ export const componentsExceptions = [ "/components/typography", ]; -export const mapToSelectGroups = (data: ComponentItem[]) => { +const mapToSelectGroups = (data: ComponentItem[]) => { const collectOptions = (items: ComponentItem[]): ListOptionType[] => { return items.flatMap((item) => { const current: ListOptionType[] = @@ -109,13 +109,6 @@ export const mapToSelectGroups = (data: ComponentItem[]) => { })); }; -export const processLogos = (logos: Logos) => ({ - mainLogo: logos.mainLogo?.[0]?.preview, - footerLogo: logos.footerLogo?.[0]?.preview, - footerReducedLogo: logos.footerReducedLogo?.[0]?.preview, - favicon: logos.favicon?.[0]?.preview, -}); - const ThemeGeneratorPreviewPage = ({ tokens, logos }: { tokens: Record; logos: Logos }) => { const [mode, setMode] = useState<"components" | "examples">("components"); @@ -157,7 +150,14 @@ const ThemeGeneratorPreviewPage = ({ tokens, logos }: { tokens: Record processLogos(logos), [logos]); + const processedLogos = useMemo(() => { + return { + mainLogo: logos.mainLogo?.[0]?.preview, + footerLogo: logos.footerLogo?.[0]?.preview, + footerReducedLogo: logos.footerReducedLogo?.[0]?.preview, + favicon: logos.favicon?.[0]?.preview, + }; + }, [logos]); return ( From 72d25ac01c1fcff2be0e203be0ae163759a06bdd Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso Date: Wed, 25 Mar 2026 09:13:55 +0100 Subject: [PATCH 6/6] Change condition of paginator render --- packages/lib/src/resultset-table/ResultsetTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/resultset-table/ResultsetTable.tsx b/packages/lib/src/resultset-table/ResultsetTable.tsx index c5dce75d6..04fd302ba 100644 --- a/packages/lib/src/resultset-table/ResultsetTable.tsx +++ b/packages/lib/src/resultset-table/ResultsetTable.tsx @@ -122,7 +122,7 @@ const DxcResultsetTable = ({ ); const renderPaginator = () => - !hidePaginator && rows.length > itemsPerPage ? ( + !hidePaginator && (rows.length > itemsPerPage || !!itemsPerPageOptions?.length || itemsPerPageFunction != null) ? (