diff --git a/package.json b/package.json index 90c1dc77ec..50563bb5c5 100644 --- a/package.json +++ b/package.json @@ -56,11 +56,11 @@ "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@typescript/native-preview": "^7.0.0-dev.20260401.1", + "@typescript/native-preview": "^7.0.0-dev.20260407.1", "@vitejs/plugin-react": "^6.0.1", - "@vitest/browser-playwright": "^4.1.2", - "@vitest/coverage-istanbul": "^4.1.2", - "@vitest/eslint-plugin": "^1.6.13", + "@vitest/browser-playwright": "^4.1.3", + "@vitest/coverage-istanbul": "^4.1.3", + "@vitest/eslint-plugin": "^1.6.14", "clsx": "^2.1.1", "ecij": "^0.4.1", "eslint": "^10.0.3", @@ -68,7 +68,7 @@ "eslint-plugin-sonarjs": "^4.0.2", "jspdf": "^4.2.0", "jspdf-autotable": "^5.0.7", - "oxfmt": "0.43.0", + "oxfmt": "0.44.0", "playwright": "~1.59.0", "postcss": "^8.5.2", "react": "^19.2.4", @@ -76,9 +76,9 @@ "tsdown": "^0.21.7", "typescript": "~6.0.2", "typescript-eslint": "^8.58.0", - "vite": "^8.0.3", - "vitest": "^4.1.2", - "vitest-browser-react": "^2.1.0" + "vite": "^8.0.6", + "vitest": "^4.1.3", + "vitest-browser-react": "^2.2.0" }, "peerDependencies": { "react": "^19.2", diff --git a/test/browser/column/cellClass.test.ts b/test/browser/column/cellClass.test.ts index 08c4a8b703..c2c182d3cb 100644 --- a/test/browser/column/cellClass.test.ts +++ b/test/browser/column/cellClass.test.ts @@ -35,8 +35,8 @@ test('cellClass is a string', async () => { await setup({ columns, rows }); const cell1 = page.getCell({ name: '0' }); const cell2 = page.getCell({ name: '1' }); - await expect.element(cell1).toHaveClass(`${cellClassname} my-cell`, { exact: true }); - await expect.element(cell2).toHaveClass(`${cellClassname} my-cell`, { exact: true }); + await expect.element(cell1).toHaveClass(cellClassname, 'my-cell', { exact: true }); + await expect.element(cell2).toHaveClass(cellClassname, 'my-cell', { exact: true }); }); test('cellClass returns a string', async () => { @@ -50,8 +50,8 @@ test('cellClass returns a string', async () => { await setup({ columns, rows }); const cell1 = page.getCell({ name: '0' }); const cell2 = page.getCell({ name: '1' }); - await expect.element(cell1).toHaveClass(`${cellClassname} my-cell-0`, { exact: true }); - await expect.element(cell2).toHaveClass(`${cellClassname} my-cell-1`, { exact: true }); + await expect.element(cell1).toHaveClass(cellClassname, 'my-cell-0', { exact: true }); + await expect.element(cell2).toHaveClass(cellClassname, 'my-cell-1', { exact: true }); }); test('cellClass returns undefined', async () => { diff --git a/test/browser/column/frozen.test.ts b/test/browser/column/frozen.test.ts index 7cfd10f783..e984ee4a3c 100644 --- a/test/browser/column/frozen.test.ts +++ b/test/browser/column/frozen.test.ts @@ -38,12 +38,8 @@ test('frozen column have a specific class, and are stable-sorted before non-froz await expect.element(cell3).toHaveTextContent('col2'); await expect.element(cell4).toHaveTextContent('col4'); - await expect - .element(cell1) - .toHaveClass(`${cellClassname} ${cellFrozenClassname}`, { exact: true }); - await expect - .element(cell2) - .toHaveClass(`${cellClassname} ${cellFrozenClassname}`, { exact: true }); + await expect.element(cell1).toHaveClass(cellClassname, cellFrozenClassname, { exact: true }); + await expect.element(cell2).toHaveClass(cellClassname, cellFrozenClassname, { exact: true }); await expect.element(cell3).toHaveClass(cellClassname, { exact: true }); await expect.element(cell4).toHaveClass(cellClassname, { exact: true }); }); diff --git a/test/browser/column/headerCellClass.test.ts b/test/browser/column/headerCellClass.test.ts index a143555952..3533915134 100644 --- a/test/browser/column/headerCellClass.test.ts +++ b/test/browser/column/headerCellClass.test.ts @@ -21,9 +21,7 @@ test('headerCellClass is either nullish or a string', async () => { await setup({ columns, rows: [] }); await expect.element(headerCells.nth(0)).toHaveClass(cellClassname, { exact: true }); - await expect - .element(headerCells.nth(1)) - .toHaveClass(`${cellClassname} my-header`, { exact: true }); + await expect.element(headerCells.nth(1)).toHaveClass(cellClassname, 'my-header', { exact: true }); }); test('columnGroup.headerCellClass is either nullish or a string', async () => { @@ -41,7 +39,5 @@ test('columnGroup.headerCellClass is either nullish or a string', async () => { await setup({ columns, rows: [] }); await expect.element(headerCells.nth(0)).toHaveClass(cellClassname, { exact: true }); - await expect - .element(headerCells.nth(1)) - .toHaveClass(`${cellClassname} my-header`, { exact: true }); + await expect.element(headerCells.nth(1)).toHaveClass(cellClassname, 'my-header', { exact: true }); }); diff --git a/test/browser/column/renderEditCell.test.tsx b/test/browser/column/renderEditCell.test.tsx index 6cdc712ccc..bf2f716a79 100644 --- a/test/browser/column/renderEditCell.test.tsx +++ b/test/browser/column/renderEditCell.test.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; -import { page, userEvent } from 'vitest/browser'; +import { page, server, userEvent } from 'vitest/browser'; import { DataGrid } from '../../../src'; import type { Column, DataGridProps } from '../../../src'; @@ -96,7 +96,7 @@ describe('Editor', () => { await userEvent.click(getCellsAtRowIndex(0).nth(0)); const activeRowCells = getRowWithCell(page.getActiveCell()).getCell(); await testCount(activeRowCells, 2); - await scrollGrid({ top: 2001 }); + scrollGrid({ top: 2001 }); await testCount(activeRowCells, 1); await expect.element(col1Editor).not.toBeInTheDocument(); await expect.element(grid).toHaveProperty('scrollTop', 2001); @@ -258,11 +258,19 @@ describe('Editor', () => { await userEvent.dblClick(page.getCell({ name: 'name0' })); await userEvent.keyboard('abc'); - - await scrollGrid({ top: 1500 }); + if (server.browser === 'firefox') { + // When typing, Firefox scroll to the caret asynchronously, + // but does not to cancel the scroll task when calling `.scroll()` on the grid + // https://github.com/mozilla-firefox/firefox/blob/287c6cf323492ae0cc031e468c1d87f623413f50/dom/html/TextControlElement.cpp#L330 + // https://github.com/mozilla-firefox/firefox/blob/287c6cf323492ae0cc031e468c1d87f623413f50/dom/base/Selection.cpp#L3828 + await new Promise(requestAnimationFrame); + // Alternatively, configuring playwright's launchOptions.slowMo to 1 works, + // but slows down the tests. + } + scrollGrid({ top: 1500 }); await userEvent.click(page.getCell({ name: 'name43' })); await expect.element(page.getActiveCell()).toHaveTextContent(/^name43$/); - await scrollGrid({ top: 0 }); + scrollGrid({ top: 0 }); await expect.element(page.getCell({ name: 'name0abc' })).toBeVisible(); }); diff --git a/test/browser/column/summaryCellClass.test.ts b/test/browser/column/summaryCellClass.test.ts index 35734cf12e..667b1257a9 100644 --- a/test/browser/column/summaryCellClass.test.ts +++ b/test/browser/column/summaryCellClass.test.ts @@ -35,7 +35,7 @@ test('summaryCellClass is a string', async () => { ]; await setup({ columns, topSummaryRows, bottomSummaryRows, rows: [] }); for (const cell of cells.all()) { - await expect.element(cell).toHaveClass(`${cellClassname} my-cell`, { exact: true }); + await expect.element(cell).toHaveClass(cellClassname, 'my-cell', { exact: true }); } }); @@ -48,10 +48,10 @@ test('summaryCellClass returns a string', async () => { } ]; await setup({ columns, topSummaryRows, bottomSummaryRows, rows: [] }); - await expect.element(cells.nth(0)).toHaveClass(`${cellClassname} my-cell-0`, { exact: true }); - await expect.element(cells.nth(1)).toHaveClass(`${cellClassname} my-cell-1`, { exact: true }); - await expect.element(cells.nth(2)).toHaveClass(`${cellClassname} my-cell-2`, { exact: true }); - await expect.element(cells.nth(3)).toHaveClass(`${cellClassname} my-cell-3`, { exact: true }); + await expect.element(cells.nth(0)).toHaveClass(cellClassname, 'my-cell-0', { exact: true }); + await expect.element(cells.nth(1)).toHaveClass(cellClassname, 'my-cell-1', { exact: true }); + await expect.element(cells.nth(2)).toHaveClass(cellClassname, 'my-cell-2', { exact: true }); + await expect.element(cells.nth(3)).toHaveClass(cellClassname, 'my-cell-3', { exact: true }); }); test('summaryCellClass returns undefined', async () => { diff --git a/test/browser/headerRowClass.test.ts b/test/browser/headerRowClass.test.ts index a532b5a46b..cb3e8ceb20 100644 --- a/test/browser/headerRowClass.test.ts +++ b/test/browser/headerRowClass.test.ts @@ -28,7 +28,5 @@ test('headerRowClass is a string', async () => { rows, headerRowClass: 'my-header-row' }); - await expect - .element(headerRow) - .toHaveClass(`${headerRowClassname} my-header-row`, { exact: true }); + await expect.element(headerRow).toHaveClass(headerRowClassname, 'my-header-row', { exact: true }); }); diff --git a/test/browser/keyboardNavigation.test.tsx b/test/browser/keyboardNavigation.test.tsx index 46806e1f4d..0ecb8561e2 100644 --- a/test/browser/keyboardNavigation.test.tsx +++ b/test/browser/keyboardNavigation.test.tsx @@ -13,7 +13,7 @@ import { const activeCell = page.getActiveCell(); const activeSelectAllCheckbox = activeCell.getSelectAllCheckbox(); -const activeSelectCheckbox = activeCell.getByRole('checkbox', { name: 'Select', exact: true }); +const activeSelectCheckbox = activeCell.getByRole('checkbox', { name: 'Select' }); type Row = undefined; @@ -279,13 +279,13 @@ test('navigation when active cell not in the viewport', async () => { await userEvent.keyboard('{Control>}{end}{/Control}{arrowup}{arrowup}'); await validateCellPosition(99, 100); await expect.element(activeRowCells).not.toHaveLength(1); - await scrollGrid({ top: 0 }); + scrollGrid({ top: 0 }); await testCount(activeRowCells, 1); await userEvent.keyboard('{arrowup}'); await validateCellPosition(99, 99); await expect.element(activeRowCells).not.toHaveLength(1); - await scrollGrid({ left: 0 }); + scrollGrid({ left: 0 }); await userEvent.keyboard('{arrowdown}'); await validateCellPosition(99, 100); @@ -293,7 +293,7 @@ test('navigation when active cell not in the viewport', async () => { '{home}{arrowright}{arrowright}{arrowright}{arrowright}{arrowright}{arrowright}{arrowright}' ); await validateCellPosition(7, 100); - await scrollGrid({ left: 2000 }); + scrollGrid({ left: 2000 }); await userEvent.keyboard('{arrowleft}'); await validateCellPosition(6, 100); }); diff --git a/test/browser/rowClass.test.ts b/test/browser/rowClass.test.ts index 7b5659dab2..b2633432b7 100644 --- a/test/browser/rowClass.test.ts +++ b/test/browser/rowClass.test.ts @@ -19,9 +19,9 @@ test('rowClass is undefined', async () => { rows: initialRows, rowClass: undefined }); - await expect.element(rows.nth(0)).toHaveClass(`${rowClassname} rdg-row-even`, { exact: true }); - await expect.element(rows.nth(1)).toHaveClass(`${rowClassname} rdg-row-odd`, { exact: true }); - await expect.element(rows.nth(2)).toHaveClass(`${rowClassname} rdg-row-even`, { exact: true }); + await expect.element(rows.nth(0)).toHaveClass(rowClassname, 'rdg-row-even', { exact: true }); + await expect.element(rows.nth(1)).toHaveClass(rowClassname, 'rdg-row-odd', { exact: true }); + await expect.element(rows.nth(2)).toHaveClass(rowClassname, 'rdg-row-even', { exact: true }); }); test('rowClass returns a string', async () => { @@ -32,13 +32,13 @@ test('rowClass returns a string', async () => { }); await expect .element(rows.nth(0)) - .toHaveClass(`${rowClassname} rdg-row-even my-row-0`, { exact: true }); + .toHaveClass(rowClassname, 'rdg-row-even my-row-0', { exact: true }); await expect .element(rows.nth(1)) - .toHaveClass(`${rowClassname} rdg-row-odd my-row-1`, { exact: true }); + .toHaveClass(rowClassname, 'rdg-row-odd my-row-1', { exact: true }); await expect .element(rows.nth(2)) - .toHaveClass(`${rowClassname} rdg-row-even my-row-2`, { exact: true }); + .toHaveClass(rowClassname, 'rdg-row-even my-row-2', { exact: true }); }); test('rowClass returns undefined', async () => { @@ -47,7 +47,7 @@ test('rowClass returns undefined', async () => { rows: initialRows, rowClass: () => undefined }); - await expect.element(rows.nth(0)).toHaveClass(`${rowClassname} rdg-row-even`, { exact: true }); - await expect.element(rows.nth(1)).toHaveClass(`${rowClassname} rdg-row-odd`, { exact: true }); - await expect.element(rows.nth(2)).toHaveClass(`${rowClassname} rdg-row-even`, { exact: true }); + await expect.element(rows.nth(0)).toHaveClass(rowClassname, 'rdg-row-even', { exact: true }); + await expect.element(rows.nth(1)).toHaveClass(rowClassname, 'rdg-row-odd', { exact: true }); + await expect.element(rows.nth(2)).toHaveClass(rowClassname, 'rdg-row-even', { exact: true }); }); diff --git a/test/browser/utils.tsx b/test/browser/utils.tsx index 381a80d023..6860786170 100644 --- a/test/browser/utils.tsx +++ b/test/browser/utils.tsx @@ -25,15 +25,9 @@ export async function validateCellPosition(columnIdx: number, rowIdx: number) { await expect.element(row).toHaveAttribute('aria-rowindex', `${rowIdx + 1}`); } -export async function scrollGrid(options: ScrollToOptions) { - await new Promise((resolve) => { - // wait for browser state to stablize before scrolling, to avoid flaky scroll-related tests - requestAnimationFrame(() => { - const gridElement = page.getGrid().element(); - gridElement.addEventListener('scrollend', resolve, { once: true }); - gridElement.scroll(options); - }); - }); +export function scrollGrid(options: ScrollToOptions) { + const grid = page.getGrid().element(); + grid.scroll(options); } export function testCount(locator: Locator, expectedCount: number) { diff --git a/test/browser/virtualization.test.ts b/test/browser/virtualization.test.ts index d39bbf578f..01fa277812 100644 --- a/test/browser/virtualization.test.ts +++ b/test/browser/virtualization.test.ts @@ -102,50 +102,50 @@ test('virtualization is enabled', async () => { await assertHeaderCells(18, 0, 17); await assertRows(34, 0, 33); await assertCells(0, 18, 0, 17); - await scrollGrid({ top: 244 }); + scrollGrid({ top: 244 }); await assertRows(39, 2, 40); - await scrollGrid({ top: 245 }); + scrollGrid({ top: 245 }); await assertRows(38, 3, 40); - await scrollGrid({ top: 419 }); + scrollGrid({ top: 419 }); await assertRows(39, 7, 45); - await scrollGrid({ top: 420 }); + scrollGrid({ top: 420 }); await assertRows(38, 8, 45); - await scrollGrid({ top: 524 }); + scrollGrid({ top: 524 }); await assertRows(39, 10, 48); - await scrollGrid({ top: 525 }); + scrollGrid({ top: 525 }); await assertRows(38, 11, 48); - await scrollGrid({ top: 1000 }); + scrollGrid({ top: 1000 }); await assertRows(39, 24, 62); // scroll height = header height + row height * row count // max top = scroll height - grid height - await scrollGrid({ top: rowHeight + rowHeight * 100 - 1080 }); + scrollGrid({ top: rowHeight + rowHeight * 100 - 1080 }); await assertRows(34, 66, 99); - await scrollGrid({ left: 92 }); + scrollGrid({ left: 92 }); await assertHeaderCells(18, 0, 17); await assertCells(66, 18, 0, 17); - await scrollGrid({ left: 93 }); + scrollGrid({ left: 93 }); await assertHeaderCells(19, 0, 18); await assertCells(66, 19, 0, 18); - await scrollGrid({ left: 209 }); + scrollGrid({ left: 209 }); await assertHeaderCells(19, 0, 18); await assertCells(66, 19, 0, 18); - await scrollGrid({ left: 210 }); + scrollGrid({ left: 210 }); await assertHeaderCells(18, 1, 18); await assertCells(66, 18, 1, 18); // max left = row width - grid width - await scrollGrid({ left: 3600 - 1920 }); + scrollGrid({ left: 3600 - 1920 }); await assertHeaderCells(17, 13, 29); await assertCells(66, 17, 13, 29); }); @@ -157,13 +157,13 @@ test('virtualization is enabled with 4 frozen columns', async () => { await assertHeaderCellIndexes(indexes); await assertCellIndexes(0, indexes); - await scrollGrid({ left: 1000 }); + scrollGrid({ left: 1000 }); indexes = [0, 1, 2, 3, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]; await assertHeaderCellIndexes(indexes); await assertCellIndexes(0, indexes); // max left = row width - grid width - await scrollGrid({ left: 3600 - 1920 }); + scrollGrid({ left: 3600 - 1920 }); indexes = [0, 1, 2, 3, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]; await assertHeaderCellIndexes(indexes); await assertCellIndexes(0, indexes); @@ -179,12 +179,12 @@ test('virtualization is enabled with all columns frozen', async () => { await assertHeaderCellIndexes(indexes); await assertCellIndexes(0, indexes); - await scrollGrid({ left: 1000 }); + scrollGrid({ left: 1000 }); await assertHeaderCellIndexes(indexes); await assertCellIndexes(0, indexes); // max left = row width - grid width - await scrollGrid({ left: 3600 - 1920 }); + scrollGrid({ left: 3600 - 1920 }); await assertHeaderCellIndexes(indexes); await assertCellIndexes(0, indexes); }); @@ -197,7 +197,7 @@ test('virtualization is enabled with 2 summary rows', async () => { 26, 27, 28, 29, 30, 31, 102, 103 ]); - await scrollGrid({ top: 1000 }); + scrollGrid({ top: 1000 }); await assertRowIndexes([ 0, 1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 102, 103 diff --git a/test/setupBrowser.ts b/test/setupBrowser.ts index 1823974c50..51a332abc8 100644 --- a/test/setupBrowser.ts +++ b/test/setupBrowser.ts @@ -34,21 +34,19 @@ locators.extend({ }, getHeaderRow(opts?: LocatorByRoleOptions) { - return this.getByRole('row', defaultToExactOpts(opts)).and( - this.getBySelector('.rdg-header-row') - ); + return this.getByRole('row', opts).and(this.getBySelector('.rdg-header-row')); }, getHeaderCell(opts?: LocatorByRoleOptions) { - return this.getByRole('columnheader', defaultToExactOpts(opts)); + return this.getByRole('columnheader', opts); }, getRow(opts?: LocatorByRoleOptions) { - return this.getByRole('row', defaultToExactOpts(opts)).and(this.getBySelector('.rdg-row')); + return this.getByRole('row', opts).and(this.getBySelector('.rdg-row')); }, getCell(opts?: LocatorByRoleOptions) { - return this.getByRole('gridcell', defaultToExactOpts(opts)); + return this.getByRole('gridcell', opts); }, getSelectAllCheckbox() { @@ -68,19 +66,6 @@ locators.extend({ } }); -function defaultToExactOpts( - opts: LocatorByRoleOptions | undefined -): LocatorByRoleOptions | undefined { - if (opts != null && opts.exact == null && typeof opts.name === 'string') { - return { - ...opts, - exact: true - }; - } - - return opts; -} - beforeEach(async () => { // 1. reset cursor position to avoid hover issues // 2. force focus to be on the page diff --git a/vite.config.ts b/vite.config.ts index c9254e508a..eabc6bb119 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -123,6 +123,9 @@ export default defineConfig( } } }, + locators: { + exact: true + }, instances: [ { browser: 'chromium',