From 2114bacdae7392a7c274d277bd5da15c79d6b905 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:49:39 +0000 Subject: [PATCH 1/6] Initial plan From a828bf4c8fc50eca3320a61850744c9ba9eaa3cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:55:24 +0000 Subject: [PATCH 2/6] feature: Add InfoCard shared component with tests Co-authored-by: jhbruhn <1036566+jhbruhn@users.noreply.github.com> --- src/components/ui/info-card.test.tsx | 160 ++++++++++++++++++++++ src/components/ui/info-card.tsx | 191 +++++++++++++++++++++++++++ vitest.config.ts | 6 + 3 files changed, 357 insertions(+) create mode 100644 src/components/ui/info-card.test.tsx create mode 100644 src/components/ui/info-card.tsx diff --git a/src/components/ui/info-card.test.tsx b/src/components/ui/info-card.test.tsx new file mode 100644 index 0000000..637dcff --- /dev/null +++ b/src/components/ui/info-card.test.tsx @@ -0,0 +1,160 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { + InfoCard, + InfoCardTitle, + InfoCardDescription, + InfoCardList, + InfoCardListItem, +} from "./info-card"; + +describe("InfoCard", () => { + describe("rendering", () => { + it("should render with default info variant", () => { + render( + + Test Title + , + ); + + expect(screen.getByText("Test Title")).toBeTruthy(); + }); + + it("should render with all variants", () => { + const variants = ["info", "warning", "error", "success"] as const; + + variants.forEach((variant) => { + const { container } = render( + + {variant} Card + , + ); + + expect(screen.getByText(`${variant} Card`)).toBeTruthy(); + expect(container.firstChild?.classList.contains("border-l-4")).toBe(true); + }); + }); + + it("should render custom icon when provided", () => { + const CustomIcon = (props: React.SVGProps) => ( + + ); + + render( + + Title + , + ); + + expect(screen.getByTestId("custom-icon")).toBeTruthy(); + }); + + it("should not render icon when showDefaultIcon is false", () => { + const { container } = render( + + Title + , + ); + + const svg = container.querySelector("svg"); + expect(svg).toBeNull(); + }); + }); + + describe("InfoCardTitle", () => { + it("should render with correct variant colors", () => { + const { rerender } = render( + Info Title, + ); + const infoTitle = screen.getByText("Info Title"); + expect(infoTitle.classList.contains("text-info-900")).toBe(true); + + rerender(Error Title); + const errorTitle = screen.getByText("Error Title"); + expect(errorTitle.classList.contains("text-danger-900")).toBe(true); + }); + }); + + describe("InfoCardDescription", () => { + it("should render description text", () => { + render( + This is a description, + ); + expect(screen.getByText("This is a description")).toBeTruthy(); + }); + + it("should apply variant colors", () => { + render( + + Warning description + , + ); + const desc = screen.getByText("Warning description"); + expect(desc.classList.contains("text-warning-800")).toBe(true); + }); + }); + + describe("InfoCardList", () => { + it("should render unordered list by default", () => { + render( + + Item 1 + Item 2 + , + ); + + const list = screen.getByRole("list"); + expect(list.tagName).toBe("UL"); + expect(list.classList.contains("list-disc")).toBe(true); + }); + + it("should render ordered list when specified", () => { + render( + + First + Second + , + ); + + const list = screen.getByRole("list"); + expect(list.tagName).toBe("OL"); + expect(list.classList.contains("list-decimal")).toBe(true); + }); + + it("should render list items", () => { + render( + + Item 1 + Item 2 + , + ); + + expect(screen.getByText("Item 1")).toBeTruthy(); + expect(screen.getByText("Item 2")).toBeTruthy(); + }); + }); + + describe("composition", () => { + it("should render full info card with all components", () => { + render( + + Success! + + Operation completed successfully + + + First step completed + Second step completed + + , + ); + + expect(screen.getByText("Success!")).toBeTruthy(); + expect( + screen.getByText("Operation completed successfully"), + ).toBeTruthy(); + expect(screen.getByText("First step completed")).toBeTruthy(); + expect(screen.getByText("Second step completed")).toBeTruthy(); + }); + }); +}); diff --git a/src/components/ui/info-card.tsx b/src/components/ui/info-card.tsx new file mode 100644 index 0000000..a2b7bee --- /dev/null +++ b/src/components/ui/info-card.tsx @@ -0,0 +1,191 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { + InformationCircleIcon, + ExclamationTriangleIcon, + CheckCircleIcon, +} from "@heroicons/react/24/solid"; + +import { cn } from "@/lib/utils"; + +const infoCardVariants = cva( + "border-l-4 p-4 rounded-lg backdrop-blur-sm", + { + variants: { + variant: { + info: "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500", + warning: + "bg-warning-50 dark:bg-warning-900/95 border-warning-600 dark:border-warning-500", + error: + "bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500", + success: + "bg-success-50 dark:bg-success-900/95 border-success-600 dark:border-success-500", + }, + }, + defaultVariants: { + variant: "info", + }, + }, +); + +const iconColorVariants = cva("w-6 h-6 flex-shrink-0 mt-0.5", { + variants: { + variant: { + info: "text-info-600 dark:text-info-400", + warning: "text-warning-600 dark:text-warning-400", + error: "text-danger-600 dark:text-danger-400", + success: "text-success-600 dark:text-success-400", + }, + }, + defaultVariants: { + variant: "info", + }, +}); + +const titleColorVariants = cva("text-base font-semibold mb-2", { + variants: { + variant: { + info: "text-info-900 dark:text-info-200", + warning: "text-warning-900 dark:text-warning-200", + error: "text-danger-900 dark:text-danger-200", + success: "text-success-900 dark:text-success-200", + }, + }, + defaultVariants: { + variant: "info", + }, +}); + +const descriptionColorVariants = cva("text-sm mb-3", { + variants: { + variant: { + info: "text-info-800 dark:text-info-300", + warning: "text-warning-800 dark:text-warning-300", + error: "text-danger-800 dark:text-danger-300", + success: "text-success-800 dark:text-success-300", + }, + }, + defaultVariants: { + variant: "info", + }, +}); + +const listColorVariants = cva("text-sm", { + variants: { + variant: { + info: "text-info-700 dark:text-info-300", + warning: "text-yellow-700 dark:text-yellow-300", + error: "text-red-700 dark:text-red-300", + success: "text-green-700 dark:text-green-300", + }, + }, + defaultVariants: { + variant: "info", + }, +}); + +interface InfoCardProps + extends React.HTMLAttributes, + VariantProps { + icon?: React.ComponentType>; + showDefaultIcon?: boolean; +} + +function InfoCard({ + className, + variant = "info", + icon: CustomIcon, + showDefaultIcon = true, + children, + ...props +}: InfoCardProps) { + // Default icons based on variant + const defaultIcons = { + info: InformationCircleIcon, + warning: ExclamationTriangleIcon, + error: ExclamationTriangleIcon, + success: CheckCircleIcon, + }; + + const Icon = CustomIcon || (showDefaultIcon ? defaultIcons[variant!] : null); + + return ( +
+
+ {Icon && } +
{children}
+
+
+ ); +} + +function InfoCardTitle({ + className, + variant = "info", + ...props +}: React.HTMLAttributes & + VariantProps) { + return ( +

+ ); +} + +function InfoCardDescription({ + className, + variant = "info", + ...props +}: React.HTMLAttributes & + VariantProps) { + return ( +

+ ); +} + +interface InfoCardListProps + extends React.HTMLAttributes, + VariantProps { + ordered?: boolean; +} + +function InfoCardList({ + className, + variant = "info", + ordered = false, + children, + ...props +}: InfoCardListProps) { + const ListComponent = ordered ? "ol" : "ul"; + const listClass = ordered ? "list-decimal" : "list-disc"; + + return ( + + {children} + + ); +} + +function InfoCardListItem({ + className, + ...props +}: React.HTMLAttributes) { + return

  • ; +} + +export { + InfoCard, + InfoCardTitle, + InfoCardDescription, + InfoCardList, + InfoCardListItem, +}; diff --git a/vitest.config.ts b/vitest.config.ts index 527491c..a265845 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,12 @@ import { defineConfig } from "vitest/config"; +import { resolve } from "path"; export default defineConfig({ + resolve: { + alias: { + "@": resolve(__dirname, "./src"), + }, + }, test: { globals: true, environment: "jsdom", From b2f0455d4cf51a7aef633bbab15c7614cd82d53a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:58:19 +0000 Subject: [PATCH 3/6] feature: Refactor ErrorPopover and StepPopover to use InfoCard Co-authored-by: jhbruhn <1036566+jhbruhn@users.noreply.github.com> --- src/components/ErrorPopover.tsx | 110 +++++++-------- .../WorkflowStepper/StepPopover.tsx | 130 ++++++------------ src/components/ui/info-card.test.tsx | 11 +- src/components/ui/info-card.tsx | 37 +++-- 4 files changed, 108 insertions(+), 180 deletions(-) diff --git a/src/components/ErrorPopover.tsx b/src/components/ErrorPopover.tsx index 17d11e4..282f26f 100644 --- a/src/components/ErrorPopover.tsx +++ b/src/components/ErrorPopover.tsx @@ -1,10 +1,13 @@ -import { - ExclamationTriangleIcon, - InformationCircleIcon, -} from "@heroicons/react/24/solid"; import { getErrorDetails } from "../utils/errorCodeHelpers"; import { PopoverContent } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; +import { + InfoCard, + InfoCardTitle, + InfoCardDescription, + InfoCardList, + InfoCardListItem, +} from "@/components/ui/info-card"; interface ErrorPopoverContentProps { machineError?: number; @@ -24,71 +27,50 @@ export function ErrorPopoverContent({ const errorMsg = pyodideError || errorMessage || ""; const isInfo = isPairingErr || errorDetails?.isInformational; - const bgColor = isInfo - ? "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500" - : "bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500"; - - const iconColor = isInfo - ? "text-info-600 dark:text-info-400" - : "text-danger-600 dark:text-danger-400"; - - const textColor = isInfo - ? "text-info-900 dark:text-info-200" - : "text-danger-900 dark:text-danger-200"; - - const descColor = isInfo - ? "text-info-800 dark:text-info-300" - : "text-danger-800 dark:text-danger-300"; - - const listColor = isInfo - ? "text-info-700 dark:text-info-300" - : "text-danger-700 dark:text-danger-300"; - - const Icon = isInfo ? InformationCircleIcon : ExclamationTriangleIcon; + const variant = isInfo ? "info" : "error"; const title = errorDetails?.title || (isPairingErr ? "Pairing Required" : "Error"); return ( - -
    - -
    -

    - {title} -

    -

    - {errorDetails?.description || errorMsg} + + + {title} + + {errorDetails?.description || errorMsg} + + {errorDetails?.solutions && errorDetails.solutions.length > 0 && ( + <> +

    + {isInfo ? "Steps:" : "How to Fix:"} +

    + + {errorDetails.solutions.map((solution, index) => ( + {solution} + ))} + + + )} + {machineError !== undefined && !errorDetails?.isInformational && ( +

    + Error Code: 0x + {machineError.toString(16).toUpperCase().padStart(2, "0")}

    - {errorDetails?.solutions && errorDetails.solutions.length > 0 && ( - <> -

    - {isInfo ? "Steps:" : "How to Fix:"} -

    -
      - {errorDetails.solutions.map((solution, index) => ( -
    1. - {solution} -
    2. - ))} -
    - - )} - {machineError !== undefined && !errorDetails?.isInformational && ( -

    - Error Code: 0x - {machineError.toString(16).toUpperCase().padStart(2, "0")} -

    - )} -
    -
    + )} +
    ); } diff --git a/src/components/WorkflowStepper/StepPopover.tsx b/src/components/WorkflowStepper/StepPopover.tsx index d625fae..741f075 100644 --- a/src/components/WorkflowStepper/StepPopover.tsx +++ b/src/components/WorkflowStepper/StepPopover.tsx @@ -5,12 +5,15 @@ */ import { forwardRef } from "react"; -import { - InformationCircleIcon, - ExclamationTriangleIcon, -} from "@heroicons/react/24/solid"; import { MachineStatus } from "../../types/machine"; import { getGuideContent } from "../../utils/workflowGuideContent"; +import { + InfoCard, + InfoCardTitle, + InfoCardDescription, + InfoCardList, + InfoCardListItem, +} from "@/components/ui/info-card"; export interface StepPopoverProps { stepId: number; @@ -22,54 +25,16 @@ export const StepPopover = forwardRef( const content = getGuideContent(stepId, machineStatus); if (!content) return null; - const colorClasses = { - info: "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500", - success: - "bg-success-50 dark:bg-success-900/95 border-success-600 dark:border-success-500", - warning: - "bg-warning-50 dark:bg-warning-900/95 border-warning-600 dark:border-warning-500", - error: - "bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500", - progress: - "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500", - }; + // Map content type to InfoCard variant + const variantMap = { + info: "info", + success: "success", + warning: "warning", + error: "error", + progress: "info", + } as const; - const iconColorClasses = { - info: "text-info-600 dark:text-info-400", - success: "text-success-600 dark:text-success-400", - warning: "text-warning-600 dark:text-warning-400", - error: "text-danger-600 dark:text-danger-400", - progress: "text-info-600 dark:text-info-400", - }; - - const textColorClasses = { - info: "text-info-900 dark:text-info-200", - success: "text-success-900 dark:text-success-200", - warning: "text-warning-900 dark:text-warning-200", - error: "text-danger-900 dark:text-danger-200", - progress: "text-info-900 dark:text-info-200", - }; - - const descColorClasses = { - info: "text-info-800 dark:text-info-300", - success: "text-success-800 dark:text-success-300", - warning: "text-warning-800 dark:text-warning-300", - error: "text-danger-800 dark:text-danger-300", - progress: "text-info-800 dark:text-info-300", - }; - - const listColorClasses = { - info: "text-blue-700 dark:text-blue-300", - success: "text-green-700 dark:text-green-300", - warning: "text-yellow-700 dark:text-yellow-300", - error: "text-red-700 dark:text-red-300", - progress: "text-cyan-700 dark:text-cyan-300", - }; - - const Icon = - content.type === "warning" - ? ExclamationTriangleIcon - : InformationCircleIcon; + const variant = variantMap[content.type]; return (
    ( role="dialog" aria-label="Step guidance" > -
    -
    - -
    -

    - {content.title} -

    -

    - {content.description} -

    - {content.items && content.items.length > 0 && ( -
      - {content.items.map((item, index) => { - // Parse **text** markdown syntax into React elements safely - const parts = item.split(/(\*\*.*?\*\*)/); - return ( -
    • - {parts.map((part, i) => { - if (part.startsWith("**") && part.endsWith("**")) { - return {part.slice(2, -2)}; - } - return part; - })} -
    • - ); - })} -
    - )} -
    -
    -
    + + {content.title} + + {content.description} + + {content.items && content.items.length > 0 && ( + + {content.items.map((item, index) => { + // Parse **text** markdown syntax into React elements safely + const parts = item.split(/(\*\*.*?\*\*)/); + return ( + + {parts.map((part, i) => { + if (part.startsWith("**") && part.endsWith("**")) { + return {part.slice(2, -2)}; + } + return part; + })} + + ); + })} + + )} +
    ); }, diff --git a/src/components/ui/info-card.test.tsx b/src/components/ui/info-card.test.tsx index 637dcff..94ef41b 100644 --- a/src/components/ui/info-card.test.tsx +++ b/src/components/ui/info-card.test.tsx @@ -31,7 +31,8 @@ describe("InfoCard", () => { ); expect(screen.getByText(`${variant} Card`)).toBeTruthy(); - expect(container.firstChild?.classList.contains("border-l-4")).toBe(true); + const firstChild = container.firstChild as HTMLElement | null; + expect(firstChild?.classList.contains("border-l-4")).toBe(true); }); }); @@ -77,9 +78,7 @@ describe("InfoCard", () => { describe("InfoCardDescription", () => { it("should render description text", () => { - render( - This is a description, - ); + render(This is a description); expect(screen.getByText("This is a description")).toBeTruthy(); }); @@ -150,9 +149,7 @@ describe("InfoCard", () => { ); expect(screen.getByText("Success!")).toBeTruthy(); - expect( - screen.getByText("Operation completed successfully"), - ).toBeTruthy(); + expect(screen.getByText("Operation completed successfully")).toBeTruthy(); expect(screen.getByText("First step completed")).toBeTruthy(); expect(screen.getByText("Second step completed")).toBeTruthy(); }); diff --git a/src/components/ui/info-card.tsx b/src/components/ui/info-card.tsx index a2b7bee..3d33856 100644 --- a/src/components/ui/info-card.tsx +++ b/src/components/ui/info-card.tsx @@ -8,25 +8,22 @@ import { import { cn } from "@/lib/utils"; -const infoCardVariants = cva( - "border-l-4 p-4 rounded-lg backdrop-blur-sm", - { - variants: { - variant: { - info: "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500", - warning: - "bg-warning-50 dark:bg-warning-900/95 border-warning-600 dark:border-warning-500", - error: - "bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500", - success: - "bg-success-50 dark:bg-success-900/95 border-success-600 dark:border-success-500", - }, - }, - defaultVariants: { - variant: "info", +const infoCardVariants = cva("border-l-4 p-4 rounded-lg backdrop-blur-sm", { + variants: { + variant: { + info: "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500", + warning: + "bg-warning-50 dark:bg-warning-900/95 border-warning-600 dark:border-warning-500", + error: + "bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500", + success: + "bg-success-50 dark:bg-success-900/95 border-success-600 dark:border-success-500", }, }, -); + defaultVariants: { + variant: "info", + }, +}); const iconColorVariants = cva("w-6 h-6 flex-shrink-0 mt-0.5", { variants: { @@ -85,7 +82,8 @@ const listColorVariants = cva("text-sm", { }); interface InfoCardProps - extends React.HTMLAttributes, + extends + React.HTMLAttributes, VariantProps { icon?: React.ComponentType>; showDefaultIcon?: boolean; @@ -145,7 +143,8 @@ function InfoCardDescription({ } interface InfoCardListProps - extends React.HTMLAttributes, + extends + React.HTMLAttributes, VariantProps { ordered?: boolean; } From df71f74396e4a95617453c4b691524fc44e240ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 17:01:20 +0000 Subject: [PATCH 4/6] fix: Address code review feedback for InfoCard component Co-authored-by: jhbruhn <1036566+jhbruhn@users.noreply.github.com> --- src/components/ui/info-card.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ui/info-card.tsx b/src/components/ui/info-card.tsx index 3d33856..2ba19d0 100644 --- a/src/components/ui/info-card.tsx +++ b/src/components/ui/info-card.tsx @@ -71,9 +71,9 @@ const listColorVariants = cva("text-sm", { variants: { variant: { info: "text-info-700 dark:text-info-300", - warning: "text-yellow-700 dark:text-yellow-300", - error: "text-red-700 dark:text-red-300", - success: "text-green-700 dark:text-green-300", + warning: "text-warning-700 dark:text-warning-300", + error: "text-danger-700 dark:text-danger-300", + success: "text-success-700 dark:text-success-300", }, }, defaultVariants: { @@ -105,7 +105,7 @@ function InfoCard({ success: CheckCircleIcon, }; - const Icon = CustomIcon || (showDefaultIcon ? defaultIcons[variant!] : null); + const Icon = CustomIcon || (showDefaultIcon ? defaultIcons[variant] : null); return (
    From edb8fa92649ae967bdd51ce14b6aa3e07a5ac2b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 17:07:23 +0000 Subject: [PATCH 5/6] fix: Move InfoCard to components folder and remove tests Co-authored-by: jhbruhn <1036566+jhbruhn@users.noreply.github.com> --- src/components/ErrorPopover.tsx | 2 +- .../{ui/info-card.tsx => InfoCard.tsx} | 0 .../WorkflowStepper/StepPopover.tsx | 2 +- src/components/ui/info-card.test.tsx | 157 ------------------ vitest.config.ts | 6 - 5 files changed, 2 insertions(+), 165 deletions(-) rename src/components/{ui/info-card.tsx => InfoCard.tsx} (100%) delete mode 100644 src/components/ui/info-card.test.tsx diff --git a/src/components/ErrorPopover.tsx b/src/components/ErrorPopover.tsx index 282f26f..fa2ab14 100644 --- a/src/components/ErrorPopover.tsx +++ b/src/components/ErrorPopover.tsx @@ -7,7 +7,7 @@ import { InfoCardDescription, InfoCardList, InfoCardListItem, -} from "@/components/ui/info-card"; +} from "./InfoCard"; interface ErrorPopoverContentProps { machineError?: number; diff --git a/src/components/ui/info-card.tsx b/src/components/InfoCard.tsx similarity index 100% rename from src/components/ui/info-card.tsx rename to src/components/InfoCard.tsx diff --git a/src/components/WorkflowStepper/StepPopover.tsx b/src/components/WorkflowStepper/StepPopover.tsx index 741f075..43273e9 100644 --- a/src/components/WorkflowStepper/StepPopover.tsx +++ b/src/components/WorkflowStepper/StepPopover.tsx @@ -13,7 +13,7 @@ import { InfoCardDescription, InfoCardList, InfoCardListItem, -} from "@/components/ui/info-card"; +} from "../InfoCard"; export interface StepPopoverProps { stepId: number; diff --git a/src/components/ui/info-card.test.tsx b/src/components/ui/info-card.test.tsx deleted file mode 100644 index 94ef41b..0000000 --- a/src/components/ui/info-card.test.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { - InfoCard, - InfoCardTitle, - InfoCardDescription, - InfoCardList, - InfoCardListItem, -} from "./info-card"; - -describe("InfoCard", () => { - describe("rendering", () => { - it("should render with default info variant", () => { - render( - - Test Title - , - ); - - expect(screen.getByText("Test Title")).toBeTruthy(); - }); - - it("should render with all variants", () => { - const variants = ["info", "warning", "error", "success"] as const; - - variants.forEach((variant) => { - const { container } = render( - - {variant} Card - , - ); - - expect(screen.getByText(`${variant} Card`)).toBeTruthy(); - const firstChild = container.firstChild as HTMLElement | null; - expect(firstChild?.classList.contains("border-l-4")).toBe(true); - }); - }); - - it("should render custom icon when provided", () => { - const CustomIcon = (props: React.SVGProps) => ( - - ); - - render( - - Title - , - ); - - expect(screen.getByTestId("custom-icon")).toBeTruthy(); - }); - - it("should not render icon when showDefaultIcon is false", () => { - const { container } = render( - - Title - , - ); - - const svg = container.querySelector("svg"); - expect(svg).toBeNull(); - }); - }); - - describe("InfoCardTitle", () => { - it("should render with correct variant colors", () => { - const { rerender } = render( - Info Title, - ); - const infoTitle = screen.getByText("Info Title"); - expect(infoTitle.classList.contains("text-info-900")).toBe(true); - - rerender(Error Title); - const errorTitle = screen.getByText("Error Title"); - expect(errorTitle.classList.contains("text-danger-900")).toBe(true); - }); - }); - - describe("InfoCardDescription", () => { - it("should render description text", () => { - render(This is a description); - expect(screen.getByText("This is a description")).toBeTruthy(); - }); - - it("should apply variant colors", () => { - render( - - Warning description - , - ); - const desc = screen.getByText("Warning description"); - expect(desc.classList.contains("text-warning-800")).toBe(true); - }); - }); - - describe("InfoCardList", () => { - it("should render unordered list by default", () => { - render( - - Item 1 - Item 2 - , - ); - - const list = screen.getByRole("list"); - expect(list.tagName).toBe("UL"); - expect(list.classList.contains("list-disc")).toBe(true); - }); - - it("should render ordered list when specified", () => { - render( - - First - Second - , - ); - - const list = screen.getByRole("list"); - expect(list.tagName).toBe("OL"); - expect(list.classList.contains("list-decimal")).toBe(true); - }); - - it("should render list items", () => { - render( - - Item 1 - Item 2 - , - ); - - expect(screen.getByText("Item 1")).toBeTruthy(); - expect(screen.getByText("Item 2")).toBeTruthy(); - }); - }); - - describe("composition", () => { - it("should render full info card with all components", () => { - render( - - Success! - - Operation completed successfully - - - First step completed - Second step completed - - , - ); - - expect(screen.getByText("Success!")).toBeTruthy(); - expect(screen.getByText("Operation completed successfully")).toBeTruthy(); - expect(screen.getByText("First step completed")).toBeTruthy(); - expect(screen.getByText("Second step completed")).toBeTruthy(); - }); - }); -}); diff --git a/vitest.config.ts b/vitest.config.ts index a265845..527491c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,12 +1,6 @@ import { defineConfig } from "vitest/config"; -import { resolve } from "path"; export default defineConfig({ - resolve: { - alias: { - "@": resolve(__dirname, "./src"), - }, - }, test: { globals: true, environment: "jsdom", From 7c3f79ae7e650afb91efa82d004cbfa04424f3c0 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sun, 28 Dec 2025 09:00:03 +0100 Subject: [PATCH 6/6] fix: TypeScript error in InfoCard icon variant check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix TypeScript build error "Type 'null' cannot be used as an index type" by adding explicit null check for variant before indexing defaultIcons. The variant from VariantProps can be null even with a default value, so TypeScript requires an explicit check. Changes: - Added null check: showDefaultIcon && variant - Added 'as const' to defaultIcons for better type inference 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/components/InfoCard.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/InfoCard.tsx b/src/components/InfoCard.tsx index 2ba19d0..40c33d0 100644 --- a/src/components/InfoCard.tsx +++ b/src/components/InfoCard.tsx @@ -103,9 +103,10 @@ function InfoCard({ warning: ExclamationTriangleIcon, error: ExclamationTriangleIcon, success: CheckCircleIcon, - }; + } as const; - const Icon = CustomIcon || (showDefaultIcon ? defaultIcons[variant] : null); + const Icon = + CustomIcon || (showDefaultIcon && variant ? defaultIcons[variant] : null); return (