From 936eceb37cd3626061d24fa24877044b92032d57 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 24 Apr 2025 11:37:17 -0500 Subject: [PATCH 01/74] Run tests in FF --- .github/workflows/ci.yml | 2 +- vite.config.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e44b02932..972ae40f00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: - name: Build website run: node --run build:website - name: Install Playwright Browsers - run: npx playwright install chromium + run: npx playwright install chromium firefox - name: Test run: node --run test timeout-minutes: 4 diff --git a/vite.config.ts b/vite.config.ts index 7ec2496bf9..e1ff3b9a6b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -102,6 +102,10 @@ export default defineConfig(({ command }) => ({ { browser: 'chromium', context: { viewport } + }, + { + browser: 'firefox', + context: { viewport } } ], commands: { resizeColumn, dragFill }, From 3a06ef87e9cd321d14b2bbc6f2024c9a69142f68 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 24 Apr 2025 12:19:11 -0500 Subject: [PATCH 02/74] Fix editor test, use istanbul for coverage --- package.json | 2 +- test/browser/column/renderEditCell.test.tsx | 9 +++++---- vite.config.ts | 2 +- website/routes/CommonFeatures.tsx | 11 +++++++++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 4ba56b6cd8..338fa3aa29 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "@typescript-eslint/parser": "^8.29.0", "@vitejs/plugin-react": "^4.4.1", "@vitest/browser": "^3.1.2", - "@vitest/coverage-v8": "^3.1.2", + "@vitest/coverage-istanbul": "^3.1.2", "@vitest/eslint-plugin": "^1.1.43", "@wyw-in-js/rollup": "^0.6.0", "@wyw-in-js/vite": "^0.6.0", diff --git a/test/browser/column/renderEditCell.test.tsx b/test/browser/column/renderEditCell.test.tsx index 2a3efc9dfc..5c3a23da9d 100644 --- a/test/browser/column/renderEditCell.test.tsx +++ b/test/browser/column/renderEditCell.test.tsx @@ -22,7 +22,7 @@ describe('Editor', () => { await userEvent.keyboard('2'); await userEvent.tab(); await expect.element(editor).not.toBeInTheDocument(); - expect(getCellsAtRowIndex(0)[0]).toHaveTextContent(/^12$/); + expect(getCellsAtRowIndex(0)[0]).toHaveTextContent(12); }); it('should open and commit changes on enter', async () => { @@ -42,7 +42,7 @@ describe('Editor', () => { page.render(); await userEvent.click(getCellsAtRowIndex(0)[0]); await userEvent.keyboard('123{enter}'); - expect(getCellsAtRowIndex(0)[0]).toHaveTextContent(/^1123$/); + expect(getCellsAtRowIndex(0)[0]).toHaveTextContent('123'); }); it('should close editor and discard changes on escape', async () => { @@ -99,6 +99,7 @@ describe('Editor', () => { const editor = page.getByRole('spinbutton', { name: 'col1-editor' }); await expect.element(editor).not.toBeInTheDocument(); expect(getGrid().element().scrollTop).toBe(2000); + await userEvent.dblClick(getCellsAtRowIndex(0)[0]); await userEvent.keyboard('123'); expect(getCellsAtRowIndex(0)).toHaveLength(2); await expect.element(editor).toHaveValue(123); @@ -193,9 +194,9 @@ describe('Editor', () => { }} /> ); - await userEvent.click(getCellsAtRowIndex(0)[1]); + await userEvent.dblClick(getCellsAtRowIndex(0)[1]); await userEvent.keyboard('yz{enter}'); - expect(getCellsAtRowIndex(0)[1]).toHaveTextContent(/^a1yz$/); + expect(getCellsAtRowIndex(0)[1]).toHaveTextContent('yz'); await userEvent.keyboard('x'); await expect .element(page.getByRole('textbox', { name: 'col2-editor' })) diff --git a/vite.config.ts b/vite.config.ts index e1ff3b9a6b..b0a0368a68 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -79,7 +79,7 @@ export default defineConfig(({ command }) => ({ test: { globals: true, coverage: { - provider: 'v8', + provider: 'istanbul', enabled: isCI, include: ['src/**/*.{ts,tsx}'], reporter: ['json'] diff --git a/website/routes/CommonFeatures.tsx b/website/routes/CommonFeatures.tsx index 0540761664..7099e843ce 100644 --- a/website/routes/CommonFeatures.tsx +++ b/website/routes/CommonFeatures.tsx @@ -207,6 +207,17 @@ function getColumns( name: 'Budget', renderCell(props) { return currencyFormatter.format(props.row.budget); + }, + renderEditCell({ row, onRowChange }) { + return ( + onRowChange({ ...row, budget: e.target.valueAsNumber })} + /> + ); } }, { From 86fde9b1e19d00d7139e43ace449d5dbe86314cf Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 24 Apr 2025 12:27:32 -0500 Subject: [PATCH 03/74] Remove optimizeDeps --- vite.config.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index b0a0368a68..309947ec4e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -73,9 +73,6 @@ export default defineConfig(({ command }) => ({ server: { open: true }, - optimizeDeps: { - include: ['@vitest/coverage-v8/browser'] - }, test: { globals: true, coverage: { From 0b070749f9cd62d0659f430c4f13d574e565b70d Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 24 Apr 2025 12:30:23 -0500 Subject: [PATCH 04/74] revert 1 change --- test/browser/column/renderEditCell.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/browser/column/renderEditCell.test.tsx b/test/browser/column/renderEditCell.test.tsx index 5c3a23da9d..150752e3fd 100644 --- a/test/browser/column/renderEditCell.test.tsx +++ b/test/browser/column/renderEditCell.test.tsx @@ -99,8 +99,8 @@ describe('Editor', () => { const editor = page.getByRole('spinbutton', { name: 'col1-editor' }); await expect.element(editor).not.toBeInTheDocument(); expect(getGrid().element().scrollTop).toBe(2000); - await userEvent.dblClick(getCellsAtRowIndex(0)[0]); - await userEvent.keyboard('123'); + // `1{backspace}` is needed to fix tests in FF + await userEvent.keyboard('1{backspace}123'); expect(getCellsAtRowIndex(0)).toHaveLength(2); await expect.element(editor).toHaveValue(123); expect(getGrid().element().scrollTop).toBe(0); From ef3f86fd4a29f17a03b605596946fa5b1272608a Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 24 Apr 2025 13:20:01 -0500 Subject: [PATCH 05/74] Fix keyboard tests --- test/browser/keyboardNavigation.test.tsx | 27 ++++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/test/browser/keyboardNavigation.test.tsx b/test/browser/keyboardNavigation.test.tsx index ad63ce8622..074f6b360f 100644 --- a/test/browser/keyboardNavigation.test.tsx +++ b/test/browser/keyboardNavigation.test.tsx @@ -26,6 +26,15 @@ const columns = [ { key: 'col7', name: 'col7' } ] as const satisfies Column[]; +async function focusGrid() { + await userEvent.tab(); + + // FF focuses the grid when tabbing into it (bug ?) + if ((document.activeElement as HTMLDivElement).classList.contains('rdg')) { + await userEvent.tab(); + } +} + test('keyboard navigation', async () => { setup({ columns, rows, topSummaryRows, bottomSummaryRows }); @@ -33,7 +42,7 @@ test('keyboard navigation', async () => { await expect.element(getSelectedCell()).not.toBeInTheDocument(); // tab into the grid - await userEvent.tab(); + await focusGrid(); validateCellPosition(0, 0); // tab to the next cell @@ -106,7 +115,7 @@ test('arrow and tab navigation', async () => { setup({ columns, rows, bottomSummaryRows }); // pressing arrowleft on the leftmost cell does nothing - await userEvent.tab(); + await focusGrid(); await userEvent.keyboard('{arrowdown}'); validateCellPosition(0, 1); await userEvent.keyboard('{arrowleft}'); @@ -145,7 +154,7 @@ test('grid enter/exit', async () => { // tab into the grid await userEvent.click(beforeButton); - await userEvent.tab(); + await focusGrid(); validateCellPosition(0, 0); // shift+tab tabs out of the grid if we are at the first cell @@ -179,7 +188,7 @@ test('grid enter/exit', async () => { test('navigation with focusable cell renderer', async () => { setup({ columns, rows: new Array(1), bottomSummaryRows }); - await userEvent.tab(); + await focusGrid(); await userEvent.keyboard('{arrowdown}'); validateCellPosition(0, 1); @@ -220,7 +229,7 @@ test('navigation when header and summary rows have focusable elements', async () ]; setup({ columns, rows: new Array(2), bottomSummaryRows }); - await userEvent.tab(); + await focusGrid(); // should set focus on the header filter expect(document.getElementById('header-filter1')).toHaveFocus(); @@ -260,7 +269,7 @@ test('navigation when selected cell not in the viewport', async () => { columns.push({ key: `col${i}`, name: `col${i}`, frozen: i < 5 }); } setup({ columns, rows, bottomSummaryRows }); - await userEvent.tab(); + await focusGrid(); validateCellPosition(0, 0); await userEvent.keyboard('{Control>}{end}{/Control}{arrowup}{arrowup}'); @@ -299,7 +308,7 @@ test('reset selected cell when column is removed', async () => { const { rerender } = page.render(); - await userEvent.tab(); + await focusGrid(); await userEvent.keyboard('{arrowdown}{arrowright}'); validateCellPosition(1, 1); @@ -321,7 +330,7 @@ test('reset selected cell when row is removed', async () => { const { rerender } = page.render(); - await userEvent.tab(); + await focusGrid(); await userEvent.keyboard('{arrowdown}{arrowdown}{arrowright}'); validateCellPosition(1, 2); @@ -332,7 +341,7 @@ test('reset selected cell when row is removed', async () => { test('should not change the left and right arrow behavior for right to left languages', async () => { setup({ rows, columns, direction: 'rtl' }); - await userEvent.tab(); + await focusGrid(); validateCellPosition(0, 0); await userEvent.tab(); validateCellPosition(1, 0); From 5c4e3add7e7cf5369a7214de2b3ff4b9d1120c95 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 24 Apr 2025 13:21:55 -0500 Subject: [PATCH 06/74] Revert --- website/routes/CommonFeatures.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/website/routes/CommonFeatures.tsx b/website/routes/CommonFeatures.tsx index 7099e843ce..0540761664 100644 --- a/website/routes/CommonFeatures.tsx +++ b/website/routes/CommonFeatures.tsx @@ -207,17 +207,6 @@ function getColumns( name: 'Budget', renderCell(props) { return currencyFormatter.format(props.row.budget); - }, - renderEditCell({ row, onRowChange }) { - return ( - onRowChange({ ...row, budget: e.target.valueAsNumber })} - /> - ); } }, { From 6ab1bbd08880ce09deee20b6a7930d87bb27229b Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 24 Apr 2025 13:37:46 -0500 Subject: [PATCH 07/74] Fix rowHeight tests --- test/browser/keyboardNavigation.test.tsx | 28 +++++++++--------------- test/browser/rowHeight.test.ts | 6 ++--- test/browser/utils.tsx | 11 +++++++++- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/test/browser/keyboardNavigation.test.tsx b/test/browser/keyboardNavigation.test.tsx index 074f6b360f..5234e67096 100644 --- a/test/browser/keyboardNavigation.test.tsx +++ b/test/browser/keyboardNavigation.test.tsx @@ -3,6 +3,7 @@ import { page, userEvent } from '@vitest/browser/context'; import { DataGrid, SelectColumn } from '../../src'; import type { Column } from '../../src'; import { + focusIntoGrid, getCellsAtRowIndex, getSelectedCell, scrollGrid, @@ -26,15 +27,6 @@ const columns = [ { key: 'col7', name: 'col7' } ] as const satisfies Column[]; -async function focusGrid() { - await userEvent.tab(); - - // FF focuses the grid when tabbing into it (bug ?) - if ((document.activeElement as HTMLDivElement).classList.contains('rdg')) { - await userEvent.tab(); - } -} - test('keyboard navigation', async () => { setup({ columns, rows, topSummaryRows, bottomSummaryRows }); @@ -42,7 +34,7 @@ test('keyboard navigation', async () => { await expect.element(getSelectedCell()).not.toBeInTheDocument(); // tab into the grid - await focusGrid(); + await focusIntoGrid(); validateCellPosition(0, 0); // tab to the next cell @@ -115,7 +107,7 @@ test('arrow and tab navigation', async () => { setup({ columns, rows, bottomSummaryRows }); // pressing arrowleft on the leftmost cell does nothing - await focusGrid(); + await focusIntoGrid(); await userEvent.keyboard('{arrowdown}'); validateCellPosition(0, 1); await userEvent.keyboard('{arrowleft}'); @@ -154,7 +146,7 @@ test('grid enter/exit', async () => { // tab into the grid await userEvent.click(beforeButton); - await focusGrid(); + await focusIntoGrid(); validateCellPosition(0, 0); // shift+tab tabs out of the grid if we are at the first cell @@ -188,7 +180,7 @@ test('grid enter/exit', async () => { test('navigation with focusable cell renderer', async () => { setup({ columns, rows: new Array(1), bottomSummaryRows }); - await focusGrid(); + await focusIntoGrid(); await userEvent.keyboard('{arrowdown}'); validateCellPosition(0, 1); @@ -229,7 +221,7 @@ test('navigation when header and summary rows have focusable elements', async () ]; setup({ columns, rows: new Array(2), bottomSummaryRows }); - await focusGrid(); + await focusIntoGrid(); // should set focus on the header filter expect(document.getElementById('header-filter1')).toHaveFocus(); @@ -269,7 +261,7 @@ test('navigation when selected cell not in the viewport', async () => { columns.push({ key: `col${i}`, name: `col${i}`, frozen: i < 5 }); } setup({ columns, rows, bottomSummaryRows }); - await focusGrid(); + await focusIntoGrid(); validateCellPosition(0, 0); await userEvent.keyboard('{Control>}{end}{/Control}{arrowup}{arrowup}'); @@ -308,7 +300,7 @@ test('reset selected cell when column is removed', async () => { const { rerender } = page.render(); - await focusGrid(); + await focusIntoGrid(); await userEvent.keyboard('{arrowdown}{arrowright}'); validateCellPosition(1, 1); @@ -330,7 +322,7 @@ test('reset selected cell when row is removed', async () => { const { rerender } = page.render(); - await focusGrid(); + await focusIntoGrid(); await userEvent.keyboard('{arrowdown}{arrowdown}{arrowright}'); validateCellPosition(1, 2); @@ -341,7 +333,7 @@ test('reset selected cell when row is removed', async () => { test('should not change the left and right arrow behavior for right to left languages', async () => { setup({ rows, columns, direction: 'rtl' }); - await focusGrid(); + await focusIntoGrid(); validateCellPosition(0, 0); await userEvent.tab(); validateCellPosition(1, 0); diff --git a/test/browser/rowHeight.test.ts b/test/browser/rowHeight.test.ts index 1c9628c6aa..6f51e05303 100644 --- a/test/browser/rowHeight.test.ts +++ b/test/browser/rowHeight.test.ts @@ -1,7 +1,7 @@ import { page, userEvent } from '@vitest/browser/context'; import type { Column, DataGridProps } from '../../src'; -import { getRows, setup } from './utils'; +import { focusIntoGrid, getRows, setup } from './utils'; type Row = number; @@ -30,7 +30,7 @@ test('rowHeight is number', async () => { }); expect(getRows()).toHaveLength(30); - await userEvent.tab(); + await focusIntoGrid(); expect(grid.scrollTop).toBe(0); await userEvent.keyboard('{Control>}{end}'); expect(grid.scrollTop + grid.clientHeight).toBe(grid.scrollHeight); @@ -46,7 +46,7 @@ test('rowHeight is function', async () => { }); expect(getRows()).toHaveLength(22); - await userEvent.tab(); + await focusIntoGrid(); expect(grid.scrollTop).toBe(0); await userEvent.keyboard('{Control>}{end}'); expect(grid.scrollTop + grid.clientHeight).toBe(grid.scrollHeight); diff --git a/test/browser/utils.tsx b/test/browser/utils.tsx index 5c476acefd..b30afda3cb 100644 --- a/test/browser/utils.tsx +++ b/test/browser/utils.tsx @@ -1,4 +1,4 @@ -import { page } from '@vitest/browser/context'; +import { page, userEvent } from '@vitest/browser/context'; import { css } from '@linaria/core'; import { DataGrid } from '../../src'; @@ -76,3 +76,12 @@ export async function scrollGrid({ await new Promise(requestAnimationFrame); } } + +export async function focusIntoGrid() { + await userEvent.tab(); + + // FF focuses the grid when tabbing into it (bug ?) + if ((document.activeElement as HTMLDivElement).classList.contains('rdg')) { + await userEvent.tab(); + } +} From 215d4c71ab4eafb3f571f0c7d018dd470f638bd2 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 24 Apr 2025 16:43:44 -0500 Subject: [PATCH 08/74] Fix focus in FF --- src/DataGrid.tsx | 22 ++++++++++++++++++---- test/browser/keyboardNavigation.test.tsx | 19 +++++++++---------- test/browser/rowHeight.test.ts | 6 +++--- test/browser/utils.tsx | 11 +---------- vite.config.ts | 4 ++-- 5 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 452d461dc3..ff108ea6a0 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -484,6 +484,7 @@ export function DataGrid(props: DataGridPr const selectedCellIsWithinViewportBounds = isCellWithinViewportBounds(selectedPosition); const scrollHeight = headerRowHeight + totalRowHeight + summaryRowsHeight + horizontalScrollbarHeight; + const shouldFocusGrid = !selectedCellIsWithinSelectionBounds; /** * The identity of the wrapper function is stable so it won't break memoization @@ -499,9 +500,7 @@ export function DataGrid(props: DataGridPr const selectRowLatest = useLatestFunc(selectRow); const handleFormatterRowChangeLatest = useLatestFunc(updateRow); const selectCellLatest = useLatestFunc(selectCell); - const selectHeaderCellLatest = useLatestFunc(({ idx, rowIdx }: Position) => { - selectCell({ rowIdx: minRowIdx + rowIdx - 1, idx }); - }); + const selectHeaderCellLatest = useLatestFunc(selectHeaderCell); /** * callbacks @@ -661,6 +660,16 @@ export function DataGrid(props: DataGridPr } } + // FF focuses the grid when tabbing into it (bug ?) + function handleFocus(event: React.FocusEvent) { + if (event.target === event.currentTarget) { + if (shouldFocusGrid) { + // Select the first header cell if there is no selected cell + selectHeaderCell({ idx: 0, rowIdx: headerRowsCount }); + } + } + } + function handleScroll(event: React.UIEvent) { const { scrollTop, scrollLeft } = event.currentTarget; flushSync(() => { @@ -867,6 +876,10 @@ export function DataGrid(props: DataGridPr } } + function selectHeaderCell({ idx, rowIdx }: Position) { + selectCell({ rowIdx: minRowIdx + rowIdx - 1, idx }); + } + function getNextPosition(key: string, ctrlKey: boolean, shiftKey: boolean): Position { const { idx, rowIdx } = selectedPosition; const isRowSelected = selectedCellIsWithinSelectionBounds && idx === -1; @@ -1216,6 +1229,7 @@ export function DataGrid(props: DataGridPr } dir={direction} ref={gridRef} + onFocus={handleFocus} onScroll={handleScroll} onKeyDown={handleKeyDown} onCopy={handleCellCopy} @@ -1252,7 +1266,7 @@ export function DataGrid(props: DataGridPr selectedPosition.rowIdx === mainHeaderRowIdx ? selectedPosition.idx : undefined } selectCell={selectHeaderCellLatest} - shouldFocusGrid={!selectedCellIsWithinSelectionBounds} + shouldFocusGrid={shouldFocusGrid} direction={direction} /> diff --git a/test/browser/keyboardNavigation.test.tsx b/test/browser/keyboardNavigation.test.tsx index 5234e67096..ad63ce8622 100644 --- a/test/browser/keyboardNavigation.test.tsx +++ b/test/browser/keyboardNavigation.test.tsx @@ -3,7 +3,6 @@ import { page, userEvent } from '@vitest/browser/context'; import { DataGrid, SelectColumn } from '../../src'; import type { Column } from '../../src'; import { - focusIntoGrid, getCellsAtRowIndex, getSelectedCell, scrollGrid, @@ -34,7 +33,7 @@ test('keyboard navigation', async () => { await expect.element(getSelectedCell()).not.toBeInTheDocument(); // tab into the grid - await focusIntoGrid(); + await userEvent.tab(); validateCellPosition(0, 0); // tab to the next cell @@ -107,7 +106,7 @@ test('arrow and tab navigation', async () => { setup({ columns, rows, bottomSummaryRows }); // pressing arrowleft on the leftmost cell does nothing - await focusIntoGrid(); + await userEvent.tab(); await userEvent.keyboard('{arrowdown}'); validateCellPosition(0, 1); await userEvent.keyboard('{arrowleft}'); @@ -146,7 +145,7 @@ test('grid enter/exit', async () => { // tab into the grid await userEvent.click(beforeButton); - await focusIntoGrid(); + await userEvent.tab(); validateCellPosition(0, 0); // shift+tab tabs out of the grid if we are at the first cell @@ -180,7 +179,7 @@ test('grid enter/exit', async () => { test('navigation with focusable cell renderer', async () => { setup({ columns, rows: new Array(1), bottomSummaryRows }); - await focusIntoGrid(); + await userEvent.tab(); await userEvent.keyboard('{arrowdown}'); validateCellPosition(0, 1); @@ -221,7 +220,7 @@ test('navigation when header and summary rows have focusable elements', async () ]; setup({ columns, rows: new Array(2), bottomSummaryRows }); - await focusIntoGrid(); + await userEvent.tab(); // should set focus on the header filter expect(document.getElementById('header-filter1')).toHaveFocus(); @@ -261,7 +260,7 @@ test('navigation when selected cell not in the viewport', async () => { columns.push({ key: `col${i}`, name: `col${i}`, frozen: i < 5 }); } setup({ columns, rows, bottomSummaryRows }); - await focusIntoGrid(); + await userEvent.tab(); validateCellPosition(0, 0); await userEvent.keyboard('{Control>}{end}{/Control}{arrowup}{arrowup}'); @@ -300,7 +299,7 @@ test('reset selected cell when column is removed', async () => { const { rerender } = page.render(); - await focusIntoGrid(); + await userEvent.tab(); await userEvent.keyboard('{arrowdown}{arrowright}'); validateCellPosition(1, 1); @@ -322,7 +321,7 @@ test('reset selected cell when row is removed', async () => { const { rerender } = page.render(); - await focusIntoGrid(); + await userEvent.tab(); await userEvent.keyboard('{arrowdown}{arrowdown}{arrowright}'); validateCellPosition(1, 2); @@ -333,7 +332,7 @@ test('reset selected cell when row is removed', async () => { test('should not change the left and right arrow behavior for right to left languages', async () => { setup({ rows, columns, direction: 'rtl' }); - await focusIntoGrid(); + await userEvent.tab(); validateCellPosition(0, 0); await userEvent.tab(); validateCellPosition(1, 0); diff --git a/test/browser/rowHeight.test.ts b/test/browser/rowHeight.test.ts index 6f51e05303..1c9628c6aa 100644 --- a/test/browser/rowHeight.test.ts +++ b/test/browser/rowHeight.test.ts @@ -1,7 +1,7 @@ import { page, userEvent } from '@vitest/browser/context'; import type { Column, DataGridProps } from '../../src'; -import { focusIntoGrid, getRows, setup } from './utils'; +import { getRows, setup } from './utils'; type Row = number; @@ -30,7 +30,7 @@ test('rowHeight is number', async () => { }); expect(getRows()).toHaveLength(30); - await focusIntoGrid(); + await userEvent.tab(); expect(grid.scrollTop).toBe(0); await userEvent.keyboard('{Control>}{end}'); expect(grid.scrollTop + grid.clientHeight).toBe(grid.scrollHeight); @@ -46,7 +46,7 @@ test('rowHeight is function', async () => { }); expect(getRows()).toHaveLength(22); - await focusIntoGrid(); + await userEvent.tab(); expect(grid.scrollTop).toBe(0); await userEvent.keyboard('{Control>}{end}'); expect(grid.scrollTop + grid.clientHeight).toBe(grid.scrollHeight); diff --git a/test/browser/utils.tsx b/test/browser/utils.tsx index b30afda3cb..5c476acefd 100644 --- a/test/browser/utils.tsx +++ b/test/browser/utils.tsx @@ -1,4 +1,4 @@ -import { page, userEvent } from '@vitest/browser/context'; +import { page } from '@vitest/browser/context'; import { css } from '@linaria/core'; import { DataGrid } from '../../src'; @@ -76,12 +76,3 @@ export async function scrollGrid({ await new Promise(requestAnimationFrame); } } - -export async function focusIntoGrid() { - await userEvent.tab(); - - // FF focuses the grid when tabbing into it (bug ?) - if ((document.activeElement as HTMLDivElement).classList.contains('rdg')) { - await userEvent.tab(); - } -} diff --git a/vite.config.ts b/vite.config.ts index 309947ec4e..ab0f20b539 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -81,7 +81,7 @@ export default defineConfig(({ command }) => ({ include: ['src/**/*.{ts,tsx}'], reporter: ['json'] }, - testTimeout: isCI ? 10000 : 5000, + testTimeout: 20_000, restoreMocks: true, sequence: { shuffle: true @@ -107,7 +107,7 @@ export default defineConfig(({ command }) => ({ ], commands: { resizeColumn, dragFill }, viewport, - headless: true, + headless: false, screenshotFailures: process.env.CI !== 'true' }, setupFiles: ['test/setupBrowser.ts'] From 398fe1375f65856895b93a6c81a8f9720aa55875 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 24 Apr 2025 16:44:07 -0500 Subject: [PATCH 09/74] headless --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index ab0f20b539..853f3836bf 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -107,7 +107,7 @@ export default defineConfig(({ command }) => ({ ], commands: { resizeColumn, dragFill }, viewport, - headless: false, + headless: true, screenshotFailures: process.env.CI !== 'true' }, setupFiles: ['test/setupBrowser.ts'] From 313bde69937dd893d9748c899053e5a3b578f115 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 24 Apr 2025 16:50:01 -0500 Subject: [PATCH 10/74] ESLint --- src/DataGrid.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index ff108ea6a0..31234294a9 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -666,6 +666,8 @@ export function DataGrid(props: DataGridPr if (shouldFocusGrid) { // Select the first header cell if there is no selected cell selectHeaderCell({ idx: 0, rowIdx: headerRowsCount }); + } else { + // TODO: check if this is needed } } } From c6decaefc00224add99c0b1452f1688d6acdbfb1 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Fri, 25 Apr 2025 08:49:57 -0500 Subject: [PATCH 11/74] Tweak focus logic --- src/DataGrid.tsx | 18 ++++++------------ src/HeaderCell.tsx | 15 ++------------- src/HeaderRow.tsx | 3 --- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 31234294a9..b9169a3710 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -660,16 +660,8 @@ export function DataGrid(props: DataGridPr } } - // FF focuses the grid when tabbing into it (bug ?) - function handleFocus(event: React.FocusEvent) { - if (event.target === event.currentTarget) { - if (shouldFocusGrid) { - // Select the first header cell if there is no selected cell - selectHeaderCell({ idx: 0, rowIdx: headerRowsCount }); - } else { - // TODO: check if this is needed - } - } + function handleFocus() { + selectHeaderCell({ idx: 0, rowIdx: headerRowsCount }); } function handleScroll(event: React.UIEvent) { @@ -1200,6 +1192,9 @@ export function DataGrid(props: DataGridPr aria-multiselectable={isSelectable ? true : undefined} aria-colcount={columns.length} aria-rowcount={ariaRowCount} + // Scrollable containers without tabIndex are keyboard focusable in Chrome only if there is no focusable element inside + // whereas they are are always focusable in Firefox. We need to set tabIndex to have a consistent behavior across browsers. + tabIndex={shouldFocusGrid ? 0 : -1} className={clsx( rootClassname, { @@ -1231,7 +1226,7 @@ export function DataGrid(props: DataGridPr } dir={direction} ref={gridRef} - onFocus={handleFocus} + onFocus={shouldFocusGrid ? handleFocus : undefined} onScroll={handleScroll} onKeyDown={handleKeyDown} onCopy={handleCellCopy} @@ -1268,7 +1263,6 @@ export function DataGrid(props: DataGridPr selectedPosition.rowIdx === mainHeaderRowIdx ? selectedPosition.idx : undefined } selectCell={selectHeaderCellLatest} - shouldFocusGrid={shouldFocusGrid} direction={direction} /> diff --git a/src/HeaderCell.tsx b/src/HeaderCell.tsx index a374d9b903..11209025ec 100644 --- a/src/HeaderCell.tsx +++ b/src/HeaderCell.tsx @@ -61,7 +61,6 @@ type SharedHeaderRowProps = Pick< | 'selectCell' | 'onColumnResize' | 'onColumnResizeEnd' - | 'shouldFocusGrid' | 'direction' | 'onColumnsReorder' >; @@ -85,7 +84,6 @@ export default function HeaderCell({ sortColumns, onSortColumnsChange, selectCell, - shouldFocusGrid, direction, dragDropKey }: HeaderCellProps) { @@ -155,14 +153,6 @@ export default function HeaderCell({ } } - function handleFocus(event: React.FocusEvent) { - onFocus?.(event); - if (shouldFocusGrid) { - // Select the first header cell if there is no selected cell - selectCell({ idx: 0, rowIdx }); - } - } - function onKeyDown(event: React.KeyboardEvent) { const { key } = event; if (sortable && (key === ' ' || key === 'Enter')) { @@ -254,14 +244,13 @@ export default function HeaderCell({ aria-rowspan={rowSpan} aria-selected={isCellSelected} aria-sort={ariaSort} - // set the tabIndex to 0 when there is no selected cell so grid can receive focus - tabIndex={shouldFocusGrid ? 0 : tabIndex} + tabIndex={tabIndex} className={className} style={{ ...getHeaderCellStyle(column, rowIdx, rowSpan), ...getCellStyle(column, colSpan) }} - onFocus={handleFocus} + onFocus={onFocus} onClick={onClick} onKeyDown={onKeyDown} {...draggableProps} diff --git a/src/HeaderRow.tsx b/src/HeaderRow.tsx index 9ece21827f..e1e0ec0b18 100644 --- a/src/HeaderRow.tsx +++ b/src/HeaderRow.tsx @@ -22,7 +22,6 @@ export interface HeaderRowProps extends SharedDataGr selectCell: (position: Position) => void; lastFrozenColumnIndex: number; selectedCellIdx: number | undefined; - shouldFocusGrid: boolean; direction: Direction; headerRowClass: Maybe; } @@ -59,7 +58,6 @@ function HeaderRow({ lastFrozenColumnIndex, selectedCellIdx, selectCell, - shouldFocusGrid, direction }: HeaderRowProps) { const dragDropKey = useId(); @@ -85,7 +83,6 @@ function HeaderRow({ onSortColumnsChange={onSortColumnsChange} sortColumns={sortColumns} selectCell={selectCell} - shouldFocusGrid={shouldFocusGrid && index === 0} direction={direction} dragDropKey={dragDropKey} /> From a84c72e4061c700f86b2280222949faf4895e921 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Fri, 25 Apr 2025 09:29:54 -0500 Subject: [PATCH 12/74] Fix test --- src/DataGrid.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index b9169a3710..7bfaf5ae15 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -660,8 +660,11 @@ export function DataGrid(props: DataGridPr } } - function handleFocus() { - selectHeaderCell({ idx: 0, rowIdx: headerRowsCount }); + function handleFocus(event: React.FocusEvent) { + // select the first header cell if the focus event is triggered by the grid + if (event.target === event.currentTarget) { + selectHeaderCell({ idx: 0, rowIdx: headerRowsCount }); + } } function handleScroll(event: React.UIEvent) { From ea0674455573fdd73ae4b92400a027cf93fe164a Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Fri, 25 Apr 2025 09:38:54 -0500 Subject: [PATCH 13/74] Fix test in FF --- test/browser/dragFill.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/browser/dragFill.test.tsx b/test/browser/dragFill.test.tsx index c0c0fdd8fe..e6f5481382 100644 --- a/test/browser/dragFill.test.tsx +++ b/test/browser/dragFill.test.tsx @@ -70,7 +70,9 @@ test('should update single row using mouse', async () => { await commands.dragFill('a1', 'a2'); await expect.element(getCellsAtRowIndex(1)[0]).toHaveTextContent('a1'); await expect.element(getCellsAtRowIndex(2)[0]).toHaveTextContent('a3'); - await expect.element(getCellsAtRowIndex(0)[0]).toHaveFocus(); + // https://bugzilla.mozilla.org/show_bug.cgi?id=1961462 + const rowIdx = navigator.userAgent.includes('Firefox') ? 1 : 0; + await expect.element(getCellsAtRowIndex(rowIdx)[0]).toHaveFocus(); }); test('should update multiple rows using mouse', async () => { From 3bfbfac87ddbf8aab2e677d4b72bf5a73748d751 Mon Sep 17 00:00:00 2001 From: amahajan Date: Fri, 16 May 2025 10:55:57 -0500 Subject: [PATCH 14/74] Revert firefox workaround --- test/browser/dragFill.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/browser/dragFill.test.tsx b/test/browser/dragFill.test.tsx index e6f5481382..c0c0fdd8fe 100644 --- a/test/browser/dragFill.test.tsx +++ b/test/browser/dragFill.test.tsx @@ -70,9 +70,7 @@ test('should update single row using mouse', async () => { await commands.dragFill('a1', 'a2'); await expect.element(getCellsAtRowIndex(1)[0]).toHaveTextContent('a1'); await expect.element(getCellsAtRowIndex(2)[0]).toHaveTextContent('a3'); - // https://bugzilla.mozilla.org/show_bug.cgi?id=1961462 - const rowIdx = navigator.userAgent.includes('Firefox') ? 1 : 0; - await expect.element(getCellsAtRowIndex(rowIdx)[0]).toHaveFocus(); + await expect.element(getCellsAtRowIndex(0)[0]).toHaveFocus(); }); test('should update multiple rows using mouse', async () => { From a00f60e76ab8294bd4e40fd152db7cca98970bfd Mon Sep 17 00:00:00 2001 From: amahajan Date: Fri, 16 May 2025 11:10:00 -0500 Subject: [PATCH 15/74] try beta version --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 75936bc6a9..80d043ee5b 100644 --- a/package.json +++ b/package.json @@ -68,8 +68,8 @@ "@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/parser": "^8.32.0", "@vitejs/plugin-react": "^4.4.1", - "@vitest/browser": "^3.1.3", - "@vitest/coverage-istanbul": "^3.1.3", + "@vitest/browser": "^3.2.0-beta.1", + "@vitest/coverage-istanbul": "^3.2.0-beta.1", "@vitest/eslint-plugin": "^1.1.44", "@wyw-in-js/rollup": "^0.6.0", "@wyw-in-js/vite": "^0.6.0", @@ -94,7 +94,7 @@ "rolldown": "^1.0.0-beta.8", "typescript": "~5.8.2", "vite": "^6.3.5", - "vitest": "^3.1.3", + "vitest": "^3.2.0-beta.1", "vitest-browser-react": "^0.1.1" }, "peerDependencies": { From cecf08bd9b66abba0328318d8d0b6bb58f3e032f Mon Sep 17 00:00:00 2001 From: amahajan Date: Fri, 16 May 2025 11:21:22 -0500 Subject: [PATCH 16/74] Revert "try beta version" This reverts commit a00f60e76ab8294bd4e40fd152db7cca98970bfd. --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 80d043ee5b..75936bc6a9 100644 --- a/package.json +++ b/package.json @@ -68,8 +68,8 @@ "@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/parser": "^8.32.0", "@vitejs/plugin-react": "^4.4.1", - "@vitest/browser": "^3.2.0-beta.1", - "@vitest/coverage-istanbul": "^3.2.0-beta.1", + "@vitest/browser": "^3.1.3", + "@vitest/coverage-istanbul": "^3.1.3", "@vitest/eslint-plugin": "^1.1.44", "@wyw-in-js/rollup": "^0.6.0", "@wyw-in-js/vite": "^0.6.0", @@ -94,7 +94,7 @@ "rolldown": "^1.0.0-beta.8", "typescript": "~5.8.2", "vite": "^6.3.5", - "vitest": "^3.2.0-beta.1", + "vitest": "^3.1.3", "vitest-browser-react": "^0.1.1" }, "peerDependencies": { From faafac22014571c876bf231d90b923a405d179d4 Mon Sep 17 00:00:00 2001 From: amahajan Date: Fri, 16 May 2025 11:21:59 -0500 Subject: [PATCH 17/74] Try pinning vitest --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 75936bc6a9..273ed68a5c 100644 --- a/package.json +++ b/package.json @@ -68,8 +68,8 @@ "@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/parser": "^8.32.0", "@vitejs/plugin-react": "^4.4.1", - "@vitest/browser": "^3.1.3", - "@vitest/coverage-istanbul": "^3.1.3", + "@vitest/browser": "3.1.1", + "@vitest/coverage-istanbul": "3.1.1", "@vitest/eslint-plugin": "^1.1.44", "@wyw-in-js/rollup": "^0.6.0", "@wyw-in-js/vite": "^0.6.0", @@ -94,7 +94,7 @@ "rolldown": "^1.0.0-beta.8", "typescript": "~5.8.2", "vite": "^6.3.5", - "vitest": "^3.1.3", + "vitest": "3.1.1", "vitest-browser-react": "^0.1.1" }, "peerDependencies": { From a50bd1df2fdba36655521849a339036949f4236f Mon Sep 17 00:00:00 2001 From: amahajan Date: Fri, 16 May 2025 11:25:11 -0500 Subject: [PATCH 18/74] Revert "Try pinning vitest" This reverts commit faafac22014571c876bf231d90b923a405d179d4. --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 273ed68a5c..75936bc6a9 100644 --- a/package.json +++ b/package.json @@ -68,8 +68,8 @@ "@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/parser": "^8.32.0", "@vitejs/plugin-react": "^4.4.1", - "@vitest/browser": "3.1.1", - "@vitest/coverage-istanbul": "3.1.1", + "@vitest/browser": "^3.1.3", + "@vitest/coverage-istanbul": "^3.1.3", "@vitest/eslint-plugin": "^1.1.44", "@wyw-in-js/rollup": "^0.6.0", "@wyw-in-js/vite": "^0.6.0", @@ -94,7 +94,7 @@ "rolldown": "^1.0.0-beta.8", "typescript": "~5.8.2", "vite": "^6.3.5", - "vitest": "3.1.1", + "vitest": "^3.1.3", "vitest-browser-react": "^0.1.1" }, "peerDependencies": { From 6a9e3ab92050a7b2eb17fcab062af0d036fff78b Mon Sep 17 00:00:00 2001 From: amahajan Date: Fri, 16 May 2025 11:26:51 -0500 Subject: [PATCH 19/74] Revrt timeout --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 853f3836bf..309947ec4e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -81,7 +81,7 @@ export default defineConfig(({ command }) => ({ include: ['src/**/*.{ts,tsx}'], reporter: ['json'] }, - testTimeout: 20_000, + testTimeout: isCI ? 10000 : 5000, restoreMocks: true, sequence: { shuffle: true From 0a38ac8b6bb1960ed20ba9bc89392aa2e7c31faa Mon Sep 17 00:00:00 2001 From: amahajan Date: Fri, 16 May 2025 11:31:14 -0500 Subject: [PATCH 20/74] try `maxWorkers` --- vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vite.config.ts b/vite.config.ts index 309947ec4e..70ae18c0f6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -75,6 +75,7 @@ export default defineConfig(({ command }) => ({ }, test: { globals: true, + maxWorkers: 8, coverage: { provider: 'istanbul', enabled: isCI, From 2f534b5eb2fef894ef81f749cdde55a12d3817b3 Mon Sep 17 00:00:00 2001 From: amahajan Date: Fri, 16 May 2025 11:34:44 -0500 Subject: [PATCH 21/74] `maxWorkers: 4,` --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 70ae18c0f6..306d69dc03 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -75,7 +75,7 @@ export default defineConfig(({ command }) => ({ }, test: { globals: true, - maxWorkers: 8, + maxWorkers: 4, coverage: { provider: 'istanbul', enabled: isCI, From 256839edf2900098c04d6a392ededb7599176558 Mon Sep 17 00:00:00 2001 From: amahajan Date: Fri, 16 May 2025 11:35:33 -0500 Subject: [PATCH 22/74] increate testTimeout --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 306d69dc03..db829a6c45 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -82,7 +82,7 @@ export default defineConfig(({ command }) => ({ include: ['src/**/*.{ts,tsx}'], reporter: ['json'] }, - testTimeout: isCI ? 10000 : 5000, + testTimeout: 20_000, restoreMocks: true, sequence: { shuffle: true From 86dabf00b0c6fe0ab69aa233b3164817708e544c Mon Sep 17 00:00:00 2001 From: amahajan Date: Tue, 19 Aug 2025 10:56:18 -0500 Subject: [PATCH 23/74] try beta --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 9abbe0d7d9..0cb7f4b21f 100644 --- a/package.json +++ b/package.json @@ -66,8 +66,8 @@ "@typescript-eslint/eslint-plugin": "^8.39.1", "@typescript-eslint/parser": "^8.39.1", "@vitejs/plugin-react": "^5.0.0", - "@vitest/browser": "^3.2.4", - "@vitest/coverage-istanbul": "^3.2.4", + "@vitest/browser": "^4.0.0-beta.8", + "@vitest/coverage-istanbul": "^4.0.0-beta.8", "@vitest/eslint-plugin": "^1.3.4", "@wyw-in-js/rollup": "^0.7.0", "@wyw-in-js/vite": "^0.7.0", @@ -90,7 +90,7 @@ "rolldown-plugin-dts": "^0.15.6", "typescript": "~5.9.2", "vite": "npm:rolldown-vite@^7.1.3", - "vitest": "^3.2.4", + "vitest": "^4.0.0-beta.8", "vitest-browser-react": "^1.0.1" }, "peerDependencies": { From 0ff8120e80a930958333b0121567f64e14832ba1 Mon Sep 17 00:00:00 2001 From: amahajan Date: Tue, 19 Aug 2025 11:27:52 -0500 Subject: [PATCH 24/74] Fix grouping test --- .../column/{grouping.test.ts => grouping.test.tsx} | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) rename test/browser/column/{grouping.test.ts => grouping.test.tsx} (96%) diff --git a/test/browser/column/grouping.test.ts b/test/browser/column/grouping.test.tsx similarity index 96% rename from test/browser/column/grouping.test.ts rename to test/browser/column/grouping.test.tsx index e9bfe4c8cd..ec5d3e5e53 100644 --- a/test/browser/column/grouping.test.ts +++ b/test/browser/column/grouping.test.tsx @@ -1,6 +1,6 @@ import { page, userEvent } from '@vitest/browser/context'; -import type { ColumnOrColumnGroup } from '../../../src'; +import { DataGrid, type ColumnOrColumnGroup } from '../../../src'; import { getSelectedCell, setup, validateCellPosition } from '../utils'; const columns: readonly ColumnOrColumnGroup>[] = [ @@ -248,11 +248,17 @@ test('grouping', async () => { }); test('keyboard navigation', async () => { - setup({ columns, rows: [{}] }); + page.render( + <> + + + + ); // no initial selection await expect.element(getSelectedCell()).not.toBeInTheDocument(); + await userEvent.click(page.getByRole('button', { name: 'Before' })); await userEvent.tab(); validateCellPosition(0, 3); From c643d5304623bad0d6be1ea4008b542ebd171fc4 Mon Sep 17 00:00:00 2001 From: amahajan Date: Tue, 19 Aug 2025 12:12:57 -0500 Subject: [PATCH 25/74] Fix a few tests --- .../{grouping.test.tsx => grouping.test.ts} | 14 ++----- test/browser/direction.test.ts | 14 +++---- test/browser/keyboardNavigation.test.tsx | 37 ++++++++----------- test/browser/rowHeight.test.ts | 8 ++-- test/browser/utils.tsx | 27 ++++++++++++-- 5 files changed, 54 insertions(+), 46 deletions(-) rename test/browser/column/{grouping.test.tsx => grouping.test.ts} (95%) diff --git a/test/browser/column/grouping.test.tsx b/test/browser/column/grouping.test.ts similarity index 95% rename from test/browser/column/grouping.test.tsx rename to test/browser/column/grouping.test.ts index ec5d3e5e53..24a7a26027 100644 --- a/test/browser/column/grouping.test.tsx +++ b/test/browser/column/grouping.test.ts @@ -1,7 +1,7 @@ import { page, userEvent } from '@vitest/browser/context'; -import { DataGrid, type ColumnOrColumnGroup } from '../../../src'; -import { getSelectedCell, setup, validateCellPosition } from '../utils'; +import { type ColumnOrColumnGroup } from '../../../src'; +import { getSelectedCell, setup, tabIntoGrid, validateCellPosition } from '../utils'; const columns: readonly ColumnOrColumnGroup>[] = [ { key: 'col1', name: 'col 1' }, @@ -248,18 +248,12 @@ test('grouping', async () => { }); test('keyboard navigation', async () => { - page.render( - <> - - - - ); + setup({ columns, rows: [{}] }, true); // no initial selection await expect.element(getSelectedCell()).not.toBeInTheDocument(); - await userEvent.click(page.getByRole('button', { name: 'Before' })); - await userEvent.tab(); + await tabIntoGrid(); validateCellPosition(0, 3); // arrow navigation diff --git a/test/browser/direction.test.ts b/test/browser/direction.test.ts index b35c4b7cf2..f77b9529fa 100644 --- a/test/browser/direction.test.ts +++ b/test/browser/direction.test.ts @@ -1,7 +1,7 @@ import { userEvent } from '@vitest/browser/context'; import type { Column } from '../../src'; -import { getGrid, getSelectedCell, setup } from './utils'; +import { getGrid, getSelectedCell, setup, tabIntoGrid } from './utils'; interface Row { id: number; @@ -22,27 +22,27 @@ const columns: readonly Column[] = [ const rows: readonly Row[] = []; test('should use left to right direction by default', async () => { - setup({ rows, columns }); + setup({ rows, columns }, true); await expect.element(getGrid()).toHaveAttribute('dir', 'ltr'); - await userEvent.tab(); + await tabIntoGrid(); await expect.element(getSelectedCell()).toHaveTextContent('ID'); await userEvent.keyboard('{ArrowRight}'); await expect.element(getSelectedCell()).toHaveTextContent('Name'); }); test('should use left to right direction if direction prop is set to ltr', async () => { - setup({ rows, columns, direction: 'ltr' }); + setup({ rows, columns, direction: 'ltr' }, true); await expect.element(getGrid()).toHaveAttribute('dir', 'ltr'); - await userEvent.tab(); + await tabIntoGrid(); await expect.element(getSelectedCell()).toHaveTextContent('ID'); await userEvent.keyboard('{ArrowRight}'); await expect.element(getSelectedCell()).toHaveTextContent('Name'); }); test('should use right to left direction if direction prop is set to rtl', async () => { - setup({ rows, columns, direction: 'rtl' }); + setup({ rows, columns, direction: 'rtl' }, true); await expect.element(getGrid()).toHaveAttribute('dir', 'rtl'); - await userEvent.tab(); + await tabIntoGrid(); await expect.element(getSelectedCell()).toHaveTextContent('ID'); await userEvent.keyboard('{ArrowLeft}'); await expect.element(getSelectedCell()).toHaveTextContent('Name'); diff --git a/test/browser/keyboardNavigation.test.tsx b/test/browser/keyboardNavigation.test.tsx index ad63ce8622..1265e13b57 100644 --- a/test/browser/keyboardNavigation.test.tsx +++ b/test/browser/keyboardNavigation.test.tsx @@ -7,6 +7,7 @@ import { getSelectedCell, scrollGrid, setup, + tabIntoGrid, validateCellPosition } from './utils'; @@ -27,13 +28,13 @@ const columns = [ ] as const satisfies Column[]; test('keyboard navigation', async () => { - setup({ columns, rows, topSummaryRows, bottomSummaryRows }); + setup({ columns, rows, topSummaryRows, bottomSummaryRows }, true); // no initial selection await expect.element(getSelectedCell()).not.toBeInTheDocument(); // tab into the grid - await userEvent.tab(); + await tabIntoGrid(); validateCellPosition(0, 0); // tab to the next cell @@ -103,10 +104,10 @@ test('keyboard navigation', async () => { }); test('arrow and tab navigation', async () => { - setup({ columns, rows, bottomSummaryRows }); + setup({ columns, rows, bottomSummaryRows }, true); // pressing arrowleft on the leftmost cell does nothing - await userEvent.tab(); + await tabIntoGrid(); await userEvent.keyboard('{arrowdown}'); validateCellPosition(0, 1); await userEvent.keyboard('{arrowleft}'); @@ -128,14 +129,7 @@ test('arrow and tab navigation', async () => { }); test('grid enter/exit', async () => { - page.render( - <> - - -
- - - ); + setup({ columns, rows: new Array(5), bottomSummaryRows }, true); const beforeButton = page.getByRole('button', { name: 'Before' }); const afterButton = page.getByRole('button', { name: 'After' }); @@ -144,8 +138,7 @@ test('grid enter/exit', async () => { await expect.element(getSelectedCell()).not.toBeInTheDocument(); // tab into the grid - await userEvent.click(beforeButton); - await userEvent.tab(); + await tabIntoGrid(); validateCellPosition(0, 0); // shift+tab tabs out of the grid if we are at the first cell @@ -178,8 +171,8 @@ test('grid enter/exit', async () => { }); test('navigation with focusable cell renderer', async () => { - setup({ columns, rows: new Array(1), bottomSummaryRows }); - await userEvent.tab(); + setup({ columns, rows: new Array(1), bottomSummaryRows }, true); + await tabIntoGrid(); await userEvent.keyboard('{arrowdown}'); validateCellPosition(0, 1); @@ -219,8 +212,8 @@ test('navigation when header and summary rows have focusable elements', async () } ]; - setup({ columns, rows: new Array(2), bottomSummaryRows }); - await userEvent.tab(); + setup({ columns, rows: new Array(2), bottomSummaryRows }, true); + await tabIntoGrid(); // should set focus on the header filter expect(document.getElementById('header-filter1')).toHaveFocus(); @@ -259,8 +252,8 @@ test('navigation when selected cell not in the viewport', async () => { for (let i = 0; i < 99; i++) { columns.push({ key: `col${i}`, name: `col${i}`, frozen: i < 5 }); } - setup({ columns, rows, bottomSummaryRows }); - await userEvent.tab(); + setup({ columns, rows, bottomSummaryRows }, true); + await tabIntoGrid(); validateCellPosition(0, 0); await userEvent.keyboard('{Control>}{end}{/Control}{arrowup}{arrowup}'); @@ -331,8 +324,8 @@ test('reset selected cell when row is removed', async () => { }); test('should not change the left and right arrow behavior for right to left languages', async () => { - setup({ rows, columns, direction: 'rtl' }); - await userEvent.tab(); + setup({ rows, columns, direction: 'rtl' }, true); + await tabIntoGrid(); validateCellPosition(0, 0); await userEvent.tab(); validateCellPosition(1, 0); diff --git a/test/browser/rowHeight.test.ts b/test/browser/rowHeight.test.ts index 1c9628c6aa..0bba6f0b45 100644 --- a/test/browser/rowHeight.test.ts +++ b/test/browser/rowHeight.test.ts @@ -1,7 +1,7 @@ import { page, userEvent } from '@vitest/browser/context'; import type { Column, DataGridProps } from '../../src'; -import { getRows, setup } from './utils'; +import { getRows, setup, tabIntoGrid } from './utils'; type Row = number; @@ -17,7 +17,7 @@ function setupGrid(rowHeight: DataGridProps['rowHeight']) { width: 80 }); } - setup({ columns, rows, rowHeight }); + setup({ columns, rows, rowHeight }, true); } test('rowHeight is number', async () => { @@ -30,7 +30,7 @@ test('rowHeight is number', async () => { }); expect(getRows()).toHaveLength(30); - await userEvent.tab(); + await tabIntoGrid(); expect(grid.scrollTop).toBe(0); await userEvent.keyboard('{Control>}{end}'); expect(grid.scrollTop + grid.clientHeight).toBe(grid.scrollHeight); @@ -46,7 +46,7 @@ test('rowHeight is function', async () => { }); expect(getRows()).toHaveLength(22); - await userEvent.tab(); + await tabIntoGrid(); expect(grid.scrollTop).toBe(0); await userEvent.keyboard('{Control>}{end}'); expect(grid.scrollTop + grid.clientHeight).toBe(grid.scrollHeight); diff --git a/test/browser/utils.tsx b/test/browser/utils.tsx index 5c476acefd..9f01850cb2 100644 --- a/test/browser/utils.tsx +++ b/test/browser/utils.tsx @@ -1,11 +1,14 @@ -import { page } from '@vitest/browser/context'; +import { page, userEvent } from '@vitest/browser/context'; import { css } from '@linaria/core'; import { DataGrid } from '../../src'; import type { DataGridProps } from '../../src'; -export function setup(props: DataGridProps) { - page.render( +export function setup( + props: DataGridProps, + renderBeforeAfterButtons = false +) { + const grid = ( (props: DataGridPro `} /> ); + + if (renderBeforeAfterButtons) { + page.render( + <> + + {grid} +
+ + + ); + } else { + page.render(grid); + } } export function getGrid() { @@ -76,3 +92,8 @@ export async function scrollGrid({ await new Promise(requestAnimationFrame); } } + +export async function tabIntoGrid() { + await userEvent.click(page.getByRole('button', { name: 'Before' })); + await userEvent.tab(); +} From 196c57ee397de01223009fd2955f2ee2b05cef11 Mon Sep 17 00:00:00 2001 From: amahajan Date: Fri, 12 Sep 2025 16:27:59 -0500 Subject: [PATCH 26/74] Format --- test/browser/column/grouping.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/browser/column/grouping.test.ts b/test/browser/column/grouping.test.ts index 24a7a26027..dbcbe4ba20 100644 --- a/test/browser/column/grouping.test.ts +++ b/test/browser/column/grouping.test.ts @@ -1,6 +1,6 @@ import { page, userEvent } from '@vitest/browser/context'; -import { type ColumnOrColumnGroup } from '../../../src'; +import type { ColumnOrColumnGroup } from '../../../src'; import { getSelectedCell, setup, tabIntoGrid, validateCellPosition } from '../utils'; const columns: readonly ColumnOrColumnGroup>[] = [ From 611bcdd48dbe21ca972960702aa4ed39a6e4b342 Mon Sep 17 00:00:00 2001 From: amahajan Date: Mon, 15 Sep 2025 13:59:44 -0500 Subject: [PATCH 27/74] Use stable version --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f4893bcf90..87a164d84e 100644 --- a/package.json +++ b/package.json @@ -66,8 +66,8 @@ "@typescript-eslint/eslint-plugin": "^8.39.1", "@typescript-eslint/parser": "^8.39.1", "@vitejs/plugin-react": "^5.0.0", - "@vitest/browser": "^4.0.0-beta.8", - "@vitest/coverage-istanbul": "^4.0.0-beta.8", + "@vitest/browser": "^3.2.4", + "@vitest/coverage-istanbul": "^3.2.4", "@vitest/eslint-plugin": "^1.3.4", "@wyw-in-js/rollup": "^0.7.0", "@wyw-in-js/vite": "^0.7.0", @@ -90,7 +90,7 @@ "rolldown-plugin-dts": "^0.16.1", "typescript": "~5.9.2", "vite": "npm:rolldown-vite@^7.1.3", - "vitest": "^4.0.0-beta.8", + "vitest": "^3.2.4", "vitest-browser-react": "^1.0.1" }, "peerDependencies": { From f78134b7c3f5ae8f5051933169a2418d03c8d31c Mon Sep 17 00:00:00 2001 From: amahajan Date: Mon, 15 Sep 2025 14:07:52 -0500 Subject: [PATCH 28/74] -1 --- test/browser/TextEditor.test.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/browser/TextEditor.test.tsx b/test/browser/TextEditor.test.tsx index 500096b947..8551fe4233 100644 --- a/test/browser/TextEditor.test.tsx +++ b/test/browser/TextEditor.test.tsx @@ -31,26 +31,24 @@ test('TextEditor', async () => { page.render(); await userEvent.dblClick(getCells()[0]); - let input = page.getByRole('textbox').element() as HTMLInputElement; - expect(input).toHaveClass('rdg-text-editor'); + const input = page.getByRole('textbox'); + await expect.element(input).toHaveClass('rdg-text-editor'); // input value is row[column.key] - expect(input).toHaveValue(initialRows[0].name); + await expect.element(input).toHaveValue(initialRows[0].name); // input is focused - expect(input).toHaveFocus(); + await expect.element(input).toHaveFocus(); // input value is fully selected - expect(input.selectionStart).toBe(0); - expect(input.selectionEnd).toBe(initialRows[0].name.length); + expect(input).toHaveSelection(initialRows[0].name); // pressing escape closes the editor without committing await userEvent.keyboard('Test{escape}'); - expect(input).not.toBeInTheDocument(); + await expect.element(input).not.toBeInTheDocument(); await expect.element(getCells()[0]).toHaveTextContent(/^Tacitus Kilgore$/); // blurring the input closes and commits the editor await userEvent.dblClick(getCells()[0]); - input = page.getByRole('textbox').element() as HTMLInputElement; await userEvent.fill(input, 'Jim Milton'); await userEvent.tab(); - expect(input).not.toBeInTheDocument(); + await expect.element(input).not.toBeInTheDocument(); await expect.element(getCells()[0]).toHaveTextContent(/^Jim Milton$/); }); From 39ea74ad5fad6ebcbdd6455e3fe44f1db8b5532f Mon Sep 17 00:00:00 2001 From: amahajan Date: Mon, 15 Sep 2025 14:09:46 -0500 Subject: [PATCH 29/74] -1 --- test/browser/column/colSpan.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/browser/column/colSpan.test.ts b/test/browser/column/colSpan.test.ts index 438a6ab970..8089bf4857 100644 --- a/test/browser/column/colSpan.test.ts +++ b/test/browser/column/colSpan.test.ts @@ -37,10 +37,12 @@ describe('colSpan', () => { setup({ columns, rows, bottomSummaryRows: [1, 2], topSummaryRows: [1, 2] }); } - it('should merges cells', () => { + it('should merges cells', async () => { setupColSpanGrid(); // header - expect(getHeaderCells()).toHaveLength(13); + await vi.waitFor(() => { + expect(getHeaderCells()).toHaveLength(13); + }); // top summary rows const topSummarryRow1 = getCellsAtRowIndex(0); From 08f0c7664fde3992091755ac51cf8c033fd0df60 Mon Sep 17 00:00:00 2001 From: amahajan Date: Mon, 15 Sep 2025 14:13:39 -0500 Subject: [PATCH 30/74] -1 --- test/browser/column/renderCell.test.tsx | 40 ++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/test/browser/column/renderCell.test.tsx b/test/browser/column/renderCell.test.tsx index ffd7f8d649..3e5a9e7048 100644 --- a/test/browser/column/renderCell.test.tsx +++ b/test/browser/column/renderCell.test.tsx @@ -93,9 +93,9 @@ describe('Custom cell renderer', () => { page.render(); const [cell] = getCells(); - expect(cell).toHaveTextContent('value: 1'); + await expect.element(cell).toHaveTextContent('value: 1'); await userEvent.click(page.getByRole('button')); - expect(cell).toHaveTextContent('value: 2'); + await expect.element(cell).toHaveTextContent('value: 2'); expect(onChange).toHaveBeenCalledExactlyOnceWith([{ id: 2 }], { column: { ...column, @@ -140,29 +140,29 @@ test('Focus child if it sets tabIndex', async () => { const button1 = page.getByRole('button', { name: 'Button 1' }); const button2 = page.getByRole('button', { name: 'Button 2' }); const cell = page.getByRole('gridcell', { name: 'Button 1 Text Button 2' }); - expect(button1).toHaveAttribute('tabindex', '-1'); - expect(cell).toHaveAttribute('tabindex', '-1'); + await expect.element(button1).toHaveAttribute('tabindex', '-1'); + await expect.element(cell).toHaveAttribute('tabindex', '-1'); await userEvent.click(page.getByText('Text')); - expect(button1).toHaveFocus(); - expect(button1).toHaveAttribute('tabindex', '0'); + await expect.element(button1).toHaveFocus(); + await expect.element(button1).toHaveAttribute('tabindex', '0'); await userEvent.tab({ shift: true }); - expect(button1).not.toHaveFocus(); - expect(button1).toHaveAttribute('tabindex', '-1'); - expect(cell).toHaveAttribute('tabindex', '-1'); + await expect.element(button1).not.toHaveFocus(); + await expect.element(button1).toHaveAttribute('tabindex', '-1'); + await expect.element(cell).toHaveAttribute('tabindex', '-1'); await userEvent.click(button1); - expect(button1).toHaveFocus(); - expect(button1).toHaveAttribute('tabindex', '0'); - expect(cell).toHaveAttribute('tabindex', '-1'); + await expect.element(button1).toHaveFocus(); + await expect.element(button1).toHaveAttribute('tabindex', '0'); + await expect.element(cell).toHaveAttribute('tabindex', '-1'); await userEvent.tab({ shift: true }); await userEvent.click(button2); - expect(button2).toHaveFocus(); + await expect.element(button2).toHaveFocus(); // It is user's responsibilty to set the tabIndex on button2 - expect(button1).toHaveAttribute('tabindex', '0'); - expect(cell).toHaveAttribute('tabindex', '-1'); + await expect.element(button1).toHaveAttribute('tabindex', '0'); + await expect.element(cell).toHaveAttribute('tabindex', '-1'); await userEvent.click(button1); - expect(button1).toHaveFocus(); - expect(button1).toHaveAttribute('tabindex', '0'); - expect(cell).toHaveAttribute('tabindex', '-1'); + await expect.element(button1).toHaveFocus(); + await expect.element(button1).toHaveAttribute('tabindex', '0'); + await expect.element(cell).toHaveAttribute('tabindex', '-1'); }); test('Cell should not steal focus when the focus is outside the grid and cell is recreated', async () => { @@ -196,8 +196,8 @@ test('Cell should not steal focus when the focus is outside the grid and cell is expect(getCellsAtRowIndex(0)[0]).toHaveFocus(); const button = page.getByRole('button', { name: 'Test' }).element(); - expect(button).not.toHaveFocus(); + await expect.element(button).not.toHaveFocus(); await userEvent.click(button); expect(getCellsAtRowIndex(0)[0]).not.toHaveFocus(); - expect(button).toHaveFocus(); + await expect.element(button).toHaveFocus(); }); From 6f8fd64c876efff89b1b1aedc315e4d24a2528a1 Mon Sep 17 00:00:00 2001 From: amahajan Date: Mon, 15 Sep 2025 14:26:12 -0500 Subject: [PATCH 31/74] -2 --- test/browser/TextEditor.test.tsx | 2 +- test/browser/sorting.test.tsx | 42 +++++++++++++++++--------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/test/browser/TextEditor.test.tsx b/test/browser/TextEditor.test.tsx index 8551fe4233..e395075796 100644 --- a/test/browser/TextEditor.test.tsx +++ b/test/browser/TextEditor.test.tsx @@ -38,7 +38,7 @@ test('TextEditor', async () => { // input is focused await expect.element(input).toHaveFocus(); // input value is fully selected - expect(input).toHaveSelection(initialRows[0].name); + await expect.element(input).toHaveSelection(initialRows[0].name); // pressing escape closes the editor without committing await userEvent.keyboard('Test{escape}'); diff --git a/test/browser/sorting.test.tsx b/test/browser/sorting.test.tsx index 8f1e829766..c39df38f02 100644 --- a/test/browser/sorting.test.tsx +++ b/test/browser/sorting.test.tsx @@ -3,7 +3,6 @@ import { page, userEvent } from '@vitest/browser/context'; import { DataGrid } from '../../src'; import type { Column, SortColumn } from '../../src/types'; -import { getHeaderCells } from './utils'; const columns: readonly Column[] = [ { key: 'colA', name: 'colA' }, @@ -41,43 +40,45 @@ function testSortColumns(expectedValue: readonly SortColumn[]) { test('should not sort if sortable is false', async () => { setup(); - const headerCell = getHeaderCells()[3]; + const headerCell = page.getByRole('columnheader', { name: 'colD' }); await userEvent.click(headerCell); - expect(headerCell).not.toHaveAttribute('aria-sort'); + await expect.element(headerCell).not.toHaveAttribute('aria-sort'); await testSortColumns([]); }); test('single column sort', async () => { setup(); - const headerCell = getHeaderCells()[0]; + const headerCell = page.getByRole('columnheader', { name: 'colA' }); await userEvent.click(headerCell); - expect(headerCell).toHaveAttribute('aria-sort', 'ascending'); + await expect.element(headerCell).toHaveAttribute('aria-sort', 'ascending'); // priority is not shown for single sort - expect(headerCell).not.toHaveTextContent('1'); + await expect.element(headerCell).not.toHaveTextContent('1'); await testSortColumns([{ columnKey: 'colA', direction: 'ASC' }]); await userEvent.click(headerCell); - expect(headerCell).toHaveAttribute('aria-sort', 'descending'); + await expect.element(headerCell).toHaveAttribute('aria-sort', 'descending'); await testSortColumns([{ columnKey: 'colA', direction: 'DESC' }]); await userEvent.click(headerCell); - expect(headerCell).not.toHaveAttribute('aria-sort'); + await expect.element(headerCell).not.toHaveAttribute('aria-sort'); await testSortColumns([]); }); test('multi column sort', async () => { setup(); - const [headerCell1, headerCell2, headerCell3] = getHeaderCells(); + const headerCell1 = page.getByRole('columnheader', { name: 'colA' }); + const headerCell2 = page.getByRole('columnheader', { name: 'colB' }); + const headerCell3 = page.getByRole('columnheader', { name: 'colC' }); await userEvent.click(headerCell1); await userEvent.keyboard('{Control>}'); await userEvent.click(headerCell2); await userEvent.click(headerCell3); // aria-sort is only added for single sort - expect(headerCell1).not.toHaveAttribute('aria-sort'); - expect(headerCell1).toHaveTextContent('1'); // priority - expect(headerCell2).not.toHaveAttribute('aria-sort'); - expect(headerCell2).toHaveTextContent('2'); - expect(headerCell3).not.toHaveAttribute('aria-sort'); - expect(headerCell3).toHaveTextContent('3'); + await expect.element(headerCell1).not.toHaveAttribute('aria-sort'); + await expect.element(headerCell1).toHaveTextContent('1'); // priority + await expect.element(headerCell2).not.toHaveAttribute('aria-sort'); + await expect.element(headerCell2).toHaveTextContent('2'); + await expect.element(headerCell3).not.toHaveAttribute('aria-sort'); + await expect.element(headerCell3).toHaveTextContent('3'); await testSortColumns([ { columnKey: 'colA', direction: 'ASC' }, { columnKey: 'colB', direction: 'DESC' }, @@ -95,19 +96,20 @@ test('multi column sort', async () => { { columnKey: 'colA', direction: 'ASC' }, { columnKey: 'colC', direction: 'ASC' } ]); - expect(headerCell3).toHaveTextContent('2'); + await expect.element(headerCell3).toHaveTextContent('2'); // clicking on a column without ctrlKey should remove multisort await userEvent.keyboard('{/Control}'); await userEvent.click(headerCell2); await testSortColumns([{ columnKey: 'colB', direction: 'DESC' }]); - expect(headerCell2).toHaveAttribute('aria-sort'); - expect(headerCell2).not.toHaveTextContent('2'); + await expect.element(headerCell2).toHaveAttribute('aria-sort'); + await expect.element(headerCell2).not.toHaveTextContent('2'); }); test('multi column sort with metakey', async () => { setup(); - const [headerCell1, headerCell2] = getHeaderCells(); + const headerCell1 = page.getByRole('columnheader', { name: 'colA' }); + const headerCell2 = page.getByRole('columnheader', { name: 'colB' }); await userEvent.click(headerCell1); await userEvent.keyboard('{Meta>}'); await userEvent.click(headerCell2); @@ -119,7 +121,7 @@ test('multi column sort with metakey', async () => { test('multi column sort with keyboard', async () => { setup(); - const [headerCell1] = getHeaderCells(); + const headerCell1 = page.getByRole('columnheader', { name: 'colA' }); await userEvent.click(headerCell1); await userEvent.keyboard(' {arrowright}{Control>}{enter}'); await testSortColumns([ From d420c4447de5f438124791300e3f4af703dd0305 Mon Sep 17 00:00:00 2001 From: amahajan Date: Mon, 15 Sep 2025 14:32:09 -0500 Subject: [PATCH 32/74] tryl gridcell role --- test/browser/copyPaste.test.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/browser/copyPaste.test.tsx b/test/browser/copyPaste.test.tsx index d71b7227a1..58447565a7 100644 --- a/test/browser/copyPaste.test.tsx +++ b/test/browser/copyPaste.test.tsx @@ -70,7 +70,7 @@ function setup() { test('should call onCellCopy on cell copy', async () => { setup(); - await userEvent.click(getCellsAtRowIndex(0)[0]); + await userEvent.click(page.getByRole('gridcell', { name: 'a1' })); await userEvent.copy(); expect(onCellCopySpy).toHaveBeenCalledExactlyOnceWith( { @@ -83,7 +83,7 @@ test('should call onCellCopy on cell copy', async () => { test('should call onCellPaste on cell paste', async () => { setup(); - await userEvent.click(getCellsAtRowIndex(0)[0]); + await userEvent.click(page.getByRole('gridcell', { name: 'a1' })); await userEvent.paste(); expect(onCellPasteSpy).toHaveBeenCalledExactlyOnceWith( { @@ -96,14 +96,14 @@ test('should call onCellPaste on cell paste', async () => { test('should not allow paste on readonly cells', async () => { setup(); - await userEvent.click(getCellsAtRowIndex(2)[0]); + await userEvent.click(page.getByRole('gridcell', { name: 'a3' })); await userEvent.paste(); expect(onCellPasteSpy).not.toHaveBeenCalled(); }); test('should allow copying a readonly cell', async () => { setup(); - await userEvent.click(getCellsAtRowIndex(2)[0]); + await userEvent.click(page.getByRole('gridcell', { name: 'a3' })); await userEvent.copy(); expect(onCellCopySpy).toHaveBeenCalledExactlyOnceWith( { @@ -131,7 +131,7 @@ test('should not allow copy/paste on header or summary cells', async () => { test('should not start editing when pressing ctrl+', async () => { setup(); - await userEvent.click(getCellsAtRowIndex(1)[0]); + await userEvent.click(page.getByRole('gridcell', { name: 'a2' })); await userEvent.keyboard('{Control>}b'); await expect.element(getSelectedCell()).not.toHaveClass('rdg-editor-container'); }); From b1de604683ac9c868e7aad597e9a5ab3277d5a1f Mon Sep 17 00:00:00 2001 From: amahajan Date: Mon, 15 Sep 2025 14:33:52 -0500 Subject: [PATCH 33/74] biome --- test/browser/copyPaste.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/browser/copyPaste.test.tsx b/test/browser/copyPaste.test.tsx index 58447565a7..5e8726ef3c 100644 --- a/test/browser/copyPaste.test.tsx +++ b/test/browser/copyPaste.test.tsx @@ -3,7 +3,7 @@ import { page, userEvent } from '@vitest/browser/context'; import { DataGrid } from '../../src'; import type { CellPasteArgs, Column } from '../../src'; -import { getCellsAtRowIndex, getSelectedCell } from './utils'; +import { getSelectedCell } from './utils'; interface Row { col: string; From 9a341b5a8c6c6e4ea0a33361b6aec085598a0d51 Mon Sep 17 00:00:00 2001 From: amahajan Date: Mon, 15 Sep 2025 15:57:29 -0500 Subject: [PATCH 34/74] Remove timeout --- vite.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index a56a7b976d..3f58c06f4f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -84,7 +84,6 @@ export default defineConfig(({ command, isPreview }) => ({ include: ['src/**/*.{ts,tsx}'], reporter: ['json'] }, - testTimeout: 20_000, restoreMocks: true, sequence: { shuffle: true From f1f752b75d1ebe018ed0ea58837126b1db076353 Mon Sep 17 00:00:00 2001 From: amahajan Date: Mon, 15 Sep 2025 16:22:37 -0500 Subject: [PATCH 35/74] Does this work? --- test/browser/rowSelection.test.tsx | 89 ++++++++++++++++-------------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/test/browser/rowSelection.test.tsx b/test/browser/rowSelection.test.tsx index a37c375a91..fafd28cac8 100644 --- a/test/browser/rowSelection.test.tsx +++ b/test/browser/rowSelection.test.tsx @@ -3,7 +3,7 @@ import { page, userEvent } from '@vitest/browser/context'; import { DataGrid, SelectColumn } from '../../src'; import type { Column } from '../../src'; -import { getCellsAtRowIndex, getRows } from './utils'; +import { getCellsAtRowIndex } from './utils'; interface Row { id: number; @@ -13,7 +13,10 @@ const columns: readonly Column[] = [ SelectColumn, { key: 'name', - name: 'Name' + name: 'Name', + renderCell(props) { + return props.row.id; + } } ]; @@ -49,11 +52,15 @@ function setup(initialRows = defaultRows) { } function testSelection(rowIdx: number, isSelected: boolean) { - expect(getRows()[rowIdx]).toHaveAttribute('aria-selected', isSelected ? 'true' : 'false'); + return expect + .element(page.getByRole('row', { name: (rowIdx + 1).toString() })) + .toHaveAttribute('aria-selected', isSelected ? 'true' : 'false'); } async function toggleSelection(rowIdx: number, shift = false) { - const element = page.getByRole('row').all()[rowIdx + 1].getByRole('checkbox', { name: 'Select' }); + const element = page + .getByRole('row', { name: (rowIdx + 1).toString() }) + .getByRole('checkbox', { name: 'Select' }); if (shift) await userEvent.keyboard('{Shift>}'); await userEvent.click(element, { force: true }); if (shift) await userEvent.keyboard('{/Shift}'); @@ -62,29 +69,29 @@ async function toggleSelection(rowIdx: number, shift = false) { test('toggle selection when checkbox is clicked', async () => { setup(); await toggleSelection(0); - testSelection(0, true); + await testSelection(0, true); await toggleSelection(1); - testSelection(1, true); + await testSelection(1, true); await toggleSelection(0); - testSelection(0, false); + await testSelection(0, false); await toggleSelection(1); - testSelection(1, false); + await testSelection(1, false); }); test('toggle selection using keyboard', async () => { setup(); - testSelection(0, false); + await testSelection(0, false); await userEvent.click(getCellsAtRowIndex(0)[0]); - testSelection(0, true); + await testSelection(0, true); await userEvent.keyboard(' '); - testSelection(0, false); + await testSelection(0, false); await userEvent.keyboard(' '); - testSelection(0, true); + await testSelection(0, true); await userEvent.keyboard('{arrowdown} '); - testSelection(1, true); + await testSelection(1, true); await userEvent.keyboard('{arrowup} '); - testSelection(0, false); + await testSelection(0, false); }); test('should partially select header checkbox', async () => { @@ -110,14 +117,14 @@ test('should partially select header checkbox', async () => { expect(headerCheckbox).toBePartiallyChecked(); await userEvent.click(headerCheckbox); - testSelection(0, false); - testSelection(1, false); - testSelection(2, false); + await testSelection(0, false); + await testSelection(1, false); + await testSelection(2, false); await userEvent.click(headerCheckbox); - testSelection(0, true); - testSelection(1, true); - testSelection(2, true); + await testSelection(0, true); + await testSelection(1, true); + await testSelection(2, true); }); test('should not select row when isRowSelectionDisabled returns true', async () => { @@ -125,18 +132,18 @@ test('should not select row when isRowSelectionDisabled returns true', async () row.id === 2} /> ); await toggleSelection(0); - testSelection(0, true); + await testSelection(0, true); await toggleSelection(1); - testSelection(1, false); + await testSelection(1, false); await toggleSelection(2); - testSelection(2, true); + await testSelection(2, true); await userEvent.click(page.getByRole('checkbox', { name: 'Select All' })); await toggleSelection(0); await toggleSelection(2, true); - testSelection(0, true); - testSelection(1, false); - testSelection(2, true); + await testSelection(0, true); + await testSelection(1, false); + await testSelection(2, true); }); test('select/deselect all rows when header checkbox is clicked', async () => { @@ -144,9 +151,9 @@ test('select/deselect all rows when header checkbox is clicked', async () => { const headerCheckbox = page.getByRole('checkbox', { name: 'Select All' }).element(); expect(headerCheckbox).not.toBeChecked(); await userEvent.click(headerCheckbox); - testSelection(0, true); - testSelection(1, true); - testSelection(2, true); + await testSelection(0, true); + await testSelection(1, true); + await testSelection(2, true); // deselecting a row should toggle header await toggleSelection(0); @@ -155,9 +162,9 @@ test('select/deselect all rows when header checkbox is clicked', async () => { expect(headerCheckbox).toBeChecked(); await userEvent.click(headerCheckbox); - testSelection(0, false); - testSelection(1, false); - testSelection(2, false); + await testSelection(0, false); + await testSelection(1, false); + await testSelection(2, false); }); test('header checkbox is not checked when there are no rows', async () => { @@ -241,23 +248,23 @@ test('select/deselect rows using shift click', async () => { setup(); await toggleSelection(0); await toggleSelection(2, true); - testSelection(0, true); - testSelection(1, true); - testSelection(2, true); + await testSelection(0, true); + await testSelection(1, true); + await testSelection(2, true); await toggleSelection(0); await toggleSelection(2, true); - testSelection(0, false); - testSelection(1, false); - testSelection(2, false); + await testSelection(0, false); + await testSelection(1, false); + await testSelection(2, false); }); test('select rows using shift space', async () => { setup(); await userEvent.click(getCellsAtRowIndex(0)[1]); await userEvent.keyboard('{Shift>} {/Shift}'); - testSelection(0, true); + await testSelection(0, true); await userEvent.keyboard(' '); - testSelection(0, true); + await testSelection(0, true); await userEvent.keyboard('{Shift>} {/Shift}'); - testSelection(0, false); + await testSelection(0, false); }); From 48fa0bb336a786f61071327f29d0acefcbb28307 Mon Sep 17 00:00:00 2001 From: amahajan Date: Mon, 15 Sep 2025 16:33:54 -0500 Subject: [PATCH 36/74] Set maxWorkers = 1 --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 3f58c06f4f..ec49a6277c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -77,7 +77,7 @@ export default defineConfig(({ command, isPreview }) => ({ }, test: { globals: true, - maxWorkers: 4, + maxWorkers: 1, coverage: { provider: 'istanbul', enabled: isCI, From 6af8584f305d1d0b6d67dae92e4433d5a2e83394 Mon Sep 17 00:00:00 2001 From: amahajan Date: Mon, 15 Sep 2025 16:41:26 -0500 Subject: [PATCH 37/74] revert a few changes --- test/browser/column/colSpan.test.ts | 6 +- test/browser/rowSelection.test.tsx | 89 +++++++++++++---------------- 2 files changed, 43 insertions(+), 52 deletions(-) diff --git a/test/browser/column/colSpan.test.ts b/test/browser/column/colSpan.test.ts index 8089bf4857..438a6ab970 100644 --- a/test/browser/column/colSpan.test.ts +++ b/test/browser/column/colSpan.test.ts @@ -37,12 +37,10 @@ describe('colSpan', () => { setup({ columns, rows, bottomSummaryRows: [1, 2], topSummaryRows: [1, 2] }); } - it('should merges cells', async () => { + it('should merges cells', () => { setupColSpanGrid(); // header - await vi.waitFor(() => { - expect(getHeaderCells()).toHaveLength(13); - }); + expect(getHeaderCells()).toHaveLength(13); // top summary rows const topSummarryRow1 = getCellsAtRowIndex(0); diff --git a/test/browser/rowSelection.test.tsx b/test/browser/rowSelection.test.tsx index fafd28cac8..a37c375a91 100644 --- a/test/browser/rowSelection.test.tsx +++ b/test/browser/rowSelection.test.tsx @@ -3,7 +3,7 @@ import { page, userEvent } from '@vitest/browser/context'; import { DataGrid, SelectColumn } from '../../src'; import type { Column } from '../../src'; -import { getCellsAtRowIndex } from './utils'; +import { getCellsAtRowIndex, getRows } from './utils'; interface Row { id: number; @@ -13,10 +13,7 @@ const columns: readonly Column[] = [ SelectColumn, { key: 'name', - name: 'Name', - renderCell(props) { - return props.row.id; - } + name: 'Name' } ]; @@ -52,15 +49,11 @@ function setup(initialRows = defaultRows) { } function testSelection(rowIdx: number, isSelected: boolean) { - return expect - .element(page.getByRole('row', { name: (rowIdx + 1).toString() })) - .toHaveAttribute('aria-selected', isSelected ? 'true' : 'false'); + expect(getRows()[rowIdx]).toHaveAttribute('aria-selected', isSelected ? 'true' : 'false'); } async function toggleSelection(rowIdx: number, shift = false) { - const element = page - .getByRole('row', { name: (rowIdx + 1).toString() }) - .getByRole('checkbox', { name: 'Select' }); + const element = page.getByRole('row').all()[rowIdx + 1].getByRole('checkbox', { name: 'Select' }); if (shift) await userEvent.keyboard('{Shift>}'); await userEvent.click(element, { force: true }); if (shift) await userEvent.keyboard('{/Shift}'); @@ -69,29 +62,29 @@ async function toggleSelection(rowIdx: number, shift = false) { test('toggle selection when checkbox is clicked', async () => { setup(); await toggleSelection(0); - await testSelection(0, true); + testSelection(0, true); await toggleSelection(1); - await testSelection(1, true); + testSelection(1, true); await toggleSelection(0); - await testSelection(0, false); + testSelection(0, false); await toggleSelection(1); - await testSelection(1, false); + testSelection(1, false); }); test('toggle selection using keyboard', async () => { setup(); - await testSelection(0, false); + testSelection(0, false); await userEvent.click(getCellsAtRowIndex(0)[0]); - await testSelection(0, true); + testSelection(0, true); await userEvent.keyboard(' '); - await testSelection(0, false); + testSelection(0, false); await userEvent.keyboard(' '); - await testSelection(0, true); + testSelection(0, true); await userEvent.keyboard('{arrowdown} '); - await testSelection(1, true); + testSelection(1, true); await userEvent.keyboard('{arrowup} '); - await testSelection(0, false); + testSelection(0, false); }); test('should partially select header checkbox', async () => { @@ -117,14 +110,14 @@ test('should partially select header checkbox', async () => { expect(headerCheckbox).toBePartiallyChecked(); await userEvent.click(headerCheckbox); - await testSelection(0, false); - await testSelection(1, false); - await testSelection(2, false); + testSelection(0, false); + testSelection(1, false); + testSelection(2, false); await userEvent.click(headerCheckbox); - await testSelection(0, true); - await testSelection(1, true); - await testSelection(2, true); + testSelection(0, true); + testSelection(1, true); + testSelection(2, true); }); test('should not select row when isRowSelectionDisabled returns true', async () => { @@ -132,18 +125,18 @@ test('should not select row when isRowSelectionDisabled returns true', async () row.id === 2} /> ); await toggleSelection(0); - await testSelection(0, true); + testSelection(0, true); await toggleSelection(1); - await testSelection(1, false); + testSelection(1, false); await toggleSelection(2); - await testSelection(2, true); + testSelection(2, true); await userEvent.click(page.getByRole('checkbox', { name: 'Select All' })); await toggleSelection(0); await toggleSelection(2, true); - await testSelection(0, true); - await testSelection(1, false); - await testSelection(2, true); + testSelection(0, true); + testSelection(1, false); + testSelection(2, true); }); test('select/deselect all rows when header checkbox is clicked', async () => { @@ -151,9 +144,9 @@ test('select/deselect all rows when header checkbox is clicked', async () => { const headerCheckbox = page.getByRole('checkbox', { name: 'Select All' }).element(); expect(headerCheckbox).not.toBeChecked(); await userEvent.click(headerCheckbox); - await testSelection(0, true); - await testSelection(1, true); - await testSelection(2, true); + testSelection(0, true); + testSelection(1, true); + testSelection(2, true); // deselecting a row should toggle header await toggleSelection(0); @@ -162,9 +155,9 @@ test('select/deselect all rows when header checkbox is clicked', async () => { expect(headerCheckbox).toBeChecked(); await userEvent.click(headerCheckbox); - await testSelection(0, false); - await testSelection(1, false); - await testSelection(2, false); + testSelection(0, false); + testSelection(1, false); + testSelection(2, false); }); test('header checkbox is not checked when there are no rows', async () => { @@ -248,23 +241,23 @@ test('select/deselect rows using shift click', async () => { setup(); await toggleSelection(0); await toggleSelection(2, true); - await testSelection(0, true); - await testSelection(1, true); - await testSelection(2, true); + testSelection(0, true); + testSelection(1, true); + testSelection(2, true); await toggleSelection(0); await toggleSelection(2, true); - await testSelection(0, false); - await testSelection(1, false); - await testSelection(2, false); + testSelection(0, false); + testSelection(1, false); + testSelection(2, false); }); test('select rows using shift space', async () => { setup(); await userEvent.click(getCellsAtRowIndex(0)[1]); await userEvent.keyboard('{Shift>} {/Shift}'); - await testSelection(0, true); + testSelection(0, true); await userEvent.keyboard(' '); - await testSelection(0, true); + testSelection(0, true); await userEvent.keyboard('{Shift>} {/Shift}'); - await testSelection(0, false); + testSelection(0, false); }); From dad063bd58355cf78fadb0484b474b2bbfe173a3 Mon Sep 17 00:00:00 2001 From: amahajan Date: Mon, 15 Sep 2025 16:47:23 -0500 Subject: [PATCH 38/74] Revert 1 more change --- test/browser/column/renderCell.test.tsx | 40 ++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/test/browser/column/renderCell.test.tsx b/test/browser/column/renderCell.test.tsx index 3e5a9e7048..ffd7f8d649 100644 --- a/test/browser/column/renderCell.test.tsx +++ b/test/browser/column/renderCell.test.tsx @@ -93,9 +93,9 @@ describe('Custom cell renderer', () => { page.render(); const [cell] = getCells(); - await expect.element(cell).toHaveTextContent('value: 1'); + expect(cell).toHaveTextContent('value: 1'); await userEvent.click(page.getByRole('button')); - await expect.element(cell).toHaveTextContent('value: 2'); + expect(cell).toHaveTextContent('value: 2'); expect(onChange).toHaveBeenCalledExactlyOnceWith([{ id: 2 }], { column: { ...column, @@ -140,29 +140,29 @@ test('Focus child if it sets tabIndex', async () => { const button1 = page.getByRole('button', { name: 'Button 1' }); const button2 = page.getByRole('button', { name: 'Button 2' }); const cell = page.getByRole('gridcell', { name: 'Button 1 Text Button 2' }); - await expect.element(button1).toHaveAttribute('tabindex', '-1'); - await expect.element(cell).toHaveAttribute('tabindex', '-1'); + expect(button1).toHaveAttribute('tabindex', '-1'); + expect(cell).toHaveAttribute('tabindex', '-1'); await userEvent.click(page.getByText('Text')); - await expect.element(button1).toHaveFocus(); - await expect.element(button1).toHaveAttribute('tabindex', '0'); + expect(button1).toHaveFocus(); + expect(button1).toHaveAttribute('tabindex', '0'); await userEvent.tab({ shift: true }); - await expect.element(button1).not.toHaveFocus(); - await expect.element(button1).toHaveAttribute('tabindex', '-1'); - await expect.element(cell).toHaveAttribute('tabindex', '-1'); + expect(button1).not.toHaveFocus(); + expect(button1).toHaveAttribute('tabindex', '-1'); + expect(cell).toHaveAttribute('tabindex', '-1'); await userEvent.click(button1); - await expect.element(button1).toHaveFocus(); - await expect.element(button1).toHaveAttribute('tabindex', '0'); - await expect.element(cell).toHaveAttribute('tabindex', '-1'); + expect(button1).toHaveFocus(); + expect(button1).toHaveAttribute('tabindex', '0'); + expect(cell).toHaveAttribute('tabindex', '-1'); await userEvent.tab({ shift: true }); await userEvent.click(button2); - await expect.element(button2).toHaveFocus(); + expect(button2).toHaveFocus(); // It is user's responsibilty to set the tabIndex on button2 - await expect.element(button1).toHaveAttribute('tabindex', '0'); - await expect.element(cell).toHaveAttribute('tabindex', '-1'); + expect(button1).toHaveAttribute('tabindex', '0'); + expect(cell).toHaveAttribute('tabindex', '-1'); await userEvent.click(button1); - await expect.element(button1).toHaveFocus(); - await expect.element(button1).toHaveAttribute('tabindex', '0'); - await expect.element(cell).toHaveAttribute('tabindex', '-1'); + expect(button1).toHaveFocus(); + expect(button1).toHaveAttribute('tabindex', '0'); + expect(cell).toHaveAttribute('tabindex', '-1'); }); test('Cell should not steal focus when the focus is outside the grid and cell is recreated', async () => { @@ -196,8 +196,8 @@ test('Cell should not steal focus when the focus is outside the grid and cell is expect(getCellsAtRowIndex(0)[0]).toHaveFocus(); const button = page.getByRole('button', { name: 'Test' }).element(); - await expect.element(button).not.toHaveFocus(); + expect(button).not.toHaveFocus(); await userEvent.click(button); expect(getCellsAtRowIndex(0)[0]).not.toHaveFocus(); - await expect.element(button).toHaveFocus(); + expect(button).toHaveFocus(); }); From 41e0d2148c72e97dd5f63312ee247a689b02b82b Mon Sep 17 00:00:00 2001 From: amahajan Date: Mon, 15 Sep 2025 16:48:15 -0500 Subject: [PATCH 39/74] one more --- test/browser/sorting.test.tsx | 42 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/test/browser/sorting.test.tsx b/test/browser/sorting.test.tsx index c39df38f02..8f1e829766 100644 --- a/test/browser/sorting.test.tsx +++ b/test/browser/sorting.test.tsx @@ -3,6 +3,7 @@ import { page, userEvent } from '@vitest/browser/context'; import { DataGrid } from '../../src'; import type { Column, SortColumn } from '../../src/types'; +import { getHeaderCells } from './utils'; const columns: readonly Column[] = [ { key: 'colA', name: 'colA' }, @@ -40,45 +41,43 @@ function testSortColumns(expectedValue: readonly SortColumn[]) { test('should not sort if sortable is false', async () => { setup(); - const headerCell = page.getByRole('columnheader', { name: 'colD' }); + const headerCell = getHeaderCells()[3]; await userEvent.click(headerCell); - await expect.element(headerCell).not.toHaveAttribute('aria-sort'); + expect(headerCell).not.toHaveAttribute('aria-sort'); await testSortColumns([]); }); test('single column sort', async () => { setup(); - const headerCell = page.getByRole('columnheader', { name: 'colA' }); + const headerCell = getHeaderCells()[0]; await userEvent.click(headerCell); - await expect.element(headerCell).toHaveAttribute('aria-sort', 'ascending'); + expect(headerCell).toHaveAttribute('aria-sort', 'ascending'); // priority is not shown for single sort - await expect.element(headerCell).not.toHaveTextContent('1'); + expect(headerCell).not.toHaveTextContent('1'); await testSortColumns([{ columnKey: 'colA', direction: 'ASC' }]); await userEvent.click(headerCell); - await expect.element(headerCell).toHaveAttribute('aria-sort', 'descending'); + expect(headerCell).toHaveAttribute('aria-sort', 'descending'); await testSortColumns([{ columnKey: 'colA', direction: 'DESC' }]); await userEvent.click(headerCell); - await expect.element(headerCell).not.toHaveAttribute('aria-sort'); + expect(headerCell).not.toHaveAttribute('aria-sort'); await testSortColumns([]); }); test('multi column sort', async () => { setup(); - const headerCell1 = page.getByRole('columnheader', { name: 'colA' }); - const headerCell2 = page.getByRole('columnheader', { name: 'colB' }); - const headerCell3 = page.getByRole('columnheader', { name: 'colC' }); + const [headerCell1, headerCell2, headerCell3] = getHeaderCells(); await userEvent.click(headerCell1); await userEvent.keyboard('{Control>}'); await userEvent.click(headerCell2); await userEvent.click(headerCell3); // aria-sort is only added for single sort - await expect.element(headerCell1).not.toHaveAttribute('aria-sort'); - await expect.element(headerCell1).toHaveTextContent('1'); // priority - await expect.element(headerCell2).not.toHaveAttribute('aria-sort'); - await expect.element(headerCell2).toHaveTextContent('2'); - await expect.element(headerCell3).not.toHaveAttribute('aria-sort'); - await expect.element(headerCell3).toHaveTextContent('3'); + expect(headerCell1).not.toHaveAttribute('aria-sort'); + expect(headerCell1).toHaveTextContent('1'); // priority + expect(headerCell2).not.toHaveAttribute('aria-sort'); + expect(headerCell2).toHaveTextContent('2'); + expect(headerCell3).not.toHaveAttribute('aria-sort'); + expect(headerCell3).toHaveTextContent('3'); await testSortColumns([ { columnKey: 'colA', direction: 'ASC' }, { columnKey: 'colB', direction: 'DESC' }, @@ -96,20 +95,19 @@ test('multi column sort', async () => { { columnKey: 'colA', direction: 'ASC' }, { columnKey: 'colC', direction: 'ASC' } ]); - await expect.element(headerCell3).toHaveTextContent('2'); + expect(headerCell3).toHaveTextContent('2'); // clicking on a column without ctrlKey should remove multisort await userEvent.keyboard('{/Control}'); await userEvent.click(headerCell2); await testSortColumns([{ columnKey: 'colB', direction: 'DESC' }]); - await expect.element(headerCell2).toHaveAttribute('aria-sort'); - await expect.element(headerCell2).not.toHaveTextContent('2'); + expect(headerCell2).toHaveAttribute('aria-sort'); + expect(headerCell2).not.toHaveTextContent('2'); }); test('multi column sort with metakey', async () => { setup(); - const headerCell1 = page.getByRole('columnheader', { name: 'colA' }); - const headerCell2 = page.getByRole('columnheader', { name: 'colB' }); + const [headerCell1, headerCell2] = getHeaderCells(); await userEvent.click(headerCell1); await userEvent.keyboard('{Meta>}'); await userEvent.click(headerCell2); @@ -121,7 +119,7 @@ test('multi column sort with metakey', async () => { test('multi column sort with keyboard', async () => { setup(); - const headerCell1 = page.getByRole('columnheader', { name: 'colA' }); + const [headerCell1] = getHeaderCells(); await userEvent.click(headerCell1); await userEvent.keyboard(' {arrowright}{Control>}{enter}'); await testSortColumns([ From fbdb6aaa42af0cd00a72c4ccd36e77060aadd904 Mon Sep 17 00:00:00 2001 From: amahajan Date: Mon, 15 Sep 2025 22:06:53 -0500 Subject: [PATCH 40/74] Revert a few more changes --- test/browser/TextEditor.test.tsx | 15 ++++++++------- test/browser/copyPaste.test.tsx | 12 ++++++------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/test/browser/TextEditor.test.tsx b/test/browser/TextEditor.test.tsx index e395075796..a18f40e461 100644 --- a/test/browser/TextEditor.test.tsx +++ b/test/browser/TextEditor.test.tsx @@ -31,24 +31,25 @@ test('TextEditor', async () => { page.render(); await userEvent.dblClick(getCells()[0]); - const input = page.getByRole('textbox'); - await expect.element(input).toHaveClass('rdg-text-editor'); + let input = page.getByRole('textbox').element() as HTMLInputElement; + expect(input).toHaveClass('rdg-text-editor'); // input value is row[column.key] - await expect.element(input).toHaveValue(initialRows[0].name); + expect(input).toHaveValue(initialRows[0].name); // input is focused - await expect.element(input).toHaveFocus(); + expect(input).toHaveFocus(); // input value is fully selected - await expect.element(input).toHaveSelection(initialRows[0].name); + expect(input).toHaveSelection(initialRows[0].name); // pressing escape closes the editor without committing await userEvent.keyboard('Test{escape}'); - await expect.element(input).not.toBeInTheDocument(); + expect(input).not.toBeInTheDocument(); await expect.element(getCells()[0]).toHaveTextContent(/^Tacitus Kilgore$/); // blurring the input closes and commits the editor await userEvent.dblClick(getCells()[0]); + input = page.getByRole('textbox').element() as HTMLInputElement; await userEvent.fill(input, 'Jim Milton'); await userEvent.tab(); - await expect.element(input).not.toBeInTheDocument(); + expect(input).not.toBeInTheDocument(); await expect.element(getCells()[0]).toHaveTextContent(/^Jim Milton$/); }); diff --git a/test/browser/copyPaste.test.tsx b/test/browser/copyPaste.test.tsx index 5e8726ef3c..d71b7227a1 100644 --- a/test/browser/copyPaste.test.tsx +++ b/test/browser/copyPaste.test.tsx @@ -3,7 +3,7 @@ import { page, userEvent } from '@vitest/browser/context'; import { DataGrid } from '../../src'; import type { CellPasteArgs, Column } from '../../src'; -import { getSelectedCell } from './utils'; +import { getCellsAtRowIndex, getSelectedCell } from './utils'; interface Row { col: string; @@ -70,7 +70,7 @@ function setup() { test('should call onCellCopy on cell copy', async () => { setup(); - await userEvent.click(page.getByRole('gridcell', { name: 'a1' })); + await userEvent.click(getCellsAtRowIndex(0)[0]); await userEvent.copy(); expect(onCellCopySpy).toHaveBeenCalledExactlyOnceWith( { @@ -83,7 +83,7 @@ test('should call onCellCopy on cell copy', async () => { test('should call onCellPaste on cell paste', async () => { setup(); - await userEvent.click(page.getByRole('gridcell', { name: 'a1' })); + await userEvent.click(getCellsAtRowIndex(0)[0]); await userEvent.paste(); expect(onCellPasteSpy).toHaveBeenCalledExactlyOnceWith( { @@ -96,14 +96,14 @@ test('should call onCellPaste on cell paste', async () => { test('should not allow paste on readonly cells', async () => { setup(); - await userEvent.click(page.getByRole('gridcell', { name: 'a3' })); + await userEvent.click(getCellsAtRowIndex(2)[0]); await userEvent.paste(); expect(onCellPasteSpy).not.toHaveBeenCalled(); }); test('should allow copying a readonly cell', async () => { setup(); - await userEvent.click(page.getByRole('gridcell', { name: 'a3' })); + await userEvent.click(getCellsAtRowIndex(2)[0]); await userEvent.copy(); expect(onCellCopySpy).toHaveBeenCalledExactlyOnceWith( { @@ -131,7 +131,7 @@ test('should not allow copy/paste on header or summary cells', async () => { test('should not start editing when pressing ctrl+', async () => { setup(); - await userEvent.click(page.getByRole('gridcell', { name: 'a2' })); + await userEvent.click(getCellsAtRowIndex(1)[0]); await userEvent.keyboard('{Control>}b'); await expect.element(getSelectedCell()).not.toHaveClass('rdg-editor-container'); }); From 9f9f9b4f3a5bc66420a7a420fd14d9e740aaadc9 Mon Sep 17 00:00:00 2001 From: amahajan Date: Tue, 16 Sep 2025 09:01:47 -0500 Subject: [PATCH 41/74] Fix resizable test --- test/browser/column/resizable.test.tsx | 21 ++++++++++++++++----- vite.config.ts | 1 + 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index 742078a447..fc796d21ae 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -151,10 +151,13 @@ test('should auto resize column when resize handle is double clicked', async () await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' }); const [, col2] = getHeaderCells(); await autoResize(col2); - await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 327.703px' }); + expect((grid.element() as HTMLDivElement).style.gridTemplateColumns).toBeOneOf([ + '100px 327.703px', // chrome + '100px 327.833px' // firefox + ]); expect(onColumnResize).toHaveBeenCalledExactlyOnceWith( expect.objectContaining(columns[1]), - 327.703125 + expect.toSatisfy((width) => width >= 327.7 && width <= 327.9) ); }); @@ -229,14 +232,22 @@ test('should remeasure flex columns when resizing a column', async () => { onColumnResize }); const grid = getGrid(); - await expect.element(grid).toHaveStyle({ gridTemplateColumns: '639.328px 639.328px 639.344px' }); + + function testGridTemplateColumns(chrome: string, firefox: string) { + expect((grid.element() as HTMLDivElement).style.gridTemplateColumns).toBeOneOf([ + chrome, + firefox + ]); + } + + testGridTemplateColumns('639.328px 639.328px 639.344px', '639.333px 639.333px 639.333px'); const [col1] = getHeaderCells(); await autoResize(col1); - await expect.element(grid).toHaveStyle({ gridTemplateColumns: '79.1406px 919.422px 919.438px' }); + testGridTemplateColumns('79.1406px 919.422px 919.438px', '79.1667px 919.417px 919.417px'); expect(onColumnResize).toHaveBeenCalledOnce(); // onColumnResize is not called if width is not changed await autoResize(col1); - await expect.element(grid).toHaveStyle({ gridTemplateColumns: '79.1406px 919.422px 919.438px' }); + testGridTemplateColumns('79.1406px 919.422px 919.438px', '79.1667px 919.417px 919.417px'); expect(onColumnResize).toHaveBeenCalledOnce(); }); diff --git a/vite.config.ts b/vite.config.ts index ec49a6277c..1747e80c0a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -77,6 +77,7 @@ export default defineConfig(({ command, isPreview }) => ({ }, test: { globals: true, + // TODO: use more workers when FF tests are stable maxWorkers: 1, coverage: { provider: 'istanbul', From 732e89e1202d1baf53f0f86bd96feb98c6ba0e3d Mon Sep 17 00:00:00 2001 From: amahajan Date: Tue, 16 Sep 2025 10:04:14 -0500 Subject: [PATCH 42/74] Add speciial case for firefox CI --- test/browser/column/resizable.test.tsx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index fc796d21ae..f2a35e272f 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -153,11 +153,12 @@ test('should auto resize column when resize handle is double clicked', async () await autoResize(col2); expect((grid.element() as HTMLDivElement).style.gridTemplateColumns).toBeOneOf([ '100px 327.703px', // chrome - '100px 327.833px' // firefox + '100px 327.833px', // firefox + '100px 400px' // firefox in CI ]); expect(onColumnResize).toHaveBeenCalledExactlyOnceWith( expect.objectContaining(columns[1]), - expect.toSatisfy((width) => width >= 327.7 && width <= 327.9) + expect.toSatisfy((width) => (width >= 327.7 && width <= 327.9) || width === 400) ); }); @@ -233,21 +234,30 @@ test('should remeasure flex columns when resizing a column', async () => { }); const grid = getGrid(); - function testGridTemplateColumns(chrome: string, firefox: string) { + function testGridTemplateColumns(chrome: string, firefox: string, firefoxCI = firefox) { expect((grid.element() as HTMLDivElement).style.gridTemplateColumns).toBeOneOf([ chrome, - firefox + firefox, + firefoxCI ]); } testGridTemplateColumns('639.328px 639.328px 639.344px', '639.333px 639.333px 639.333px'); const [col1] = getHeaderCells(); await autoResize(col1); - testGridTemplateColumns('79.1406px 919.422px 919.438px', '79.1667px 919.417px 919.417px'); + testGridTemplateColumns( + '79.1406px 919.422px 919.438px', + '79.1667px 919.417px 919.417px', + '100.5px 908.75px 908.75px' + ); expect(onColumnResize).toHaveBeenCalledOnce(); // onColumnResize is not called if width is not changed await autoResize(col1); - testGridTemplateColumns('79.1406px 919.422px 919.438px', '79.1667px 919.417px 919.417px'); + testGridTemplateColumns( + '79.1406px 919.422px 919.438px', + '79.1667px 919.417px 919.417px', + '100.5px 908.75px 908.75px' + ); expect(onColumnResize).toHaveBeenCalledOnce(); }); From fdf411cee1204fe52a448efbd20c78aedc573d08 Mon Sep 17 00:00:00 2001 From: amahajan Date: Tue, 16 Sep 2025 10:09:16 -0500 Subject: [PATCH 43/74] revert 1 change --- test/browser/column/renderEditCell.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/browser/column/renderEditCell.test.tsx b/test/browser/column/renderEditCell.test.tsx index 150752e3fd..c6a70361e7 100644 --- a/test/browser/column/renderEditCell.test.tsx +++ b/test/browser/column/renderEditCell.test.tsx @@ -194,7 +194,7 @@ describe('Editor', () => { }} /> ); - await userEvent.dblClick(getCellsAtRowIndex(0)[1]); + await userEvent.click(getCellsAtRowIndex(0)[1]); await userEvent.keyboard('yz{enter}'); expect(getCellsAtRowIndex(0)[1]).toHaveTextContent('yz'); await userEvent.keyboard('x'); From 3aaf277ea71238c56df858ce1b2b26fd468a012f Mon Sep 17 00:00:00 2001 From: amahajan Date: Tue, 16 Sep 2025 13:08:42 -0500 Subject: [PATCH 44/74] Fix test --- test/browser/column/renderEditCell.test.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/browser/column/renderEditCell.test.tsx b/test/browser/column/renderEditCell.test.tsx index c6a70361e7..a6a6a654c4 100644 --- a/test/browser/column/renderEditCell.test.tsx +++ b/test/browser/column/renderEditCell.test.tsx @@ -195,8 +195,9 @@ describe('Editor', () => { /> ); await userEvent.click(getCellsAtRowIndex(0)[1]); - await userEvent.keyboard('yz{enter}'); - expect(getCellsAtRowIndex(0)[1]).toHaveTextContent('yz'); + // TODO: await userEvent.keyboard('yz{enter}'); fails in FF + await userEvent.keyboard('{enter}yz{enter}'); + expect(getCellsAtRowIndex(0)[1]).toHaveTextContent('a1yz'); await userEvent.keyboard('x'); await expect .element(page.getByRole('textbox', { name: 'col2-editor' })) @@ -265,7 +266,7 @@ describe('Editor', () => { await userEvent.keyboard('abc'); await scrollGrid({ scrollTop: 1500 }); - expect(getCellsAtRowIndex(40)[1]).toHaveTextContent(/^40$/); + expect(getCellsAtRowIndex(40)[1]).toHaveTextContent(/^20$/); await userEvent.click(getCellsAtRowIndex(40)[1]); await expect.element(getSelectedCell()).toHaveTextContent(/^40$/); await scrollGrid({ scrollTop: 0 }); From dff828bc8868aef65f4d7fb9bb3170e558ffc1de Mon Sep 17 00:00:00 2001 From: amahajan Date: Tue, 16 Sep 2025 13:12:38 -0500 Subject: [PATCH 45/74] Revert accidental change --- test/browser/column/renderEditCell.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/browser/column/renderEditCell.test.tsx b/test/browser/column/renderEditCell.test.tsx index a6a6a654c4..625659a4d6 100644 --- a/test/browser/column/renderEditCell.test.tsx +++ b/test/browser/column/renderEditCell.test.tsx @@ -266,7 +266,7 @@ describe('Editor', () => { await userEvent.keyboard('abc'); await scrollGrid({ scrollTop: 1500 }); - expect(getCellsAtRowIndex(40)[1]).toHaveTextContent(/^20$/); + expect(getCellsAtRowIndex(40)[1]).toHaveTextContent(/^40$/); await userEvent.click(getCellsAtRowIndex(40)[1]); await expect.element(getSelectedCell()).toHaveTextContent(/^40$/); await scrollGrid({ scrollTop: 0 }); From e740506c076867ae59f3feabe97c44fc53d83843 Mon Sep 17 00:00:00 2001 From: amahajan Date: Tue, 16 Sep 2025 13:26:34 -0500 Subject: [PATCH 46/74] tweak --- test/browser/column/renderEditCell.test.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/browser/column/renderEditCell.test.tsx b/test/browser/column/renderEditCell.test.tsx index 625659a4d6..5373016938 100644 --- a/test/browser/column/renderEditCell.test.tsx +++ b/test/browser/column/renderEditCell.test.tsx @@ -41,8 +41,9 @@ describe('Editor', () => { it('should open editor when user types', async () => { page.render(); await userEvent.click(getCellsAtRowIndex(0)[0]); - await userEvent.keyboard('123{enter}'); - expect(getCellsAtRowIndex(0)[0]).toHaveTextContent('123'); + // TODO: await userEvent.keyboard('123{enter}'); fails in FF + await userEvent.keyboard('{enter}123{enter}'); + expect(getCellsAtRowIndex(0)[0]).toHaveTextContent('1123'); }); it('should close editor and discard changes on escape', async () => { @@ -99,8 +100,8 @@ describe('Editor', () => { const editor = page.getByRole('spinbutton', { name: 'col1-editor' }); await expect.element(editor).not.toBeInTheDocument(); expect(getGrid().element().scrollTop).toBe(2000); - // `1{backspace}` is needed to fix tests in FF - await userEvent.keyboard('1{backspace}123'); + // TODO: await userEvent.keyboard('123'); fails in FF + await userEvent.keyboard('{enter}123'); expect(getCellsAtRowIndex(0)).toHaveLength(2); await expect.element(editor).toHaveValue(123); expect(getGrid().element().scrollTop).toBe(0); From 4308ef9469fd38bb4fae75fcfa9caa91b461abaa Mon Sep 17 00:00:00 2001 From: amahajan Date: Tue, 16 Sep 2025 13:50:13 -0500 Subject: [PATCH 47/74] Try webkit --- .github/workflows/ci.yml | 2 +- vite.config.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e847ae2a63..31ca14580f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - name: Build website run: node --run build:website - name: Install Playwright Browsers - run: npx playwright install chromium firefox + run: npx playwright install chromium firefox webkit - name: Test run: node --run test timeout-minutes: 4 diff --git a/vite.config.ts b/vite.config.ts index 1747e80c0a..520a0ad476 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -106,6 +106,10 @@ export default defineConfig(({ command, isPreview }) => ({ { browser: 'firefox', context: { viewport } + }, + { + browser: 'webkit', + context: { viewport } } ], commands: { resizeColumn, dragFill }, From b3b47fb2de8fb9ab55486a7c4e040deb9e6d8b68 Mon Sep 17 00:00:00 2001 From: amahajan Date: Tue, 16 Sep 2025 14:05:46 -0500 Subject: [PATCH 48/74] try `--with-deps` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31ca14580f..ad458ffc93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - name: Build website run: node --run build:website - name: Install Playwright Browsers - run: npx playwright install chromium firefox webkit + run: npx playwright install --with-deps - name: Test run: node --run test timeout-minutes: 4 From 8983007c81dfd5955ddbb4492d2f6fb7eddc1b54 Mon Sep 17 00:00:00 2001 From: amahajan Date: Thu, 9 Oct 2025 18:58:33 -0500 Subject: [PATCH 49/74] Increase timeout --- .github/workflows/ci.yml | 2 +- vite.config.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index adc3ce8f9e..4f94f58950 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: run: npx playwright install --with-deps - name: Test run: node --run test - timeout-minutes: 4 + timeout-minutes: 10 - name: Upload coverage uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 with: diff --git a/vite.config.ts b/vite.config.ts index aeaf03c5ec..82ce4dfb65 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -95,6 +95,8 @@ export default defineConfig(({ command, isPreview }) => ({ }, test: { globals: true, + slowTestThreshold: 1000, + testTimeout: 40_000, // TODO: use more workers when FF tests are stable maxWorkers: 1, coverage: { From d979f910fb11f38c6163a1274bd2dc39bf6fc5cf Mon Sep 17 00:00:00 2001 From: amahajan Date: Thu, 9 Oct 2025 18:59:34 -0500 Subject: [PATCH 50/74] remove `maxWorkers` --- vite.config.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 82ce4dfb65..9ecea320da 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -97,8 +97,6 @@ export default defineConfig(({ command, isPreview }) => ({ globals: true, slowTestThreshold: 1000, testTimeout: 40_000, - // TODO: use more workers when FF tests are stable - maxWorkers: 1, coverage: { provider: 'istanbul', enabled: isCI, From 86c9dcbd562570d02d5949ce82046a9c876990c7 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 19:12:49 +0100 Subject: [PATCH 51/74] fix resizable tests --- test/browser/column/resizable.test.tsx | 48 ++++++++++++++++++-------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index 52994c9e61..8515fb6bf6 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -126,7 +126,12 @@ test('should auto resize column when resize handle is double clicked', async () }); await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' }); await autoResize('col2'); - await testGridTemplateColumns('100px 327.703px', '100px 327.833px', '100px 400px'); + await testGridTemplateColumns({ + chrome: '100px 327.703px', + firefox: '100px 327.833px', + firefoxCI: '100px 400px', + webkit: '100px 327.703125px' + }); expect(onColumnResize).toHaveBeenCalledExactlyOnceWith( expect.objectContaining(columns[1]), // Due to differences in text rendering between browsers the measured width can vary @@ -207,21 +212,25 @@ test('should remeasure flex columns when resizing a column', async () => { onColumnResize }); - await testGridTemplateColumns('639.328px 639.328px 639.344px', '639.333px 639.333px 639.333px'); + await testGridTemplateColumns({ + chrome: '639.328px 639.328px 639.344px', + firefox: '639.333px 639.333px 639.333px', + webkit: '639.328125px 639.328125px 639.328125px' + }); await autoResize('col1'); - await testGridTemplateColumns( - '79.1406px 919.422px 919.438px', - '79.1667px 919.417px 919.417px', - '100.5px 908.75px 908.75px' - ); + await testGridTemplateColumns({ + chrome: '79.1406px 919.422px 919.438px', + firefox: '79.1667px 919.417px 919.417px', + webkit: '79.140625px 919.421875px 919.421875px' + }); expect(onColumnResize).toHaveBeenCalledOnce(); // onColumnResize is not called if width is not changed await autoResize('col1'); - await testGridTemplateColumns( - '79.1406px 919.422px 919.438px', - '79.1667px 919.417px 919.417px', - '100.5px 908.75px 908.75px' - ); + await testGridTemplateColumns({ + chrome: '79.1406px 919.422px 919.438px', + firefox: '79.1667px 919.417px 919.417px', + webkit: '79.140625px 919.421875px 919.421875px' + }); expect(onColumnResize).toHaveBeenCalledOnce(); }); @@ -312,9 +321,20 @@ test('should use columnWidths and onColumnWidthsChange props when provided', asy ); }); -async function testGridTemplateColumns(chrome: string, firefox: string, firefoxCI = firefox) { +async function testGridTemplateColumns(obj: { + chrome: string; + firefox: string; + firefoxCI?: string; + webkit: string; +}) { const gridTemplateColumns = - server.browser === 'chromium' ? chrome : import.meta.env.CI ? firefoxCI : firefox; + server.browser === 'chromium' + ? obj.chrome + : server.browser === 'webkit' + ? obj.webkit + : import.meta.env.CI + ? (obj.firefoxCI ?? obj.firefox) + : obj.firefox; await expect.element(grid).toHaveStyle({ gridTemplateColumns }); } From 6a260b51be2daffb8f463c444c7ac0a8be32c9c8 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 19:20:17 +0100 Subject: [PATCH 52/74] skip a couple tests --- test/browser/column/renderCell.test.tsx | 70 +++++++++++---------- test/browser/column/renderEditCell.test.tsx | 43 +++++++------ 2 files changed, 62 insertions(+), 51 deletions(-) diff --git a/test/browser/column/renderCell.test.tsx b/test/browser/column/renderCell.test.tsx index d0bdaf7ff1..45581ce51d 100644 --- a/test/browser/column/renderCell.test.tsx +++ b/test/browser/column/renderCell.test.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { page, userEvent } from 'vitest/browser'; +import { page, server, userEvent } from 'vitest/browser'; import { DataGrid } from '../../../src'; import type { Column } from '../../../src'; @@ -164,40 +164,46 @@ test('Focus child if it sets tabIndex', async () => { await expect.element(cell).toHaveAttribute('tabindex', '-1'); }); -test('Cell should not steal focus when the focus is outside the grid and cell is recreated', async () => { - const columns: readonly Column[] = [{ key: 'id', name: 'ID' }]; +test( + 'Cell should not steal focus when the focus is outside the grid and cell is recreated', + // TODO: make the test pass in webkit + { skip: server.browser === 'webkit' }, + async () => { + const columns: readonly Column[] = [{ key: 'id', name: 'ID' }]; - function FormatterTest() { - const [rows, setRows] = useState((): readonly Row[] => [{ id: 1 }]); + function FormatterTest() { + const [rows, setRows] = useState((): readonly Row[] => [{ id: 1 }]); - function onClick() { - setRows([{ id: 2 }]); - } + function onClick() { + setRows([{ id: 2 }]); + } - return ( - <> - - row.id} - /> - - ); - } + return ( + <> + + row.id} + /> + + ); + } - await page.render(); + await page.render(); - const cell = getCellsAtRowIndex(0).nth(0); - await userEvent.click(cell); - await expect.element(cell).toHaveFocus(); + const cell = getCellsAtRowIndex(0).nth(0); + await userEvent.click(cell); + await expect.element(cell).toHaveFocus(); - const button = page.getByRole('button', { name: 'Test' }); - await expect.element(button).not.toHaveFocus(); - await userEvent.click(button); - await expect.element(cell).not.toHaveFocus(); - await expect.element(button).toHaveFocus(); -}); + const button = page.getByRole('button', { name: 'Test' }); + await expect.element(button).not.toHaveFocus(); + await userEvent.click(button); + await expect.element(button).toHaveFocus(); + await expect.element(cell).not.toHaveFocus(); + await expect.element(button).toHaveFocus(); + } +); diff --git a/test/browser/column/renderEditCell.test.tsx b/test/browser/column/renderEditCell.test.tsx index bf2f716a79..d171485fdc 100644 --- a/test/browser/column/renderEditCell.test.tsx +++ b/test/browser/column/renderEditCell.test.tsx @@ -86,26 +86,31 @@ describe('Editor', () => { ]); }); - it('should scroll to the editor if active cell is not in the viewport', async () => { - const rows: Row[] = []; - for (let i = 0; i < 99; i++) { - rows.push({ col1: i, col2: `${i}` }); - } + it( + 'should scroll to the editor if active cell is not in the viewport', + // TODO: make the test pass in webkit + { skip: server.browser === 'webkit' }, + async () => { + const rows: Row[] = []; + for (let i = 0; i < 99; i++) { + rows.push({ col1: i, col2: `${i}` }); + } - await page.render(); - await userEvent.click(getCellsAtRowIndex(0).nth(0)); - const activeRowCells = getRowWithCell(page.getActiveCell()).getCell(); - await testCount(activeRowCells, 2); - scrollGrid({ top: 2001 }); - await testCount(activeRowCells, 1); - await expect.element(col1Editor).not.toBeInTheDocument(); - await expect.element(grid).toHaveProperty('scrollTop', 2001); - // TODO: await userEvent.keyboard('123'); fails in FF - await userEvent.keyboard('{enter}123'); - await testCount(activeRowCells, 2); - await expect.element(col1Editor).toHaveValue(123); - await expect.element(grid).toHaveProperty('scrollTop', 0); - }); + await page.render(); + await userEvent.click(getCellsAtRowIndex(0).nth(0)); + const activeRowCells = getRowWithCell(page.getActiveCell()).getCell(); + await testCount(activeRowCells, 2); + scrollGrid({ top: 2001 }); + await testCount(activeRowCells, 1); + await expect.element(col1Editor).not.toBeInTheDocument(); + await expect.element(grid).toHaveProperty('scrollTop', 2001); + // TODO: await userEvent.keyboard('123'); fails in FF + await userEvent.keyboard('{enter}123'); + await testCount(activeRowCells, 2); + await expect.element(col1Editor).toHaveValue(123); + await expect.element(grid).toHaveProperty('scrollTop', 0); + } + ); describe('editable', () => { it('should be editable if an editor is specified and editable is undefined/null', async () => { From 63b8c8d62450b6448c01eff16922194bbfff76ae Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 19:20:30 +0100 Subject: [PATCH 53/74] revert timeout increase --- .github/workflows/ci.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/update-screenshots.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5e80d4da9..ed0bb3abe4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: - name: Test run: node --run test:ci - timeout-minutes: 10 + timeout-minutes: 4 - name: Upload test failure artifacts if: failure() diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7ef440ee41..f98b09872b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -54,7 +54,7 @@ jobs: - name: Test run: node --run test:ci - timeout-minutes: 10 + timeout-minutes: 4 - name: Update version run: | diff --git a/.github/workflows/update-screenshots.yml b/.github/workflows/update-screenshots.yml index eaa94775e7..344152eadd 100644 --- a/.github/workflows/update-screenshots.yml +++ b/.github/workflows/update-screenshots.yml @@ -46,7 +46,7 @@ jobs: run: | rm -r test/**/screenshots/** node --run test:ci:update - timeout-minutes: 10 + timeout-minutes: 4 - name: Push new screenshots run: | From 6d7d44811c82ba68b2d7df2b47db63b64d04ea5d Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 19:21:43 +0100 Subject: [PATCH 54/74] log grid-template-columns --- test/browser/column/resizable.test.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index 8515fb6bf6..01c559e2ce 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -336,5 +336,21 @@ async function testGridTemplateColumns(obj: { ? (obj.firefoxCI ?? obj.firefox) : obj.firefox; + const a = grid.element().style.gridTemplateColumns; + const b = window.getComputedStyle(grid.element()).gridTemplateColumns; + const c = + server.browser === 'firefox' + ? '' + : grid.element().computedStyleMap().get('grid-template-columns')?.toString(); + + console.log( + server.browser, + gridTemplateColumns, + a === gridTemplateColumns, + b === gridTemplateColumns, + c === gridTemplateColumns, + { a, b, c } + ); + await expect.element(grid).toHaveStyle({ gridTemplateColumns }); } From c3ddeb2d49844720067c99e05d1c053729d9c8ee Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 19:22:49 +0100 Subject: [PATCH 55/74] remove testTimeout --- vite.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 39cdbad90e..1a93d40cb5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -120,7 +120,6 @@ export default defineConfig(({ isPreview }): ViteUserConfig => ({ } }, slowTestThreshold: 1000, - testTimeout: 40_000, browser: { headless: true, ui: false, From 70e4b6dff26fd09f84f5f17b808a7a5c8430f7e3 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 19:24:12 +0100 Subject: [PATCH 56/74] ignore lint --- test/browser/column/resizable.test.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index 01c559e2ce..203bd2e06f 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -326,12 +326,15 @@ async function testGridTemplateColumns(obj: { firefox: string; firefoxCI?: string; webkit: string; + webkitCI?: string; }) { const gridTemplateColumns = server.browser === 'chromium' ? obj.chrome : server.browser === 'webkit' - ? obj.webkit + ? import.meta.env.CI + ? (obj.webkitCI ?? obj.webkit) + : obj.webkit : import.meta.env.CI ? (obj.firefoxCI ?? obj.firefox) : obj.firefox; @@ -343,6 +346,7 @@ async function testGridTemplateColumns(obj: { ? '' : grid.element().computedStyleMap().get('grid-template-columns')?.toString(); + // eslint-disable-next-line no-console console.log( server.browser, gridTemplateColumns, From a1c0c7275d72b486a332477f9e3a64f9f0b5f530 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 19:27:52 +0100 Subject: [PATCH 57/74] without deps? --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed0bb3abe4..ba4a2acaff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: run: git diff --exit-code website/routeTree.gen.ts - name: Install Playwright Browsers - run: npx playwright install --with-deps + run: npx playwright install timeout-minutes: 2 - name: Test From d13c1ae5b1ca35ae2e51096325ed95f6954df524 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 19:34:29 +0100 Subject: [PATCH 58/74] split --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba4a2acaff..eef0a4def5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,10 @@ jobs: run: git diff --exit-code website/routeTree.gen.ts - name: Install Playwright Browsers - run: npx playwright install + run: | + npx playwright install --list + npx playwright install chromium firefox --no-shell + npx playwright install webkit --with-deps timeout-minutes: 2 - name: Test From e76cc9968ded1b15a05afd76933a5e6189ceca7c Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 19:36:34 +0100 Subject: [PATCH 59/74] don't list --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eef0a4def5..70dd293eb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,6 @@ jobs: - name: Install Playwright Browsers run: | - npx playwright install --list npx playwright install chromium firefox --no-shell npx playwright install webkit --with-deps timeout-minutes: 2 From 441f5df38772974f92a7e84180bf7594c7294d3c Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 19:41:18 +0100 Subject: [PATCH 60/74] like this? --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70dd293eb8..1758ce3426 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,9 +40,8 @@ jobs: - name: Install Playwright Browsers run: | - npx playwright install chromium firefox --no-shell - npx playwright install webkit --with-deps - timeout-minutes: 2 + npx playwright install --with-deps --no-shell + timeout-minutes: 3 - name: Test run: node --run test:ci From 1eec2761ccd1a791d64e1de73afdebacade8fc39 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 19:51:21 +0100 Subject: [PATCH 61/74] tweak resizable tests --- .github/workflows/ci.yml | 3 +-- .github/workflows/publish.yml | 4 ++-- .github/workflows/update-screenshots.yml | 5 +++-- test/browser/column/resizable.test.tsx | 11 +++++++---- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1758ce3426..b1e4d1265f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,8 +39,7 @@ jobs: run: git diff --exit-code website/routeTree.gen.ts - name: Install Playwright Browsers - run: | - npx playwright install --with-deps --no-shell + run: npx playwright install --with-deps --no-shell timeout-minutes: 3 - name: Test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f98b09872b..113428e13d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -49,8 +49,8 @@ jobs: run: git diff --exit-code website/routeTree.gen.ts - name: Install Playwright Browsers - run: npx playwright install --with-deps - timeout-minutes: 2 + run: npx playwright install --with-deps --no-shell + timeout-minutes: 3 - name: Test run: node --run test:ci diff --git a/.github/workflows/update-screenshots.yml b/.github/workflows/update-screenshots.yml index 344152eadd..938899e2de 100644 --- a/.github/workflows/update-screenshots.yml +++ b/.github/workflows/update-screenshots.yml @@ -38,9 +38,10 @@ jobs: - name: Install dependencies run: npm ci + - name: Install Playwright Browsers - run: npx playwright install --with-deps - timeout-minutes: 2 + run: npx playwright install --with-deps --no-shell + timeout-minutes: 3 - name: Update screenshots run: | diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index 203bd2e06f..a1f5a887b3 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -130,7 +130,8 @@ test('should auto resize column when resize handle is double clicked', async () chrome: '100px 327.703px', firefox: '100px 327.833px', firefoxCI: '100px 400px', - webkit: '100px 327.703125px' + webkit: '100px 327.703125px', + webkitCI: '100px 400px' }); expect(onColumnResize).toHaveBeenCalledExactlyOnceWith( expect.objectContaining(columns[1]), @@ -139,7 +140,7 @@ test('should auto resize column when resize handle is double clicked', async () (width) => // Chrome and Firefox on windows (width >= 327.7 && width <= 327.9) || - // Firefox on CI + // Firefox/WebKit in CI width === 400 ) ); @@ -221,7 +222,8 @@ test('should remeasure flex columns when resizing a column', async () => { await testGridTemplateColumns({ chrome: '79.1406px 919.422px 919.438px', firefox: '79.1667px 919.417px 919.417px', - webkit: '79.140625px 919.421875px 919.421875px' + webkit: '79.140625px 919.421875px 919.421875px', + webkitCI: '102.796875px 907.59375px 907.59375px' }); expect(onColumnResize).toHaveBeenCalledOnce(); // onColumnResize is not called if width is not changed @@ -229,7 +231,8 @@ test('should remeasure flex columns when resizing a column', async () => { await testGridTemplateColumns({ chrome: '79.1406px 919.422px 919.438px', firefox: '79.1667px 919.417px 919.417px', - webkit: '79.140625px 919.421875px 919.421875px' + webkit: '79.140625px 919.421875px 919.421875px', + webkitCI: '102.796875px 907.59375px 907.59375px' }); expect(onColumnResize).toHaveBeenCalledOnce(); }); From f754599b85110d02bca993d596ac1af26dbbfd56 Mon Sep 17 00:00:00 2001 From: nstepien <567105+nstepien@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:53:27 +0000 Subject: [PATCH 62/74] Update screenshots --- .../basic-grid-webkit-linux.png | Bin 0 -> 20839 bytes .../tree-grid-webkit-linux.png | Bin 0 -> 14529 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/visual/screenshots/basicGrid.test.tsx/basic-grid-webkit-linux.png create mode 100644 test/visual/screenshots/treeGrid.test.tsx/tree-grid-webkit-linux.png diff --git a/test/visual/screenshots/basicGrid.test.tsx/basic-grid-webkit-linux.png b/test/visual/screenshots/basicGrid.test.tsx/basic-grid-webkit-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..a83255d885f62aa794256d99189affe175036537 GIT binary patch literal 20839 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 Pli9md=gXuo%)k8?Dfu Date: Wed, 19 Aug 2026 19:56:23 +0100 Subject: [PATCH 63/74] increase timeout some more --- .github/workflows/ci.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/update-screenshots.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1e4d1265f..fb79a869e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: - name: Install Playwright Browsers run: npx playwright install --with-deps --no-shell - timeout-minutes: 3 + timeout-minutes: 4 - name: Test run: node --run test:ci diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 113428e13d..6133b06277 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -50,7 +50,7 @@ jobs: - name: Install Playwright Browsers run: npx playwright install --with-deps --no-shell - timeout-minutes: 3 + timeout-minutes: 4 - name: Test run: node --run test:ci diff --git a/.github/workflows/update-screenshots.yml b/.github/workflows/update-screenshots.yml index 938899e2de..8484114075 100644 --- a/.github/workflows/update-screenshots.yml +++ b/.github/workflows/update-screenshots.yml @@ -41,7 +41,7 @@ jobs: - name: Install Playwright Browsers run: npx playwright install --with-deps --no-shell - timeout-minutes: 3 + timeout-minutes: 4 - name: Update screenshots run: | From f3a1cd6427ad4cdec03e496a3ffd1f0c9e2ca47d Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 20:07:18 +0100 Subject: [PATCH 64/74] fix resizable tests --- .github/workflows/ci.yml | 4 +++- test/browser/column/resizable.test.tsx | 19 ++----------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb79a869e0..0dfd9e1c3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,9 @@ jobs: run: git diff --exit-code website/routeTree.gen.ts - name: Install Playwright Browsers - run: npx playwright install --with-deps --no-shell + run: | + npx playwright install chromium firefox webkit --no-shell + npx playwright install webkit --with-deps timeout-minutes: 4 - name: Test diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index a1f5a887b3..aba6f25f0f 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -222,6 +222,7 @@ test('should remeasure flex columns when resizing a column', async () => { await testGridTemplateColumns({ chrome: '79.1406px 919.422px 919.438px', firefox: '79.1667px 919.417px 919.417px', + firefoxCI: '100.5px 908.75px 908.75px', webkit: '79.140625px 919.421875px 919.421875px', webkitCI: '102.796875px 907.59375px 907.59375px' }); @@ -231,6 +232,7 @@ test('should remeasure flex columns when resizing a column', async () => { await testGridTemplateColumns({ chrome: '79.1406px 919.422px 919.438px', firefox: '79.1667px 919.417px 919.417px', + firefoxCI: '100.5px 908.75px 908.75px', webkit: '79.140625px 919.421875px 919.421875px', webkitCI: '102.796875px 907.59375px 907.59375px' }); @@ -342,22 +344,5 @@ async function testGridTemplateColumns(obj: { ? (obj.firefoxCI ?? obj.firefox) : obj.firefox; - const a = grid.element().style.gridTemplateColumns; - const b = window.getComputedStyle(grid.element()).gridTemplateColumns; - const c = - server.browser === 'firefox' - ? '' - : grid.element().computedStyleMap().get('grid-template-columns')?.toString(); - - // eslint-disable-next-line no-console - console.log( - server.browser, - gridTemplateColumns, - a === gridTemplateColumns, - b === gridTemplateColumns, - c === gridTemplateColumns, - { a, b, c } - ); - await expect.element(grid).toHaveStyle({ gridTemplateColumns }); } From a5df38a9b01bb9f57d17492c62a74de926114055 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 21:21:46 +0100 Subject: [PATCH 65/74] idk --- .github/workflows/ci.yml | 4 +--- .github/workflows/publish.yml | 2 +- .github/workflows/update-screenshots.yml | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dfd9e1c3f..5796c47017 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,9 +39,7 @@ jobs: run: git diff --exit-code website/routeTree.gen.ts - name: Install Playwright Browsers - run: | - npx playwright install chromium firefox webkit --no-shell - npx playwright install webkit --with-deps + run: npx playwright install chromium firefox webkit --with-deps --no-shell timeout-minutes: 4 - name: Test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6133b06277..f45139bd17 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -49,7 +49,7 @@ jobs: run: git diff --exit-code website/routeTree.gen.ts - name: Install Playwright Browsers - run: npx playwright install --with-deps --no-shell + run: npx playwright install chromium firefox webkit --with-deps --no-shell timeout-minutes: 4 - name: Test diff --git a/.github/workflows/update-screenshots.yml b/.github/workflows/update-screenshots.yml index 8484114075..b0672521e3 100644 --- a/.github/workflows/update-screenshots.yml +++ b/.github/workflows/update-screenshots.yml @@ -40,7 +40,7 @@ jobs: run: npm ci - name: Install Playwright Browsers - run: npx playwright install --with-deps --no-shell + run: npx playwright install chromium firefox webkit --with-deps --no-shell timeout-minutes: 4 - name: Update screenshots From 1f3fd89fd69d5548147cc4b917d0c709be6620f1 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 21:36:07 +0100 Subject: [PATCH 66/74] try fixing copy/paste tests --- test/browser/TreeDataGrid.test.tsx | 48 ++++++++++++++++-------------- test/browser/copyPaste.test.tsx | 48 +++++++++++++++++------------- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/test/browser/TreeDataGrid.test.tsx b/test/browser/TreeDataGrid.test.tsx index 96963dca57..972086844a 100644 --- a/test/browser/TreeDataGrid.test.tsx +++ b/test/browser/TreeDataGrid.test.tsx @@ -422,29 +422,33 @@ test('copy/paste when grouping is enabled', async () => { await userEvent.click(page.getCell({ name: 'USA' })); await userEvent.copy(); - expect(onCellCopySpy).toHaveBeenCalledExactlyOnceWith( - { - column: expect.objectContaining(columns[2]), - row: { - country: 'USA', - id: 2, - year: 2021 - } - }, - expect.anything() - ); + await expect + .poll(() => onCellCopySpy) + .toHaveBeenCalledExactlyOnceWith( + { + column: expect.objectContaining(columns[2]), + row: { + country: 'USA', + id: 2, + year: 2021 + } + }, + expect.anything() + ); await userEvent.paste(); - expect(onCellPasteSpy).toHaveBeenCalledExactlyOnceWith( - { - column: expect.objectContaining(columns[2]), - row: { - country: 'USA', - id: 2, - year: 2021 - } - }, - expect.anything() - ); + await expect + .poll(() => onCellPasteSpy) + .toHaveBeenCalledExactlyOnceWith( + { + column: expect.objectContaining(columns[2]), + row: { + country: 'USA', + id: 2, + year: 2021 + } + }, + expect.anything() + ); }); test('update row using cell renderer', async () => { diff --git a/test/browser/copyPaste.test.tsx b/test/browser/copyPaste.test.tsx index a063235dc1..6e013d92cd 100644 --- a/test/browser/copyPaste.test.tsx +++ b/test/browser/copyPaste.test.tsx @@ -72,26 +72,30 @@ test('should call onCellCopy on cell copy', async () => { await setup(); await userEvent.click(getCellsAtRowIndex(0).nth(0)); await userEvent.copy(); - expect(onCellCopySpy).toHaveBeenCalledExactlyOnceWith( - { - row: initialRows[0], - column: expect.objectContaining(columns[0]) - }, - expect.anything() - ); + await expect + .poll(() => onCellCopySpy) + .toHaveBeenCalledExactlyOnceWith( + { + row: initialRows[0], + column: expect.objectContaining(columns[0]) + }, + expect.anything() + ); }); test('should call onCellPaste on cell paste', async () => { await setup(); await userEvent.click(getCellsAtRowIndex(0).nth(0)); await userEvent.paste(); - expect(onCellPasteSpy).toHaveBeenCalledExactlyOnceWith( - { - row: initialRows[0], - column: expect.objectContaining(columns[0]) - }, - expect.anything() - ); + await expect + .poll(() => onCellPasteSpy) + .toHaveBeenCalledExactlyOnceWith( + { + row: initialRows[0], + column: expect.objectContaining(columns[0]) + }, + expect.anything() + ); }); test('should not allow paste on readonly cells', async () => { @@ -105,13 +109,15 @@ test('should allow copying a readonly cell', async () => { await setup(); await userEvent.click(getCellsAtRowIndex(2).nth(0)); await userEvent.copy(); - expect(onCellCopySpy).toHaveBeenCalledExactlyOnceWith( - { - row: initialRows[2], - column: expect.objectContaining(columns[0]) - }, - expect.anything() - ); + await expect + .poll(() => onCellCopySpy) + .toHaveBeenCalledExactlyOnceWith( + { + row: initialRows[2], + column: expect.objectContaining(columns[0]) + }, + expect.anything() + ); }); test('should not allow copy/paste on header or summary cells', async () => { From 339ba1735878d835ea9066984c7e5caa89376600 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 22:17:57 +0100 Subject: [PATCH 67/74] tweak apt-get --- .github/workflows/ci.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5796c47017..7e74d623bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,16 @@ jobs: run: git diff --exit-code website/routeTree.gen.ts - name: Install Playwright Browsers - run: npx playwright install chromium firefox webkit --with-deps --no-shell + run: | + sudo tee /etc/apt/apt.conf.d/99-ci >/dev/null <<'EOF' + Acquire::Retries "0"; + Acquire::http::Timeout "10"; + Acquire::https::Timeout "10"; + Acquire::http::Dl-Limit "0"; + Acquire::Languages "none"; + APT::Install-Recommends "false"; + EOF + npx playwright install chromium firefox webkit --with-deps --no-shell timeout-minutes: 4 - name: Test From 566c5a21f0dcca14c5ec983d7900cc322053177e Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 22:24:39 +0100 Subject: [PATCH 68/74] Revert "try fixing copy/paste tests" This reverts commit 1f3fd89fd69d5548147cc4b917d0c709be6620f1. --- test/browser/TreeDataGrid.test.tsx | 48 ++++++++++++++---------------- test/browser/copyPaste.test.tsx | 48 +++++++++++++----------------- 2 files changed, 43 insertions(+), 53 deletions(-) diff --git a/test/browser/TreeDataGrid.test.tsx b/test/browser/TreeDataGrid.test.tsx index 972086844a..96963dca57 100644 --- a/test/browser/TreeDataGrid.test.tsx +++ b/test/browser/TreeDataGrid.test.tsx @@ -422,33 +422,29 @@ test('copy/paste when grouping is enabled', async () => { await userEvent.click(page.getCell({ name: 'USA' })); await userEvent.copy(); - await expect - .poll(() => onCellCopySpy) - .toHaveBeenCalledExactlyOnceWith( - { - column: expect.objectContaining(columns[2]), - row: { - country: 'USA', - id: 2, - year: 2021 - } - }, - expect.anything() - ); + expect(onCellCopySpy).toHaveBeenCalledExactlyOnceWith( + { + column: expect.objectContaining(columns[2]), + row: { + country: 'USA', + id: 2, + year: 2021 + } + }, + expect.anything() + ); await userEvent.paste(); - await expect - .poll(() => onCellPasteSpy) - .toHaveBeenCalledExactlyOnceWith( - { - column: expect.objectContaining(columns[2]), - row: { - country: 'USA', - id: 2, - year: 2021 - } - }, - expect.anything() - ); + expect(onCellPasteSpy).toHaveBeenCalledExactlyOnceWith( + { + column: expect.objectContaining(columns[2]), + row: { + country: 'USA', + id: 2, + year: 2021 + } + }, + expect.anything() + ); }); test('update row using cell renderer', async () => { diff --git a/test/browser/copyPaste.test.tsx b/test/browser/copyPaste.test.tsx index 6e013d92cd..a063235dc1 100644 --- a/test/browser/copyPaste.test.tsx +++ b/test/browser/copyPaste.test.tsx @@ -72,30 +72,26 @@ test('should call onCellCopy on cell copy', async () => { await setup(); await userEvent.click(getCellsAtRowIndex(0).nth(0)); await userEvent.copy(); - await expect - .poll(() => onCellCopySpy) - .toHaveBeenCalledExactlyOnceWith( - { - row: initialRows[0], - column: expect.objectContaining(columns[0]) - }, - expect.anything() - ); + expect(onCellCopySpy).toHaveBeenCalledExactlyOnceWith( + { + row: initialRows[0], + column: expect.objectContaining(columns[0]) + }, + expect.anything() + ); }); test('should call onCellPaste on cell paste', async () => { await setup(); await userEvent.click(getCellsAtRowIndex(0).nth(0)); await userEvent.paste(); - await expect - .poll(() => onCellPasteSpy) - .toHaveBeenCalledExactlyOnceWith( - { - row: initialRows[0], - column: expect.objectContaining(columns[0]) - }, - expect.anything() - ); + expect(onCellPasteSpy).toHaveBeenCalledExactlyOnceWith( + { + row: initialRows[0], + column: expect.objectContaining(columns[0]) + }, + expect.anything() + ); }); test('should not allow paste on readonly cells', async () => { @@ -109,15 +105,13 @@ test('should allow copying a readonly cell', async () => { await setup(); await userEvent.click(getCellsAtRowIndex(2).nth(0)); await userEvent.copy(); - await expect - .poll(() => onCellCopySpy) - .toHaveBeenCalledExactlyOnceWith( - { - row: initialRows[2], - column: expect.objectContaining(columns[0]) - }, - expect.anything() - ); + expect(onCellCopySpy).toHaveBeenCalledExactlyOnceWith( + { + row: initialRows[2], + column: expect.objectContaining(columns[0]) + }, + expect.anything() + ); }); test('should not allow copy/paste on header or summary cells', async () => { From c3c7bf66a66f8a7ccb1dd60a0bbca2c58837e232 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 23:25:49 +0100 Subject: [PATCH 69/74] skip copy/paste tests in webkit+ci --- test/browser/TreeDataGrid.test.tsx | 4 ++-- test/browser/column/renderCell.test.tsx | 2 +- test/browser/column/renderEditCell.test.tsx | 2 +- test/browser/column/resizable.test.tsx | 5 +++-- test/browser/copyPaste.test.tsx | 8 ++++---- test/browser/utils.tsx | 5 ++++- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/test/browser/TreeDataGrid.test.tsx b/test/browser/TreeDataGrid.test.tsx index 96963dca57..4ecc1ff171 100644 --- a/test/browser/TreeDataGrid.test.tsx +++ b/test/browser/TreeDataGrid.test.tsx @@ -4,7 +4,7 @@ import { page, userEvent } from 'vitest/browser'; import type { Column } from '../../src'; import { renderTextEditor, SelectColumn, TreeDataGrid } from '../../src'; import { rowActiveClassname } from '../../src/style/row'; -import { getCellsAtRowIndex, getRowWithCell, testCount, testRowCount } from './utils'; +import { canCopyPaste, getCellsAtRowIndex, getRowWithCell, testCount, testRowCount } from './utils'; const treeGrid = page.getTreeGrid(); const headerRow = treeGrid.getHeaderRow(); @@ -412,7 +412,7 @@ test('cell navigation in a treegrid', async () => { await testRowCount(4); }); -test('copy/paste when grouping is enabled', async () => { +test('copy/paste when grouping is enabled', { fails: !canCopyPaste }, async () => { await setup(['year']); await userEvent.click(page.getCell({ name: '2021' })); await userEvent.copy(); diff --git a/test/browser/column/renderCell.test.tsx b/test/browser/column/renderCell.test.tsx index 45581ce51d..f7c700c755 100644 --- a/test/browser/column/renderCell.test.tsx +++ b/test/browser/column/renderCell.test.tsx @@ -167,7 +167,7 @@ test('Focus child if it sets tabIndex', async () => { test( 'Cell should not steal focus when the focus is outside the grid and cell is recreated', // TODO: make the test pass in webkit - { skip: server.browser === 'webkit' }, + { fails: server.browser === 'webkit' }, async () => { const columns: readonly Column[] = [{ key: 'id', name: 'ID' }]; diff --git a/test/browser/column/renderEditCell.test.tsx b/test/browser/column/renderEditCell.test.tsx index d171485fdc..558ff2bdd1 100644 --- a/test/browser/column/renderEditCell.test.tsx +++ b/test/browser/column/renderEditCell.test.tsx @@ -89,7 +89,7 @@ describe('Editor', () => { it( 'should scroll to the editor if active cell is not in the viewport', // TODO: make the test pass in webkit - { skip: server.browser === 'webkit' }, + { fails: server.browser === 'webkit' }, async () => { const rows: Row[] = []; for (let i = 0; i < 99; i++) { diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index aba6f25f0f..65d5d129eb 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -333,10 +333,11 @@ async function testGridTemplateColumns(obj: { webkit: string; webkitCI?: string; }) { + const { browser } = server; const gridTemplateColumns = - server.browser === 'chromium' + browser === 'chromium' ? obj.chrome - : server.browser === 'webkit' + : browser === 'webkit' ? import.meta.env.CI ? (obj.webkitCI ?? obj.webkit) : obj.webkit diff --git a/test/browser/copyPaste.test.tsx b/test/browser/copyPaste.test.tsx index a063235dc1..3ca0595e4a 100644 --- a/test/browser/copyPaste.test.tsx +++ b/test/browser/copyPaste.test.tsx @@ -3,7 +3,7 @@ import { page, userEvent } from 'vitest/browser'; import { DataGrid } from '../../src'; import type { CellPasteArgs, Column } from '../../src'; -import { getCellsAtRowIndex, safeTab } from './utils'; +import { canCopyPaste, getCellsAtRowIndex, safeTab } from './utils'; interface Row { col: string; @@ -68,7 +68,7 @@ function setup() { return page.render(); } -test('should call onCellCopy on cell copy', async () => { +test('should call onCellCopy on cell copy', { fails: !canCopyPaste }, async () => { await setup(); await userEvent.click(getCellsAtRowIndex(0).nth(0)); await userEvent.copy(); @@ -81,7 +81,7 @@ test('should call onCellCopy on cell copy', async () => { ); }); -test('should call onCellPaste on cell paste', async () => { +test('should call onCellPaste on cell paste', { fails: !canCopyPaste }, async () => { await setup(); await userEvent.click(getCellsAtRowIndex(0).nth(0)); await userEvent.paste(); @@ -101,7 +101,7 @@ test('should not allow paste on readonly cells', async () => { expect(onCellPasteSpy).not.toHaveBeenCalled(); }); -test('should allow copying a readonly cell', async () => { +test('should allow copying a readonly cell', { fails: !canCopyPaste }, async () => { await setup(); await userEvent.click(getCellsAtRowIndex(2).nth(0)); await userEvent.copy(); diff --git a/test/browser/utils.tsx b/test/browser/utils.tsx index 6860786170..23047a5aec 100644 --- a/test/browser/utils.tsx +++ b/test/browser/utils.tsx @@ -1,8 +1,11 @@ -import { page, userEvent, type Locator } from 'vitest/browser'; +import { page, server, userEvent, type Locator } from 'vitest/browser'; import { DataGrid } from '../../src'; import type { DataGridProps } from '../../src'; +// copy/paste do not work in webkit in CI +export const canCopyPaste = !(import.meta.env.CI && server.browser === 'webkit'); + export function setup(props: DataGridProps) { return page.render(); } From aa36819c9dc528757f71538ede7ee0d00d80bc37 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Wed, 19 Aug 2026 23:44:23 +0100 Subject: [PATCH 70/74] tweak dragFill --- vite.config.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 1a93d40cb5..9b7d05e4e4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -31,12 +31,21 @@ const resizeColumn: BrowserCommand<[name: string, resizeBy: number | readonly nu }; // TODO: remove when `userEvent.pointer` is supported -const dragFill: BrowserCommand<[from: string, to: string]> = async ({ page, iframe }, from, to) => { +const dragFill: BrowserCommand<[from: string, to: string]> = async ( + { page, iframe, project }, + from, + to +) => { await iframe.getByRole('gridcell', { name: from, exact: true }).click(); await iframe.locator('.rdg-cell-drag-handle').hover(); await page.mouse.down(); - const toCell = iframe.getByRole('gridcell', { name: to, exact: true }); - await toCell.hover(); + await iframe.getByRole('gridcell', { name: to, exact: true }).hover(); + if (project.name.includes('webkit')) { + // let React re-render after handleDragHandlePointerMove calls setDraggedOverRowIdx() + await new Promise((resolve) => { + setTimeout(resolve, 20); + }); + } await page.mouse.up(); }; From f7a9ff8c9d70dd90447d715402aa89c92a8faf62 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Thu, 20 Aug 2026 01:56:26 +0100 Subject: [PATCH 71/74] fix a couple tests --- src/DataGrid.tsx | 7 ++ test/browser/column/renderCell.test.tsx | 76 ++++++++++----------- test/browser/column/renderEditCell.test.tsx | 43 ++++++------ 3 files changed, 64 insertions(+), 62 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index f676967c9a..74c08a6497 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -694,6 +694,13 @@ export function DataGrid(props: DataGridPr } if (isCellEditable(activePosition) && isDefaultCellInput(event, onCellPaste != null)) { + const { target } = event; + + // ensure cell is fully visible + if (target instanceof HTMLElement) { + scrollIntoView(target.closest('.rdg-cell')); + } + setActivePosition(({ idx, rowIdx }) => ({ idx, rowIdx, diff --git a/test/browser/column/renderCell.test.tsx b/test/browser/column/renderCell.test.tsx index f7c700c755..4b36de5dca 100644 --- a/test/browser/column/renderCell.test.tsx +++ b/test/browser/column/renderCell.test.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { page, server, userEvent } from 'vitest/browser'; +import { page, userEvent } from 'vitest/browser'; import { DataGrid } from '../../../src'; import type { Column } from '../../../src'; @@ -164,46 +164,46 @@ test('Focus child if it sets tabIndex', async () => { await expect.element(cell).toHaveAttribute('tabindex', '-1'); }); -test( - 'Cell should not steal focus when the focus is outside the grid and cell is recreated', - // TODO: make the test pass in webkit - { fails: server.browser === 'webkit' }, - async () => { - const columns: readonly Column[] = [{ key: 'id', name: 'ID' }]; +test('Cell should not steal focus when the focus is outside the grid and cell is recreated', async () => { + const columns: readonly Column[] = [{ key: 'id', name: 'ID' }]; - function FormatterTest() { - const [rows, setRows] = useState((): readonly Row[] => [{ id: 1 }]); + function FormatterTest() { + const [rows, setRows] = useState((): readonly Row[] => [{ id: 1 }]); - function onClick() { - setRows([{ id: 2 }]); - } - - return ( - <> - - row.id} - /> - - ); + function onClick() { + setRows([{ id: 2 }]); } - await page.render(); + return ( + <> + + row.id} + /> + + ); + } - const cell = getCellsAtRowIndex(0).nth(0); - await userEvent.click(cell); - await expect.element(cell).toHaveFocus(); + await page.render(); - const button = page.getByRole('button', { name: 'Test' }); - await expect.element(button).not.toHaveFocus(); - await userEvent.click(button); - await expect.element(button).toHaveFocus(); - await expect.element(cell).not.toHaveFocus(); - await expect.element(button).toHaveFocus(); - } -); + const cell = getCellsAtRowIndex(0).nth(0); + await userEvent.click(cell); + await expect.element(cell).toHaveFocus(); + + const button = page.getByRole('button', { name: 'Test' }); + await expect.element(button).not.toHaveFocus(); + await userEvent.click(button); + await expect.element(button).toHaveFocus(); + await expect.element(cell).not.toHaveFocus(); + await expect.element(button).toHaveFocus(); +}); diff --git a/test/browser/column/renderEditCell.test.tsx b/test/browser/column/renderEditCell.test.tsx index 558ff2bdd1..bf2f716a79 100644 --- a/test/browser/column/renderEditCell.test.tsx +++ b/test/browser/column/renderEditCell.test.tsx @@ -86,31 +86,26 @@ describe('Editor', () => { ]); }); - it( - 'should scroll to the editor if active cell is not in the viewport', - // TODO: make the test pass in webkit - { fails: server.browser === 'webkit' }, - async () => { - const rows: Row[] = []; - for (let i = 0; i < 99; i++) { - rows.push({ col1: i, col2: `${i}` }); - } - - await page.render(); - await userEvent.click(getCellsAtRowIndex(0).nth(0)); - const activeRowCells = getRowWithCell(page.getActiveCell()).getCell(); - await testCount(activeRowCells, 2); - scrollGrid({ top: 2001 }); - await testCount(activeRowCells, 1); - await expect.element(col1Editor).not.toBeInTheDocument(); - await expect.element(grid).toHaveProperty('scrollTop', 2001); - // TODO: await userEvent.keyboard('123'); fails in FF - await userEvent.keyboard('{enter}123'); - await testCount(activeRowCells, 2); - await expect.element(col1Editor).toHaveValue(123); - await expect.element(grid).toHaveProperty('scrollTop', 0); + it('should scroll to the editor if active cell is not in the viewport', async () => { + const rows: Row[] = []; + for (let i = 0; i < 99; i++) { + rows.push({ col1: i, col2: `${i}` }); } - ); + + await page.render(); + await userEvent.click(getCellsAtRowIndex(0).nth(0)); + const activeRowCells = getRowWithCell(page.getActiveCell()).getCell(); + await testCount(activeRowCells, 2); + scrollGrid({ top: 2001 }); + await testCount(activeRowCells, 1); + await expect.element(col1Editor).not.toBeInTheDocument(); + await expect.element(grid).toHaveProperty('scrollTop', 2001); + // TODO: await userEvent.keyboard('123'); fails in FF + await userEvent.keyboard('{enter}123'); + await testCount(activeRowCells, 2); + await expect.element(col1Editor).toHaveValue(123); + await expect.element(grid).toHaveProperty('scrollTop', 0); + }); describe('editable', () => { it('should be editable if an editor is specified and editable is undefined/null', async () => { From 42f4ea3b82bacdb92b55792ed2c227c5a41d7702 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Thu, 20 Aug 2026 02:04:30 +0100 Subject: [PATCH 72/74] try trimming apt config --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e74d623bc..2b495f36be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,9 +44,6 @@ jobs: Acquire::Retries "0"; Acquire::http::Timeout "10"; Acquire::https::Timeout "10"; - Acquire::http::Dl-Limit "0"; - Acquire::Languages "none"; - APT::Install-Recommends "false"; EOF npx playwright install chromium firefox webkit --with-deps --no-shell timeout-minutes: 4 From d7e46064025b99abcb2296ca662da3472baa56bb Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Thu, 20 Aug 2026 02:11:14 +0100 Subject: [PATCH 73/74] sync `Install Playwright Browsers` steps --- .github/workflows/ci.yml | 2 +- .github/workflows/publish.yml | 10 ++++++++-- .github/workflows/update-screenshots.yml | 10 ++++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b495f36be..8a6fe2cb1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: Acquire::https::Timeout "10"; EOF npx playwright install chromium firefox webkit --with-deps --no-shell - timeout-minutes: 4 + timeout-minutes: 2 - name: Test run: node --run test:ci diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f45139bd17..d9c733956a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -49,8 +49,14 @@ jobs: run: git diff --exit-code website/routeTree.gen.ts - name: Install Playwright Browsers - run: npx playwright install chromium firefox webkit --with-deps --no-shell - timeout-minutes: 4 + run: | + sudo tee /etc/apt/apt.conf.d/99-ci >/dev/null <<'EOF' + Acquire::Retries "0"; + Acquire::http::Timeout "10"; + Acquire::https::Timeout "10"; + EOF + npx playwright install chromium firefox webkit --with-deps --no-shell + timeout-minutes: 2 - name: Test run: node --run test:ci diff --git a/.github/workflows/update-screenshots.yml b/.github/workflows/update-screenshots.yml index b0672521e3..3893fcfcd9 100644 --- a/.github/workflows/update-screenshots.yml +++ b/.github/workflows/update-screenshots.yml @@ -40,8 +40,14 @@ jobs: run: npm ci - name: Install Playwright Browsers - run: npx playwright install chromium firefox webkit --with-deps --no-shell - timeout-minutes: 4 + run: | + sudo tee /etc/apt/apt.conf.d/99-ci >/dev/null <<'EOF' + Acquire::Retries "0"; + Acquire::http::Timeout "10"; + Acquire::https::Timeout "10"; + EOF + npx playwright install chromium firefox webkit --with-deps --no-shell + timeout-minutes: 2 - name: Update screenshots run: | From 60bd119cae36e82d275f0e64c43dffb8f0cf45fc Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Thu, 20 Aug 2026 18:14:57 +0100 Subject: [PATCH 74/74] simplify --- src/DataGrid.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 74c08a6497..877bee1e66 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -620,7 +620,8 @@ export function DataGrid(props: DataGridPr if (!(target instanceof Element)) return; - const isCellEvent = target.closest('.rdg-cell') !== null; + const cell = target.closest('.rdg-cell'); + const isCellEvent = cell !== null; const isRowEvent = isTreeGrid && target.role === 'row'; if (!isCellEvent && !isRowEvent) return; @@ -638,7 +639,7 @@ export function DataGrid(props: DataGridPr navigate(event); break; default: - handleCellInput(event); + handleCellInput(event, cell); break; } } @@ -678,7 +679,7 @@ export function DataGrid(props: DataGridPr updateRow(column, activePosition.rowIdx, updatedRow); } - function handleCellInput(event: KeyboardEvent) { + function handleCellInput(event: KeyboardEvent, cell: Element | null) { if (!activePositionIsCellInViewport) return; const row = getActiveRow(); const { key, shiftKey } = event; @@ -694,12 +695,8 @@ export function DataGrid(props: DataGridPr } if (isCellEditable(activePosition) && isDefaultCellInput(event, onCellPaste != null)) { - const { target } = event; - // ensure cell is fully visible - if (target instanceof HTMLElement) { - scrollIntoView(target.closest('.rdg-cell')); - } + scrollIntoView(cell); setActivePosition(({ idx, rowIdx }) => ({ idx,