import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, it, expect } from "vitest"; import * as fc from "fast-check"; import { ReadOnlyGrid } from "./ReadOnlyGrid"; import { GridColumn } from "./GridColumn"; // --- Shared arbitraries --- const FIELD_NAMES = ["name", "age", "email", "city", "score"] as const; type FieldName = (typeof FIELD_NAMES)[number]; /** Generate a single data row with values for all known fields */ const arbRow: fc.Arbitrary> = fc.record({ name: fc.string({ minLength: 1, maxLength: 20 }).map((s) => s.replace(/\s+/g, "_")), age: fc.integer({ min: 0, max: 120 }), email: fc.emailAddress(), city: fc.string({ minLength: 1, maxLength: 15 }).map((s) => s.replace(/\s+/g, "_")), score: fc.integer({ min: 0, max: 100 }), }); /** Generate a non-empty data array (1-20 rows) */ const arbDataArray = fc.array(arbRow, { minLength: 1, maxLength: 20 }); /** Generate a column config with a string field accessor and visible */ const arbVisibleColumn: fc.Arbitrary<{ label: string; value: FieldName; showWhen: true; }> = fc.record({ label: fc.string({ minLength: 1, maxLength: 15 }).map((s) => `Col_${s.replace(/\s+/g, "_")}`), value: fc.constantFrom(...FIELD_NAMES), showWhen: fc.constant(true as const), }); /** Generate a hidden column */ const arbHiddenColumn: fc.Arbitrary<{ label: string; value: FieldName; showWhen: false; }> = fc.record({ label: fc.string({ minLength: 1, maxLength: 15 }).map((s) => `Hidden_${s.replace(/\s+/g, "_")}`), value: fc.constantFrom(...FIELD_NAMES), showWhen: fc.constant(false as const), }); /** Generate a mixed set of columns: 1-5 visible, 0-2 hidden */ const arbColumnSet = fc .tuple( fc.array(arbVisibleColumn, { minLength: 1, maxLength: 5 }), fc.array(arbHiddenColumn, { minLength: 0, maxLength: 2 }) ) .map(([visible, hidden]) => ({ all: [...visible, ...hidden], visible, hidden, })); /** Generate a non-empty trimmed string suitable for display text. * Uses a prefix to avoid collisions with cell content in the DOM. */ const arbDisplayText = (prefix: string) => fc .string({ minLength: 1, maxLength: 20 }) .map((s) => `${prefix}_${s.replace(/\s+/g, "x")}`); // ============================================================================= // Property 1: Data rendering dimensions // Feature: read-only-grid, Property 1: Data rendering dimensions // Validates: Requirements 1.1, 2.5 // ============================================================================= describe("Property 1: Data rendering dimensions", () => { it("renders exactly N data rows and M visible columns for any valid input", () => { fc.assert( fc.property( fc.array(arbRow, { minLength: 1, maxLength: 10 }), fc.integer({ min: 1, max: 5 }), fc.integer({ min: 0, max: 2 }), (data, visibleCount, hiddenCount) => { // Build column configs from counts const visibleCols = FIELD_NAMES.slice(0, visibleCount).map((f, i) => ({ label: `Col${i}`, value: f, showWhen: true as const, })); const hiddenCols = FIELD_NAMES.slice(0, hiddenCount).map((f, i) => ({ label: `Hidden${i}`, value: f, showWhen: false as const, })); const allCols = [...visibleCols, ...hiddenCols]; const { unmount } = render( {allCols.map((col, i) => ( ))} ); const expectedRows = data.length; const expectedCols = visibleCount; // All rows in the table = 1 header + N data rows const allRows = screen.getAllByRole("row"); expect(allRows).toHaveLength(expectedRows + 1); // Data cells = N rows × M visible columns const cells = screen.getAllByRole("cell"); expect(cells).toHaveLength(expectedRows * expectedCols); // Column headers = M visible columns const headers = screen.getAllByRole("columnheader"); expect(headers).toHaveLength(expectedCols); unmount(); } ), { numRuns: 100 } ); }, 30000); }); // ============================================================================= // Property 2: Cell value resolution // Feature: read-only-grid, Property 2: Cell value resolution // Validates: Requirements 1.3, 1.4 // ============================================================================= describe("Property 2: Cell value resolution", () => { it("resolves string field accessors to the correct row values", () => { fc.assert( fc.property(arbDataArray, arbVisibleColumn, (data, col) => { const { unmount } = render( ); const cells = screen.getAllByRole("cell"); expect(cells).toHaveLength(data.length); data.forEach((row, i) => { const expected = String(row[col.value] ?? ""); expect(cells[i]).toHaveTextContent(expected); }); unmount(); }), { numRuns: 100 } ); }, 30000); it("resolves function accessors to the correct computed values", () => { fc.assert( fc.property(arbDataArray, (data) => { // Function accessor that concatenates index and name field const accessor = (row: any, index: number) => `row${index}_${row.name}`; const { unmount } = render( ); const cells = screen.getAllByRole("cell"); expect(cells).toHaveLength(data.length); data.forEach((row, i) => { expect(cells[i]).toHaveTextContent(`row${i}_${row.name}`); }); unmount(); }), { numRuns: 100 } ); }, 30000); }); // ============================================================================= // Property 3: Column configuration application // Feature: read-only-grid, Property 3: Column configuration application // Validates: Requirements 2.1, 2.2, 2.3, 2.4 // Note: align, width, and backgroundColor styling are not implemented yet (Task 8). // For now, we test that label text appears in column headers. // ============================================================================= describe("Property 3: Column configuration application", () => { it("renders each visible column's label text in the corresponding header", () => { fc.assert( fc.property(arbDataArray, arbColumnSet, (data, columns) => { const { unmount } = render( {columns.all.map((col, i) => ( ))} ); const headers = screen.getAllByRole("columnheader"); expect(headers).toHaveLength(columns.visible.length); // Each visible column's label appears in the corresponding header columns.visible.forEach((col, i) => { expect(headers[i]).toHaveTextContent(col.label); }); // Hidden column labels should NOT appear in any header columns.hidden.forEach((col) => { const matchingHeaders = headers.filter((h) => h.textContent?.includes(col.label) ); // Only fail if the hidden label uniquely doesn't appear // (it could coincidentally match a visible label, so we check headers only) const headerTexts = headers.map((h) => h.textContent); if (!headerTexts.some((t) => t?.includes(col.label))) { expect( screen.queryByRole("columnheader", { name: col.label }) ).not.toBeInTheDocument(); } }); unmount(); }), { numRuns: 100 } ); }, 30000); }); // ============================================================================= // Property 4: Grid metadata rendering // Feature: read-only-grid, Property 4: Grid metadata rendering // Validates: Requirements 3.1, 3.3, 3.5, 3.6 // ============================================================================= describe("Property 4: Grid metadata rendering", () => { /** Generate a non-empty array of validation message strings */ const arbValidations = fc.array(arbDisplayText("val"), { minLength: 1, maxLength: 5, }); it("renders label, instructions, all validations, and aria-label for any valid metadata", () => { fc.assert( fc.property( arbDataArray, arbDisplayText("lbl"), arbDisplayText("ins"), arbValidations, arbDisplayText("acc"), (data, label, instructions, validations, accessibilityText) => { const { unmount, container } = render( ); // Requirement 3.1: label text is rendered (in a