From 07b12dffee8532fad8bc9e66e681297c0a177418 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Wed, 29 Jul 2026 15:28:40 +0200 Subject: [PATCH 01/13] test: add more adapters reactivity tests --- .../tests/adapter-lifecycle.test.ts | 351 +++++++ .../tests/angularReactivityFeature.test.ts | 12 +- .../angular-table/tests/injectTable.test.ts | 8 +- .../react-table/tests/adapter-parity.test.tsx | 959 ++++++++++++++++++ .../tests/createTableHook.test.tsx | 195 ++++ packages/react-table/tests/ssr.test.tsx | 122 +++ packages/react-table/tests/useTable.test.tsx | 4 + packages/react-table/vite.config.ts | 1 - packages/solid-table/package.json | 2 + packages/solid-table/src/createTable.ts | 43 +- .../tests/unit/adapterReactivity.test.ts | 294 ++++++ .../tests/unit/createTable.test.ts | 130 +++ .../solid-table/tests/unit/reactivity.test.ts | 374 ++++++- .../solid-table/tests/unit/rendering.test.ts | 208 ++++ packages/solid-table/tsconfig.json | 2 +- packages/solid-table/vite.config.ts | 3 + packages/vue-table/package.json | 2 + .../tests/unit/adapter-lifecycle.test.ts | 237 +++++ .../vue-table/tests/unit/rendering.test.ts | 406 ++++++++ packages/vue-table/tests/unit/signals.test.ts | 5 +- .../vue-table/tests/unit/useTable.test.ts | 443 ++++++++ packages/vue-table/tsconfig.json | 2 +- 22 files changed, 3784 insertions(+), 19 deletions(-) create mode 100644 packages/angular-table/tests/adapter-lifecycle.test.ts create mode 100644 packages/react-table/tests/adapter-parity.test.tsx create mode 100644 packages/react-table/tests/createTableHook.test.tsx create mode 100644 packages/react-table/tests/ssr.test.tsx create mode 100644 packages/solid-table/tests/unit/adapterReactivity.test.ts create mode 100644 packages/solid-table/tests/unit/createTable.test.ts create mode 100644 packages/solid-table/tests/unit/rendering.test.ts create mode 100644 packages/vue-table/tests/unit/adapter-lifecycle.test.ts create mode 100644 packages/vue-table/tests/unit/rendering.test.ts create mode 100644 packages/vue-table/tests/unit/useTable.test.ts diff --git a/packages/angular-table/tests/adapter-lifecycle.test.ts b/packages/angular-table/tests/adapter-lifecycle.test.ts new file mode 100644 index 0000000000..236aea4a51 --- /dev/null +++ b/packages/angular-table/tests/adapter-lifecycle.test.ts @@ -0,0 +1,351 @@ +import { Component, effect, signal } from '@angular/core' +import { By } from '@angular/platform-browser' +import { TestBed } from '@angular/core/testing' +import { createAtom } from '@tanstack/angular-store' +import { describe, expect, test, vi } from 'vitest' +import { + TanStackTable, + TanStackTableCell, + TanStackTableHeader, + createTableHook, + injectTable, + stockFeatures, +} from '../src' +import type { ColumnDef, RowSelectionState, TableOptions } from '../src' + +describe('Angular adapter lifecycle and option ownership', () => { + type Data = { id: string; title: string } + + const data: Array = [{ id: '1', title: 'Title' }] + const columns: Array> = [ + { + id: 'id', + accessorKey: 'id', + }, + { + id: 'title', + accessorKey: 'title', + }, + ] + + test('unsubscribes wrapped external atoms when the host is destroyed', () => { + const rowSelectionAtom = createAtom({}) + const subscribeSpy = vi.spyOn(rowSelectionAtom, 'subscribe') + + @Component({ + standalone: true, + template: ``, + }) + class HostComponent { + readonly table = injectTable(() => ({ + data, + columns, + features: stockFeatures, + getRowId: (row) => row.id, + atoms: { + rowSelection: rowSelectionAtom, + }, + })) + } + + const fixture = TestBed.createComponent(HostComponent) + const table = fixture.componentInstance.table + + expect(table.atoms.rowSelection.get()).toEqual({}) + expect(subscribeSpy).toHaveBeenCalledTimes(1) + + const subscription = subscribeSpy.mock.results[0]!.value + const unsubscribeSpy = vi.spyOn(subscription, 'unsubscribe') + + rowSelectionAtom.set({ 1: true }) + TestBed.tick() + expect(table.atoms.rowSelection.get()).toEqual({ 1: true }) + + fixture.destroy() + + expect(unsubscribeSpy).toHaveBeenCalledTimes(1) + + rowSelectionAtom.set({}) + expect(table.atoms.rowSelection.get()).toEqual({ 1: true }) + }) + + test('preserves the last controlled value when ownership is released', () => { + const controlledState = signal< + { rowSelection: RowSelectionState } | undefined + >({ + rowSelection: { 1: true }, + }) + const table = TestBed.runInInjectionContext(() => + injectTable(() => ({ + data, + columns, + features: stockFeatures, + getRowId: (row) => row.id, + state: controlledState(), + })), + ) + + expect(table.atoms.rowSelection.get()).toEqual({ 1: true }) + TestBed.tick() + + controlledState.set(undefined) + TestBed.tick() + expect(table.atoms.rowSelection.get()).toEqual({ 1: true }) + + table.getRow('1').toggleSelected(false) + TestBed.tick() + expect(table.atoms.rowSelection.get()).toEqual({}) + + controlledState.set({ rowSelection: { 1: true } }) + TestBed.tick() + expect(table.atoms.rowSelection.get()).toEqual({ 1: true }) + + table.getRow('1').toggleSelected(false) + TestBed.tick() + expect(table.atoms.rowSelection.get()).toEqual({ 1: true }) + + controlledState.set(undefined) + TestBed.tick() + expect(table.atoms.rowSelection.get()).toEqual({}) + }) + + test('gives an external atom precedence over controlled state', () => { + const rowSelectionAtom = createAtom({ 2: true }) + const controlledState = signal({ 1: true }) + const table = TestBed.runInInjectionContext(() => + injectTable(() => ({ + data, + columns, + features: stockFeatures, + getRowId: (row) => row.id, + atoms: { + rowSelection: rowSelectionAtom, + }, + state: { + rowSelection: controlledState(), + }, + })), + ) + + expect(table.atoms.rowSelection.get()).toEqual({ 2: true }) + TestBed.tick() + + controlledState.set({ 3: true }) + TestBed.tick() + expect(table.atoms.rowSelection.get()).toEqual({ 2: true }) + + rowSelectionAtom.set({ 1: true, 2: true }) + TestBed.tick() + expect(table.atoms.rowSelection.get()).toEqual({ + 1: true, + 2: true, + }) + }) + + test('coalesces rapid controlled updates into one reactive publication', () => { + const controlledState = signal({}) + const table = TestBed.runInInjectionContext(() => + injectTable(() => ({ + data, + columns, + features: stockFeatures, + getRowId: (row) => row.id, + state: { + rowSelection: controlledState(), + }, + })), + ) + const stateCaptor = vi.fn<(state: RowSelectionState) => void>() + + TestBed.runInInjectionContext(() => { + effect(() => stateCaptor(table.atoms.rowSelection.get())) + }) + + TestBed.tick() + + controlledState.set({ 1: true }) + controlledState.set({ 1: true, 2: true }) + controlledState.set({ 2: true }) + TestBed.tick() + + expect(stateCaptor.mock.calls).toEqual([[{}], [{ 2: true }]]) + }) + + test('synchronizes dynamic columns, callbacks, and metadata', () => { + const dynamicColumns = signal(columns) + const getRowId = signal<(row: Data) => string>((row) => row.id) + const meta = signal({ label: 'initial' }) + const table = TestBed.runInInjectionContext(() => + injectTable(() => ({ + data, + columns: dynamicColumns(), + features: stockFeatures, + getRowId: getRowId(), + meta: meta(), + })), + ) + + expect(table.getAllLeafColumns().map((column) => column.id)).toEqual([ + 'id', + 'title', + ]) + expect(table.getRowId(data[0]!, 0)).toBe('1') + expect(table.options.meta).toEqual({ label: 'initial' }) + TestBed.tick() + + dynamicColumns.set([columns[1]!]) + getRowId.set((row) => `row-${row.id}`) + meta.set({ label: 'updated' }) + TestBed.tick() + + expect(table.getAllLeafColumns().map((column) => column.id)).toEqual([ + 'title', + ]) + expect(table.getRowId(data[0]!, 0)).toBe('row-1') + expect(table.options.meta).toEqual({ label: 'updated' }) + }) + + test('attaches createTableHook components to the matching table objects', () => { + const TableToolbar = () => 'table toolbar' + const CellBadge = () => 'cell badge' + const HeaderBadge = () => 'header badge' + const { injectAppTable } = createTableHook({ + features: stockFeatures, + tableComponents: { TableToolbar }, + cellComponents: { CellBadge }, + headerComponents: { HeaderBadge }, + }) + const table = TestBed.runInInjectionContext(() => + injectAppTable( + () => + ({ + data, + columns, + getRowId: (row) => row.id, + }) satisfies Omit< + TableOptions, + 'features' + >, + ), + ) + + const cell = table.getRow('1').getAllCells()[0]! + const header = table.getFlatHeaders()[0]! + + expect(table.TableToolbar).toBe(TableToolbar) + expect(table.appCell(cell)).toBe(cell) + expect(table.appCell(cell).CellBadge).toBe(CellBadge) + expect(table.appHeader(header)).toBe(header) + expect(table.appHeader(header).HeaderBadge).toBe(HeaderBadge) + expect(table.appFooter(header)).toBe(header) + expect(table.appFooter(header).HeaderBadge).toBe(HeaderBadge) + }) + + test('provides createTableHook table, cell, and header contexts through DI', () => { + const TableToolbar = () => 'table toolbar' + const CellBadge = () => 'cell badge' + const HeaderBadge = () => 'header badge' + const { + injectAppTable, + injectTableContext, + injectTableCellContext, + injectTableHeaderContext, + } = createTableHook({ + features: stockFeatures, + tableComponents: { TableToolbar }, + cellComponents: { CellBadge }, + headerComponents: { HeaderBadge }, + }) + + @Component({ + selector: 'app-table-context-consumer', + standalone: true, + template: ``, + }) + class TableContextConsumer { + readonly table = injectTableContext() + } + + @Component({ + selector: 'app-cell-context-consumer', + standalone: true, + template: ``, + }) + class CellContextConsumer { + readonly cell = injectTableCellContext() + } + + @Component({ + selector: 'app-header-context-consumer', + standalone: true, + template: ``, + }) + class HeaderContextConsumer { + readonly header = injectTableHeaderContext() + } + + @Component({ + standalone: true, + imports: [ + TanStackTable, + TanStackTableCell, + TanStackTableHeader, + TableContextConsumer, + CellContextConsumer, + HeaderContextConsumer, + ], + template: ` +
+ +
+
+ +
+
+ +
+ `, + }) + class HostComponent { + readonly table = injectAppTable( + () => + ({ + data, + columns, + getRowId: (row) => row.id, + }) satisfies Omit< + TableOptions, + 'features' + >, + ) + + get cell() { + return this.table.getRow('1').getAllCells()[0]! + } + + get header() { + return this.table.getFlatHeaders()[0]! + } + } + + const fixture = TestBed.createComponent(HostComponent) + fixture.detectChanges() + + const tableConsumer = fixture.debugElement.query( + By.directive(TableContextConsumer), + ).componentInstance as TableContextConsumer + const cellConsumer = fixture.debugElement.query( + By.directive(CellContextConsumer), + ).componentInstance as CellContextConsumer + const headerConsumer = fixture.debugElement.query( + By.directive(HeaderContextConsumer), + ).componentInstance as HeaderContextConsumer + + expect(tableConsumer.table()).toBe(fixture.componentInstance.table) + expect(tableConsumer.table().TableToolbar).toBe(TableToolbar) + expect(cellConsumer.cell()).toBe(fixture.componentInstance.cell) + expect(cellConsumer.cell().CellBadge).toBe(CellBadge) + expect(headerConsumer.header()).toBe(fixture.componentInstance.header) + expect(headerConsumer.header().HeaderBadge).toBe(HeaderBadge) + }) +}) diff --git a/packages/angular-table/tests/angularReactivityFeature.test.ts b/packages/angular-table/tests/angularReactivityFeature.test.ts index 4811498fa9..a4edd8b679 100644 --- a/packages/angular-table/tests/angularReactivityFeature.test.ts +++ b/packages/angular-table/tests/angularReactivityFeature.test.ts @@ -95,6 +95,7 @@ describe('angularReactivityFeature', () => { expect(isSelectedRow1Captor.mock.calls).toEqual([[false], [true], [true]]) expect(cellGetValueCaptor.mock.calls).toEqual([['1'], ['1']]) + expect(cellGetValueMemoizedCaptor.mock.calls).toEqual([['1']]) expect(columnIsVisibleCaptor.mock.calls).toEqual([ [true], [true], @@ -142,7 +143,8 @@ describe('angularReactivityFeature', () => { test('table store can be subscribed from another reactive effect', () => { const table = createTestTable() - const tableStateCaptor = vi.fn() + const tableStateCaptor = + vi.fn<(state: ReturnType) => void>() TestBed.runInInjectionContext(() => { effect((onCleanup) => { @@ -154,7 +156,13 @@ describe('angularReactivityFeature', () => { }) }) - expect(() => TestBed.tick()).not.toThrow() + TestBed.tick() + table.toggleAllRowsSelected(true) + TestBed.tick() + + expect( + tableStateCaptor.mock.calls.map(([state]) => state.rowSelection), + ).toEqual([{}, { 1: true }]) }) test('table state reacts to every external signal state update', () => { diff --git a/packages/angular-table/tests/injectTable.test.ts b/packages/angular-table/tests/injectTable.test.ts index cf79c2f110..00cff24908 100644 --- a/packages/angular-table/tests/injectTable.test.ts +++ b/packages/angular-table/tests/injectTable.test.ts @@ -17,7 +17,7 @@ import { RowModel, injectTable } from '../src' import type { PaginationState } from '../src' describe('injectTable', () => { - test('should support required signal inputs', () => { + test('should support required signal inputs', async () => { @Component({ selector: 'app-table', template: ``, @@ -45,7 +45,7 @@ describe('injectTable', () => { const fixture = TestBed.createComponent(RootComponent) fixture.detectChanges() - fixture.whenRenderingDone() + await fixture.whenRenderingDone() }) describe('Proxy table', () => { @@ -76,7 +76,7 @@ describe('injectTable', () => { test('supports "Object.keys"', () => { const keys = Object.keys(table) - expect(keys.some((k) => ['state', 'getRowModel'].includes(k))) + expect(keys).toEqual(expect.arrayContaining(['options', 'getRowModel'])) }) test('Row model is reactive', () => { @@ -125,7 +125,7 @@ describe('injectTable', () => { // optionsStore is a signal -> so if updated with state in queuemicrotask will trigger twice expect(coreRowModelFn).toHaveBeenCalledTimes(2) expect(coreRowModelFn.mock.calls[0]![0].rows.length).toEqual(10) - expect(coreRowModelFn.mock.calls[0]![0].rows.length).toEqual(10) + expect(coreRowModelFn.mock.calls[1]![0].rows.length).toEqual(10) expect(rowModelFn).toHaveBeenCalledTimes(2) expect(rowModelFn.mock.calls[0]![0].rows.length).toEqual(5) diff --git a/packages/react-table/tests/adapter-parity.test.tsx b/packages/react-table/tests/adapter-parity.test.tsx new file mode 100644 index 0000000000..d5a4c1791c --- /dev/null +++ b/packages/react-table/tests/adapter-parity.test.tsx @@ -0,0 +1,959 @@ +// @vitest-environment jsdom + +import * as React from 'react' +import { createRoot } from 'react-dom/client' +import { + createPaginatedRowModel, + stockFeatures, + tableFeatures, +} from '@tanstack/table-core' +import { createAtom } from '@tanstack/react-store' +import { afterEach, describe, expect, test, vi } from 'vitest' +import { useTable } from '../src' +import type { + ColumnDef, + PaginationState, + RowSelectionState, +} from '@tanstack/table-core' +import type { ReactTable } from '../src' +import type { Root } from 'react-dom/client' + +type Data = { + id: string + title: string +} + +const columns: Array> = [ + { + id: 'id', + header: 'Id', + accessorKey: 'id', + cell: (context) => context.getValue(), + }, + { + id: 'title', + header: 'Title', + accessorKey: 'title', + cell: (context) => context.getValue(), + }, +] + +const paginatedFeatures = tableFeatures({ + ...stockFeatures, + paginatedRowModel: createPaginatedRowModel(), +}) +const paginatedColumns: Array> = [ + { + id: 'id', + header: 'Id', + accessorKey: 'id', + cell: (context) => context.getValue(), + }, + { + id: 'title', + header: 'Title', + accessorKey: 'title', + cell: (context) => context.getValue(), + }, +] + +let container: HTMLDivElement | undefined +let root: Root | undefined + +function render(element: React.ReactNode) { + container = document.createElement('div') + document.body.append(container) + root = createRoot(container) + + React.act(() => { + root!.render(element) + }) +} + +function text(testId: string) { + return container?.querySelector(`[data-testid="${testId}"]`)?.textContent +} + +function click(action: string) { + const button = container?.querySelector( + `[data-action="${action}"]`, + ) + + expect(button).toBeTruthy() + button!.dispatchEvent(new MouseEvent('click', { bubbles: true })) +} + +function unmount() { + React.act(() => { + root!.unmount() + }) + container?.remove() + container = undefined + root = undefined +} + +const MemoizedCellValueObserver = React.memo( + function MemoizedCellValueObserver({ + value, + capture, + }: { + value: unknown + capture: (value: unknown) => void + }) { + capture(value) + return null + }, +) + +afterEach(() => { + if (root) { + unmount() + } + + vi.restoreAllMocks() +}) + +describe('React adapter parity', () => { + test('accepts required data props and updates row reads when the prop changes', async () => { + function TableHarness({ data }: { data: Array }) { + const table = useTable({ + data, + features: stockFeatures, + columns, + getRowId: (row) => row.id, + autoResetPageIndex: false, + }) + + return ( + <> + + {table + .getRowModel() + .rows.map((row) => row.id) + .join(',')} + + + {table + .getRowModel() + .rows.map((row) => row.getValue('title')) + .join(',')} + + + ) + } + + render() + + expect(text('row-ids')).toBe('1') + expect(text('row-titles')).toBe('Title') + + await React.act(async () => { + root!.render( + , + ) + await Promise.resolve() + }) + + expect(text('row-ids')).toBe('1,2') + expect(text('row-titles')).toBe('Updated,Added') + }) + + test('exposes the complete table surface through property checks and enumeration', () => { + function TableHarness() { + const table = useTable( + { + data: [{ id: '1', title: 'Title' }], + features: stockFeatures, + columns, + getRowId: (row) => row.id, + }, + () => null, + ) + + return ( + + {JSON.stringify({ + hasFeatures: '_features' in table, + hasOptions: 'options' in table, + hasState: 'state' in table, + hasRowModel: 'getRowModel' in table, + hasNotFound: 'notFound' in table, + keys: Object.keys(table), + row: table.getRow('1').original, + })} + + ) + } + + render() + + const surface = JSON.parse(text('table-surface')!) as { + hasFeatures: boolean + hasOptions: boolean + hasState: boolean + hasRowModel: boolean + hasNotFound: boolean + keys: Array + row: Data + } + + expect(surface).toMatchObject({ + hasFeatures: true, + hasOptions: true, + hasState: true, + hasRowModel: true, + hasNotFound: false, + row: { + id: '1', + title: 'Title', + }, + }) + expect(surface.keys).toEqual( + expect.arrayContaining(['_features', 'options', 'state', 'getRowModel']), + ) + }) + + test('updates the paginated row model without invalidating the core row model', () => { + const data = Array.from({ length: 10 }, (_, index) => ({ + id: String(index), + title: `Title ${index}`, + })) + const coreRowModelCaptor = vi.fn() + const rowModelCaptor = vi.fn() + function TableHarness() { + const [pagination, setPagination] = React.useState({ + pageIndex: 0, + pageSize: 5, + }) + const table = useTable( + { + data, + features: paginatedFeatures, + columns: paginatedColumns, + getRowId: (row) => row.id, + state: { pagination }, + onPaginationChange: setPagination, + }, + (state) => state.pagination, + ) + const coreRowModel = table.getCoreRowModel() + const rowModel = table.getRowModel() + + coreRowModelCaptor(coreRowModel) + rowModelCaptor(rowModel) + + return ( + <> + + {coreRowModel.rows.map((row) => row.id).join(',')} + + + {rowModel.rows.map((row) => row.id).join(',')} + + + ) } @@ -651,32 +595,22 @@ describe('useTable state subscriptions', () => { , ) - const pageIndex = () => - container?.querySelector('[data-testid="page-index"]')?.textContent - const atomPageIndex = () => - container?.querySelector('[data-testid="atom-page-index"]')?.textContent - const subscribedPageIndex = () => - container?.querySelector('[data-testid="subscribed-page-index"]') - ?.textContent - const nextPage = () => - container?.querySelector('button')?.dispatchEvent( - new MouseEvent('click', { - bubbles: true, - }), - ) + const pageIndex = () => text('page-index') + const atomPageIndex = () => text('atom-page-index') + const subscribedPageIndex = () => text('subscribed-page-index') expect(pageIndex()).toBe('0') expect(atomPageIndex()).toBe('0') expect(subscribedPageIndex()).toBe('0') - React.act(() => { - nextPage() + act(() => { + click('controlled-next-page') }) expect(pageIndex()).toBe('1') expect(atomPageIndex()).toBe('1') expect(subscribedPageIndex()).toBe('1') - React.act(() => { - nextPage() + act(() => { + click('controlled-next-page') }) expect(pageIndex()).toBe('2') expect(atomPageIndex()).toBe('2') @@ -746,6 +680,7 @@ describe('useTable state subscriptions', () => { - @@ -829,31 +756,23 @@ describe('useTable state subscriptions', () => { render() - const selectedPageIndex = () => - container?.querySelector('[data-testid="selected-page-index"]') - ?.textContent - const subscribedPageSize = () => - container?.querySelector('[data-testid="subscribed-page-size"]') - ?.textContent + const selectedPageIndex = () => text('selected-page-index') + const subscribedPageSize = () => text('subscribed-page-size') expect(harnessRenderCount).toBe(1) expect(selectedPageIndex()).toBe('0') expect(subscribedPageSize()).toBe('10') - React.act(() => { - container - ?.querySelector('[data-action="resize-page"]') - ?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + act(() => { + click('resize-page') }) expect(harnessRenderCount).toBe(1) expect(selectedPageIndex()).toBe('0') expect(subscribedPageSize()).toBe('20') - React.act(() => { - container - ?.querySelector('[data-action="next-page"]') - ?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + act(() => { + click('next-page') }) expect(harnessRenderCount).toBe(2) @@ -893,13 +812,13 @@ describe('useTable state subscriptions', () => { {table.atoms.pagination.get().pageIndex} + ) } @@ -978,26 +887,18 @@ describe('useTable state subscriptions', () => { render() - const pageIndex = () => - container?.querySelector('[data-testid="page-index"]')?.textContent - const selectedState = () => - container?.querySelector('[data-testid="selected-state"]')?.textContent - const nextPage = () => - container?.querySelector('button')?.dispatchEvent( - new MouseEvent('click', { - bubbles: true, - }), - ) + const pageIndex = () => text('page-index') + const selectedState = () => text('selected-state') expect(selectedState()).toBe('{"pageIndex":0}') expect(pageIndex()).toBe('0') - React.act(() => { - nextPage() + act(() => { + click('uncontrolled-next-page') }) expect(pageIndex()).toBe('1') - React.act(() => { - nextPage() + act(() => { + click('uncontrolled-next-page') }) expect(pageIndex()).toBe('2') diff --git a/packages/solid-table/package.json b/packages/solid-table/package.json index 294c12db1d..fc78aa22be 100644 --- a/packages/solid-table/package.json +++ b/packages/solid-table/package.json @@ -65,6 +65,7 @@ "@tanstack/table-core": "workspace:*" }, "devDependencies": { + "@solidjs/testing-library": "^0.8.10", "solid-js": "^1.9.13", "vite-plugin-solid": "^2.11.12" }, diff --git a/packages/solid-table/tests/unit/flexRender.test.ts b/packages/solid-table/tests/unit/flexRender.test.ts new file mode 100644 index 0000000000..8f7d79ee2c --- /dev/null +++ b/packages/solid-table/tests/unit/flexRender.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test, vi } from 'vitest' +import { createRoot } from 'solid-js' +import { flexRender } from '../../src/FlexRender' + +describe('flexRender', () => { + test('handles empty, static, and component templates', () => { + createRoot((dispose) => { + expect(flexRender(undefined, { value: 'unused' })).toBeNull() + expect(flexRender('static', { value: 'unused' })).toBe('static') + + const Component = vi.fn((props: { value: string }) => { + return `component:${props.value}` + }) + + expect(flexRender(Component, { value: 'rendered' })).toBe( + 'component:rendered', + ) + expect(Component).toHaveBeenCalledWith({ value: 'rendered' }) + dispose() + }) + }) +}) diff --git a/packages/solid-table/tests/unit/rendering.test.ts b/packages/solid-table/tests/unit/rendering.test.ts deleted file mode 100644 index 5efc295f88..0000000000 --- a/packages/solid-table/tests/unit/rendering.test.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { describe, expect, test, vi } from 'vitest' -import { createRoot, getOwner, runWithOwner } from 'solid-js' -import { stockFeatures } from '@tanstack/table-core' -import { FlexRender, flexRender } from '../../src/FlexRender' -import { createTable } from '../../src/createTable' -import { createTableHook } from '../../src/createTableHook' -import type { ColumnDef } from '@tanstack/table-core' - -function resolveSolidOutput(value: unknown): unknown { - let current = value - while (typeof current === 'function') { - current = current() - } - return current -} - -describe('FlexRender', () => { - type Data = { id: string; name: string } - const columns: Array> = [ - { - id: 'name', - accessorKey: 'name', - header: ({ column }) => `header:${column.id}`, - cell: ({ getValue }) => `cell:${getValue()}`, - aggregatedCell: ({ getValue }) => `aggregate:${getValue()}`, - footer: ({ column }) => `footer:${column.id}`, - }, - ] - - test('flexRender handles empty, static, and component templates', () => { - createRoot((dispose) => { - expect(flexRender(undefined, { value: 'unused' })).toBeNull() - expect(flexRender('static', { value: 'unused' })).toBe('static') - - const Component = vi.fn((props: { value: string }) => { - return `component:${props.value}` - }) - - expect(flexRender(Component, { value: 'rendered' })).toBe( - 'component:rendered', - ) - expect(Component).toHaveBeenCalledWith({ value: 'rendered' }) - dispose() - }) - }) - - test('renders cell, aggregate, placeholder, header, and footer templates', () => { - createRoot((dispose) => { - const table = createTable({ - data: [{ id: '1', name: 'Ada' }], - columns, - features: stockFeatures, - getRowId: (row) => row.id, - }) - const cell = table.getRowModel().rows[0]!.getAllCells()[0]! - const header = table.getHeaderGroups()[0]!.headers[0]! - const footer = table.getFooterGroups()[0]!.headers[0]! - - expect(resolveSolidOutput(FlexRender({ cell }))).toBe('cell:Ada') - expect(resolveSolidOutput(FlexRender({ header }))).toBe('header:name') - expect(resolveSolidOutput(FlexRender({ footer }))).toBe('footer:name') - - vi.spyOn(cell, 'getIsAggregated').mockReturnValue(true) - expect(resolveSolidOutput(FlexRender({ cell }))).toBe('aggregate:Ada') - - vi.spyOn(cell, 'getIsAggregated').mockReturnValue(false) - vi.spyOn(cell, 'getIsPlaceholder').mockReturnValue(true) - expect(resolveSolidOutput(FlexRender({ cell }))).toBeNull() - dispose() - }) - }) -}) - -describe('createTableHook runtime', () => { - type Data = { id: string; name: string } - const TableBadge = () => 'table-badge' - const CellBadge = () => 'cell-badge' - const HeaderBadge = () => 'header-badge' - - function createTestHook() { - return createTableHook({ - features: stockFeatures, - enableRowSelection: false, - getRowId: (row) => row.id, - tableComponents: { TableBadge }, - cellComponents: { CellBadge }, - headerComponents: { HeaderBadge }, - }) - } - - test('binds features and components while per-table options override defaults', () => { - const hook = createTestHook() - const columnHelper = hook.createAppColumnHelper() - const columns = columnHelper.columns([ - columnHelper.accessor('name', { - header: 'Name', - cell: ({ getValue }) => getValue(), - }), - ]) - - createRoot((dispose) => { - const table = hook.createAppTable({ - data: [{ id: '1', name: 'Ada' }], - columns, - enableRowSelection: true, - }) - - expect(hook.appFeatures).toBe(stockFeatures) - expect(table.getRow('1').getCanSelect()).toBe(true) - expect(table.TableBadge).toBe(TableBadge) - expect(table.FlexRender).toBe(FlexRender) - expect(table.AppTable).toEqual(expect.any(Function)) - expect(table.AppCell).toEqual(expect.any(Function)) - expect(table.AppHeader).toEqual(expect.any(Function)) - expect(table.AppFooter).toEqual(expect.any(Function)) - dispose() - }) - }) - - test('provides table, cell, and header contexts with bound render helpers', () => { - const hook = createTestHook() - const columnHelper = hook.createAppColumnHelper() - const columns = columnHelper.columns([ - columnHelper.accessor('name', { - header: ({ column }) => `header:${column.id}`, - cell: ({ getValue }) => `cell:${getValue()}`, - footer: ({ column }) => `footer:${column.id}`, - }), - ]) - let dispose!: () => void - const { owner, table } = createRoot((rootDispose) => { - dispose = rootDispose - return { - owner: getOwner()!, - table: hook.createAppTable({ - data: [{ id: '1', name: 'Ada' }], - columns, - }), - } - }) - - try { - runWithOwner(owner, () => { - let tableFromContext: unknown - const tableOutput = table.AppTable({ - get children() { - tableFromContext = hook.useTableContext() - return 'inside-table' - }, - }) - expect(resolveSolidOutput(tableOutput)).toBe('inside-table') - expect(tableFromContext).toBe(table) - - const cell = table.getRow('1').getAllCells()[0]! - let cellFromContext: unknown - let extendedCell: unknown - const cellOutput = table.AppCell({ - cell, - children: (value) => { - extendedCell = value - cellFromContext = hook.useCellContext() - expect(value.CellBadge).toBe(CellBadge) - return value.FlexRender() - }, - }) - expect(resolveSolidOutput(cellOutput)).toBe('cell:Ada') - expect(extendedCell).toBe(cell) - expect(cellFromContext).toBe(cell) - - const header = table.getHeaderGroups()[0]!.headers[0]! - let headerFromContext: unknown - const headerOutput = table.AppHeader({ - header, - children: (value) => { - headerFromContext = hook.useHeaderContext() - expect(value.HeaderBadge).toBe(HeaderBadge) - return value.FlexRender() - }, - }) - expect(resolveSolidOutput(headerOutput)).toBe('header:name') - expect(headerFromContext).toBe(header) - - const footer = table.getFooterGroups()[0]!.headers[0]! - const footerOutput = table.AppFooter({ - header: footer, - children: (value) => value.FlexRender(), - }) - expect(resolveSolidOutput(footerOutput)).toBe('footer:name') - }) - } finally { - dispose() - } - }) - - test('context hooks fail with actionable errors outside their providers', () => { - const hook = createTestHook() - - expect(() => hook.useTableContext()).toThrow( - '`useTableContext` must be used within an `AppTable` component', - ) - expect(() => hook.useCellContext()).toThrow( - '`useCellContext` must be used within an `AppCell` component', - ) - expect(() => hook.useHeaderContext()).toThrow( - '`useHeaderContext` must be used within an `AppHeader` or `AppFooter` component', - ) - }) -}) diff --git a/packages/solid-table/tests/unit/rendering.test.tsx b/packages/solid-table/tests/unit/rendering.test.tsx new file mode 100644 index 0000000000..f45b5170e1 --- /dev/null +++ b/packages/solid-table/tests/unit/rendering.test.tsx @@ -0,0 +1,300 @@ +// @vitest-environment jsdom + +import { afterEach, describe, expect, test } from 'vitest' +import { cleanup, render, screen } from '@solidjs/testing-library' +import { stockFeatures } from '@tanstack/table-core' +import { FlexRender } from '../../src/FlexRender' +import { createTable } from '../../src/createTable' +import { createTableHook } from '../../src/createTableHook' +import type { ColumnDef } from '@tanstack/table-core' + +afterEach(() => cleanup()) + +describe('FlexRender', () => { + type Data = { id: string; name: string } + type CellMode = 'aggregate' | 'normal' | 'placeholder' + const columns: Array> = [ + { + id: 'name', + accessorKey: 'name', + header: ({ column }) => `header:${column.id}`, + cell: ({ getValue }) => `cell:${getValue()}`, + aggregatedCell: ({ getValue }) => `aggregate:${getValue()}`, + footer: ({ column }) => `footer:${column.id}`, + }, + ] + + function CellHarness(props: { mode: CellMode }) { + const table = createTable({ + data: [{ id: '1', name: 'Ada' }], + columns, + features: stockFeatures, + getRowId: (row) => row.id, + }) + const cell = table.getRowModel().rows[0]!.getAllCells()[0]! + + if (props.mode === 'aggregate') { + Object.assign(cell, { + getIsAggregated: () => true, + }) + } + + if (props.mode === 'placeholder') { + Object.assign(cell, { + getIsAggregated: () => false, + getIsPlaceholder: () => true, + }) + } + + return ( +
+ +
+ ) + } + + function HeaderFooterHarness() { + const table = createTable({ + data: [{ id: '1', name: 'Ada' }], + columns, + features: stockFeatures, + getRowId: (row) => row.id, + }) + const header = table.getHeaderGroups()[0]!.headers[0]! + const footer = table.getFooterGroups()[0]!.headers[0]! + + return ( + <> +
+ +
+
+ +
+ + ) + } + + test('renders cell, aggregate, placeholder, header, and footer templates', () => { + render(() => ( + <> + + + + + + )) + + expect(screen.getByTestId('cell-normal').textContent).toBe('cell:Ada') + expect(screen.getByTestId('cell-aggregate').textContent).toBe( + 'aggregate:Ada', + ) + expect(screen.getByTestId('cell-placeholder').textContent).toBe('') + expect(screen.getByTestId('header').textContent).toBe('header:name') + expect(screen.getByTestId('footer').textContent).toBe('footer:name') + }) +}) + +describe('createTableHook runtime', () => { + type Data = { id: string; name: string } + const TableBadge = () => table-badge + const CellBadge = () => cell-badge + const HeaderBadge = () => header-badge + + function createTestHook() { + return createTableHook({ + features: stockFeatures, + enableRowSelection: false, + getRowId: (row) => row.id, + tableComponents: { TableBadge }, + cellComponents: { CellBadge }, + headerComponents: { HeaderBadge }, + }) + } + + test('binds features and components while per-table options override defaults', () => { + const hook = createTestHook() + const columnHelper = hook.createAppColumnHelper() + const columns = columnHelper.columns([ + columnHelper.accessor('name', { + header: 'Name', + cell: ({ getValue }) => getValue(), + }), + ]) + let tableRef: ReturnType> | undefined + + function Harness() { + const table = hook.createAppTable({ + data: [{ id: '1', name: 'Ada' }], + columns, + enableRowSelection: true, + }) + tableRef = table + + return ( + + + {String(table.getRow('1').getCanSelect())} + + + + ) + } + + render(() => ) + + expect(screen.getByTestId('can-select').textContent).toBe('true') + expect(screen.getByTestId('table-badge').textContent).toBe('table-badge') + expect(hook.appFeatures).toBe(stockFeatures) + expect(tableRef?.TableBadge).toBe(TableBadge) + expect(tableRef?.FlexRender).toBe(FlexRender) + expect(tableRef?.AppTable).toEqual(expect.any(Function)) + expect(tableRef?.AppCell).toEqual(expect.any(Function)) + expect(tableRef?.AppHeader).toEqual(expect.any(Function)) + expect(tableRef?.AppFooter).toEqual(expect.any(Function)) + }) + + test('provides table, cell, and header contexts with bound render helpers', () => { + const hook = createTestHook() + const columnHelper = hook.createAppColumnHelper() + const columns = columnHelper.columns([ + columnHelper.accessor('name', { + header: ({ column }) => `header:${column.id}`, + cell: ({ getValue }) => `cell:${getValue()}`, + footer: ({ column }) => `footer:${column.id}`, + }), + ]) + let tableFromContext: unknown + let cellFromContext: unknown + let headerFromContext: unknown + let footerFromContext: unknown + let tableRef: ReturnType> | undefined + + function Harness() { + const table = hook.createAppTable({ + data: [{ id: '1', name: 'Ada' }], + columns, + }) + tableRef = table + const cell = table.getRow('1').getAllCells()[0]! + const header = table.getHeaderGroups()[0]!.headers[0]! + const footer = table.getFooterGroups()[0]!.headers[0]! + + function TableContextProbe() { + tableFromContext = hook.useTableContext() + return ( + + {String(tableFromContext === table)} + + ) + } + + return ( + + + + + {(value) => { + cellFromContext = hook.useCellContext() + return ( + <> + + {String(cellFromContext === cell)} + + + {String(value.CellBadge === CellBadge)} + + + + + + + ) + }} + + + {(value) => { + headerFromContext = hook.useHeaderContext() + return ( + <> + + {String(headerFromContext === header)} + + + {String(value.HeaderBadge === HeaderBadge)} + + + + + + + ) + }} + + + {(value) => { + footerFromContext = hook.useHeaderContext() + return ( + <> + + {String(footerFromContext === footer)} + + + + + + ) + }} + + + ) + } + + render(() => ) + + expect(screen.getByTestId('table-context').textContent).toBe('true') + expect(screen.getByTestId('cell-context').textContent).toBe('true') + expect(screen.getByTestId('header-context').textContent).toBe('true') + expect(screen.getByTestId('footer-context').textContent).toBe('true') + expect(screen.getByTestId('cell-bound').textContent).toBe('true') + expect(screen.getByTestId('header-bound').textContent).toBe('true') + expect(screen.getByTestId('table-badge').textContent).toBe('table-badge') + expect(screen.getByTestId('cell-badge').textContent).toBe('cell-badge') + expect(screen.getByTestId('header-badge').textContent).toBe('header-badge') + expect(screen.getByTestId('cell-render').textContent).toBe('cell:Ada') + expect(screen.getByTestId('header-render').textContent).toBe('header:name') + expect(screen.getByTestId('footer-render').textContent).toBe('footer:name') + expect(tableFromContext).toBe(tableRef) + }) + + test('context hooks fail with actionable errors outside their providers', () => { + const hook = createTestHook() + + function TableContextFailure() { + hook.useTableContext() + return null + } + + function CellContextFailure() { + hook.useCellContext() + return null + } + + function HeaderContextFailure() { + hook.useHeaderContext() + return null + } + + expect(() => render(() => )).toThrow( + '`useTableContext` must be used within an `AppTable` component', + ) + cleanup() + expect(() => render(() => )).toThrow( + '`useCellContext` must be used within an `AppCell` component', + ) + cleanup() + expect(() => render(() => )).toThrow( + '`useHeaderContext` must be used within an `AppHeader` or `AppFooter` component', + ) + }) +}) diff --git a/packages/vue-table/package.json b/packages/vue-table/package.json index 05302ee54d..fe7554bc27 100644 --- a/packages/vue-table/package.json +++ b/packages/vue-table/package.json @@ -52,6 +52,7 @@ "@tanstack/table-core": "workspace:*" }, "devDependencies": { + "@testing-library/vue": "^8.1.0", "@vitejs/plugin-vue": "^6.0.7", "eslint-plugin-vue": "^10.9.2", "vue": "^3.5.38" diff --git a/packages/vue-table/tests/unit/flexRender.test.ts b/packages/vue-table/tests/unit/flexRender.test.ts new file mode 100644 index 0000000000..454e3c6995 --- /dev/null +++ b/packages/vue-table/tests/unit/flexRender.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test, vi } from 'vitest' +import { defineComponent, h, isVNode } from 'vue' +import { flexRender } from '../../src/FlexRender' + +describe('flexRender', () => { + test('returns primitives, callbacks, VNodes, and component objects', () => { + const props = { value: 'Ada' } + const renderCallback = vi.fn( + (context: typeof props) => `Hello ${context.value}`, + ) + + expect(flexRender('Plain text', props)).toBe('Plain text') + expect(flexRender(renderCallback, props)).toBe('Hello Ada') + expect(renderCallback).toHaveBeenCalledWith(props) + + const existingVNode = h('strong', 'Existing') + expect(flexRender(() => existingVNode, props)).toBe(existingVNode) + + const NameComponent = defineComponent({ + props: { + value: { + type: String, + required: true, + }, + }, + setup(componentProps) { + return () => h('span', `Name: ${componentProps.value}`) + }, + }) + const componentVNode = flexRender(NameComponent, props) + + expect(isVNode(componentVNode)).toBe(true) + expect(componentVNode.type).toBe(NameComponent) + expect(componentVNode.props).toMatchObject(props) + }) +}) diff --git a/packages/vue-table/tests/unit/rendering.test.ts b/packages/vue-table/tests/unit/rendering.test.ts index 2b7d689b62..d096e664e8 100644 --- a/packages/vue-table/tests/unit/rendering.test.ts +++ b/packages/vue-table/tests/unit/rendering.test.ts @@ -1,152 +1,15 @@ -import { describe, expect, test, vi } from 'vitest' -import { createRenderer, defineComponent, h, isVNode } from 'vue' +// @vitest-environment jsdom + +import { afterEach, describe, expect, test, vi } from 'vitest' +import { defineComponent, h } from 'vue' +import { cleanup, render, screen } from '@testing-library/vue' import { stockFeatures } from '@tanstack/table-core' -import { FlexRender, flexRender } from '../../src/FlexRender' +import { FlexRender } from '../../src/FlexRender' import { createTableHook } from '../../src/createTableHook' -import type { Component } from 'vue' - -interface HostNode { - type: string - text: string - children: Array - parent: HostNode | null - props: Record -} - -function createHostNode(type: string, text = ''): HostNode { - return { - type, - text, - children: [], - parent: null, - props: {}, - } -} - -function insertHostNode( - child: HostNode, - parent: HostNode, - anchor: HostNode | null = null, -) { - child.parent = parent - const anchorIndex = anchor ? parent.children.indexOf(anchor) : -1 - - if (anchorIndex === -1) { - parent.children.push(child) - } else { - parent.children.splice(anchorIndex, 0, child) - } -} - -const memoryRenderer = createRenderer({ - patchProp(element, key, _previousValue, nextValue) { - element.props[key] = nextValue - }, - insert: insertHostNode, - remove(child) { - if (!child.parent) { - return - } - - const index = child.parent.children.indexOf(child) - if (index !== -1) { - child.parent.children.splice(index, 1) - } - child.parent = null - }, - createElement(type) { - return createHostNode(type) - }, - createText(text) { - return createHostNode('#text', text) - }, - createComment(text) { - return createHostNode('#comment', text) - }, - setText(node, text) { - node.text = text - }, - setElementText(node, text) { - node.text = text - node.children = [] - }, - parentNode(node) { - return node.parent - }, - nextSibling(node) { - if (!node.parent) { - return null - } - const index = node.parent.children.indexOf(node) - return node.parent.children[index + 1] ?? null - }, - querySelector() { - return null - }, - setScopeId() {}, - cloneNode(node) { - return { - ...node, - children: [...node.children], - parent: null, - props: { ...node.props }, - } - }, - insertStaticContent(content, parent, anchor) { - const node = createHostNode('#static', content) - insertHostNode(node, parent, anchor) - return [node, node] - }, -}) - -function mount(component: Component) { - const root = createHostNode('#root') - const app = memoryRenderer.createApp(component) - app.mount(root) - - return { - root, - unmount: () => app.unmount(), - } -} - -function readText(node: HostNode): string { - return node.text + node.children.map(readText).join('') -} +afterEach(cleanup) describe('FlexRender', () => { - test('renders primitives, callbacks, VNodes, and component objects', () => { - const props = { value: 'Ada' } - const renderCallback = vi.fn( - (context: typeof props) => `Hello ${context.value}`, - ) - - expect(flexRender('Plain text', props)).toBe('Plain text') - expect(flexRender(renderCallback, props)).toBe('Hello Ada') - expect(renderCallback).toHaveBeenCalledWith(props) - - const existingVNode = h('strong', 'Existing') - expect(flexRender(() => existingVNode, props)).toBe(existingVNode) - - const NameComponent = defineComponent({ - props: { - value: { - type: String, - required: true, - }, - }, - setup(componentProps) { - return () => h('span', `Name: ${componentProps.value}`) - }, - }) - const componentVNode = flexRender(NameComponent, props) - - expect(isVNode(componentVNode)).toBe(true) - expect(componentVNode.type).toBe(NameComponent) - expect(componentVNode.props).toMatchObject(props) - }) - test('supports cell modes, header/footer shorthand, and legacy props', () => { const normalContext = { value: 'Normal' } const aggregatedContext = { value: 'Aggregated' } @@ -169,7 +32,7 @@ describe('FlexRender', () => { const Root = defineComponent({ setup() { return () => - h('section', [ + h('section', { 'data-testid': 'flex-render-output' }, [ h(FlexRender, { cell: { column: { @@ -231,9 +94,9 @@ describe('FlexRender', () => { }, }) - const mounted = mount(Root) + render(Root) - expect(readText(mounted.root)).toBe( + expect(screen.getByTestId('flex-render-output').textContent).toBe( 'cell:Normalsum:Aggregatedheader:Titlefooter:Totallegacy:Legacy', ) expect(normalCellRenderer).toHaveBeenCalledOnce() @@ -242,8 +105,6 @@ describe('FlexRender', () => { expect(placeholderRenderer).not.toHaveBeenCalled() expect(headerRenderer).toHaveBeenCalledWith(headerContext) expect(footerRenderer).toHaveBeenCalledWith(footerContext) - - mounted.unmount() }) }) @@ -252,17 +113,20 @@ describe('createTableHook', () => { const TableBadge = defineComponent({ setup() { - return () => h('span', 'table-component') + return () => + h('span', { 'data-testid': 'table-component' }, 'table-component') }, }) const CellBadge = defineComponent({ setup() { - return () => h('span', 'cell-component') + return () => + h('span', { 'data-testid': 'cell-component' }, 'cell-component') }, }) const HeaderBadge = defineComponent({ setup() { - return () => h('span', 'header-component') + return () => + h('span', { 'data-testid': 'header-component' }, 'header-component') }, }) @@ -291,6 +155,7 @@ describe('createTableHook', () => { let originalCell: unknown let originalHeader: unknown let originalFooter: unknown + let firstRowId: string | undefined const TableConsumer = defineComponent({ setup() { @@ -339,50 +204,63 @@ describe('createTableHook', () => { originalCell = cell originalHeader = header originalFooter = footer + firstRowId = row.id return () => - h(table.AppTable, null, { - default: () => [ - h(TableConsumer), - h( - table.AppCell, - { cell }, - { - default: () => h(CellConsumer), - }, - ), - h( - table.AppHeader, - { header }, - { - default: () => h(HeaderConsumer), - }, - ), - h( - table.AppFooter, - { header: footer }, - { - default: () => h(FooterConsumer), - }, - ), - ], - }) + h( + 'main', + { 'data-testid': 'table-hook-output' }, + h(table.AppTable, null, { + default: () => [ + h(TableConsumer), + h( + table.AppCell, + { cell }, + { + default: () => h(CellConsumer), + }, + ), + h( + table.AppHeader, + { header }, + { + default: () => h(HeaderConsumer), + }, + ), + h( + table.AppFooter, + { header: footer }, + { + default: () => h(FooterConsumer), + }, + ), + ], + }), + ) }, }) - const mounted = mount(Root) + render(Root) expect(hook.appFeatures).toBe(stockFeatures) + expect(firstRowId).toBe('row-1') expect(tableContextCaptor).toHaveBeenCalledWith(createdTable) expect(cellContextCaptor).toHaveBeenCalledWith(originalCell) expect(headerContextCaptor).toHaveBeenCalledWith(originalHeader) expect(footerContextCaptor).toHaveBeenCalledWith(originalFooter) - expect(readText(mounted.root)).toBe( + expect(screen.getByTestId('table-component').textContent).toBe( + 'table-component', + ) + expect(screen.getByTestId('cell-component').textContent).toBe( + 'cell-component', + ) + expect(screen.getByTestId('header-component').textContent).toBe( + 'header-component', + ) + expect(screen.getByTestId('table-hook-output').textContent).toBe( 'table-componentcell-componentcell:First' + 'header-componentheader:titlefooter:title', ) - - mounted.unmount() }) test.each([ @@ -398,9 +276,12 @@ describe('createTableHook', () => { }, }) - expect(() => mount(Consumer)).toThrowError( - new RegExp(`\\\`${name}\\\` must be used within`), - ) - warning.mockRestore() + try { + expect(() => render(Consumer)).toThrowError( + new RegExp(`\\\`${name}\\\` must be used within`), + ) + } finally { + warning.mockRestore() + } }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6ece96525..a9e73d8496 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13494,6 +13494,9 @@ importers: '@eslint-react/eslint-plugin': specifier: ^5.9.2 version: 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@types/react': specifier: 19.2.16 version: 19.2.16 @@ -13565,6 +13568,9 @@ importers: specifier: workspace:* version: link:../table-core devDependencies: + '@solidjs/testing-library': + specifier: ^0.8.10 + version: 0.8.10(solid-js@1.9.13) solid-js: specifier: ^1.9.13 version: 1.9.13 @@ -13659,6 +13665,9 @@ importers: specifier: workspace:* version: link:../table-core devDependencies: + '@testing-library/vue': + specifier: ^8.1.0 + version: 8.1.0(@vue/compiler-dom@3.5.38)(@vue/compiler-sfc@3.5.38)(@vue/server-renderer@3.5.38(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3)) '@vitejs/plugin-vue': specifier: ^6.0.7 version: 6.0.7(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0))(vue@3.5.38(typescript@6.0.3)) @@ -18855,6 +18864,16 @@ packages: peerDependencies: solid-js: '>=1.8.4' + '@solidjs/testing-library@0.8.10': + resolution: {integrity: sha512-qdeuIerwyq7oQTIrrKvV0aL9aFeuwTd86VYD3afdq5HYEwoox1OBTJy4y8A3TFZr8oAR0nujYgCzY/8wgHGfeQ==} + engines: {node: '>= 14'} + peerDependencies: + '@solidjs/router': '>=0.9.0' + solid-js: '>=1.0.0' + peerDependenciesMeta: + '@solidjs/router': + optional: true + '@spectrum-icons/ui@3.7.1': resolution: {integrity: sha512-veQymocUYo5OciXQajSailOdbWe+k6+2ehfF8D4d0V923D4xOUadtT253xXZ5vEQjPat6Kyp2WDKeQNjd7kL1w==} peerDependencies: @@ -19567,10 +19586,43 @@ packages: peerDependencies: vue: ^2.7.0 || ^3.0.0 + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/dom@9.3.4': + resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} + engines: {node: '>=14'} + '@testing-library/jest-dom@6.9.1': resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/vue@8.1.0': + resolution: {integrity: sha512-ls4RiHO1ta4mxqqajWRh8158uFObVrrtAPoxk7cIp4HrnQUj/ScKzqz53HxYpG3X6Zb7H2v+0eTGLSoy8HQ2nA==} + engines: {node: '>=14'} + peerDependencies: + '@vue/compiler-sfc': '>= 3' + vue: '>= 3' + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + '@ts-morph/common@0.22.0': resolution: {integrity: sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==} @@ -20119,6 +20171,16 @@ packages: '@vue/shared@3.5.38': resolution: {integrity: sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==} + '@vue/test-utils@2.4.11': + resolution: {integrity: sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==} + peerDependencies: + '@vue/compiler-dom': 3.x + '@vue/server-renderer': 3.x + vue: 3.x + peerDependenciesMeta: + '@vue/server-renderer': + optional: true + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -20548,6 +20610,12 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.1: resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} engines: {node: '>= 0.4'} @@ -21102,6 +21170,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + connect-history-api-fallback@2.0.0: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} engines: {node: '>=0.8'} @@ -21451,6 +21522,10 @@ packages: dedent-js@1.0.1: resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -21586,6 +21661,11 @@ packages: resolution: {integrity: sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA==} engines: {node: '>=0.8'} + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + editorconfig@3.0.2: resolution: {integrity: sha512-T0ix8GhtxyKVfUFEcvdNDt3YGqlwkFHbD4/5bgFUDgFmxhI/cSRAeJ87/Sz//Cq8Eam6JX/e23RkoFO71P7aAA==} engines: {node: '>=20'} @@ -21769,6 +21849,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} @@ -22743,6 +22826,10 @@ packages: resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} engines: {node: '>= 10'} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -23024,6 +23111,11 @@ packages: jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + js-string-escape@1.0.1: resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} engines: {node: '>= 0.8'} @@ -23709,6 +23801,11 @@ packages: resolution: {integrity: sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==} engines: {node: '>=6'} + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + nopt@9.0.0: resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} engines: {node: ^20.17.0 || >=22.9.0} @@ -23784,6 +23881,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -24229,6 +24330,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -24956,6 +25061,10 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -26312,8 +26421,78 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + js-cookie@3.0.8: + resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + vue-component-type-helpers@3.3.8: + resolution: {integrity: sha512-troqCMmQodQDqUqn63NQaFi+CDSclSe7sc8VEBFqf5GFLqmGR2Ph3P2WEC7qwpRVyEWsTi/aAr4vyOe/B1hU3g==} + snapshots: + '@one-ini/wasm@0.1.1': {} + + '@types/aria-query@5.0.4': {} + + abbrev@2.0.0: {} + + ansi-styles@5.2.0: {} + + commander@10.0.1: {} + + dequal@2.0.3: {} + + dom-accessibility-api@0.5.16: {} + + ini@1.3.8: {} + + js-cookie@3.0.8: {} + + lz-string@1.5.0: {} + + proto-list@1.2.4: {} + + react-is@17.0.2: {} + + vue-component-type-helpers@3.3.8: {} + '@adobe/css-tools@4.5.0': {} '@adobe/react-spectrum-ui@1.2.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': @@ -32291,6 +32470,11 @@ snapshots: dependencies: solid-js: 1.9.13 + '@solidjs/testing-library@0.8.10(solid-js@1.9.13)': + dependencies: + '@testing-library/dom': 10.4.1 + solid-js: 1.9.13 + '@spectrum-icons/ui@3.7.1(@adobe/react-spectrum@3.47.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@adobe/react-spectrum': 3.47.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -33156,6 +33340,28 @@ snapshots: '@tanstack/virtual-core': 3.17.3 vue: 3.5.38(typescript@6.0.3) + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/dom@9.3.4': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + '@testing-library/jest-dom@6.9.1': dependencies: '@adobe/css-tools': 4.5.0 @@ -33165,6 +33371,28 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@testing-library/vue@8.1.0(@vue/compiler-dom@3.5.38)(@vue/compiler-sfc@3.5.38)(@vue/server-renderer@3.5.38(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3))': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 9.3.4 + '@vue/test-utils': 2.4.11(@vue/compiler-dom@3.5.38)(@vue/server-renderer@3.5.38(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3)) + vue: 3.5.38(typescript@6.0.3) + optionalDependencies: + '@vue/compiler-sfc': 3.5.38 + transitivePeerDependencies: + - '@vue/compiler-dom' + - '@vue/server-renderer' + '@ts-morph/common@0.22.0': dependencies: fast-glob: 3.3.3 @@ -33880,6 +34108,15 @@ snapshots: '@vue/shared@3.5.38': {} + '@vue/test-utils@2.4.11(@vue/compiler-dom@3.5.38)(@vue/server-renderer@3.5.38(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3))': + dependencies: + '@vue/compiler-dom': 3.5.38 + js-beautify: 1.15.4 + vue: 3.5.38(typescript@6.0.3) + vue-component-type-helpers: 3.3.8 + optionalDependencies: + '@vue/server-renderer': 3.5.38(vue@3.5.38(typescript@6.0.3)) + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -34658,6 +34895,14 @@ snapshots: dependencies: tslib: 2.8.1 + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + aria-query@5.3.1: {} aria-query@5.3.2: {} @@ -35386,6 +35631,11 @@ snapshots: concat-map@0.0.1: {} + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + connect-history-api-fallback@2.0.0: {} consolidate@1.0.4(@babel/core@7.29.7)(handlebars@4.7.9)(lodash@4.18.1)(mustache@4.2.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(underscore@1.13.8): @@ -35612,6 +35862,27 @@ snapshots: dedent-js@1.0.1: {} + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.1 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -35729,6 +36000,13 @@ snapshots: errlop: 2.2.0 semver: 6.3.1 + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.9 + semver: 7.8.5 + editorconfig@3.0.2: dependencies: '@one-ini/wasm': 0.2.1 @@ -36027,6 +36305,18 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.9 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + es-module-lexer@2.1.0: {} es-object-atoms@1.1.1: @@ -37300,6 +37590,11 @@ snapshots: ipaddr.js@2.4.0: {} + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -37571,6 +37866,14 @@ snapshots: jose@6.2.3: {} + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.5.0 + js-cookie: 3.0.8 + nopt: 7.2.1 + js-string-escape@1.0.1: {} js-tokens@10.0.0: {} @@ -38308,6 +38611,10 @@ snapshots: node-watch@0.7.3: {} + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + nopt@9.0.0: dependencies: abbrev: 4.0.0 @@ -38503,6 +38810,11 @@ snapshots: object-inspect@1.13.4: {} + object-is@1.1.6: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + object-keys@1.1.1: {} object.assign@4.1.7: @@ -39090,6 +39402,12 @@ snapshots: prettier@3.8.4: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -40061,6 +40379,14 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} From 0423b644bc6a76cd0255b4f3316a0cf15046ded3 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Wed, 29 Jul 2026 17:27:47 +0200 Subject: [PATCH 03/13] test: add more adapters reactivity tests --- .../angular-table/tests/injectTable.test.ts | 64 +- packages/react-table/src/FlexRender.tsx | 10 +- .../tests/adapterReactivity.test.tsx | 572 ++---------------- .../tests/createTableHook.test.tsx | 80 +++ packages/react-table/tests/ssr.test.tsx | 54 +- packages/react-table/tests/useTable.test.tsx | 29 +- packages/solid-table/src/FlexRender.tsx | 2 +- .../tests/unit/adapterReactivity.test.ts | 27 +- .../tests/unit/createTable.test.ts | 78 +-- .../solid-table/tests/unit/flexRender.test.ts | 1 + .../solid-table/tests/unit/reactivity.test.ts | 220 ++----- .../solid-table/tests/unit/rendering.test.tsx | 76 ++- packages/vue-table/src/FlexRender.ts | 8 + packages/vue-table/src/createTableHook.ts | 9 +- .../tests/unit/adapter-lifecycle.test.ts | 33 +- .../vue-table/tests/unit/flexRender.test.ts | 10 + .../vue-table/tests/unit/rendering.test.ts | 127 +++- packages/vue-table/tests/unit/signals.test.ts | 32 - .../vue-table/tests/unit/useTable.test.ts | 245 +++----- 19 files changed, 628 insertions(+), 1049 deletions(-) diff --git a/packages/angular-table/tests/injectTable.test.ts b/packages/angular-table/tests/injectTable.test.ts index 00cff24908..aee244543d 100644 --- a/packages/angular-table/tests/injectTable.test.ts +++ b/packages/angular-table/tests/injectTable.test.ts @@ -1,4 +1,3 @@ -import { isProxy } from 'node:util/types' import { describe, expect, test, vi } from 'vitest' import { ChangeDetectionStrategy, @@ -8,16 +7,19 @@ import { signal, } from '@angular/core' import { TestBed } from '@angular/core/testing' +import { By } from '@angular/platform-browser' import { ColumnDef, createPaginatedRowModel, stockFeatures, } from '@tanstack/table-core' -import { RowModel, injectTable } from '../src' +import { injectTable } from '../src' import type { PaginationState } from '../src' describe('injectTable', () => { test('should support required signal inputs', async () => { + type Data = { id: string; title: string } + @Component({ selector: 'app-table', template: ``, @@ -25,27 +27,53 @@ describe('injectTable', () => { changeDetection: ChangeDetectionStrategy.OnPush, }) class TableComponent { - data = input.required>() + data = input.required>() table = injectTable(() => ({ data: this.data(), features: stockFeatures, columns: [], + getRowId: (row) => row.id, })) } @Component({ selector: 'app-root', imports: [TableComponent], - template: ` `, + template: ``, changeDetection: ChangeDetectionStrategy.OnPush, }) - class RootComponent {} + class RootComponent { + readonly data = signal>([{ id: '1', title: 'First' }]) + } const fixture = TestBed.createComponent(RootComponent) fixture.detectChanges() + await fixture.whenRenderingDone() + + const tableComponent = fixture.debugElement.query( + By.directive(TableComponent), + ).componentInstance as TableComponent + expect( + tableComponent.table.getRowModel().rows.map((row) => row.original), + ).toEqual([{ id: '1', title: 'First' }]) + TestBed.tick() + + fixture.componentInstance.data.set([ + { id: '1', title: 'Updated' }, + { id: '2', title: 'Second' }, + ]) + fixture.detectChanges() + TestBed.tick() await fixture.whenRenderingDone() + + expect( + tableComponent.table.getRowModel().rows.map((row) => row.original), + ).toEqual([ + { id: '1', title: 'Updated' }, + { id: '2', title: 'Second' }, + ]) }) describe('Proxy table', () => { @@ -64,12 +92,8 @@ describe('injectTable', () => { })), ) - test('table is proxy', () => { - expect(isProxy(table)).toBe(true) - }) - test('supports "in" operator', () => { - expect('_features' in table).toBe(true) + expect('atoms' in table).toBe(true) expect('options' in table).toBe(true) expect('notFound' in table).toBe(false) }) @@ -80,10 +104,7 @@ describe('injectTable', () => { }) test('Row model is reactive', () => { - const coreRowModelFn = - vi.fn<(model: RowModel) => void>() - const rowModelFn = - vi.fn<(model: RowModel) => void>() + const rowCounts = vi.fn<(count: number) => void>() const pagination = signal({ pageSize: 5, pageIndex: 0, @@ -112,8 +133,8 @@ describe('injectTable', () => { }, })) - effect(() => coreRowModelFn(table.getCoreRowModel())) - effect(() => rowModelFn(table.getRowModel())) + const initialCoreRowModel = table.getCoreRowModel() + effect(() => rowCounts(table.getRowModel().rows.length)) TestBed.tick() @@ -121,15 +142,8 @@ describe('injectTable', () => { TestBed.tick() - // TODO: pagination state update twice during first table construct - // optionsStore is a signal -> so if updated with state in queuemicrotask will trigger twice - expect(coreRowModelFn).toHaveBeenCalledTimes(2) - expect(coreRowModelFn.mock.calls[0]![0].rows.length).toEqual(10) - expect(coreRowModelFn.mock.calls[1]![0].rows.length).toEqual(10) - - expect(rowModelFn).toHaveBeenCalledTimes(2) - expect(rowModelFn.mock.calls[0]![0].rows.length).toEqual(5) - expect(rowModelFn.mock.calls[1]![0].rows.length).toEqual(3) + expect(rowCounts.mock.calls).toEqual([[5], [3]]) + expect(table.getCoreRowModel()).toBe(initialCoreRowModel) }) }) }) diff --git a/packages/react-table/src/FlexRender.tsx b/packages/react-table/src/FlexRender.tsx index d345d06cec..80662f348a 100644 --- a/packages/react-table/src/FlexRender.tsx +++ b/packages/react-table/src/FlexRender.tsx @@ -46,11 +46,11 @@ export function flexRender( Comp: Renderable, props: TProps, ): ReactNode | JSX.Element { - return !Comp ? null : isReactComponent(Comp) ? ( - - ) : ( - Comp - ) + if (Comp === null || Comp === undefined) { + return null + } + + return isReactComponent(Comp) ? : Comp } /** diff --git a/packages/react-table/tests/adapterReactivity.test.tsx b/packages/react-table/tests/adapterReactivity.test.tsx index beda8a17f2..658808482f 100644 --- a/packages/react-table/tests/adapterReactivity.test.tsx +++ b/packages/react-table/tests/adapterReactivity.test.tsx @@ -82,19 +82,6 @@ function unmount() { renderedView = undefined } -const MemoizedCellValueObserver = React.memo( - function MemoizedCellValueObserver({ - value, - capture, - }: { - value: unknown - capture: (value: unknown) => void - }) { - capture(value) - return null - }, -) - afterEach(() => { cleanup() renderedView = undefined @@ -104,56 +91,7 @@ afterEach(() => { // Adapter contract only: React/store ownership, subscriptions, lifecycle, and // option refreshes. Row-model algorithms remain covered by table-core. describe('React adapter reactivity and lifecycle', () => { - test('accepts required data props and updates row reads when the prop changes', async () => { - function TableHarness({ data }: { data: Array }) { - const table = useTable({ - data, - features: stockFeatures, - columns, - getRowId: (row) => row.id, - autoResetPageIndex: false, - }) - - return ( - <> - - {table - .getRowModel() - .rows.map((row) => row.id) - .join(',')} - - - {table - .getRowModel() - .rows.map((row) => row.getValue('title')) - .join(',')} - - - ) - } - - render() - - expect(text('row-ids')).toBe('1') - expect(text('row-titles')).toBe('Title') - - await act(async () => { - renderedView!.rerender( - , - ) - await Promise.resolve() - }) - - expect(text('row-ids')).toBe('1,2') - expect(text('row-titles')).toBe('Updated,Added') - }) - - test('exposes the complete table surface through property checks and enumeration', () => { + test('exposes React adapter APIs through the returned table surface', () => { function TableHarness() { const table = useTable( { @@ -168,13 +106,12 @@ describe('React adapter reactivity and lifecycle', () => { return ( {JSON.stringify({ - hasFeatures: '_features' in table, hasOptions: 'options' in table, hasState: 'state' in table, hasRowModel: 'getRowModel' in table, - hasNotFound: 'notFound' in table, + hasSubscribe: 'Subscribe' in table, + hasFlexRender: 'FlexRender' in table, keys: Object.keys(table), - row: table.getRow('1').original, })} ) @@ -183,28 +120,30 @@ describe('React adapter reactivity and lifecycle', () => { render() const surface = JSON.parse(text('table-surface')) as { - hasFeatures: boolean hasOptions: boolean hasState: boolean hasRowModel: boolean - hasNotFound: boolean + hasSubscribe: boolean + hasFlexRender: boolean keys: Array - row: Data } - expect(surface).toMatchObject({ - hasFeatures: true, + expect(surface).toEqual({ hasOptions: true, hasState: true, hasRowModel: true, - hasNotFound: false, - row: { - id: '1', - title: 'Title', - }, + hasSubscribe: true, + hasFlexRender: true, + keys: expect.any(Array), }) expect(surface.keys).toEqual( - expect.arrayContaining(['_features', 'options', 'state', 'getRowModel']), + expect.arrayContaining([ + 'options', + 'state', + 'Subscribe', + 'FlexRender', + 'getRowModel', + ]), ) }) @@ -273,114 +212,8 @@ describe('React adapter reactivity and lifecycle', () => { expect(rowModelCaptor.mock.calls[1]![0].rows).toHaveLength(3) }) - test('row, cell, and column reads update only for their subscribed inputs', () => { - const isSelectedCaptor = vi.fn<(value: boolean) => void>() - const idValueCaptor = vi.fn<(value: unknown) => void>() - const memoizedIdValueCaptor = vi.fn<(value: unknown) => void>() - const titleValueCaptor = vi.fn<(value: unknown) => void>() - const columnIsVisibleCaptor = vi.fn<(value: boolean) => void>() - - function TableHarness() { - const [data, setData] = React.useState>([ - { id: '1', title: 'Title' }, - ]) - const table = useTable( - { - data, - features: stockFeatures, - columns, - getRowId: (row) => row.id, - }, - () => null, - ) - - const cells = table.getRow('1').getAllCells() - idValueCaptor(cells[0]!.getValue()) - titleValueCaptor(cells[1]!.getValue()) - - return ( - <> - - Boolean(selection['1'])} - > - {() => { - isSelectedCaptor(table.getRow('1').getIsSelected()) - return null - }} - - visibility.id !== false} - > - {() => { - columnIsVisibleCaptor(table.getColumn('id')!.getIsVisible()) - return null - }} - - + ) } @@ -651,32 +595,22 @@ describe('useTable state subscriptions', () => { , ) - const pageIndex = () => - container?.querySelector('[data-testid="page-index"]')?.textContent - const atomPageIndex = () => - container?.querySelector('[data-testid="atom-page-index"]')?.textContent - const subscribedPageIndex = () => - container?.querySelector('[data-testid="subscribed-page-index"]') - ?.textContent - const nextPage = () => - container?.querySelector('button')?.dispatchEvent( - new MouseEvent('click', { - bubbles: true, - }), - ) + const pageIndex = () => text('page-index') + const atomPageIndex = () => text('atom-page-index') + const subscribedPageIndex = () => text('subscribed-page-index') expect(pageIndex()).toBe('0') expect(atomPageIndex()).toBe('0') expect(subscribedPageIndex()).toBe('0') - React.act(() => { - nextPage() + act(() => { + click('controlled-next-page') }) expect(pageIndex()).toBe('1') expect(atomPageIndex()).toBe('1') expect(subscribedPageIndex()).toBe('1') - React.act(() => { - nextPage() + act(() => { + click('controlled-next-page') }) expect(pageIndex()).toBe('2') expect(atomPageIndex()).toBe('2') @@ -746,6 +680,7 @@ describe('useTable state subscriptions', () => { - @@ -829,31 +756,23 @@ describe('useTable state subscriptions', () => { render() - const selectedPageIndex = () => - container?.querySelector('[data-testid="selected-page-index"]') - ?.textContent - const subscribedPageSize = () => - container?.querySelector('[data-testid="subscribed-page-size"]') - ?.textContent + const selectedPageIndex = () => text('selected-page-index') + const subscribedPageSize = () => text('subscribed-page-size') expect(harnessRenderCount).toBe(1) expect(selectedPageIndex()).toBe('0') expect(subscribedPageSize()).toBe('10') - React.act(() => { - container - ?.querySelector('[data-action="resize-page"]') - ?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + act(() => { + click('resize-page') }) expect(harnessRenderCount).toBe(1) expect(selectedPageIndex()).toBe('0') expect(subscribedPageSize()).toBe('20') - React.act(() => { - container - ?.querySelector('[data-action="next-page"]') - ?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + act(() => { + click('next-page') }) expect(harnessRenderCount).toBe(2) @@ -893,13 +812,13 @@ describe('useTable state subscriptions', () => { {table.atoms.pagination.get().pageIndex} + ) } @@ -978,26 +887,18 @@ describe('useTable state subscriptions', () => { render() - const pageIndex = () => - container?.querySelector('[data-testid="page-index"]')?.textContent - const selectedState = () => - container?.querySelector('[data-testid="selected-state"]')?.textContent - const nextPage = () => - container?.querySelector('button')?.dispatchEvent( - new MouseEvent('click', { - bubbles: true, - }), - ) + const pageIndex = () => text('page-index') + const selectedState = () => text('selected-state') expect(selectedState()).toBe('{"pageIndex":0}') expect(pageIndex()).toBe('0') - React.act(() => { - nextPage() + act(() => { + click('uncontrolled-next-page') }) expect(pageIndex()).toBe('1') - React.act(() => { - nextPage() + act(() => { + click('uncontrolled-next-page') }) expect(pageIndex()).toBe('2') diff --git a/packages/solid-table/package.json b/packages/solid-table/package.json index 294c12db1d..fc78aa22be 100644 --- a/packages/solid-table/package.json +++ b/packages/solid-table/package.json @@ -65,6 +65,7 @@ "@tanstack/table-core": "workspace:*" }, "devDependencies": { + "@solidjs/testing-library": "^0.8.10", "solid-js": "^1.9.13", "vite-plugin-solid": "^2.11.12" }, diff --git a/packages/solid-table/tests/unit/flexRender.test.ts b/packages/solid-table/tests/unit/flexRender.test.ts new file mode 100644 index 0000000000..8f7d79ee2c --- /dev/null +++ b/packages/solid-table/tests/unit/flexRender.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test, vi } from 'vitest' +import { createRoot } from 'solid-js' +import { flexRender } from '../../src/FlexRender' + +describe('flexRender', () => { + test('handles empty, static, and component templates', () => { + createRoot((dispose) => { + expect(flexRender(undefined, { value: 'unused' })).toBeNull() + expect(flexRender('static', { value: 'unused' })).toBe('static') + + const Component = vi.fn((props: { value: string }) => { + return `component:${props.value}` + }) + + expect(flexRender(Component, { value: 'rendered' })).toBe( + 'component:rendered', + ) + expect(Component).toHaveBeenCalledWith({ value: 'rendered' }) + dispose() + }) + }) +}) diff --git a/packages/solid-table/tests/unit/rendering.test.ts b/packages/solid-table/tests/unit/rendering.test.ts deleted file mode 100644 index 5efc295f88..0000000000 --- a/packages/solid-table/tests/unit/rendering.test.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { describe, expect, test, vi } from 'vitest' -import { createRoot, getOwner, runWithOwner } from 'solid-js' -import { stockFeatures } from '@tanstack/table-core' -import { FlexRender, flexRender } from '../../src/FlexRender' -import { createTable } from '../../src/createTable' -import { createTableHook } from '../../src/createTableHook' -import type { ColumnDef } from '@tanstack/table-core' - -function resolveSolidOutput(value: unknown): unknown { - let current = value - while (typeof current === 'function') { - current = current() - } - return current -} - -describe('FlexRender', () => { - type Data = { id: string; name: string } - const columns: Array> = [ - { - id: 'name', - accessorKey: 'name', - header: ({ column }) => `header:${column.id}`, - cell: ({ getValue }) => `cell:${getValue()}`, - aggregatedCell: ({ getValue }) => `aggregate:${getValue()}`, - footer: ({ column }) => `footer:${column.id}`, - }, - ] - - test('flexRender handles empty, static, and component templates', () => { - createRoot((dispose) => { - expect(flexRender(undefined, { value: 'unused' })).toBeNull() - expect(flexRender('static', { value: 'unused' })).toBe('static') - - const Component = vi.fn((props: { value: string }) => { - return `component:${props.value}` - }) - - expect(flexRender(Component, { value: 'rendered' })).toBe( - 'component:rendered', - ) - expect(Component).toHaveBeenCalledWith({ value: 'rendered' }) - dispose() - }) - }) - - test('renders cell, aggregate, placeholder, header, and footer templates', () => { - createRoot((dispose) => { - const table = createTable({ - data: [{ id: '1', name: 'Ada' }], - columns, - features: stockFeatures, - getRowId: (row) => row.id, - }) - const cell = table.getRowModel().rows[0]!.getAllCells()[0]! - const header = table.getHeaderGroups()[0]!.headers[0]! - const footer = table.getFooterGroups()[0]!.headers[0]! - - expect(resolveSolidOutput(FlexRender({ cell }))).toBe('cell:Ada') - expect(resolveSolidOutput(FlexRender({ header }))).toBe('header:name') - expect(resolveSolidOutput(FlexRender({ footer }))).toBe('footer:name') - - vi.spyOn(cell, 'getIsAggregated').mockReturnValue(true) - expect(resolveSolidOutput(FlexRender({ cell }))).toBe('aggregate:Ada') - - vi.spyOn(cell, 'getIsAggregated').mockReturnValue(false) - vi.spyOn(cell, 'getIsPlaceholder').mockReturnValue(true) - expect(resolveSolidOutput(FlexRender({ cell }))).toBeNull() - dispose() - }) - }) -}) - -describe('createTableHook runtime', () => { - type Data = { id: string; name: string } - const TableBadge = () => 'table-badge' - const CellBadge = () => 'cell-badge' - const HeaderBadge = () => 'header-badge' - - function createTestHook() { - return createTableHook({ - features: stockFeatures, - enableRowSelection: false, - getRowId: (row) => row.id, - tableComponents: { TableBadge }, - cellComponents: { CellBadge }, - headerComponents: { HeaderBadge }, - }) - } - - test('binds features and components while per-table options override defaults', () => { - const hook = createTestHook() - const columnHelper = hook.createAppColumnHelper() - const columns = columnHelper.columns([ - columnHelper.accessor('name', { - header: 'Name', - cell: ({ getValue }) => getValue(), - }), - ]) - - createRoot((dispose) => { - const table = hook.createAppTable({ - data: [{ id: '1', name: 'Ada' }], - columns, - enableRowSelection: true, - }) - - expect(hook.appFeatures).toBe(stockFeatures) - expect(table.getRow('1').getCanSelect()).toBe(true) - expect(table.TableBadge).toBe(TableBadge) - expect(table.FlexRender).toBe(FlexRender) - expect(table.AppTable).toEqual(expect.any(Function)) - expect(table.AppCell).toEqual(expect.any(Function)) - expect(table.AppHeader).toEqual(expect.any(Function)) - expect(table.AppFooter).toEqual(expect.any(Function)) - dispose() - }) - }) - - test('provides table, cell, and header contexts with bound render helpers', () => { - const hook = createTestHook() - const columnHelper = hook.createAppColumnHelper() - const columns = columnHelper.columns([ - columnHelper.accessor('name', { - header: ({ column }) => `header:${column.id}`, - cell: ({ getValue }) => `cell:${getValue()}`, - footer: ({ column }) => `footer:${column.id}`, - }), - ]) - let dispose!: () => void - const { owner, table } = createRoot((rootDispose) => { - dispose = rootDispose - return { - owner: getOwner()!, - table: hook.createAppTable({ - data: [{ id: '1', name: 'Ada' }], - columns, - }), - } - }) - - try { - runWithOwner(owner, () => { - let tableFromContext: unknown - const tableOutput = table.AppTable({ - get children() { - tableFromContext = hook.useTableContext() - return 'inside-table' - }, - }) - expect(resolveSolidOutput(tableOutput)).toBe('inside-table') - expect(tableFromContext).toBe(table) - - const cell = table.getRow('1').getAllCells()[0]! - let cellFromContext: unknown - let extendedCell: unknown - const cellOutput = table.AppCell({ - cell, - children: (value) => { - extendedCell = value - cellFromContext = hook.useCellContext() - expect(value.CellBadge).toBe(CellBadge) - return value.FlexRender() - }, - }) - expect(resolveSolidOutput(cellOutput)).toBe('cell:Ada') - expect(extendedCell).toBe(cell) - expect(cellFromContext).toBe(cell) - - const header = table.getHeaderGroups()[0]!.headers[0]! - let headerFromContext: unknown - const headerOutput = table.AppHeader({ - header, - children: (value) => { - headerFromContext = hook.useHeaderContext() - expect(value.HeaderBadge).toBe(HeaderBadge) - return value.FlexRender() - }, - }) - expect(resolveSolidOutput(headerOutput)).toBe('header:name') - expect(headerFromContext).toBe(header) - - const footer = table.getFooterGroups()[0]!.headers[0]! - const footerOutput = table.AppFooter({ - header: footer, - children: (value) => value.FlexRender(), - }) - expect(resolveSolidOutput(footerOutput)).toBe('footer:name') - }) - } finally { - dispose() - } - }) - - test('context hooks fail with actionable errors outside their providers', () => { - const hook = createTestHook() - - expect(() => hook.useTableContext()).toThrow( - '`useTableContext` must be used within an `AppTable` component', - ) - expect(() => hook.useCellContext()).toThrow( - '`useCellContext` must be used within an `AppCell` component', - ) - expect(() => hook.useHeaderContext()).toThrow( - '`useHeaderContext` must be used within an `AppHeader` or `AppFooter` component', - ) - }) -}) diff --git a/packages/solid-table/tests/unit/rendering.test.tsx b/packages/solid-table/tests/unit/rendering.test.tsx new file mode 100644 index 0000000000..f45b5170e1 --- /dev/null +++ b/packages/solid-table/tests/unit/rendering.test.tsx @@ -0,0 +1,300 @@ +// @vitest-environment jsdom + +import { afterEach, describe, expect, test } from 'vitest' +import { cleanup, render, screen } from '@solidjs/testing-library' +import { stockFeatures } from '@tanstack/table-core' +import { FlexRender } from '../../src/FlexRender' +import { createTable } from '../../src/createTable' +import { createTableHook } from '../../src/createTableHook' +import type { ColumnDef } from '@tanstack/table-core' + +afterEach(() => cleanup()) + +describe('FlexRender', () => { + type Data = { id: string; name: string } + type CellMode = 'aggregate' | 'normal' | 'placeholder' + const columns: Array> = [ + { + id: 'name', + accessorKey: 'name', + header: ({ column }) => `header:${column.id}`, + cell: ({ getValue }) => `cell:${getValue()}`, + aggregatedCell: ({ getValue }) => `aggregate:${getValue()}`, + footer: ({ column }) => `footer:${column.id}`, + }, + ] + + function CellHarness(props: { mode: CellMode }) { + const table = createTable({ + data: [{ id: '1', name: 'Ada' }], + columns, + features: stockFeatures, + getRowId: (row) => row.id, + }) + const cell = table.getRowModel().rows[0]!.getAllCells()[0]! + + if (props.mode === 'aggregate') { + Object.assign(cell, { + getIsAggregated: () => true, + }) + } + + if (props.mode === 'placeholder') { + Object.assign(cell, { + getIsAggregated: () => false, + getIsPlaceholder: () => true, + }) + } + + return ( +
+ +
+ ) + } + + function HeaderFooterHarness() { + const table = createTable({ + data: [{ id: '1', name: 'Ada' }], + columns, + features: stockFeatures, + getRowId: (row) => row.id, + }) + const header = table.getHeaderGroups()[0]!.headers[0]! + const footer = table.getFooterGroups()[0]!.headers[0]! + + return ( + <> +
+ +
+
+ +
+ + ) + } + + test('renders cell, aggregate, placeholder, header, and footer templates', () => { + render(() => ( + <> + + + + + + )) + + expect(screen.getByTestId('cell-normal').textContent).toBe('cell:Ada') + expect(screen.getByTestId('cell-aggregate').textContent).toBe( + 'aggregate:Ada', + ) + expect(screen.getByTestId('cell-placeholder').textContent).toBe('') + expect(screen.getByTestId('header').textContent).toBe('header:name') + expect(screen.getByTestId('footer').textContent).toBe('footer:name') + }) +}) + +describe('createTableHook runtime', () => { + type Data = { id: string; name: string } + const TableBadge = () => table-badge + const CellBadge = () => cell-badge + const HeaderBadge = () => header-badge + + function createTestHook() { + return createTableHook({ + features: stockFeatures, + enableRowSelection: false, + getRowId: (row) => row.id, + tableComponents: { TableBadge }, + cellComponents: { CellBadge }, + headerComponents: { HeaderBadge }, + }) + } + + test('binds features and components while per-table options override defaults', () => { + const hook = createTestHook() + const columnHelper = hook.createAppColumnHelper() + const columns = columnHelper.columns([ + columnHelper.accessor('name', { + header: 'Name', + cell: ({ getValue }) => getValue(), + }), + ]) + let tableRef: ReturnType> | undefined + + function Harness() { + const table = hook.createAppTable({ + data: [{ id: '1', name: 'Ada' }], + columns, + enableRowSelection: true, + }) + tableRef = table + + return ( + + + {String(table.getRow('1').getCanSelect())} + + + + ) + } + + render(() => ) + + expect(screen.getByTestId('can-select').textContent).toBe('true') + expect(screen.getByTestId('table-badge').textContent).toBe('table-badge') + expect(hook.appFeatures).toBe(stockFeatures) + expect(tableRef?.TableBadge).toBe(TableBadge) + expect(tableRef?.FlexRender).toBe(FlexRender) + expect(tableRef?.AppTable).toEqual(expect.any(Function)) + expect(tableRef?.AppCell).toEqual(expect.any(Function)) + expect(tableRef?.AppHeader).toEqual(expect.any(Function)) + expect(tableRef?.AppFooter).toEqual(expect.any(Function)) + }) + + test('provides table, cell, and header contexts with bound render helpers', () => { + const hook = createTestHook() + const columnHelper = hook.createAppColumnHelper() + const columns = columnHelper.columns([ + columnHelper.accessor('name', { + header: ({ column }) => `header:${column.id}`, + cell: ({ getValue }) => `cell:${getValue()}`, + footer: ({ column }) => `footer:${column.id}`, + }), + ]) + let tableFromContext: unknown + let cellFromContext: unknown + let headerFromContext: unknown + let footerFromContext: unknown + let tableRef: ReturnType> | undefined + + function Harness() { + const table = hook.createAppTable({ + data: [{ id: '1', name: 'Ada' }], + columns, + }) + tableRef = table + const cell = table.getRow('1').getAllCells()[0]! + const header = table.getHeaderGroups()[0]!.headers[0]! + const footer = table.getFooterGroups()[0]!.headers[0]! + + function TableContextProbe() { + tableFromContext = hook.useTableContext() + return ( + + {String(tableFromContext === table)} + + ) + } + + return ( + + + + + {(value) => { + cellFromContext = hook.useCellContext() + return ( + <> + + {String(cellFromContext === cell)} + + + {String(value.CellBadge === CellBadge)} + + + + + + + ) + }} + + + {(value) => { + headerFromContext = hook.useHeaderContext() + return ( + <> + + {String(headerFromContext === header)} + + + {String(value.HeaderBadge === HeaderBadge)} + + + + + + + ) + }} + + + {(value) => { + footerFromContext = hook.useHeaderContext() + return ( + <> + + {String(footerFromContext === footer)} + + + + + + ) + }} + + + ) + } + + render(() => ) + + expect(screen.getByTestId('table-context').textContent).toBe('true') + expect(screen.getByTestId('cell-context').textContent).toBe('true') + expect(screen.getByTestId('header-context').textContent).toBe('true') + expect(screen.getByTestId('footer-context').textContent).toBe('true') + expect(screen.getByTestId('cell-bound').textContent).toBe('true') + expect(screen.getByTestId('header-bound').textContent).toBe('true') + expect(screen.getByTestId('table-badge').textContent).toBe('table-badge') + expect(screen.getByTestId('cell-badge').textContent).toBe('cell-badge') + expect(screen.getByTestId('header-badge').textContent).toBe('header-badge') + expect(screen.getByTestId('cell-render').textContent).toBe('cell:Ada') + expect(screen.getByTestId('header-render').textContent).toBe('header:name') + expect(screen.getByTestId('footer-render').textContent).toBe('footer:name') + expect(tableFromContext).toBe(tableRef) + }) + + test('context hooks fail with actionable errors outside their providers', () => { + const hook = createTestHook() + + function TableContextFailure() { + hook.useTableContext() + return null + } + + function CellContextFailure() { + hook.useCellContext() + return null + } + + function HeaderContextFailure() { + hook.useHeaderContext() + return null + } + + expect(() => render(() => )).toThrow( + '`useTableContext` must be used within an `AppTable` component', + ) + cleanup() + expect(() => render(() => )).toThrow( + '`useCellContext` must be used within an `AppCell` component', + ) + cleanup() + expect(() => render(() => )).toThrow( + '`useHeaderContext` must be used within an `AppHeader` or `AppFooter` component', + ) + }) +}) diff --git a/packages/vue-table/package.json b/packages/vue-table/package.json index 05302ee54d..fe7554bc27 100644 --- a/packages/vue-table/package.json +++ b/packages/vue-table/package.json @@ -52,6 +52,7 @@ "@tanstack/table-core": "workspace:*" }, "devDependencies": { + "@testing-library/vue": "^8.1.0", "@vitejs/plugin-vue": "^6.0.7", "eslint-plugin-vue": "^10.9.2", "vue": "^3.5.38" diff --git a/packages/vue-table/tests/unit/flexRender.test.ts b/packages/vue-table/tests/unit/flexRender.test.ts new file mode 100644 index 0000000000..454e3c6995 --- /dev/null +++ b/packages/vue-table/tests/unit/flexRender.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test, vi } from 'vitest' +import { defineComponent, h, isVNode } from 'vue' +import { flexRender } from '../../src/FlexRender' + +describe('flexRender', () => { + test('returns primitives, callbacks, VNodes, and component objects', () => { + const props = { value: 'Ada' } + const renderCallback = vi.fn( + (context: typeof props) => `Hello ${context.value}`, + ) + + expect(flexRender('Plain text', props)).toBe('Plain text') + expect(flexRender(renderCallback, props)).toBe('Hello Ada') + expect(renderCallback).toHaveBeenCalledWith(props) + + const existingVNode = h('strong', 'Existing') + expect(flexRender(() => existingVNode, props)).toBe(existingVNode) + + const NameComponent = defineComponent({ + props: { + value: { + type: String, + required: true, + }, + }, + setup(componentProps) { + return () => h('span', `Name: ${componentProps.value}`) + }, + }) + const componentVNode = flexRender(NameComponent, props) + + expect(isVNode(componentVNode)).toBe(true) + expect(componentVNode.type).toBe(NameComponent) + expect(componentVNode.props).toMatchObject(props) + }) +}) diff --git a/packages/vue-table/tests/unit/rendering.test.ts b/packages/vue-table/tests/unit/rendering.test.ts index 2b7d689b62..d096e664e8 100644 --- a/packages/vue-table/tests/unit/rendering.test.ts +++ b/packages/vue-table/tests/unit/rendering.test.ts @@ -1,152 +1,15 @@ -import { describe, expect, test, vi } from 'vitest' -import { createRenderer, defineComponent, h, isVNode } from 'vue' +// @vitest-environment jsdom + +import { afterEach, describe, expect, test, vi } from 'vitest' +import { defineComponent, h } from 'vue' +import { cleanup, render, screen } from '@testing-library/vue' import { stockFeatures } from '@tanstack/table-core' -import { FlexRender, flexRender } from '../../src/FlexRender' +import { FlexRender } from '../../src/FlexRender' import { createTableHook } from '../../src/createTableHook' -import type { Component } from 'vue' - -interface HostNode { - type: string - text: string - children: Array - parent: HostNode | null - props: Record -} - -function createHostNode(type: string, text = ''): HostNode { - return { - type, - text, - children: [], - parent: null, - props: {}, - } -} - -function insertHostNode( - child: HostNode, - parent: HostNode, - anchor: HostNode | null = null, -) { - child.parent = parent - const anchorIndex = anchor ? parent.children.indexOf(anchor) : -1 - - if (anchorIndex === -1) { - parent.children.push(child) - } else { - parent.children.splice(anchorIndex, 0, child) - } -} - -const memoryRenderer = createRenderer({ - patchProp(element, key, _previousValue, nextValue) { - element.props[key] = nextValue - }, - insert: insertHostNode, - remove(child) { - if (!child.parent) { - return - } - - const index = child.parent.children.indexOf(child) - if (index !== -1) { - child.parent.children.splice(index, 1) - } - child.parent = null - }, - createElement(type) { - return createHostNode(type) - }, - createText(text) { - return createHostNode('#text', text) - }, - createComment(text) { - return createHostNode('#comment', text) - }, - setText(node, text) { - node.text = text - }, - setElementText(node, text) { - node.text = text - node.children = [] - }, - parentNode(node) { - return node.parent - }, - nextSibling(node) { - if (!node.parent) { - return null - } - const index = node.parent.children.indexOf(node) - return node.parent.children[index + 1] ?? null - }, - querySelector() { - return null - }, - setScopeId() {}, - cloneNode(node) { - return { - ...node, - children: [...node.children], - parent: null, - props: { ...node.props }, - } - }, - insertStaticContent(content, parent, anchor) { - const node = createHostNode('#static', content) - insertHostNode(node, parent, anchor) - return [node, node] - }, -}) - -function mount(component: Component) { - const root = createHostNode('#root') - const app = memoryRenderer.createApp(component) - app.mount(root) - - return { - root, - unmount: () => app.unmount(), - } -} - -function readText(node: HostNode): string { - return node.text + node.children.map(readText).join('') -} +afterEach(cleanup) describe('FlexRender', () => { - test('renders primitives, callbacks, VNodes, and component objects', () => { - const props = { value: 'Ada' } - const renderCallback = vi.fn( - (context: typeof props) => `Hello ${context.value}`, - ) - - expect(flexRender('Plain text', props)).toBe('Plain text') - expect(flexRender(renderCallback, props)).toBe('Hello Ada') - expect(renderCallback).toHaveBeenCalledWith(props) - - const existingVNode = h('strong', 'Existing') - expect(flexRender(() => existingVNode, props)).toBe(existingVNode) - - const NameComponent = defineComponent({ - props: { - value: { - type: String, - required: true, - }, - }, - setup(componentProps) { - return () => h('span', `Name: ${componentProps.value}`) - }, - }) - const componentVNode = flexRender(NameComponent, props) - - expect(isVNode(componentVNode)).toBe(true) - expect(componentVNode.type).toBe(NameComponent) - expect(componentVNode.props).toMatchObject(props) - }) - test('supports cell modes, header/footer shorthand, and legacy props', () => { const normalContext = { value: 'Normal' } const aggregatedContext = { value: 'Aggregated' } @@ -169,7 +32,7 @@ describe('FlexRender', () => { const Root = defineComponent({ setup() { return () => - h('section', [ + h('section', { 'data-testid': 'flex-render-output' }, [ h(FlexRender, { cell: { column: { @@ -231,9 +94,9 @@ describe('FlexRender', () => { }, }) - const mounted = mount(Root) + render(Root) - expect(readText(mounted.root)).toBe( + expect(screen.getByTestId('flex-render-output').textContent).toBe( 'cell:Normalsum:Aggregatedheader:Titlefooter:Totallegacy:Legacy', ) expect(normalCellRenderer).toHaveBeenCalledOnce() @@ -242,8 +105,6 @@ describe('FlexRender', () => { expect(placeholderRenderer).not.toHaveBeenCalled() expect(headerRenderer).toHaveBeenCalledWith(headerContext) expect(footerRenderer).toHaveBeenCalledWith(footerContext) - - mounted.unmount() }) }) @@ -252,17 +113,20 @@ describe('createTableHook', () => { const TableBadge = defineComponent({ setup() { - return () => h('span', 'table-component') + return () => + h('span', { 'data-testid': 'table-component' }, 'table-component') }, }) const CellBadge = defineComponent({ setup() { - return () => h('span', 'cell-component') + return () => + h('span', { 'data-testid': 'cell-component' }, 'cell-component') }, }) const HeaderBadge = defineComponent({ setup() { - return () => h('span', 'header-component') + return () => + h('span', { 'data-testid': 'header-component' }, 'header-component') }, }) @@ -291,6 +155,7 @@ describe('createTableHook', () => { let originalCell: unknown let originalHeader: unknown let originalFooter: unknown + let firstRowId: string | undefined const TableConsumer = defineComponent({ setup() { @@ -339,50 +204,63 @@ describe('createTableHook', () => { originalCell = cell originalHeader = header originalFooter = footer + firstRowId = row.id return () => - h(table.AppTable, null, { - default: () => [ - h(TableConsumer), - h( - table.AppCell, - { cell }, - { - default: () => h(CellConsumer), - }, - ), - h( - table.AppHeader, - { header }, - { - default: () => h(HeaderConsumer), - }, - ), - h( - table.AppFooter, - { header: footer }, - { - default: () => h(FooterConsumer), - }, - ), - ], - }) + h( + 'main', + { 'data-testid': 'table-hook-output' }, + h(table.AppTable, null, { + default: () => [ + h(TableConsumer), + h( + table.AppCell, + { cell }, + { + default: () => h(CellConsumer), + }, + ), + h( + table.AppHeader, + { header }, + { + default: () => h(HeaderConsumer), + }, + ), + h( + table.AppFooter, + { header: footer }, + { + default: () => h(FooterConsumer), + }, + ), + ], + }), + ) }, }) - const mounted = mount(Root) + render(Root) expect(hook.appFeatures).toBe(stockFeatures) + expect(firstRowId).toBe('row-1') expect(tableContextCaptor).toHaveBeenCalledWith(createdTable) expect(cellContextCaptor).toHaveBeenCalledWith(originalCell) expect(headerContextCaptor).toHaveBeenCalledWith(originalHeader) expect(footerContextCaptor).toHaveBeenCalledWith(originalFooter) - expect(readText(mounted.root)).toBe( + expect(screen.getByTestId('table-component').textContent).toBe( + 'table-component', + ) + expect(screen.getByTestId('cell-component').textContent).toBe( + 'cell-component', + ) + expect(screen.getByTestId('header-component').textContent).toBe( + 'header-component', + ) + expect(screen.getByTestId('table-hook-output').textContent).toBe( 'table-componentcell-componentcell:First' + 'header-componentheader:titlefooter:title', ) - - mounted.unmount() }) test.each([ @@ -398,9 +276,12 @@ describe('createTableHook', () => { }, }) - expect(() => mount(Consumer)).toThrowError( - new RegExp(`\\\`${name}\\\` must be used within`), - ) - warning.mockRestore() + try { + expect(() => render(Consumer)).toThrowError( + new RegExp(`\\\`${name}\\\` must be used within`), + ) + } finally { + warning.mockRestore() + } }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6ece96525..a9e73d8496 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13494,6 +13494,9 @@ importers: '@eslint-react/eslint-plugin': specifier: ^5.9.2 version: 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@types/react': specifier: 19.2.16 version: 19.2.16 @@ -13565,6 +13568,9 @@ importers: specifier: workspace:* version: link:../table-core devDependencies: + '@solidjs/testing-library': + specifier: ^0.8.10 + version: 0.8.10(solid-js@1.9.13) solid-js: specifier: ^1.9.13 version: 1.9.13 @@ -13659,6 +13665,9 @@ importers: specifier: workspace:* version: link:../table-core devDependencies: + '@testing-library/vue': + specifier: ^8.1.0 + version: 8.1.0(@vue/compiler-dom@3.5.38)(@vue/compiler-sfc@3.5.38)(@vue/server-renderer@3.5.38(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3)) '@vitejs/plugin-vue': specifier: ^6.0.7 version: 6.0.7(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0))(vue@3.5.38(typescript@6.0.3)) @@ -18855,6 +18864,16 @@ packages: peerDependencies: solid-js: '>=1.8.4' + '@solidjs/testing-library@0.8.10': + resolution: {integrity: sha512-qdeuIerwyq7oQTIrrKvV0aL9aFeuwTd86VYD3afdq5HYEwoox1OBTJy4y8A3TFZr8oAR0nujYgCzY/8wgHGfeQ==} + engines: {node: '>= 14'} + peerDependencies: + '@solidjs/router': '>=0.9.0' + solid-js: '>=1.0.0' + peerDependenciesMeta: + '@solidjs/router': + optional: true + '@spectrum-icons/ui@3.7.1': resolution: {integrity: sha512-veQymocUYo5OciXQajSailOdbWe+k6+2ehfF8D4d0V923D4xOUadtT253xXZ5vEQjPat6Kyp2WDKeQNjd7kL1w==} peerDependencies: @@ -19567,10 +19586,43 @@ packages: peerDependencies: vue: ^2.7.0 || ^3.0.0 + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/dom@9.3.4': + resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} + engines: {node: '>=14'} + '@testing-library/jest-dom@6.9.1': resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/vue@8.1.0': + resolution: {integrity: sha512-ls4RiHO1ta4mxqqajWRh8158uFObVrrtAPoxk7cIp4HrnQUj/ScKzqz53HxYpG3X6Zb7H2v+0eTGLSoy8HQ2nA==} + engines: {node: '>=14'} + peerDependencies: + '@vue/compiler-sfc': '>= 3' + vue: '>= 3' + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + '@ts-morph/common@0.22.0': resolution: {integrity: sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==} @@ -20119,6 +20171,16 @@ packages: '@vue/shared@3.5.38': resolution: {integrity: sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==} + '@vue/test-utils@2.4.11': + resolution: {integrity: sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==} + peerDependencies: + '@vue/compiler-dom': 3.x + '@vue/server-renderer': 3.x + vue: 3.x + peerDependenciesMeta: + '@vue/server-renderer': + optional: true + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -20548,6 +20610,12 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.1: resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} engines: {node: '>= 0.4'} @@ -21102,6 +21170,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + connect-history-api-fallback@2.0.0: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} engines: {node: '>=0.8'} @@ -21451,6 +21522,10 @@ packages: dedent-js@1.0.1: resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -21586,6 +21661,11 @@ packages: resolution: {integrity: sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA==} engines: {node: '>=0.8'} + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + editorconfig@3.0.2: resolution: {integrity: sha512-T0ix8GhtxyKVfUFEcvdNDt3YGqlwkFHbD4/5bgFUDgFmxhI/cSRAeJ87/Sz//Cq8Eam6JX/e23RkoFO71P7aAA==} engines: {node: '>=20'} @@ -21769,6 +21849,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} @@ -22743,6 +22826,10 @@ packages: resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} engines: {node: '>= 10'} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -23024,6 +23111,11 @@ packages: jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + js-string-escape@1.0.1: resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} engines: {node: '>= 0.8'} @@ -23709,6 +23801,11 @@ packages: resolution: {integrity: sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==} engines: {node: '>=6'} + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + nopt@9.0.0: resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} engines: {node: ^20.17.0 || >=22.9.0} @@ -23784,6 +23881,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -24229,6 +24330,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -24956,6 +25061,10 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -26312,8 +26421,78 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + js-cookie@3.0.8: + resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + vue-component-type-helpers@3.3.8: + resolution: {integrity: sha512-troqCMmQodQDqUqn63NQaFi+CDSclSe7sc8VEBFqf5GFLqmGR2Ph3P2WEC7qwpRVyEWsTi/aAr4vyOe/B1hU3g==} + snapshots: + '@one-ini/wasm@0.1.1': {} + + '@types/aria-query@5.0.4': {} + + abbrev@2.0.0: {} + + ansi-styles@5.2.0: {} + + commander@10.0.1: {} + + dequal@2.0.3: {} + + dom-accessibility-api@0.5.16: {} + + ini@1.3.8: {} + + js-cookie@3.0.8: {} + + lz-string@1.5.0: {} + + proto-list@1.2.4: {} + + react-is@17.0.2: {} + + vue-component-type-helpers@3.3.8: {} + '@adobe/css-tools@4.5.0': {} '@adobe/react-spectrum-ui@1.2.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': @@ -32291,6 +32470,11 @@ snapshots: dependencies: solid-js: 1.9.13 + '@solidjs/testing-library@0.8.10(solid-js@1.9.13)': + dependencies: + '@testing-library/dom': 10.4.1 + solid-js: 1.9.13 + '@spectrum-icons/ui@3.7.1(@adobe/react-spectrum@3.47.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@adobe/react-spectrum': 3.47.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -33156,6 +33340,28 @@ snapshots: '@tanstack/virtual-core': 3.17.3 vue: 3.5.38(typescript@6.0.3) + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/dom@9.3.4': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + '@testing-library/jest-dom@6.9.1': dependencies: '@adobe/css-tools': 4.5.0 @@ -33165,6 +33371,28 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@testing-library/vue@8.1.0(@vue/compiler-dom@3.5.38)(@vue/compiler-sfc@3.5.38)(@vue/server-renderer@3.5.38(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3))': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 9.3.4 + '@vue/test-utils': 2.4.11(@vue/compiler-dom@3.5.38)(@vue/server-renderer@3.5.38(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3)) + vue: 3.5.38(typescript@6.0.3) + optionalDependencies: + '@vue/compiler-sfc': 3.5.38 + transitivePeerDependencies: + - '@vue/compiler-dom' + - '@vue/server-renderer' + '@ts-morph/common@0.22.0': dependencies: fast-glob: 3.3.3 @@ -33880,6 +34108,15 @@ snapshots: '@vue/shared@3.5.38': {} + '@vue/test-utils@2.4.11(@vue/compiler-dom@3.5.38)(@vue/server-renderer@3.5.38(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3))': + dependencies: + '@vue/compiler-dom': 3.5.38 + js-beautify: 1.15.4 + vue: 3.5.38(typescript@6.0.3) + vue-component-type-helpers: 3.3.8 + optionalDependencies: + '@vue/server-renderer': 3.5.38(vue@3.5.38(typescript@6.0.3)) + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -34658,6 +34895,14 @@ snapshots: dependencies: tslib: 2.8.1 + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + aria-query@5.3.1: {} aria-query@5.3.2: {} @@ -35386,6 +35631,11 @@ snapshots: concat-map@0.0.1: {} + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + connect-history-api-fallback@2.0.0: {} consolidate@1.0.4(@babel/core@7.29.7)(handlebars@4.7.9)(lodash@4.18.1)(mustache@4.2.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(underscore@1.13.8): @@ -35612,6 +35862,27 @@ snapshots: dedent-js@1.0.1: {} + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.1 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -35729,6 +36000,13 @@ snapshots: errlop: 2.2.0 semver: 6.3.1 + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.9 + semver: 7.8.5 + editorconfig@3.0.2: dependencies: '@one-ini/wasm': 0.2.1 @@ -36027,6 +36305,18 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.9 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + es-module-lexer@2.1.0: {} es-object-atoms@1.1.1: @@ -37300,6 +37590,11 @@ snapshots: ipaddr.js@2.4.0: {} + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -37571,6 +37866,14 @@ snapshots: jose@6.2.3: {} + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.5.0 + js-cookie: 3.0.8 + nopt: 7.2.1 + js-string-escape@1.0.1: {} js-tokens@10.0.0: {} @@ -38308,6 +38611,10 @@ snapshots: node-watch@0.7.3: {} + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + nopt@9.0.0: dependencies: abbrev: 4.0.0 @@ -38503,6 +38810,11 @@ snapshots: object-inspect@1.13.4: {} + object-is@1.1.6: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + object-keys@1.1.1: {} object.assign@4.1.7: @@ -39090,6 +39402,12 @@ snapshots: prettier@3.8.4: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -40061,6 +40379,14 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} From b621dbdaf398a40023dd545de32555e35c3a6331 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Wed, 29 Jul 2026 17:27:47 +0200 Subject: [PATCH 07/13] test: add more adapters reactivity tests --- .../angular-table/tests/injectTable.test.ts | 64 +- packages/react-table/src/FlexRender.tsx | 10 +- .../tests/adapterReactivity.test.tsx | 572 ++---------------- .../tests/createTableHook.test.tsx | 80 +++ packages/react-table/tests/ssr.test.tsx | 54 +- packages/react-table/tests/useTable.test.tsx | 29 +- packages/solid-table/src/FlexRender.tsx | 2 +- .../tests/unit/adapterReactivity.test.ts | 27 +- .../tests/unit/createTable.test.ts | 78 +-- .../solid-table/tests/unit/flexRender.test.ts | 1 + .../solid-table/tests/unit/reactivity.test.ts | 220 ++----- .../solid-table/tests/unit/rendering.test.tsx | 76 ++- packages/vue-table/src/FlexRender.ts | 8 + packages/vue-table/src/createTableHook.ts | 9 +- .../tests/unit/adapter-lifecycle.test.ts | 33 +- .../vue-table/tests/unit/flexRender.test.ts | 10 + .../vue-table/tests/unit/rendering.test.ts | 127 +++- packages/vue-table/tests/unit/signals.test.ts | 32 - .../vue-table/tests/unit/useTable.test.ts | 245 +++----- 19 files changed, 628 insertions(+), 1049 deletions(-) diff --git a/packages/angular-table/tests/injectTable.test.ts b/packages/angular-table/tests/injectTable.test.ts index 00cff24908..aee244543d 100644 --- a/packages/angular-table/tests/injectTable.test.ts +++ b/packages/angular-table/tests/injectTable.test.ts @@ -1,4 +1,3 @@ -import { isProxy } from 'node:util/types' import { describe, expect, test, vi } from 'vitest' import { ChangeDetectionStrategy, @@ -8,16 +7,19 @@ import { signal, } from '@angular/core' import { TestBed } from '@angular/core/testing' +import { By } from '@angular/platform-browser' import { ColumnDef, createPaginatedRowModel, stockFeatures, } from '@tanstack/table-core' -import { RowModel, injectTable } from '../src' +import { injectTable } from '../src' import type { PaginationState } from '../src' describe('injectTable', () => { test('should support required signal inputs', async () => { + type Data = { id: string; title: string } + @Component({ selector: 'app-table', template: ``, @@ -25,27 +27,53 @@ describe('injectTable', () => { changeDetection: ChangeDetectionStrategy.OnPush, }) class TableComponent { - data = input.required>() + data = input.required>() table = injectTable(() => ({ data: this.data(), features: stockFeatures, columns: [], + getRowId: (row) => row.id, })) } @Component({ selector: 'app-root', imports: [TableComponent], - template: ` `, + template: ``, changeDetection: ChangeDetectionStrategy.OnPush, }) - class RootComponent {} + class RootComponent { + readonly data = signal>([{ id: '1', title: 'First' }]) + } const fixture = TestBed.createComponent(RootComponent) fixture.detectChanges() + await fixture.whenRenderingDone() + + const tableComponent = fixture.debugElement.query( + By.directive(TableComponent), + ).componentInstance as TableComponent + expect( + tableComponent.table.getRowModel().rows.map((row) => row.original), + ).toEqual([{ id: '1', title: 'First' }]) + TestBed.tick() + + fixture.componentInstance.data.set([ + { id: '1', title: 'Updated' }, + { id: '2', title: 'Second' }, + ]) + fixture.detectChanges() + TestBed.tick() await fixture.whenRenderingDone() + + expect( + tableComponent.table.getRowModel().rows.map((row) => row.original), + ).toEqual([ + { id: '1', title: 'Updated' }, + { id: '2', title: 'Second' }, + ]) }) describe('Proxy table', () => { @@ -64,12 +92,8 @@ describe('injectTable', () => { })), ) - test('table is proxy', () => { - expect(isProxy(table)).toBe(true) - }) - test('supports "in" operator', () => { - expect('_features' in table).toBe(true) + expect('atoms' in table).toBe(true) expect('options' in table).toBe(true) expect('notFound' in table).toBe(false) }) @@ -80,10 +104,7 @@ describe('injectTable', () => { }) test('Row model is reactive', () => { - const coreRowModelFn = - vi.fn<(model: RowModel) => void>() - const rowModelFn = - vi.fn<(model: RowModel) => void>() + const rowCounts = vi.fn<(count: number) => void>() const pagination = signal({ pageSize: 5, pageIndex: 0, @@ -112,8 +133,8 @@ describe('injectTable', () => { }, })) - effect(() => coreRowModelFn(table.getCoreRowModel())) - effect(() => rowModelFn(table.getRowModel())) + const initialCoreRowModel = table.getCoreRowModel() + effect(() => rowCounts(table.getRowModel().rows.length)) TestBed.tick() @@ -121,15 +142,8 @@ describe('injectTable', () => { TestBed.tick() - // TODO: pagination state update twice during first table construct - // optionsStore is a signal -> so if updated with state in queuemicrotask will trigger twice - expect(coreRowModelFn).toHaveBeenCalledTimes(2) - expect(coreRowModelFn.mock.calls[0]![0].rows.length).toEqual(10) - expect(coreRowModelFn.mock.calls[1]![0].rows.length).toEqual(10) - - expect(rowModelFn).toHaveBeenCalledTimes(2) - expect(rowModelFn.mock.calls[0]![0].rows.length).toEqual(5) - expect(rowModelFn.mock.calls[1]![0].rows.length).toEqual(3) + expect(rowCounts.mock.calls).toEqual([[5], [3]]) + expect(table.getCoreRowModel()).toBe(initialCoreRowModel) }) }) }) diff --git a/packages/react-table/src/FlexRender.tsx b/packages/react-table/src/FlexRender.tsx index d345d06cec..80662f348a 100644 --- a/packages/react-table/src/FlexRender.tsx +++ b/packages/react-table/src/FlexRender.tsx @@ -46,11 +46,11 @@ export function flexRender( Comp: Renderable, props: TProps, ): ReactNode | JSX.Element { - return !Comp ? null : isReactComponent(Comp) ? ( - - ) : ( - Comp - ) + if (Comp === null || Comp === undefined) { + return null + } + + return isReactComponent(Comp) ? : Comp } /** diff --git a/packages/react-table/tests/adapterReactivity.test.tsx b/packages/react-table/tests/adapterReactivity.test.tsx index beda8a17f2..658808482f 100644 --- a/packages/react-table/tests/adapterReactivity.test.tsx +++ b/packages/react-table/tests/adapterReactivity.test.tsx @@ -82,19 +82,6 @@ function unmount() { renderedView = undefined } -const MemoizedCellValueObserver = React.memo( - function MemoizedCellValueObserver({ - value, - capture, - }: { - value: unknown - capture: (value: unknown) => void - }) { - capture(value) - return null - }, -) - afterEach(() => { cleanup() renderedView = undefined @@ -104,56 +91,7 @@ afterEach(() => { // Adapter contract only: React/store ownership, subscriptions, lifecycle, and // option refreshes. Row-model algorithms remain covered by table-core. describe('React adapter reactivity and lifecycle', () => { - test('accepts required data props and updates row reads when the prop changes', async () => { - function TableHarness({ data }: { data: Array }) { - const table = useTable({ - data, - features: stockFeatures, - columns, - getRowId: (row) => row.id, - autoResetPageIndex: false, - }) - - return ( - <> - - {table - .getRowModel() - .rows.map((row) => row.id) - .join(',')} - - - {table - .getRowModel() - .rows.map((row) => row.getValue('title')) - .join(',')} - - - ) - } - - render() - - expect(text('row-ids')).toBe('1') - expect(text('row-titles')).toBe('Title') - - await act(async () => { - renderedView!.rerender( - , - ) - await Promise.resolve() - }) - - expect(text('row-ids')).toBe('1,2') - expect(text('row-titles')).toBe('Updated,Added') - }) - - test('exposes the complete table surface through property checks and enumeration', () => { + test('exposes React adapter APIs through the returned table surface', () => { function TableHarness() { const table = useTable( { @@ -168,13 +106,12 @@ describe('React adapter reactivity and lifecycle', () => { return ( {JSON.stringify({ - hasFeatures: '_features' in table, hasOptions: 'options' in table, hasState: 'state' in table, hasRowModel: 'getRowModel' in table, - hasNotFound: 'notFound' in table, + hasSubscribe: 'Subscribe' in table, + hasFlexRender: 'FlexRender' in table, keys: Object.keys(table), - row: table.getRow('1').original, })} ) @@ -183,28 +120,30 @@ describe('React adapter reactivity and lifecycle', () => { render() const surface = JSON.parse(text('table-surface')) as { - hasFeatures: boolean hasOptions: boolean hasState: boolean hasRowModel: boolean - hasNotFound: boolean + hasSubscribe: boolean + hasFlexRender: boolean keys: Array - row: Data } - expect(surface).toMatchObject({ - hasFeatures: true, + expect(surface).toEqual({ hasOptions: true, hasState: true, hasRowModel: true, - hasNotFound: false, - row: { - id: '1', - title: 'Title', - }, + hasSubscribe: true, + hasFlexRender: true, + keys: expect.any(Array), }) expect(surface.keys).toEqual( - expect.arrayContaining(['_features', 'options', 'state', 'getRowModel']), + expect.arrayContaining([ + 'options', + 'state', + 'Subscribe', + 'FlexRender', + 'getRowModel', + ]), ) }) @@ -273,114 +212,8 @@ describe('React adapter reactivity and lifecycle', () => { expect(rowModelCaptor.mock.calls[1]![0].rows).toHaveLength(3) }) - test('row, cell, and column reads update only for their subscribed inputs', () => { - const isSelectedCaptor = vi.fn<(value: boolean) => void>() - const idValueCaptor = vi.fn<(value: unknown) => void>() - const memoizedIdValueCaptor = vi.fn<(value: unknown) => void>() - const titleValueCaptor = vi.fn<(value: unknown) => void>() - const columnIsVisibleCaptor = vi.fn<(value: boolean) => void>() - - function TableHarness() { - const [data, setData] = React.useState>([ - { id: '1', title: 'Title' }, - ]) - const table = useTable( - { - data, - features: stockFeatures, - columns, - getRowId: (row) => row.id, - }, - () => null, - ) - - const cells = table.getRow('1').getAllCells() - idValueCaptor(cells[0]!.getValue()) - titleValueCaptor(cells[1]!.getValue()) - - return ( - <> - - Boolean(selection['1'])} - > - {() => { - isSelectedCaptor(table.getRow('1').getIsSelected()) - return null - }} - - visibility.id !== false} - > - {() => { - columnIsVisibleCaptor(table.getColumn('id')!.getIsVisible()) - return null - }} - - ) } render() - expect(text('core-row-ids')).toBe('0,1,2,3,4,5,6,7,8,9') - expect(text('page-row-ids')).toBe('0,1,2,3,4') + expect(text('Core row IDs')).toBe('0,1,2,3,4,5,6,7,8,9') + expect(text('Page row IDs')).toBe('0,1,2,3,4') act(() => { - click('set-page-size-3') + click('Set page size to 3') }) - expect(text('core-row-ids')).toBe('0,1,2,3,4,5,6,7,8,9') - expect(text('page-row-ids')).toBe('0,1,2') + expect(text('Core row IDs')).toBe('0,1,2,3,4,5,6,7,8,9') + expect(text('Page row IDs')).toBe('0,1,2') expect(coreRowModelCaptor).toHaveBeenCalledTimes(2) expect(rowModelCaptor).toHaveBeenCalledTimes(2) expect(coreRowModelCaptor.mock.calls[0]![0]).toBe( @@ -236,7 +235,7 @@ describe('React adapter reactivity and lifecycle', () => { selector={(selection) => Boolean(selection['1'])} > {(selected) => ( - + {String(selected)} )} @@ -244,39 +243,38 @@ describe('React adapter reactivity and lifecycle', () => { {(selection) => { return ( - + {JSON.stringify(selection)} ) }} - ) } render() - expect(text('external-row-selected')).toBe('false') - expect(text('external-row-selection')).toBe('{}') + expect(text('External row selected')).toBe('false') + expect(text('External row selection')).toBe('{}') act(() => { rowSelectionAtom.set({ 1: true }) }) - expect(text('external-row-selected')).toBe('true') - expect(text('external-row-selection')).toBe('{"1":true}') + expect(text('External row selected')).toBe('true') + expect(text('External row selection')).toBe('{"1":true}') act(() => { - click('toggle-external-row') + click('Toggle external row') }) expect(rowSelectionAtom.get()).toEqual({}) - expect(text('external-row-selected')).toBe('false') - expect(text('external-row-selection')).toBe('{}') + expect(text('External row selected')).toBe('false') + expect(text('External row selection')).toBe('{}') }) test('stops root and isolated React observers after unmount', () => { @@ -320,7 +318,7 @@ describe('React adapter reactivity and lifecycle', () => { {(snapshot) => { isolatedStoreCaptor(snapshot) return ( - + {String(snapshot.selected)} ) @@ -335,7 +333,7 @@ describe('React adapter reactivity and lifecycle', () => { rowSelectionAtom.set({ 1: true }) }) - expect(text('lifecycle-selection')).toBe('true') + expect(text('Lifecycle selection')).toBe('true') unmount() @@ -408,48 +406,48 @@ describe('React adapter reactivity and lifecycle', () => { return ( <> - {row.id} - + {row.id} + {table .getAllLeafColumns() .map((column) => column.id) .join(',')} - + {table .getAllLeafColumns() .map((column) => column.columnDef.header) .join(',')} - + {String(cells[0]!.getValue())} - + {String(cells[1]!.renderValue())} - ) } render() - expect(text('dynamic-row-id')).toBe('1') - expect(text('dynamic-column-ids')).toBe('title,missing') - expect(text('dynamic-headers')).toBe('Title,Missing') - expect(text('dynamic-cell-value')).toBe('Alpha') - expect(text('dynamic-fallback')).toBe('initial fallback') + expect(text('Dynamic row ID')).toBe('1') + expect(text('Dynamic column IDs')).toBe('title,missing') + expect(text('Dynamic headers')).toBe('Title,Missing') + expect(text('Dynamic cell value')).toBe('Alpha') + expect(text('Dynamic fallback')).toBe('initial fallback') await act(async () => { - click('refresh-options') + click('Refresh options') await Promise.resolve() }) - expect(text('dynamic-row-id')).toBe('alternate-2') - expect(text('dynamic-column-ids')).toBe('status,missing') - expect(text('dynamic-headers')).toBe('Status,Absent') - expect(text('dynamic-cell-value')).toBe('ready') - expect(text('dynamic-fallback')).toBe('updated fallback') + expect(text('Dynamic row ID')).toBe('alternate-2') + expect(text('Dynamic column IDs')).toBe('status,missing') + expect(text('Dynamic headers')).toBe('Status,Absent') + expect(text('Dynamic cell value')).toBe('ready') + expect(text('Dynamic fallback')).toBe('updated fallback') expect(renderCaptor.mock.calls).toEqual([[0], [1]]) }) }) diff --git a/packages/react-table/tests/createTableHook.test.tsx b/packages/react-table/tests/createTableHook.test.tsx index eac7a5f7a0..d5f7fe966e 100644 --- a/packages/react-table/tests/createTableHook.test.tsx +++ b/packages/react-table/tests/createTableHook.test.tsx @@ -20,7 +20,7 @@ function RowCount() { const table = contexts.useTableContext() return ( - + {table.getRowModel().rows.length} ) @@ -29,13 +29,13 @@ function RowCount() { function NameCell() { const cell = contexts.useCellContext() - return {cell.getValue().toUpperCase()} + return {cell.getValue().toUpperCase()} } function NameHeader() { const header = contexts.useHeaderContext() - return Header {header.column.id} + return Header {header.column.id} } const appTable = createTableHook({ @@ -57,12 +57,12 @@ const columns = columnHelper.columns([ }), ]) -function text(testId: string) { - return screen.getByTestId(testId).textContent +function text(name: string) { + return screen.getByRole('status', { name }).textContent } -function click(action: string) { - fireEvent.click(screen.getByTestId(action)) +function click(name: string) { + fireEvent.click(screen.getByRole('button', { name })) } afterEach(() => { @@ -91,7 +91,7 @@ describe('createTableHook runtime', () => { {(selectedCount) => ( <> - {selectedCount} + {selectedCount} {(extendedHeader) => } @@ -102,7 +102,7 @@ describe('createTableHook runtime', () => { {(extendedCell, selected) => ( <> - + {String(selected)} @@ -111,10 +111,9 @@ describe('createTableHook runtime', () => { {(extendedFooter) => } - )} @@ -123,19 +122,19 @@ describe('createTableHook runtime', () => { render() - expect(text('bound-row-count')).toBe('1') - expect(text('bound-header')).toBe('Header name') - expect(text('bound-cell')).toBe('ADA') + expect(text('Bound row count')).toBe('1') + expect(screen.getByText('Header name')).toBeTruthy() + expect(screen.getByText('ADA')).toBeTruthy() expect(screen.getByText('Name footer')).toBeTruthy() - expect(text('app-table-selection')).toBe('0') - expect(text('app-cell-selection')).toBe('false') + expect(text('App table selection')).toBe('0') + expect(text('App cell selection')).toBe('false') act(() => { - click('select-app-row') + click('Select app row') }) - expect(text('app-table-selection')).toBe('1') - expect(text('app-cell-selection')).toBe('true') + expect(text('App table selection')).toBe('1') + expect(text('App cell selection')).toBe('true') }) test('keeps App wrappers mounted while their contexts receive new table objects', () => { @@ -153,12 +152,13 @@ describe('createTableHook runtime', () => { return ( <> + setDraft(event.target.value)} /> - {cell.getValue()} + {cell.getValue()} ) } @@ -191,29 +191,26 @@ describe('createTableHook runtime', () => { )} - ) } render() - const input = screen.getByTestId('stateful-cell-input') + const input = screen.getByRole('textbox', { name: 'Cell draft' }) fireEvent.change(input, { target: { value: 'edited' } }) - expect(text('latest-cell-value')).toBe('Ada') + expect(text('Latest cell value')).toBe('Ada') expect(mountCaptor).toHaveBeenCalledOnce() act(() => { - click('refresh-app-table') + click('Refresh app table') }) - expect(screen.getByTestId('stateful-cell-input')).toBe(input) + expect(screen.getByRole('textbox', { name: 'Cell draft' })).toBe(input) expect((input as HTMLInputElement).value).toBe('edited') - expect(text('latest-cell-value')).toBe('Grace') + expect(text('Latest cell value')).toBe('Grace') expect(mountCaptor).toHaveBeenCalledOnce() expect(unmountCaptor).not.toHaveBeenCalled() }) diff --git a/packages/react-table/tests/useTable.test.tsx b/packages/react-table/tests/useTable.test.tsx index 7c11e7f8e3..6505e12508 100644 --- a/packages/react-table/tests/useTable.test.tsx +++ b/packages/react-table/tests/useTable.test.tsx @@ -44,7 +44,7 @@ const IsolatedPaginationSubscriber = React.memo( return ( state.pagination.pageIndex}> {(pageIndex) => ( - {pageIndex} + {pageIndex} )} ) @@ -55,12 +55,12 @@ function render(element: React.ReactNode) { return testingLibraryRender(element) } -function text(testId: string) { - return screen.getByTestId(testId).textContent +function text(name: string) { + return screen.getByRole('status', { name }).textContent } -function click(testId: string) { - fireEvent.click(screen.getByTestId(testId)) +function click(name: string) { + fireEvent.click(screen.getByRole('button', { name })) } afterEach(() => { @@ -96,9 +96,7 @@ describe('useTable state subscriptions', () => { ) return ( - - {table.state} - + {table.state} ) } @@ -113,7 +111,7 @@ describe('useTable state subscriptions', () => { }) }) - expect(text('controlled-source-page-index')).toBe('1') + expect(text('Controlled source page index')).toBe('1') expect(harnessRenderCount).toBe(2) }) @@ -143,12 +141,11 @@ describe('useTable state subscriptions', () => { return ( <> - + + {table.state.pageIndex} @@ -157,13 +154,13 @@ describe('useTable state subscriptions', () => { render() - const pageIndex = () => text('ownership-page-index') + const pageIndex = () => text('Ownership page index') expect(pageIndex()).toBe('5') expect(harnessRenderCount).toBe(1) act(() => { - click('toggle-ownership') + click('Toggle ownership') }) // Releasing control exposes the last committed controlled value in the @@ -172,21 +169,21 @@ describe('useTable state subscriptions', () => { expect(harnessRenderCount).toBe(2) act(() => { - click('next-page') + click('Next page') }) expect(pageIndex()).toBe('6') expect(harnessRenderCount).toBe(3) act(() => { - click('toggle-ownership') + click('Toggle ownership') }) expect(pageIndex()).toBe('5') expect(harnessRenderCount).toBe(4) act(() => { - click('next-page') + click('Next page') }) // The controlled prop owns the slice again; base writes are not observed @@ -215,30 +212,27 @@ describe('useTable state subscriptions', () => { return ( <> - + ) } render() - const isolatedPageIndex = () => text('isolated-page-index') + const isolatedPageIndex = () => text('Isolated page index') expect(isolatedPageIndex()).toBe('5') act(() => { - click('change-base') + click('Change base') }) // The controlled value still wins even though the internal base moved. expect(isolatedPageIndex()).toBe('5') act(() => { - click('release-control') + click('Release control') }) expect(isolatedPageIndex()).toBe('6') @@ -268,8 +262,8 @@ describe('useTable state subscriptions', () => { return ( <> - {table.state} - + {table.state} + {table.getRowModel().rows[0]?.original.id} @@ -296,8 +290,8 @@ describe('useTable state subscriptions', () => { }) }) - expect(text('external-atom-page-index')).toBe('1') - expect(text('external-atom-first-row')).toBe('20') + expect(text('External atom page index')).toBe('1') + expect(text('External atom first row')).toBe('20') expect(harnessRenderCount).toBe(2) }) @@ -337,22 +331,19 @@ describe('useTable state subscriptions', () => { return ( <> - {table.state} - + {table.state} + {table.getRowModel().rows[0]?.original.id} - ) } render() - expect(text('mixed-page-index')).toBe('3') - expect(text('mixed-first-row')).toBe('30') + expect(text('Mixed page index')).toBe('3') + expect(text('Mixed first row')).toBe('30') expect(harnessRenderCount).toBe(1) act(() => { @@ -362,7 +353,7 @@ describe('useTable state subscriptions', () => { }) }) - expect(text('mixed-page-index')).toBe('3') + expect(text('Mixed page index')).toBe('3') expect(harnessRenderCount).toBe(2) act(() => { @@ -381,20 +372,20 @@ describe('useTable state subscriptions', () => { }) }) - expect(text('mixed-page-index')).toBe('4') - expect(text('mixed-first-row')).toBe('40') + expect(text('Mixed page index')).toBe('4') + expect(text('Mixed first row')).toBe('40') expect(harnessRenderCount).toBe(3) act(() => { - click('mixed-next-page') + click('Next page') }) expect(paginationAtom.get()).toEqual({ pageIndex: 5, pageSize: 10, }) - expect(text('mixed-page-index')).toBe('5') - expect(text('mixed-first-row')).toBe('50') + expect(text('Mixed page index')).toBe('5') + expect(text('Mixed first row')).toBe('50') expect(harnessRenderCount).toBe(4) }) @@ -434,11 +425,10 @@ describe('useTable state subscriptions', () => { return ( <> - + {table.state.pageIndex} state.pagination.pageIndex}> {(pageIndex) => ( - {pageIndex} + {pageIndex} )} @@ -457,19 +447,19 @@ describe('useTable state subscriptions', () => { render() - expect(text('committed-page-index')).toBe('0') + expect(text('Committed page index')).toBe('0') const storeNotifications: Array = [] const subscription = committedTable!.store.subscribe((state) => { storeNotifications.push(state.pagination.pageIndex) }) act(() => { - click('suspend-render') + click('Suspend next render') }) // The previous UI is still committed while the transition is suspended. - expect(text('committed-page-index')).toBe('0') - expect(text('isolated-page-index')).toBe('0') + expect(text('Committed page index')).toBe('0') + expect(text('Isolated page index')).toBe('0') const committedBasePageIndex = committedTable!.baseAtoms.pagination.get().pageIndex @@ -497,12 +487,14 @@ describe('useTable state subscriptions', () => { return ( <> - + {table.state.pageIndex} state.pagination.pageIndex}> {(pageIndex) => ( - {pageIndex} + + {pageIndex} + )} @@ -526,8 +518,8 @@ describe('useTable state subscriptions', () => { expect(latestTable!.atoms.pagination.get()).toEqual(pagination) expect(latestTable!.store.get().pagination).toEqual(pagination) - expect(text('imperative-page-index')).toBe('3') - expect(text('imperative-subscriber')).toBe('3') + expect(text('Imperative page index')).toBe('3') + expect(text('Imperative subscriber page index')).toBe('3') expect(notifications).toEqual([3]) subscription.unsubscribe() @@ -575,23 +567,18 @@ describe('useTable state subscriptions', () => { return ( <> - + {table.state.pagination.pageIndex} - + {table.atoms.pagination.get().pageIndex} state.pagination.pageIndex}> {(pageIndex) => ( - {pageIndex} + {pageIndex} )} - + ) } @@ -604,22 +591,22 @@ describe('useTable state subscriptions', () => { , ) - const pageIndex = () => text('page-index') - const atomPageIndex = () => text('atom-page-index') - const subscribedPageIndex = () => text('subscribed-page-index') + const pageIndex = () => text('Selected page index') + const atomPageIndex = () => text('Atom page index') + const subscribedPageIndex = () => text('Subscribed page index') expect(pageIndex()).toBe('0') expect(atomPageIndex()).toBe('0') expect(subscribedPageIndex()).toBe('0') act(() => { - click('controlled-next-page') + click('Next page') }) expect(pageIndex()).toBe('1') expect(atomPageIndex()).toBe('1') expect(subscribedPageIndex()).toBe('1') act(() => { - click('controlled-next-page') + click('Next page') }) expect(pageIndex()).toBe('2') expect(atomPageIndex()).toBe('2') @@ -684,12 +671,11 @@ describe('useTable state subscriptions', () => { return ( <> - + {pagination.pageIndex} - + + ) } render() - const selectedPageIndex = () => text('selected-page-index') - const subscribedPageSize = () => text('subscribed-page-size') + const selectedPageIndex = () => text('Selected page index') + const subscribedPageSize = () => text('Subscribed page size') expect(harnessRenderCount).toBe(1) expect(selectedPageIndex()).toBe('0') expect(subscribedPageSize()).toBe('10') act(() => { - click('resize-page') + click('Resize page') }) expect(harnessRenderCount).toBe(1) @@ -781,7 +760,7 @@ describe('useTable state subscriptions', () => { expect(subscribedPageSize()).toBe('20') act(() => { - click('next-page') + click('Next page') }) expect(harnessRenderCount).toBe(2) @@ -813,25 +792,17 @@ describe('useTable state subscriptions', () => { return ( <> - {tick} - + {tick} + {table.state.pagination.pageIndex} - + {table.atoms.pagination.get().pageIndex} - - + ) } @@ -843,19 +814,19 @@ describe('useTable state subscriptions', () => { expect(harnessRenderCount).toBe(1) act(() => { - click('unrelated-update') + click('Unrelated update') }) - expect(text('unrelated-tick')).toBe('1') - expect(text('recreated-page-index')).toBe('0') + expect(text('Unrelated tick')).toBe('1') + expect(text('Recreated page index')).toBe('0') expect(harnessRenderCount).toBe(2) act(() => { - click('controlled-next-page') + click('Next page') }) - expect(text('recreated-page-index')).toBe('1') - expect(text('recreated-atom-page-index')).toBe('1') + expect(text('Recreated page index')).toBe('1') + expect(text('Recreated atom page index')).toBe('1') expect(harnessRenderCount).toBe(3) const errors = consoleError.mock.calls.flat().map(String).join('\n') @@ -878,16 +849,11 @@ describe('useTable state subscriptions', () => { return ( <> - {table.state.pageIndex} - + {table.state.pageIndex} + {JSON.stringify(table.state)} - + ) } @@ -896,18 +862,18 @@ describe('useTable state subscriptions', () => { render() - const pageIndex = () => text('page-index') - const selectedState = () => text('selected-state') + const pageIndex = () => text('Page index') + const selectedState = () => text('Selected state') expect(selectedState()).toBe('{"pageIndex":0}') expect(pageIndex()).toBe('0') act(() => { - click('uncontrolled-next-page') + click('Next page') }) expect(pageIndex()).toBe('1') act(() => { - click('uncontrolled-next-page') + click('Next page') }) expect(pageIndex()).toBe('2') diff --git a/packages/solid-table/tests/unit/adapterReactivity.test.ts b/packages/solid-table/tests/unit/adapterReactivity.test.ts index 9464ff2c50..a07f574d7b 100644 --- a/packages/solid-table/tests/unit/adapterReactivity.test.ts +++ b/packages/solid-table/tests/unit/adapterReactivity.test.ts @@ -9,7 +9,6 @@ import { import { createAtom } from '@tanstack/store' import { stockFeatures } from '@tanstack/table-core' import { createTable } from '../../src/createTable' -import type { Observer } from '@tanstack/store' import type { ColumnDef, RowSelectionState } from '@tanstack/table-core' describe('Solid adapter lifecycle and option ownership', () => { diff --git a/packages/solid-table/tests/unit/rendering.test.tsx b/packages/solid-table/tests/unit/rendering.test.tsx index 2c5589db69..8eada82546 100644 --- a/packages/solid-table/tests/unit/rendering.test.tsx +++ b/packages/solid-table/tests/unit/rendering.test.tsx @@ -44,9 +44,9 @@ describe('FlexRender', () => { } return ( -
+ -
+
) } @@ -62,12 +62,12 @@ describe('FlexRender', () => { return ( <> -
+ -
-
+ + -
+
) } @@ -82,13 +82,21 @@ describe('FlexRender', () => { )) - expect(screen.getByTestId('cell-normal').textContent).toBe('cell:Ada') - expect(screen.getByTestId('cell-aggregate').textContent).toBe( - 'aggregate:Ada', + expect( + screen.getByRole('status', { name: 'normal cell' }).textContent, + ).toBe('cell:Ada') + expect( + screen.getByRole('status', { name: 'aggregate cell' }).textContent, + ).toBe('aggregate:Ada') + expect( + screen.getByRole('status', { name: 'placeholder cell' }).textContent, + ).toBe('') + expect(screen.getByRole('status', { name: 'header' }).textContent).toBe( + 'header:name', + ) + expect(screen.getByRole('status', { name: 'footer' }).textContent).toBe( + 'footer:name', ) - expect(screen.getByTestId('cell-placeholder').textContent).toBe('') - expect(screen.getByTestId('header').textContent).toBe('header:name') - expect(screen.getByTestId('footer').textContent).toBe('footer:name') }) test('updates when a truthy cell prop is replaced with a new instance', () => { @@ -107,24 +115,27 @@ describe('FlexRender', () => { return ( <> -
+ -
- ) } render(() => ) - expect(screen.getByTestId('reactive-cell').textContent).toBe('cell:Ada') + expect( + screen.getByRole('status', { name: 'rendered cell' }).textContent, + ).toBe('cell:Ada') - fireEvent.click(screen.getByTestId('replace-cell')) + fireEvent.click(screen.getByRole('button', { name: 'Replace cell' })) - expect(screen.getByTestId('reactive-cell').textContent).toBe('cell:Grace') + expect( + screen.getByRole('status', { name: 'rendered cell' }).textContent, + ).toBe('cell:Grace') }) }) @@ -140,36 +151,41 @@ describe('table.Subscribe', () => { return ( <> - + {(atoms) => ( {String(Boolean(atoms.rowSelection.get()['1']))} )} - ) } render(() => ) - expect(screen.getByTestId('subscribed-selection').textContent).toBe('false') + expect( + screen.getByRole('status', { name: 'subscribed selection' }).textContent, + ).toBe('false') - fireEvent.click(screen.getByTestId('select-subscribed-row')) + fireEvent.click( + screen.getByRole('button', { name: 'Select subscribed row' }), + ) - expect(screen.getByTestId('subscribed-selection').textContent).toBe('true') + expect( + screen.getByRole('status', { name: 'subscribed selection' }).textContent, + ).toBe('true') }) }) describe('createTableHook runtime', () => { type Data = { id: string; name: string } - const TableBadge = () => table-badge - const CellBadge = () => cell-badge - const HeaderBadge = () => header-badge + const TableBadge = () => table-badge + const CellBadge = () => cell-badge + const HeaderBadge = () => header-badge function createTestHook() { return createTableHook({ @@ -203,9 +219,9 @@ describe('createTableHook runtime', () => { return ( - + {String(table.getRow('1').getCanSelect())} - + ) @@ -213,8 +229,10 @@ describe('createTableHook runtime', () => { render(() => ) - expect(screen.getByTestId('can-select').textContent).toBe('true') - expect(screen.getByTestId('table-badge').textContent).toBe('table-badge') + expect( + screen.getByRole('status', { name: 'row can be selected' }).textContent, + ).toBe('true') + expect(screen.getByText('table-badge').textContent).toBe('table-badge') expect(hook.appFeatures).toBe(stockFeatures) expect(tableRef?.TableBadge).toBe(TableBadge) expect(tableRef?.FlexRender).toBe(FlexRender) @@ -253,9 +271,9 @@ describe('createTableHook runtime', () => { function TableContextProbe() { tableFromContext = hook.useTableContext() return ( - + {String(tableFromContext === table)} - +
) } @@ -268,16 +286,16 @@ describe('createTableHook runtime', () => { cellFromContext = hook.useCellContext() return ( <> - + {String(cellFromContext === cell)} - - +
+ {String(value.CellBadge === CellBadge)} - + - + - +
) }} @@ -287,16 +305,16 @@ describe('createTableHook runtime', () => { headerFromContext = hook.useHeaderContext() return ( <> - + {String(headerFromContext === header)} - - +
+ {String(value.HeaderBadge === HeaderBadge)} - + - + - +
) }} @@ -306,12 +324,12 @@ describe('createTableHook runtime', () => { footerFromContext = hook.useHeaderContext() return ( <> - + {String(footerFromContext === footer)} - - +
+ - + ) }} @@ -322,18 +340,40 @@ describe('createTableHook runtime', () => { render(() => ) - expect(screen.getByTestId('table-context').textContent).toBe('true') - expect(screen.getByTestId('cell-context').textContent).toBe('true') - expect(screen.getByTestId('header-context').textContent).toBe('true') - expect(screen.getByTestId('footer-context').textContent).toBe('true') - expect(screen.getByTestId('cell-bound').textContent).toBe('true') - expect(screen.getByTestId('header-bound').textContent).toBe('true') - expect(screen.getByTestId('table-badge').textContent).toBe('table-badge') - expect(screen.getByTestId('cell-badge').textContent).toBe('cell-badge') - expect(screen.getByTestId('header-badge').textContent).toBe('header-badge') - expect(screen.getByTestId('cell-render').textContent).toBe('cell:Ada') - expect(screen.getByTestId('header-render').textContent).toBe('header:name') - expect(screen.getByTestId('footer-render').textContent).toBe('footer:name') + expect( + screen.getByRole('status', { name: 'table context matches' }).textContent, + ).toBe('true') + expect( + screen.getByRole('status', { name: 'cell context matches' }).textContent, + ).toBe('true') + expect( + screen.getByRole('status', { name: 'header context matches' }) + .textContent, + ).toBe('true') + expect( + screen.getByRole('status', { name: 'footer context matches' }) + .textContent, + ).toBe('true') + expect( + screen.getByRole('status', { name: 'cell component is bound' }) + .textContent, + ).toBe('true') + expect( + screen.getByRole('status', { name: 'header component is bound' }) + .textContent, + ).toBe('true') + expect(screen.getByText('table-badge').textContent).toBe('table-badge') + expect(screen.getByText('cell-badge').textContent).toBe('cell-badge') + expect(screen.getByText('header-badge').textContent).toBe('header-badge') + expect( + screen.getByRole('status', { name: 'rendered cell' }).textContent, + ).toBe('cell:Ada') + expect( + screen.getByRole('status', { name: 'rendered header' }).textContent, + ).toBe('header:name') + expect( + screen.getByRole('status', { name: 'rendered footer' }).textContent, + ).toBe('footer:name') expect(tableFromContext).toBe(tableRef) }) diff --git a/packages/vue-table/tests/unit/rendering.test.ts b/packages/vue-table/tests/unit/rendering.test.ts index 1ace1ecf9a..d90866dd73 100644 --- a/packages/vue-table/tests/unit/rendering.test.ts +++ b/packages/vue-table/tests/unit/rendering.test.ts @@ -10,6 +10,10 @@ import { useTable } from '../../src/useTable' afterEach(cleanup) +function outputText(name: string) { + return screen.getByRole('status', { name }).textContent +} + describe('FlexRender', () => { test('supports cell modes, header/footer shorthand, and legacy props', () => { const normalContext = { value: 'Normal' } @@ -33,7 +37,7 @@ describe('FlexRender', () => { const Root = defineComponent({ setup() { return () => - h('section', { 'data-testid': 'flex-render-output' }, [ + h('output', { 'aria-label': 'Flex render output' }, [ h(FlexRender, { cell: { column: { @@ -97,7 +101,7 @@ describe('FlexRender', () => { render(Root) - expect(screen.getByTestId('flex-render-output').textContent).toBe( + expect(outputText('Flex render output')).toBe( 'cell:Normalsum:Aggregatedheader:Titlefooter:Totallegacy:Legacy', ) expect(normalCellRenderer).toHaveBeenCalledOnce() @@ -121,8 +125,8 @@ describe('FlexRender', () => { setup() { return () => h( - 'div', - { 'data-testid': 'reactive-flex-render' }, + 'output', + { 'aria-label': 'Reactive flex render' }, h(FlexRender, { cell: cell.value }), ) }, @@ -130,9 +134,7 @@ describe('FlexRender', () => { render(Root) - expect(screen.getByTestId('reactive-flex-render').textContent).toBe( - 'cell:Ada', - ) + expect(outputText('Reactive flex render')).toBe('cell:Ada') cell.value = { column: cell.value.column, @@ -140,9 +142,7 @@ describe('FlexRender', () => { } await nextTick() - expect(screen.getByTestId('reactive-flex-render').textContent).toBe( - 'cell:Grace', - ) + expect(outputText('Reactive flex render')).toBe('cell:Grace') }) }) @@ -163,25 +163,30 @@ describe('table.Subscribe', () => { children: (atoms) => h( 'output', - { 'data-testid': 'subscribed-selection' }, + { 'aria-label': 'Subscribed row selection' }, String(Boolean(atoms.rowSelection.get()['1'])), ), }), - h('button', { - 'data-testid': 'select-subscribed-row', - onClick: () => table.getRow('1').toggleSelected(true), - }), + h( + 'button', + { + onClick: () => table.getRow('1').toggleSelected(true), + }, + 'Select subscribed row', + ), ]) }, }) render(Root) - expect(screen.getByTestId('subscribed-selection').textContent).toBe('false') + expect(outputText('Subscribed row selection')).toBe('false') - await fireEvent.click(screen.getByTestId('select-subscribed-row')) + await fireEvent.click( + screen.getByRole('button', { name: 'Select subscribed row' }), + ) - expect(screen.getByTestId('subscribed-selection').textContent).toBe('true') + expect(outputText('Subscribed row selection')).toBe('true') }) }) @@ -190,20 +195,17 @@ describe('createTableHook', () => { const TableBadge = defineComponent({ setup() { - return () => - h('span', { 'data-testid': 'table-component' }, 'table-component') + return () => h('span', 'table-component') }, }) const CellBadge = defineComponent({ setup() { - return () => - h('span', { 'data-testid': 'cell-component' }, 'cell-component') + return () => h('span', 'cell-component') }, }) const HeaderBadge = defineComponent({ setup() { - return () => - h('span', { 'data-testid': 'header-component' }, 'header-component') + return () => h('span', 'header-component') }, }) @@ -287,7 +289,6 @@ describe('createTableHook', () => { return () => h( 'main', - { 'data-testid': 'table-hook-output' }, h(table.AppTable, null, { default: () => [ h(TableConsumer), @@ -326,16 +327,16 @@ describe('createTableHook', () => { expect(cellContextCaptor).toHaveBeenCalledWith(originalCell) expect(headerContextCaptor).toHaveBeenCalledWith(originalHeader) expect(footerContextCaptor).toHaveBeenCalledWith(originalFooter) - expect(screen.getByTestId('table-component').textContent).toBe( + expect(screen.getByText('table-component').textContent).toBe( 'table-component', ) - expect(screen.getByTestId('cell-component').textContent).toBe( + expect(screen.getByText('cell-component').textContent).toBe( 'cell-component', ) - expect(screen.getByTestId('header-component').textContent).toBe( + expect(screen.getByText('header-component').textContent).toBe( 'header-component', ) - expect(screen.getByTestId('table-hook-output').textContent).toBe( + expect(screen.getByRole('main').textContent).toBe( 'table-componentcell-componentcell:First' + 'header-componentheader:titlefooter:title', ) @@ -347,7 +348,7 @@ describe('createTableHook', () => { setup() { const cell = hook.useCellContext() return () => - h('span', { 'data-testid': 'bound-cell-mode' }, h(cell.FlexRender)) + h('output', { 'aria-label': 'Bound cell mode' }, h(cell.FlexRender)) }, }) const Root = defineComponent({ @@ -378,14 +379,12 @@ describe('createTableHook', () => { render(Root) - expect(screen.getByTestId('bound-cell-mode').textContent).toBe( - 'aggregate:First', - ) + expect(outputText('Bound cell mode')).toBe('aggregate:First') mode.value = 'placeholder' await nextTick() - expect(screen.getByTestId('bound-cell-mode').textContent).toBe('') + expect(outputText('Bound cell mode')).toBe('') }) test.each([ From ebbf30621e73675bd2ab0a2291515c35905a3b2e Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Wed, 29 Jul 2026 23:27:39 +0200 Subject: [PATCH 12/13] fix solid flexrender condition branch --- packages/solid-table/src/FlexRender.tsx | 28 ++++++----- .../solid-table/tests/unit/rendering.test.tsx | 49 +++++++++++++++++++ 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/packages/solid-table/src/FlexRender.tsx b/packages/solid-table/src/FlexRender.tsx index 2972a50a12..7109255129 100644 --- a/packages/solid-table/src/FlexRender.tsx +++ b/packages/solid-table/src/FlexRender.tsx @@ -1,4 +1,4 @@ -import { Match, Switch, createComponent } from 'solid-js' +import { Match, Show, Switch, createComponent } from 'solid-js' import type { JSX } from 'solid-js' import type { Cell, @@ -105,16 +105,22 @@ export function FlexRender< const groupingDef = def as typeof def & { aggregatedCell?: typeof def.cell } - if (groupingCell.getIsAggregated?.()) { - return flexRender( - groupingDef.aggregatedCell ?? def.cell, - c.getContext(), - ) - } - if (groupingCell.getIsPlaceholder?.()) { - return null - } - return flexRender(def.cell, c.getContext()) + + return ( + + {flexRender(def.cell, c.getContext())} + + } + > + {flexRender( + groupingDef.aggregatedCell ?? def.cell, + c.getContext(), + )} + + ) }} diff --git a/packages/solid-table/tests/unit/rendering.test.tsx b/packages/solid-table/tests/unit/rendering.test.tsx index 8eada82546..124a1844fa 100644 --- a/packages/solid-table/tests/unit/rendering.test.tsx +++ b/packages/solid-table/tests/unit/rendering.test.tsx @@ -99,6 +99,55 @@ describe('FlexRender', () => { ) }) + test('reacts when a cell changes grouping mode', () => { + function GroupingCellHarness() { + const [mode, setMode] = createSignal('normal') + const table = createTable({ + data: [{ id: '1', name: 'Ada' }], + columns, + features: stockFeatures, + getRowId: (row) => row.id, + }) + const cell = table.getRowModel().rows[0]!.getAllCells()[0]! + + vi.spyOn(cell, 'getIsAggregated').mockImplementation( + () => mode() === 'aggregate', + ) + vi.spyOn(cell, 'getIsPlaceholder').mockImplementation( + () => mode() === 'placeholder', + ) + + return ( + <> + + + + + + + + ) + } + + render(() => ) + + const renderedCell = () => + screen.getByRole('status', { name: 'grouping cell' }).textContent + + expect(renderedCell()).toBe('cell:Ada') + + fireEvent.click(screen.getByRole('button', { name: 'Show aggregate' })) + expect(renderedCell()).toBe('aggregate:Ada') + + fireEvent.click(screen.getByRole('button', { name: 'Show placeholder' })) + expect(renderedCell()).toBe('') + + fireEvent.click(screen.getByRole('button', { name: 'Show normal' })) + expect(renderedCell()).toBe('cell:Ada') + }) + test('updates when a truthy cell prop is replaced with a new instance', () => { function ReactiveCellHarness() { const [data, setData] = createSignal>([ From 2fe6d8152ae74c021c712d8b2ef5e60382b80218 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Wed, 29 Jul 2026 23:30:06 +0200 Subject: [PATCH 13/13] revert solid mergeProps checking for undefined state --- packages/solid-table/src/createTable.ts | 43 ++----------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/packages/solid-table/src/createTable.ts b/packages/solid-table/src/createTable.ts index 57e490eb8e..3a5a91c80d 100644 --- a/packages/solid-table/src/createTable.ts +++ b/packages/solid-table/src/createTable.ts @@ -14,7 +14,6 @@ import type { Table, TableFeatures, TableOptions, - TableState, } from '@tanstack/table-core' export type SolidTable< @@ -41,27 +40,6 @@ export type SolidTable< FlexRender: typeof FlexRender } -function overrideStateOption< - TFeatures extends TableFeatures, - TData extends RowData, ->( - options: TableOptions, - state: Partial> | undefined, -): TableOptions { - return Object.defineProperties( - Object.create(Object.getPrototypeOf(options)), - { - ...Object.getOwnPropertyDescriptors(options), - state: { - configurable: true, - enumerable: true, - value: state, - writable: true, - }, - }, - ) as TableOptions -} - /** * Creates a Solid table instance backed by Solid-aware TanStack Store atoms. * @@ -100,17 +78,7 @@ export function createTable< defaultOptions: TableOptions, options: TableOptions, ) => { - const merged = mergeProps(defaultOptions, options) as TableOptions< - TFeatures, - TData - > - - // Solid's mergeProps treats `undefined` as a request to fall back to - // the previous source. For a controlled table state, however, - // `state: undefined` explicitly releases ownership back to the table. - return Object.prototype.hasOwnProperty.call(options, 'state') - ? overrideStateOption(merged, options.state) - : merged + return mergeProps(defaultOptions, options) }, }, mergedOptions, @@ -131,14 +99,7 @@ export function createTable< untrack(() => { table.setOptions((prev) => { - const nextOptions = mergeProps(prev, mergedOptions) as TableOptions< - TFeatures, - TData - > - - return Object.prototype.hasOwnProperty.call(tableOptions, 'state') - ? overrideStateOption(nextOptions, userState) - : nextOptions + return mergeProps(prev, mergedOptions) as TableOptions }) }) })