From 9c6a32b64c0b69dff05c7dfda9f7881b44891d81 Mon Sep 17 00:00:00 2001 From: Indu Thomas <162161814+ts-ithomas@users.noreply.github.com> Date: Wed, 13 May 2026 14:57:37 -0500 Subject: [PATCH 1/3] test(integration): add consumer-perspective DataAppWorkflow story MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Storybook play-function story that exercises the UI kit from the outside — importing exclusively from the public package surface (@tetrascience-npm/tetrascience-react-ui) and building a realistic compound-registry data app on top of it. Five scenarios are covered: initial render, sort, filter, pagination, and row action callback. If any of these break for a downstream consumer, CI will catch it here first. Co-Authored-By: Claude Sonnet 4.6 --- .../composed/DataAppWorkflow.stories.tsx | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 src/components/composed/DataAppWorkflow.stories.tsx diff --git a/src/components/composed/DataAppWorkflow.stories.tsx b/src/components/composed/DataAppWorkflow.stories.tsx new file mode 100644 index 00000000..3474d6ac --- /dev/null +++ b/src/components/composed/DataAppWorkflow.stories.tsx @@ -0,0 +1,323 @@ +/** + * Consumer-perspective integration tests for the UI kit. + * + * These stories simulate a developer who has installed the kit and built a + * real scientific data app on top of it — importing only from the public + * package surface, wiring real data, and exercising the full interaction + * loop (sort → filter → paginate → row action). + * + * If these tests break, a real app built on ts-lib-ui-kit would also break. + */ +import { + Badge, + Button, + DataTable, + DataTableFilter, + DataTablePagination, + TableToolbar, +} from "@tetrascience-npm/tetrascience-react-ui" +import * as React from "react" +import { expect, fn, userEvent, within } from "storybook/test" + +import type { Meta, StoryObj } from "@storybook/react-vite" +import type { ColumnDef } from "@tanstack/react-table" + +// --------------------------------------------------------------------------- +// Sample dataset — a compound registry a scientist would see in a data app +// --------------------------------------------------------------------------- + +interface Compound { + id: string + name: string + category: string + status: "Active" | "Inactive" | "Under Review" + purity: number + mw: number +} + +const compounds: Compound[] = [ + { id: "CPD-001", name: "Aspirin", category: "Analgesic", status: "Active", purity: 99.2, mw: 180.16 }, + { id: "CPD-002", name: "Ibuprofen", category: "Analgesic", status: "Active", purity: 99.8, mw: 206.29 }, + { id: "CPD-003", name: "Metformin", category: "Antidiabetic", status: "Active", purity: 99.0, mw: 129.16 }, + { id: "CPD-004", name: "Penicillin G", category: "Antibiotic", status: "Inactive", purity: 97.1, mw: 334.39 }, + { id: "CPD-005", name: "Omeprazole", category: "Antacid", status: "Active", purity: 98.9, mw: 345.42 }, + { id: "CPD-006", name: "Loratadine", category: "Antihistamine", status: "Inactive", purity: 99.5, mw: 382.88 }, + { id: "CPD-007", name: "Amoxicillin", category: "Antibiotic", status: "Active", purity: 96.3, mw: 365.40 }, + { id: "CPD-008", name: "Atorvastatin", category: "Statin", status: "Active", purity: 98.1, mw: 558.64 }, + { id: "CPD-009", name: "Lisinopril", category: "ACE Inhibitor", status: "Under Review", purity: 97.8, mw: 405.49 }, + { id: "CPD-010", name: "Sertraline", category: "Antidepressant",status: "Active", purity: 98.4, mw: 306.23 }, + { id: "CPD-011", name: "Ciprofloxacin", category: "Antibiotic", status: "Active", purity: 99.1, mw: 331.34 }, + { id: "CPD-012", name: "Warfarin", category: "Anticoagulant", status: "Under Review", purity: 98.7, mw: 308.33 }, +] + +const statusVariant: Record = { + "Active": "default", + "Inactive": "secondary", + "Under Review": "outline", +} + +// --------------------------------------------------------------------------- +// Consumer app component — what a developer would actually build +// --------------------------------------------------------------------------- + +function CompoundRegistry({ onView }: { onView: (compound: Compound) => void }) { + const columns: ColumnDef[] = [ + { accessorKey: "id", header: "ID" }, + { accessorKey: "name", header: "Name" }, + { accessorKey: "category", header: "Category" }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => { + const status = row.getValue("status") + return {status} + }, + }, + { accessorKey: "purity", header: "Purity (%)" }, + { accessorKey: "mw", header: "MW (g/mol)" }, + { + id: "actions", + header: "Actions", + cell: ({ row }) => ( + + ), + }, + ] + + return ( +
+

Compound Registry

+ + + + + + +
+ ) +} + +// --------------------------------------------------------------------------- +// Storybook meta +// --------------------------------------------------------------------------- + +const meta: Meta = { + title: "Integration/DataAppWorkflow", + component: CompoundRegistry, + parameters: { + layout: "fullscreen", + docs: { + description: { + component: + "End-to-end consumer workflow test. Imports exclusively from the public package surface and exercises sort, filter, pagination, and row actions the way a real data app would.", + }, + }, + }, +} + +export default meta + +type Story = StoryObj + +// --------------------------------------------------------------------------- +// Story 1: Table renders correctly for a consumer app +// --------------------------------------------------------------------------- + +export const RendersWithData: Story = { + args: { onView: fn() }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + + await step("Table renders with correct column headers", async () => { + expect(canvas.getByRole("table")).toBeInTheDocument() + expect(canvas.getByRole("columnheader", { name: "Name" })).toBeInTheDocument() + expect(canvas.getByRole("columnheader", { name: "Category" })).toBeInTheDocument() + expect(canvas.getByRole("columnheader", { name: "Status" })).toBeInTheDocument() + }) + + await step("First page shows 5 data rows (defaultPageSize=5)", async () => { + // getAllByRole("row") includes header — subtract 1 + const dataRows = canvas.getAllByRole("row").slice(1) + expect(dataRows).toHaveLength(5) + }) + + await step("Status column renders Badge components", async () => { + // At least one Active badge visible on first page + const activeBadges = canvas.getAllByText("Active") + expect(activeBadges.length).toBeGreaterThan(0) + }) + + await step("Each row has a View action button", async () => { + const viewButtons = canvas.getAllByRole("button", { name: "View" }) + expect(viewButtons).toHaveLength(5) + }) + }, + parameters: { + zephyr: { testCaseId: "" }, + }, +} + +// --------------------------------------------------------------------------- +// Story 2: Sorting — clicking a column header sorts the data +// --------------------------------------------------------------------------- + +export const SortByName: Story = { + args: { onView: fn() }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + + await step("Initial render shows table", async () => { + expect(canvas.getByRole("table")).toBeInTheDocument() + }) + + await step("Clicking Name header sorts ascending", async () => { + const nameHeader = canvas.getByRole("columnheader", { name: "Name" }) + await userEvent.click(nameHeader) + const rows = canvas.getAllByRole("row").slice(1) + expect(rows.length).toBeGreaterThan(0) + // First row should now be Amoxicillin (alphabetically first on page 1) + expect(rows[0]).toHaveTextContent("Amoxicillin") + }) + + await step("Clicking Name header again sorts descending", async () => { + const nameHeader = canvas.getByRole("columnheader", { name: "Name" }) + await userEvent.click(nameHeader) + const rows = canvas.getAllByRole("row").slice(1) + // First row should now be the last alphabetically visible: Warfarin + expect(rows[0]).toHaveTextContent("Warfarin") + }) + }, + parameters: { + zephyr: { testCaseId: "" }, + }, +} + +// --------------------------------------------------------------------------- +// Story 3: Filtering — typing in the filter narrows visible rows +// --------------------------------------------------------------------------- + +export const FilterByCategory: Story = { + args: { onView: fn() }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + const body = within(canvasElement.ownerDocument.body) + + await step("Open filter panel and add a filter row", async () => { + await userEvent.click(canvas.getByRole("button", { name: /filter/i })) + await userEvent.click(body.getByRole("button", { name: /add filter/i })) + expect(body.getByPlaceholderText(/value/i)).toBeInTheDocument() + }) + + await step("Select Category column in the filter", async () => { + // The column selector is a combobox — open it and pick Category + const columnSelects = body.getAllByRole("combobox") + await userEvent.click(columnSelects[0]) + const categoryOption = body.getByRole("option", { name: "Category" }) + await userEvent.click(categoryOption) + }) + + await step("Typing 'Antibiotic' filters to only antibiotic rows", async () => { + await userEvent.type(body.getByPlaceholderText(/value/i), "Antibiotic") + const rows = canvas.getAllByRole("row").slice(1) + // 3 antibiotics in the dataset: Penicillin G, Amoxicillin, Ciprofloxacin + expect(rows.length).toBeLessThanOrEqual(3) + rows.forEach((row) => expect(row).toHaveTextContent("Antibiotic")) + }) + + await step("Clear all restores all rows", async () => { + await userEvent.click(body.getByRole("button", { name: /clear all/i })) + const rows = canvas.getAllByRole("row").slice(1) + // Back to page 1 with 5 rows + expect(rows).toHaveLength(5) + }) + }, + parameters: { + zephyr: { testCaseId: "" }, + }, +} + +// --------------------------------------------------------------------------- +// Story 4: Pagination — navigating between pages +// --------------------------------------------------------------------------- + +export const Pagination: Story = { + args: { onView: fn() }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + + await step("Page 1 shows first 5 compounds and pagination summary", async () => { + expect(canvas.getByRole("table")).toBeInTheDocument() + expect(canvas.getAllByRole("row").slice(1)).toHaveLength(5) + expect(canvas.getByText(/1–5 of 12/)).toBeInTheDocument() + }) + + await step("Clicking Next shows page 2 with remaining compounds", async () => { + await userEvent.click(canvas.getByRole("button", { name: /next/i })) + const rows = canvas.getAllByRole("row").slice(1) + expect(rows).toHaveLength(5) + expect(canvas.getByText(/6–10 of 12/)).toBeInTheDocument() + }) + + await step("Clicking Next again shows the last page", async () => { + await userEvent.click(canvas.getByRole("button", { name: /next/i })) + const rows = canvas.getAllByRole("row").slice(1) + // Last page has 2 rows (12 total, page size 5) + expect(rows).toHaveLength(2) + expect(canvas.getByText(/11–12 of 12/)).toBeInTheDocument() + }) + + await step("Clicking Previous returns to page 2", async () => { + await userEvent.click(canvas.getByRole("button", { name: /previous/i })) + expect(canvas.getByText(/6–10 of 12/)).toBeInTheDocument() + }) + }, + parameters: { + zephyr: { testCaseId: "" }, + }, +} + +// --------------------------------------------------------------------------- +// Story 5: Row action — clicking View fires the callback with correct data +// --------------------------------------------------------------------------- + +export const RowActionCallback: Story = { + args: { onView: fn() }, + play: async ({ canvasElement, step, args }) => { + const canvas = within(canvasElement) + + await step("Table renders with View buttons", async () => { + expect(canvas.getAllByRole("button", { name: "View" })).toHaveLength(5) + }) + + await step("Clicking View on first row calls onView with correct compound", async () => { + const viewButtons = canvas.getAllByRole("button", { name: "View" }) + await userEvent.click(viewButtons[0]) + expect(args.onView).toHaveBeenCalledTimes(1) + // First compound in the dataset is Aspirin (CPD-001) + expect(args.onView).toHaveBeenCalledWith( + expect.objectContaining({ id: "CPD-001", name: "Aspirin" }), + ) + }) + + await step("Clicking View on a second row calls onView again with that compound", async () => { + const viewButtons = canvas.getAllByRole("button", { name: "View" }) + await userEvent.click(viewButtons[1]) + expect(args.onView).toHaveBeenCalledTimes(2) + expect(args.onView).toHaveBeenLastCalledWith( + expect.objectContaining({ id: "CPD-002", name: "Ibuprofen" }), + ) + }) + }, + parameters: { + zephyr: { testCaseId: "" }, + }, +} From 734e05cce56c01ac547d8fffa5d0a1d8b042cd2a Mon Sep 17 00:00:00 2001 From: Indu Thomas <162161814+ts-ithomas@users.noreply.github.com> Date: Wed, 13 May 2026 19:26:01 -0500 Subject: [PATCH 2/3] test(integration): address review feedback on DataAppWorkflow story - Move file from src/components/composed/ to src/stories/integration/ (composed/ is for real components, not test fixtures) - Remove unused React namespace import flagged by Copilot - Fix filter assertion: toHaveLength(3) instead of toBeLessThanOrEqual(3) - Update file header to accurately describe what is tested (source tree, not compiled dist/) and note the gap a consumer build test would fill - Add "Integration" to storybook preview.ts sort order so the section is positioned deliberately rather than falling into the wildcard bucket Co-Authored-By: Claude Sonnet 4.6 --- .storybook/preview.ts | 1 + .../integration}/DataAppWorkflow.stories.tsx | 104 ++++++++---------- 2 files changed, 45 insertions(+), 60 deletions(-) rename src/{components/composed => stories/integration}/DataAppWorkflow.stories.tsx (64%) diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 62cb3e61..f26aaec6 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -41,6 +41,7 @@ const preview: Preview = { "Components", "AI Elements", ["Chat", "*"], + "Integration", "*", "Legacy", ], diff --git a/src/components/composed/DataAppWorkflow.stories.tsx b/src/stories/integration/DataAppWorkflow.stories.tsx similarity index 64% rename from src/components/composed/DataAppWorkflow.stories.tsx rename to src/stories/integration/DataAppWorkflow.stories.tsx index 3474d6ac..15cfbe4a 100644 --- a/src/components/composed/DataAppWorkflow.stories.tsx +++ b/src/stories/integration/DataAppWorkflow.stories.tsx @@ -1,12 +1,15 @@ /** - * Consumer-perspective integration tests for the UI kit. + * Source-tree integration tests for the UI kit. * - * These stories simulate a developer who has installed the kit and built a - * real scientific data app on top of it — importing only from the public - * package surface, wiring real data, and exercising the full interaction - * loop (sort → filter → paginate → row action). + * These stories wire up a realistic scientific data app using components + * imported via the package alias (which resolves to src/index.ts in this + * repo — not the compiled dist/). They catch interaction regressions in + * sort, filter, pagination, and row actions across the component surface, + * but do not validate the published build artifact. * - * If these tests break, a real app built on ts-lib-ui-kit would also break. + * For a consumer build smoke test (verifying dist/ exports are intact after + * a release), a separate CI job running against the compiled output would be + * needed. */ import { Badge, @@ -16,7 +19,6 @@ import { DataTablePagination, TableToolbar, } from "@tetrascience-npm/tetrascience-react-ui" -import * as React from "react" import { expect, fn, userEvent, within } from "storybook/test" import type { Meta, StoryObj } from "@storybook/react-vite" @@ -36,18 +38,18 @@ interface Compound { } const compounds: Compound[] = [ - { id: "CPD-001", name: "Aspirin", category: "Analgesic", status: "Active", purity: 99.2, mw: 180.16 }, - { id: "CPD-002", name: "Ibuprofen", category: "Analgesic", status: "Active", purity: 99.8, mw: 206.29 }, - { id: "CPD-003", name: "Metformin", category: "Antidiabetic", status: "Active", purity: 99.0, mw: 129.16 }, - { id: "CPD-004", name: "Penicillin G", category: "Antibiotic", status: "Inactive", purity: 97.1, mw: 334.39 }, - { id: "CPD-005", name: "Omeprazole", category: "Antacid", status: "Active", purity: 98.9, mw: 345.42 }, - { id: "CPD-006", name: "Loratadine", category: "Antihistamine", status: "Inactive", purity: 99.5, mw: 382.88 }, - { id: "CPD-007", name: "Amoxicillin", category: "Antibiotic", status: "Active", purity: 96.3, mw: 365.40 }, - { id: "CPD-008", name: "Atorvastatin", category: "Statin", status: "Active", purity: 98.1, mw: 558.64 }, - { id: "CPD-009", name: "Lisinopril", category: "ACE Inhibitor", status: "Under Review", purity: 97.8, mw: 405.49 }, - { id: "CPD-010", name: "Sertraline", category: "Antidepressant",status: "Active", purity: 98.4, mw: 306.23 }, - { id: "CPD-011", name: "Ciprofloxacin", category: "Antibiotic", status: "Active", purity: 99.1, mw: 331.34 }, - { id: "CPD-012", name: "Warfarin", category: "Anticoagulant", status: "Under Review", purity: 98.7, mw: 308.33 }, + { id: "CPD-001", name: "Aspirin", category: "Analgesic", status: "Active", purity: 99.2, mw: 180.16 }, + { id: "CPD-002", name: "Ibuprofen", category: "Analgesic", status: "Active", purity: 99.8, mw: 206.29 }, + { id: "CPD-003", name: "Metformin", category: "Antidiabetic", status: "Active", purity: 99.0, mw: 129.16 }, + { id: "CPD-004", name: "Penicillin G", category: "Antibiotic", status: "Inactive", purity: 97.1, mw: 334.39 }, + { id: "CPD-005", name: "Omeprazole", category: "Antacid", status: "Active", purity: 98.9, mw: 345.42 }, + { id: "CPD-006", name: "Loratadine", category: "Antihistamine", status: "Inactive", purity: 99.5, mw: 382.88 }, + { id: "CPD-007", name: "Amoxicillin", category: "Antibiotic", status: "Active", purity: 96.3, mw: 365.40 }, + { id: "CPD-008", name: "Atorvastatin", category: "Statin", status: "Active", purity: 98.1, mw: 558.64 }, + { id: "CPD-009", name: "Lisinopril", category: "ACE Inhibitor", status: "Under Review", purity: 97.8, mw: 405.49 }, + { id: "CPD-010", name: "Sertraline", category: "Antidepressant", status: "Active", purity: 98.4, mw: 306.23 }, + { id: "CPD-011", name: "Ciprofloxacin", category: "Antibiotic", status: "Active", purity: 99.1, mw: 331.34 }, + { id: "CPD-012", name: "Warfarin", category: "Anticoagulant", status: "Under Review", purity: 98.7, mw: 308.33 }, ] const statusVariant: Record = { @@ -57,7 +59,7 @@ const statusVariant: Record void }) { @@ -118,7 +120,7 @@ const meta: Meta = { docs: { description: { component: - "End-to-end consumer workflow test. Imports exclusively from the public package surface and exercises sort, filter, pagination, and row actions the way a real data app would.", + "Source-tree integration tests. Exercises sort, filter, pagination, and row actions across DataTable, Badge, Button, and toolbar components in a realistic data-app composition.", }, }, }, @@ -129,7 +131,7 @@ export default meta type Story = StoryObj // --------------------------------------------------------------------------- -// Story 1: Table renders correctly for a consumer app +// Story 1: Table renders correctly // --------------------------------------------------------------------------- export const RendersWithData: Story = { @@ -145,13 +147,11 @@ export const RendersWithData: Story = { }) await step("First page shows 5 data rows (defaultPageSize=5)", async () => { - // getAllByRole("row") includes header — subtract 1 const dataRows = canvas.getAllByRole("row").slice(1) expect(dataRows).toHaveLength(5) }) await step("Status column renders Badge components", async () => { - // At least one Active badge visible on first page const activeBadges = canvas.getAllByText("Active") expect(activeBadges.length).toBeGreaterThan(0) }) @@ -167,7 +167,7 @@ export const RendersWithData: Story = { } // --------------------------------------------------------------------------- -// Story 2: Sorting — clicking a column header sorts the data +// Story 2: Sorting // --------------------------------------------------------------------------- export const SortByName: Story = { @@ -180,19 +180,14 @@ export const SortByName: Story = { }) await step("Clicking Name header sorts ascending", async () => { - const nameHeader = canvas.getByRole("columnheader", { name: "Name" }) - await userEvent.click(nameHeader) + await userEvent.click(canvas.getByRole("columnheader", { name: "Name" })) const rows = canvas.getAllByRole("row").slice(1) - expect(rows.length).toBeGreaterThan(0) - // First row should now be Amoxicillin (alphabetically first on page 1) expect(rows[0]).toHaveTextContent("Amoxicillin") }) await step("Clicking Name header again sorts descending", async () => { - const nameHeader = canvas.getByRole("columnheader", { name: "Name" }) - await userEvent.click(nameHeader) + await userEvent.click(canvas.getByRole("columnheader", { name: "Name" })) const rows = canvas.getAllByRole("row").slice(1) - // First row should now be the last alphabetically visible: Warfarin expect(rows[0]).toHaveTextContent("Warfarin") }) }, @@ -202,7 +197,7 @@ export const SortByName: Story = { } // --------------------------------------------------------------------------- -// Story 3: Filtering — typing in the filter narrows visible rows +// Story 3: Filtering // --------------------------------------------------------------------------- export const FilterByCategory: Story = { @@ -218,26 +213,22 @@ export const FilterByCategory: Story = { }) await step("Select Category column in the filter", async () => { - // The column selector is a combobox — open it and pick Category const columnSelects = body.getAllByRole("combobox") await userEvent.click(columnSelects[0]) - const categoryOption = body.getByRole("option", { name: "Category" }) - await userEvent.click(categoryOption) + await userEvent.click(body.getByRole("option", { name: "Category" })) }) - await step("Typing 'Antibiotic' filters to only antibiotic rows", async () => { + await step("Typing 'Antibiotic' filters to exactly 3 matching rows", async () => { await userEvent.type(body.getByPlaceholderText(/value/i), "Antibiotic") const rows = canvas.getAllByRole("row").slice(1) - // 3 antibiotics in the dataset: Penicillin G, Amoxicillin, Ciprofloxacin - expect(rows.length).toBeLessThanOrEqual(3) + // Dataset has exactly 3 antibiotics: Penicillin G, Amoxicillin, Ciprofloxacin + expect(rows).toHaveLength(3) rows.forEach((row) => expect(row).toHaveTextContent("Antibiotic")) }) await step("Clear all restores all rows", async () => { await userEvent.click(body.getByRole("button", { name: /clear all/i })) - const rows = canvas.getAllByRole("row").slice(1) - // Back to page 1 with 5 rows - expect(rows).toHaveLength(5) + expect(canvas.getAllByRole("row").slice(1)).toHaveLength(5) }) }, parameters: { @@ -246,7 +237,7 @@ export const FilterByCategory: Story = { } // --------------------------------------------------------------------------- -// Story 4: Pagination — navigating between pages +// Story 4: Pagination // --------------------------------------------------------------------------- export const Pagination: Story = { @@ -254,28 +245,24 @@ export const Pagination: Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement) - await step("Page 1 shows first 5 compounds and pagination summary", async () => { - expect(canvas.getByRole("table")).toBeInTheDocument() + await step("Page 1 shows first 5 rows and pagination summary", async () => { expect(canvas.getAllByRole("row").slice(1)).toHaveLength(5) expect(canvas.getByText(/1–5 of 12/)).toBeInTheDocument() }) - await step("Clicking Next shows page 2 with remaining compounds", async () => { + await step("Next → page 2", async () => { await userEvent.click(canvas.getByRole("button", { name: /next/i })) - const rows = canvas.getAllByRole("row").slice(1) - expect(rows).toHaveLength(5) + expect(canvas.getAllByRole("row").slice(1)).toHaveLength(5) expect(canvas.getByText(/6–10 of 12/)).toBeInTheDocument() }) - await step("Clicking Next again shows the last page", async () => { + await step("Next → last page with 2 rows", async () => { await userEvent.click(canvas.getByRole("button", { name: /next/i })) - const rows = canvas.getAllByRole("row").slice(1) - // Last page has 2 rows (12 total, page size 5) - expect(rows).toHaveLength(2) + expect(canvas.getAllByRole("row").slice(1)).toHaveLength(2) expect(canvas.getByText(/11–12 of 12/)).toBeInTheDocument() }) - await step("Clicking Previous returns to page 2", async () => { + await step("Previous → back to page 2", async () => { await userEvent.click(canvas.getByRole("button", { name: /previous/i })) expect(canvas.getByText(/6–10 of 12/)).toBeInTheDocument() }) @@ -286,7 +273,7 @@ export const Pagination: Story = { } // --------------------------------------------------------------------------- -// Story 5: Row action — clicking View fires the callback with correct data +// Story 5: Row action callback // --------------------------------------------------------------------------- export const RowActionCallback: Story = { @@ -299,18 +286,15 @@ export const RowActionCallback: Story = { }) await step("Clicking View on first row calls onView with correct compound", async () => { - const viewButtons = canvas.getAllByRole("button", { name: "View" }) - await userEvent.click(viewButtons[0]) + await userEvent.click(canvas.getAllByRole("button", { name: "View" })[0]) expect(args.onView).toHaveBeenCalledTimes(1) - // First compound in the dataset is Aspirin (CPD-001) expect(args.onView).toHaveBeenCalledWith( expect.objectContaining({ id: "CPD-001", name: "Aspirin" }), ) }) - await step("Clicking View on a second row calls onView again with that compound", async () => { - const viewButtons = canvas.getAllByRole("button", { name: "View" }) - await userEvent.click(viewButtons[1]) + await step("Clicking View on second row calls onView with correct compound", async () => { + await userEvent.click(canvas.getAllByRole("button", { name: "View" })[1]) expect(args.onView).toHaveBeenCalledTimes(2) expect(args.onView).toHaveBeenLastCalledWith( expect.objectContaining({ id: "CPD-002", name: "Ibuprofen" }), From c5f7325b8c85e29bdf67423e3e9add1a756263f5 Mon Sep 17 00:00:00 2001 From: Indu Thomas <162161814+ts-ithomas@users.noreply.github.com> Date: Wed, 13 May 2026 19:34:34 -0500 Subject: [PATCH 3/3] fix(integration): click inner sort div instead of th element The sort onClick handler is attached to the inner div inside the th (data-table.tsx:233), not the th itself. Clicking the columnheader role element never triggered sorting. Fix mirrors the pattern used in the existing ReorderWithSorting story. Co-Authored-By: Claude Sonnet 4.6 --- src/stories/integration/DataAppWorkflow.stories.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stories/integration/DataAppWorkflow.stories.tsx b/src/stories/integration/DataAppWorkflow.stories.tsx index 15cfbe4a..75976144 100644 --- a/src/stories/integration/DataAppWorkflow.stories.tsx +++ b/src/stories/integration/DataAppWorkflow.stories.tsx @@ -180,13 +180,14 @@ export const SortByName: Story = { }) await step("Clicking Name header sorts ascending", async () => { - await userEvent.click(canvas.getByRole("columnheader", { name: "Name" })) + // Click the inner sort div, not the — the onClick handler lives there + await userEvent.click(canvas.getByText("Name")) const rows = canvas.getAllByRole("row").slice(1) expect(rows[0]).toHaveTextContent("Amoxicillin") }) await step("Clicking Name header again sorts descending", async () => { - await userEvent.click(canvas.getByRole("columnheader", { name: "Name" })) + await userEvent.click(canvas.getByText("Name")) const rows = canvas.getAllByRole("row").slice(1) expect(rows[0]).toHaveTextContent("Warfarin") })