diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 8a606707..d0bb5f87 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -10,6 +10,6 @@ "module": "ESNext", "skipLibCheck": true }, - "include": ["/**/*.ts"], + "include": ["**/*.ts"], "exclude": ["dist", "node_modules"] } diff --git a/packages/next-admin/src/components/Cell.test.tsx b/packages/next-admin/src/components/Cell.test.tsx new file mode 100644 index 00000000..44725095 --- /dev/null +++ b/packages/next-admin/src/components/Cell.test.tsx @@ -0,0 +1,277 @@ +import { describe, it, expect, vi } from "vitest"; +import React from "react"; +import { render, screen } from "@testing-library/react"; +import Cell from "./Cell"; +import { ConfigProvider } from "../context/ConfigContext"; +import { ListDataFieldValue } from "../types"; + +const mockConfigValue = { + basePath: "/admin", + apiBasePath: "/api/admin", + isAppDir: true, + options: {}, + nextAdminContext: { locale: "en" }, +}; + +const CellWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe("Cell component", () => { + describe("boolean values", () => { + it("should render default boolean formatting for true", () => { + const cell: ListDataFieldValue = { + type: "scalar", + value: true, + __nextadmin_formatted: "true", + }; + + const { container } = render( + + + + ); + + expect(container.textContent).toBe("true"); + }); + + it("should render default boolean formatting for false", () => { + const cell: ListDataFieldValue = { + type: "scalar", + value: false, + __nextadmin_formatted: "false", + }; + + const { container } = render( + + + + ); + + expect(container.textContent).toBe("false"); + }); + + it("should render string formatter result for boolean", () => { + const cell: ListDataFieldValue = { + type: "scalar", + value: true, + __nextadmin_formatted: "Yes", + }; + + const { container } = render( + + + + ); + + expect(container.textContent).toBe("Yes"); + }); + + it("should render ReactNode formatter result for boolean", () => { + const cell: ListDataFieldValue = { + type: "scalar", + value: true, + __nextadmin_formatted:
Custom
, + }; + + render( + + + + ); + + const element = screen.getByTestId("custom-format"); + expect(element).toBeDefined(); + expect(element.textContent).toBe("Custom"); + }); + + it("should render complex ReactNode formatter for boolean", () => { + const cell: ListDataFieldValue = { + type: "scalar", + value: false, + __nextadmin_formatted: ( + Unpublished + ), + }; + + render( + + + + ); + + const element = screen.getByTestId("bool-strong"); + expect(element).toBeDefined(); + expect(element.tagName).toBe("STRONG"); + expect(element.textContent).toBe("Unpublished"); + }); + + it("should call formatter function for boolean and render ReactNode result", () => { + const cell: ListDataFieldValue = { + type: "scalar", + value: true, + __nextadmin_formatted: "true", + }; + + const formatter = vi.fn((value: boolean) => ( + Published + )); + + render( + + true} + /> + + ); + + expect(formatter).toHaveBeenCalled(); + const element = screen.getByTestId("formatted-bool"); + expect(element).toBeDefined(); + expect(element.textContent).toBe("Published"); + }); + + it("should call formatter function for boolean false and render result", () => { + const cell: ListDataFieldValue = { + type: "scalar", + value: false, + __nextadmin_formatted: "false", + }; + + const formatter = vi.fn((value: boolean) => ( + Not Published + )); + + render( + + false} + /> + + ); + + expect(formatter).toHaveBeenCalled(); + const element = screen.getByTestId("unpublished"); + expect(element).toBeDefined(); + expect(element.textContent).toBe("Not Published"); + }); + + it("should call formatter function that returns string for boolean", () => { + const cell: ListDataFieldValue = { + type: "scalar", + value: true, + __nextadmin_formatted: "true", + }; + + const formatter = vi.fn((value: boolean) => (value ? "Yes" : "No")); + + const { container } = render( + + true} + /> + + ); + + expect(formatter).toHaveBeenCalled(); + expect(container.textContent).toBe("Yes"); + }); + }); + + describe("other scalar types", () => { + it("should render string values", () => { + const cell: ListDataFieldValue = { + type: "scalar", + value: "test string", + __nextadmin_formatted: "test string", + }; + + const { container } = render( + + + + ); + + expect(container.textContent).toBe("test string"); + }); + + it("should render number values", () => { + const cell: ListDataFieldValue = { + type: "scalar", + value: 42, + __nextadmin_formatted: "42", + }; + + const { container } = render( + + + + ); + + expect(container.textContent).toBe("42"); + }); + }); + + describe("count type", () => { + it("should render count values", () => { + const cell: ListDataFieldValue = { + type: "count", + value: 5, + __nextadmin_formatted: "5", + }; + + const { container } = render( + + + + ); + + expect(container.textContent).toBe("5"); + }); + }); + + describe("date type", () => { + it("should render date values", () => { + const date = new Date("2024-01-01"); + const cell: ListDataFieldValue = { + type: "date", + value: date, + __nextadmin_formatted: "2024-01-01", + }; + + const { container } = render( + + + + ); + + expect(container.textContent).toContain("2024-01-01"); + }); + }); + + describe("ReactElement at top level", () => { + it("should render ReactElement directly when provided as __nextadmin_formatted", () => { + const cell: ListDataFieldValue = { + type: "scalar", + value: "test", + __nextadmin_formatted: Top Level, + }; + + render( + + + + ); + + const element = screen.getByTestId("top-level"); + expect(element).toBeDefined(); + expect(element.textContent).toBe("Top Level"); + }); + }); +}); diff --git a/packages/next-admin/src/components/Cell.tsx b/packages/next-admin/src/components/Cell.tsx index b2dc0c1f..e641356b 100644 --- a/packages/next-admin/src/components/Cell.tsx +++ b/packages/next-admin/src/components/Cell.tsx @@ -89,6 +89,10 @@ export default function Cell({ cell, formatter, copyable, getRawData }: Props) { ); } else if (cell.type === "scalar" && typeof cell.value === "boolean") { + // Check if cellValue is a React element (from a custom formatter) + if (React.isValidElement(cellValue)) { + return renderCustomElement(cellValue); + } return (