diff --git a/package.json b/package.json index 39f5c13..f379fac 100644 --- a/package.json +++ b/package.json @@ -24,15 +24,15 @@ "dev": "vite", "buildx": "vite build", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "preview": "vite preview", + "test:e2e": "vitest run", + "test:watch": "vitest --ui" }, "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", - "bootstrap": "4.5.0", - "@ag-grid-community/react": "~31.0.2", - "@ag-grid-community/core": "~31.0.2", "@ag-grid-community/client-side-row-model": "~31.0.2", + "@ag-grid-community/core": "~31.0.2", + "@ag-grid-community/react": "~31.0.2", + "@ag-grid-community/styles": "~31.0.2", "@ag-grid-enterprise/charts": "~31.0.2", "@ag-grid-enterprise/clipboard": "~31.0.2", "@ag-grid-enterprise/column-tool-panel": "~31.0.2", @@ -51,16 +51,24 @@ "@ag-grid-enterprise/sparklines": "~31.0.2", "@ag-grid-enterprise/status-bar": "~31.0.2", "@ag-grid-enterprise/viewport-row-model": "~31.0.2", - "@ag-grid-community/styles": "~31.0.2" + "bootstrap": "4.5.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { + "@testing-library/jest-dom": "^6.4.1", + "@testing-library/react": "^14.2.0", + "@testing-library/user-event": "^14.5.2", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react": "^4.0.3", + "@vitest/ui": "^1.2.2", "eslint": "^8.45.0", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", - "vite": "^4.4.5" + "jsdom": "^24.0.0", + "vite": "^4.4.5", + "vitest": "^1.2.2" } -} \ No newline at end of file +} diff --git a/src/tests/basic.test.tsx b/src/tests/basic.test.tsx new file mode 100644 index 0000000..a9a82c9 --- /dev/null +++ b/src/tests/basic.test.tsx @@ -0,0 +1,49 @@ +import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model'; +import { ColDef } from '@ag-grid-community/core'; +import { AgGridReact } from '@ag-grid-community/react'; +import { render, screen } from '@testing-library/react'; +import React, { useRef, useState } from 'react'; + +import { expect, describe, test } from 'vitest'; + +interface RowData { + make: string; + model: string; + price: number; +} + +const App = () => { + const gridRef = useRef>(null); + + const [rowData] = useState([ + { make: 'Toyota', model: 'Celica', price: 35000 }, + { make: 'Ford', model: 'Mondeo', price: 32000 }, + { make: 'Porsche', model: 'Boxster', price: 72000 } + ]); + const [colDefs, setColDefs] = useState[]>([ + { field: 'make' }, + { field: 'model' }, + { field: 'price' }, + ]); + + return ( +
+ + ref={gridRef} + rowData={rowData} + columnDefs={colDefs} + modules={[ClientSideRowModelModule]} /> +
+ ); +}; + +describe('Basic Grid', () => { + + test('render basic grid', async () => { + render(); + expect(screen.findByText('Boxster')).toBeDefined(); + expect(screen.findByText('72000')).toBeDefined(); + + }); + +}); diff --git a/src/tests/cellRenderer.test.tsx b/src/tests/cellRenderer.test.tsx new file mode 100644 index 0000000..e2a3aee --- /dev/null +++ b/src/tests/cellRenderer.test.tsx @@ -0,0 +1,81 @@ +import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model'; +import { ColDef } from '@ag-grid-community/core'; +import { AgGridReact, CustomCellRendererProps } from '@ag-grid-community/react'; +import { render, screen } from '@testing-library/react'; +import React, { useRef, useState } from 'react'; +import { act } from 'react-dom/test-utils'; +import { describe, expect, test } from 'vitest'; + +interface RowData { + make: string; + model: string; + price: number; + bought: boolean; +} + +// cell renderer that contains a button +const BuyCellRenderer = (props: CustomCellRendererProps) => { + const buttonClick = () => { + props.node.setDataValue('bought', true); + }; + + return ( + <> + {props.data?.bought ? + Bought a {props.data?.make} : + + } + + ); +}; + + +const App = () => { + const gridRef = useRef>(null); + + const [rowData] = useState([ + { make: 'Toyota', model: 'Celica', price: 35000, bought: false }, + { make: 'Ford', model: 'Mondeo', price: 32000, bought: false }, + { make: 'Porsche', model: 'Boxster', price: 72000, bought: false } + ]); + const [colDefs, setColDefs] = useState[]>([ + { field: 'make' }, + { field: 'model' }, + { field: 'price' }, + { field: 'bought', cellRenderer: BuyCellRenderer } + ]); + + return ( +
+ + ref={gridRef} + rowData={rowData} + columnDefs={colDefs} + reactiveCustomComponents + modules={[ClientSideRowModelModule]} /> +
+ ); +}; + +describe('Basic Grid', () => { + + test('render basic grid', async () => { + render(); + await screen.findByText('Boxster') + }); + + test('render grid and then sort by price', async () => { + render(); + + let porcheButton = await screen.findByText('Buy: Porsche'); + expect(porcheButton).toBeDefined(); + + // Handy debug function to see the center rows of the grid + // screen.debug(document.querySelector('.ag-center-cols-container')!); + + act(() => porcheButton.click()); + + let bought = await screen.findByText('Bought a Porsche'); + expect(bought).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/src/tests/clickingRows.test.tsx b/src/tests/clickingRows.test.tsx new file mode 100644 index 0000000..acf989b --- /dev/null +++ b/src/tests/clickingRows.test.tsx @@ -0,0 +1,85 @@ +import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model'; +import { ColDef, RowClickedEvent } from '@ag-grid-community/core'; +import { expect, describe, test, beforeEach } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React, { useCallback, useRef, useState } from 'react'; +import { AgGridReact } from '@ag-grid-community/react'; + +interface RowData { + make: string; + model: string; + price: number; +} + +const App = () => { + const gridRef = useRef>(null); + + const [rowData] = useState([ + { make: 'Toyota', model: 'Celica', price: 35000 }, + { make: 'Ford', model: 'Mondeo', price: 32000 }, + { make: 'Porsche', model: 'Boxster', price: 72000 } + ]); + const [colDefs, setColDefs] = useState[]>([ + { field: 'make' }, + { field: 'model' }, + { field: 'price' }, + ]); + + const [rowClicked, setRowClicked] = useState(null); + const [rowDoubleClicked, setRowDoubleClicked] = useState(null); + + const onRowClicked = useCallback((params: RowClickedEvent) => { + setRowClicked(params.data); + }, []); + + const onRowDoubleClicked = useCallback((params: RowClickedEvent) => { + setRowDoubleClicked(params.data); + }, []); + + return ( +
+
Row Clicked: {rowClicked?.make}
+
Row Double Clicked: {rowDoubleClicked?.make}
+
+ + ref={gridRef} + rowData={rowData} + columnDefs={colDefs} + onRowClicked={onRowClicked} + onRowDoubleClicked={onRowDoubleClicked} + modules={[ClientSideRowModelModule]} /> +
+
+ ); +}; + +describe('Row Clicks Grid', () => { + + // render basic AgGridReact + test('render grid and click a row', async () => { + render(); + + let row = await screen.findByText('Ford'); + expect(row).toBeDefined(); + + await userEvent.click(row); + + let rowClicked = await screen.findByTestId('rowClicked'); + expect(rowClicked.textContent).toBe('Row Clicked: Ford'); + }); + + // render basic AgGridReact + test('render grid and double click a row', async () => { + render(); + + let row = await screen.findByText('Porsche'); + expect(row).toBeDefined(); + + await userEvent.dblClick(row); + + let rowClicked = await screen.findByTestId('rowDoubleClicked'); + expect(rowClicked.textContent).toBe('Row Double Clicked: Porsche'); + }); + +}); \ No newline at end of file diff --git a/src/tests/editor.test.tsx b/src/tests/editor.test.tsx new file mode 100644 index 0000000..0ee36ec --- /dev/null +++ b/src/tests/editor.test.tsx @@ -0,0 +1,63 @@ +import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model'; +import { ColDef } from '@ag-grid-community/core'; +import { AgGridReact } from '@ag-grid-community/react'; +import { describe, test, expect } from 'vitest'; +import { fireEvent, render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React, { useRef, useState } from 'react'; + +interface RowData { + make: string; + model: string; + price: number; +} + +const App = () => { + const gridRef = useRef>(null); + + const [rowData] = useState([ + { make: 'Toyota', model: 'Celica', price: 35000 }, + { make: 'Ford', model: 'Mondeo', price: 32000 }, + { make: 'Porsche', model: 'Boxster', price: 72000 } + ]); + const [colDefs, setColDefs] = useState[]>([ + { field: 'make' }, + { field: 'model' }, + { field: 'price', editable: true, valueFormatter: (params) => "$" + params.value.toLocaleString()}, + ]); + + return ( +
+ + ref={gridRef} + rowData={rowData} + columnDefs={colDefs} + modules={[ClientSideRowModelModule]} /> +
+ ); +}; + +describe('Edit Cell Grid', () => { + + test('double click cell to edit', async () => { + render(); + + let porschePrice = await screen.findByText('$72,000') + expect(porschePrice).toBeDefined(); + + // double click to enter edit mode + await userEvent.dblClick(porschePrice); + + let input: HTMLInputElement = within(porschePrice).getByLabelText('Input Editor'); + + await userEvent.keyboard('100000'); + + // press enter to save + fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); + + porschePrice = await screen.findByText('$100,000') + expect(porschePrice).toBeDefined(); + + }); + +}); diff --git a/src/tests/filter.test.tsx b/src/tests/filter.test.tsx new file mode 100644 index 0000000..1c12559 --- /dev/null +++ b/src/tests/filter.test.tsx @@ -0,0 +1,76 @@ +import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model'; +import { ColDef } from '@ag-grid-community/core'; +import { describe, expect, test } from 'vitest'; +import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React, { useRef, useState } from 'react'; +import { act } from 'react-dom/test-utils'; +import { AgGridReact } from '@ag-grid-community/react'; + +interface RowData { + make: string; + model: string; + price: number; +} + +const App = () => { + const gridRef = useRef>(null); + + const [rowData] = useState([ + { make: 'Toyota', model: 'Celica', price: 35000 }, + { make: 'Ford', model: 'Mondeo', price: 32000 }, + { make: 'Porsche', model: 'Boxster', price: 72000 } + ]); + const [colDefs, setColDefs] = useState[]>([ + { field: 'make' }, + { field: 'model' }, + { field: 'price', filter: true, floatingFilter: true }, + ]); + + return ( +
+ + ref={gridRef} + rowData={rowData} + columnDefs={colDefs} + modules={[ClientSideRowModelModule]} /> +
+ ); +}; + +describe('Filter Grid Data', () => { + + test('enter value to floating filter', async () => { + render(); + + let fordCell = await screen.findByText('Ford') + expect(fordCell).toBeDefined(); + + let priceFloatingFilters: HTMLInputElement[] = await screen.findAllByLabelText('Price Filter Input'); + let priceFloatingFilter = priceFloatingFilters[0]; + + expect(priceFloatingFilter).toBeDefined(); + + let rows = await screen.findAllByRole('row'); + + // 3 rows + the header rows (2) + expect(rows.length).toBe(5); + + // Click to focus the floating filter input + await userEvent.click(priceFloatingFilter); + await act(async () => { + return await userEvent.keyboard('32000'); + }); + + await waitForElementToBeRemoved(() => { + // Wait for Porsche row to be filtered out + return screen.queryByText('Porsche') + }); + + rows = await screen.findAllByRole('row'); + // 1 rows + the header rows (2) + expect(rows.length).toBe(3); + + }); + +}); \ No newline at end of file diff --git a/src/tests/sorting.test.tsx b/src/tests/sorting.test.tsx new file mode 100644 index 0000000..a044afb --- /dev/null +++ b/src/tests/sorting.test.tsx @@ -0,0 +1,76 @@ +import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model'; +import { ColDef } from '@ag-grid-community/core'; +import { AgGridReact } from '@ag-grid-community/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React, { useRef, useState } from 'react'; +import { describe, expect, test } from 'vitest'; + +interface RowData { + make: string; + model: string; + price: number; +} + +const App = () => { + const gridRef = useRef>(null); + + const [rowData] = useState([ + { make: 'Toyota', model: 'Celica', price: 35000 }, + { make: 'Ford', model: 'Mondeo', price: 32000 }, + { make: 'Porsche', model: 'Boxster', price: 72000 } + ]); + const [colDefs, setColDefs] = useState[]>([ + { field: 'make' }, + { field: 'model' }, + { field: 'price' }, + ]); + + return ( +
+ + ref={gridRef} + rowData={rowData} + columnDefs={colDefs} + ensureDomOrder // Required to test sorting via test queries so that DOM order matches the order of the rows in the grid + modules={[ClientSideRowModelModule]} /> +
+ ); +}; + +describe('Sorting Grid Data', () => { + + test('render basic grid', async () => { + render(); + await screen.findByText('Boxster') + + }); + + test('render grid and then sort by price', async () => { + render(); + + let rowsBefore: string[] = []; + document.querySelectorAll('.ag-row').forEach((row, index) => { + rowsBefore.push(row.textContent!); + }); + + expect(rowsBefore[0]).toBe('ToyotaCelica35000'); + expect(rowsBefore[1]).toBe('FordMondeo32000'); + expect(rowsBefore[2]).toBe('PorscheBoxster72000'); + + // Click the price header to sort by price + const priceHeader = (await screen.findByText('Price')); + await userEvent.click(priceHeader); + + + let rowsAfter: string[] = []; + document.querySelectorAll('.ag-row').forEach((row, index) => { + rowsAfter.push(row.textContent!); + }); + + expect(rowsAfter[0]).toBe('FordMondeo32000'); + expect(rowsAfter[1]).toBe('ToyotaCelica35000'); + expect(rowsAfter[2]).toBe('PorscheBoxster72000'); + }); + +}); \ No newline at end of file diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..a9cab0a --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,8 @@ +import { expect, afterEach } from 'vitest'; +import { cleanup } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; + + +afterEach(() => { + cleanup(); +}); \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index f363cfe..09f51dd 100644 --- a/vite.config.js +++ b/vite.config.js @@ -5,8 +5,12 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], resolve: { - // for ag-grid local dev only - not necessary if not using symlinks - preserveSymlinks: true, dedupe: ['@ag-grid-community/core'] - } + }, + test: { + environment: 'jsdom', + setupFiles: ['./tests/setup.ts'], + testMatch: ['./tests/**/*.test.tsx'], + globals: true + } })