Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ export default [
'testing-library/prefer-presence-queries': 1,
'testing-library/prefer-query-by-disappearance': 1,
'testing-library/prefer-query-matchers': 0,
'testing-library/prefer-screen-queries': 1,
'testing-library/prefer-screen-queries': 0,
Copy link
Collaborator Author

@amanmahajan7 amanmahajan7 Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to disable it as this complains about page...

'testing-library/prefer-user-event': 1,
'testing-library/render-result-naming-convention': 0
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@
"rollup-plugin-postcss": "^4.0.2",
"typescript": "~5.6.2",
"vite": "^5.4.5",
"vitest": "^2.1.1"
"vitest": "^2.1.1",
"vitest-browser-react": "^0.0.3"
},
"peerDependencies": {
"react": "^18.0 || ^19.0",
Expand Down
34 changes: 21 additions & 13 deletions test/browser/TextEditor.test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { useState } from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { page, userEvent } from '@vitest/browser/context';

import DataGrid, { textEditor } from '../../src';
import type { Column } from '../../src';
import { getCells } from './utils';
import { getCellsNew } from './utils';

interface Row {
readonly name: string;
}

const columns: readonly Column<Row>[] = [{ key: 'name', name: 'Name', renderEditCell: textEditor }];
const columns: readonly Column<Row>[] = [
{
key: 'name',
name: 'Name',
renderEditCell: textEditor,
editorOptions: {
commitOnOutsideClick: false
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this so we can test the blur event

}
}
];
const initialRows: readonly Row[] = [{ name: 'Tacitus Kilgore' }];

function Test() {
Expand All @@ -20,10 +28,10 @@ function Test() {
}

test('TextEditor', async () => {
render(<Test />);
page.render(<Test />);

await userEvent.dblClick(getCells()[0]);
let input: HTMLInputElement | null = screen.getByRole<HTMLInputElement>('textbox');
await userEvent.dblClick(getCellsNew()[0]);
let input = page.getByRole('textbox').element() as HTMLInputElement;
expect(input).toHaveClass('rdg-text-editor');
// input value is row[column.key]
expect(input).toHaveValue(initialRows[0].name);
Expand All @@ -36,13 +44,13 @@ test('TextEditor', async () => {
// pressing escape closes the editor without committing
await userEvent.keyboard('Test{escape}');
expect(input).not.toBeInTheDocument();
expect(getCells()[0]).toHaveTextContent(/^Tacitus Kilgore$/);
await expect.element(getCellsNew()[0]).toHaveTextContent(/^Tacitus Kilgore$/);

// blurring the input closes and commits the editor
await userEvent.dblClick(getCells()[0]);
input = screen.getByRole<HTMLInputElement>('textbox');
await userEvent.keyboard('Jim Milton');
fireEvent.blur(input);
await userEvent.dblClick(getCellsNew()[0]);
input = page.getByRole('textbox').element() as HTMLInputElement;
await userEvent.fill(input, 'Jim Milton');
await userEvent.tab();
expect(input).not.toBeInTheDocument();
expect(getCells()[0]).toHaveTextContent(/^Jim Milton$/);
await expect.element(getCellsNew()[0]).toHaveTextContent(/^Jim Milton$/);
});
155 changes: 76 additions & 79 deletions test/browser/column/renderEditCell.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { waitFor } from '@testing-library/react';
import { page, userEvent } from '@vitest/browser/context';

import DataGrid from '../../../src';
import type { Column, DataGridProps } from '../../../src';
Expand All @@ -14,79 +14,77 @@ interface Row {

describe('Editor', () => {
it('should open editor on double click', async () => {
render(<EditorTest />);
page.render(<EditorTest />);
const editor = page.getByRole('spinbutton', { name: 'col1-editor' });
await userEvent.click(getCellsAtRowIndex(0)[0]);
expect(screen.queryByRole('spinbutton', { name: 'col1-editor' })).not.toBeInTheDocument();
expect(editor.query()).not.toBeInTheDocument();
await userEvent.dblClick(getCellsAtRowIndex(0)[0]);
expect(screen.getByRole('spinbutton', { name: 'col1-editor' })).toHaveValue(1);
await expect.element(editor).toHaveValue(1);
await userEvent.keyboard('2');
await userEvent.tab();
expect(screen.queryByRole('spinbutton', { name: 'col1-editor' })).not.toBeInTheDocument();
expect(getCellsAtRowIndex(0)[0]).toHaveTextContent(/^21$/);
expect(editor.query()).not.toBeInTheDocument();
expect(getCellsAtRowIndex(0)[0]).toHaveTextContent(/^12$/);
});

it('should open and commit changes on enter', async () => {
render(<EditorTest />);
page.render(<EditorTest />);
const editor = page.getByRole('spinbutton', { name: 'col1-editor' });
await userEvent.click(getCellsAtRowIndex(0)[0]);
expect(screen.queryByRole('spinbutton', { name: 'col1-editor' })).not.toBeInTheDocument();
expect(editor.query()).not.toBeInTheDocument();
await userEvent.keyboard('{enter}');
expect(screen.getByRole('spinbutton', { name: 'col1-editor' })).toHaveValue(1);
await expect.element(editor).toHaveValue(1);
await userEvent.keyboard('3{enter}');
expect(getCellsAtRowIndex(0)[0]).toHaveTextContent(/^31$/);
expect(getCellsAtRowIndex(0)[0]).toHaveTextContent(/^13$/);
expect(getCellsAtRowIndex(0)[0]).toHaveFocus();
expect(screen.queryByRole('spinbutton', { name: 'col1-editor' })).not.toBeInTheDocument();
expect(editor.query()).not.toBeInTheDocument();
});

it('should open editor when user types', async () => {
render(<EditorTest />);
page.render(<EditorTest />);
await userEvent.click(getCellsAtRowIndex(0)[0]);
await userEvent.keyboard('123{enter}');
expect(getCellsAtRowIndex(0)[0]).toHaveTextContent(/^1231$/);
expect(getCellsAtRowIndex(0)[0]).toHaveTextContent(/^1123$/);
});

it('should close editor and discard changes on escape', async () => {
render(<EditorTest />);
page.render(<EditorTest />);
await userEvent.dblClick(getCellsAtRowIndex(0)[0]);
expect(screen.getByRole('spinbutton', { name: 'col1-editor' })).toHaveValue(1);
const editor = page.getByRole('spinbutton', { name: 'col1-editor' });
await expect.element(editor).toHaveValue(1);
await userEvent.keyboard('2222{escape}');
expect(screen.queryByRole('spinbutton', { name: 'col1-editor' })).not.toBeInTheDocument();
expect(editor.query()).not.toBeInTheDocument();
expect(getCellsAtRowIndex(0)[0]).toHaveTextContent(/^1$/);
expect(getCellsAtRowIndex(0)[0]).toHaveFocus();
});

it('should commit changes and close editor when clicked outside', async () => {
render(<EditorTest />);
page.render(<EditorTest />);
await userEvent.dblClick(getCellsAtRowIndex(0)[0]);
const editor = screen.getByRole('spinbutton', { name: 'col1-editor' });
expect(editor).toHaveValue(1);
const editor = page.getByRole('spinbutton', { name: 'col1-editor' });
await expect.element(editor).toHaveValue(1);
await userEvent.keyboard('2222');
await userEvent.click(screen.getByText('outside'));
await userEvent.click(page.getByText('outside'));
await waitFor(() => {
expect(editor).not.toBeInTheDocument();
expect(editor.query()).not.toBeInTheDocument();
});
expect(getCellsAtRowIndex(0)[0]).toHaveTextContent(/^22221$/);
expect(getCellsAtRowIndex(0)[0]).toHaveTextContent(/^12222$/);
});

it('should commit quickly enough on outside clicks so click event handlers access the latest rows state', async () => {
const onSave = vi.fn();
render(<EditorTest onSave={onSave} />);
const user = userEvent.setup();
await user.dblClick(getCellsAtRowIndex(0)[0]);
await user.keyboard('234');
page.render(<EditorTest onSave={onSave} />);
await userEvent.dblClick(getCellsAtRowIndex(0)[0]);
await userEvent.keyboard('234');
expect(onSave).not.toHaveBeenCalled();
const saveButton = screen.getByRole('button', { name: 'save' });
const saveButton = page.getByRole('button', { name: 'save' });

// await userEvent.click() triggers both mousedown and click, but without delay,
// which isn't realistic, and isn't enough to trigger outside click detection
await user.pointer([{ keys: '[MouseLeft>]', target: saveButton }]);
await act(async () => {
await new Promise(requestAnimationFrame);
});
await user.pointer({ keys: '[/MouseLeft]' });
await userEvent.click(saveButton, { delay: 10 });
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😎


expect(onSave).toHaveBeenCalledTimes(1);
expect(onSave).toHaveBeenCalledWith([
{ col1: 2341, col2: 'a1' },
{ col1: 1234, col2: 'a1' },
{ col1: 2, col2: 'a2' }
]);
});
Expand All @@ -97,101 +95,100 @@ describe('Editor', () => {
rows.push({ col1: i, col2: `${i}` });
}

render(<EditorTest gridRows={rows} />);
page.render(<EditorTest gridRows={rows} />);
await userEvent.click(getCellsAtRowIndex(0)[0]);
expect(getCellsAtRowIndex(0)).toHaveLength(2);
await scrollGrid({ scrollTop: 2000 });
expect(getCellsAtRowIndex(0)).toHaveLength(1);
expect(screen.queryByRole('spinbutton', { name: 'col1-editor' })).not.toBeInTheDocument();
const editor = page.getByRole('spinbutton', { name: 'col1-editor' });
expect(editor.query()).not.toBeInTheDocument();
expect(getGrid().scrollTop).toBe(2000);
await userEvent.keyboard('123');
expect(getCellsAtRowIndex(0)).toHaveLength(2);
expect(screen.getByRole('spinbutton', { name: 'col1-editor' })).toHaveValue(1230);
await expect.element(editor).toHaveValue(123);
expect(getGrid().scrollTop).toBe(0);
});

describe('editable', () => {
it('should be editable if an editor is specified and editable is undefined/null', async () => {
render(<EditorTest />);
page.render(<EditorTest />);
const cell = getCellsAtRowIndex(0)[1];
expect(cell).not.toHaveAttribute('aria-readonly');
await userEvent.dblClick(cell);
expect(screen.getByRole('textbox', { name: 'col2-editor' })).toBeInTheDocument();
await expect.element(page.getByRole('textbox', { name: 'col2-editor' })).toBeInTheDocument();
});

it('should be editable if an editor is specified and editable is set to true', async () => {
render(<EditorTest editable />);
page.render(<EditorTest editable />);
await userEvent.dblClick(getCellsAtRowIndex(0)[1]);
expect(screen.getByRole('textbox', { name: 'col2-editor' })).toBeInTheDocument();
await expect.element(page.getByRole('textbox', { name: 'col2-editor' })).toBeInTheDocument();
});

it('should not be editable if editable is false', async () => {
render(<EditorTest editable={false} />);
page.render(<EditorTest editable={false} />);
const cell = getCellsAtRowIndex(0)[1];
expect(cell).toHaveAttribute('aria-readonly', 'true');
await userEvent.dblClick(cell);
expect(screen.queryByRole('textbox', { name: 'col2-editor' })).not.toBeInTheDocument();
// eslint-disable-next-line testing-library/prefer-presence-queries
expect(page.getByRole('textbox', { name: 'col2-editor' }).query()).not.toBeInTheDocument();
});

it('should not be editable if editable function returns false', async () => {
render(<EditorTest editable={(row) => row.col1 === 2} />);
page.render(<EditorTest editable={(row) => row.col1 === 2} />);
await userEvent.dblClick(getCellsAtRowIndex(0)[1]);
expect(screen.queryByRole('textbox', { name: 'col2-editor' })).not.toBeInTheDocument();
const editor = page.getByRole('textbox', { name: 'col2-editor' });
expect(editor.query()).not.toBeInTheDocument();

await userEvent.dblClick(getCellsAtRowIndex(1)[1]);
expect(screen.getByRole('textbox', { name: 'col2-editor' })).toBeInTheDocument();
await expect.element(editor).toBeInTheDocument();
});
});

describe('editorOptions', () => {
it('should detect outside click if editor is rendered in a portal', async () => {
render(<EditorTest createEditorPortal editorOptions={{ displayCellContent: true }} />);
page.render(<EditorTest createEditorPortal editorOptions={{ displayCellContent: true }} />);
await userEvent.dblClick(getCellsAtRowIndex(0)[1]);
const editor1 = screen.getByRole('textbox', { name: 'col2-editor' });
expect(editor1).toHaveValue('a1');
const editor1 = page.getByRole('textbox', { name: 'col2-editor' });
await expect.element(editor1).toHaveValue('a1');
await userEvent.keyboard('23');
// The cell value should update as the editor value is changed
expect(getCellsAtRowIndex(0)[1]).toHaveTextContent(/^a123$/);
// clicking in a portal does not count as an outside click
await userEvent.click(editor1);
expect(editor1).toBeInTheDocument();
await expect.element(editor1).toBeInTheDocument();
// true outside clicks are still detected
await userEvent.click(screen.getByText('outside'));
await userEvent.click(page.getByText('outside'));
await waitFor(() => {
expect(editor1).not.toBeInTheDocument();
expect(editor1.query()).not.toBeInTheDocument();
});
expect(getCellsAtRowIndex(0)[1]).not.toHaveFocus();

await userEvent.dblClick(getCellsAtRowIndex(0)[1]);
const editor2 = screen.getByRole('textbox', { name: 'col2-editor' });
await userEvent.click(editor2);
await userEvent.click(page.getByRole('textbox', { name: 'col2-editor' }));
await userEvent.keyboard('{enter}');
expect(getCellsAtRowIndex(0)[1]).toHaveFocus();
});

it('should not commit on outside click if commitOnOutsideClick is false', async () => {
render(
page.render(
<EditorTest
editorOptions={{
commitOnOutsideClick: false
}}
/>
);
await userEvent.dblClick(getCellsAtRowIndex(0)[1]);
const editor = screen.getByRole('textbox', { name: 'col2-editor' });
expect(editor).toBeInTheDocument();
await userEvent.click(screen.getByText('outside'));
await act(async () => {
await new Promise(requestAnimationFrame);
});
expect(editor).toBeInTheDocument();
const editor = page.getByRole('textbox', { name: 'col2-editor' });
await expect.element(editor).toBeInTheDocument();
await userEvent.click(page.getByText('outside'));
await expect.element(editor).toBeInTheDocument();
await userEvent.click(editor);
await userEvent.keyboard('{enter}');
expect(editor).not.toBeInTheDocument();
expect(editor.query()).not.toBeInTheDocument();
});

it('should not open editor if onCellKeyDown prevents the default event', async () => {
render(
page.render(
<EditorTest
onCellKeyDown={(args, event) => {
if (args.mode === 'SELECT' && event.key === 'x') {
Expand All @@ -204,11 +201,12 @@ describe('Editor', () => {
await userEvent.keyboard('yz{enter}');
expect(getCellsAtRowIndex(0)[1]).toHaveTextContent(/^a1yz$/);
await userEvent.keyboard('x');
expect(screen.queryByRole('textbox', { name: 'col2-editor' })).not.toBeInTheDocument();
// eslint-disable-next-line testing-library/prefer-presence-queries
expect(page.getByRole('textbox', { name: 'col2-editor' }).query()).not.toBeInTheDocument();
});

it('should prevent navigation if onCellKeyDown prevents the default event', async () => {
render(
page.render(
<EditorTest
onCellKeyDown={(args, event) => {
if (args.mode === 'EDIT' && event.key === 'ArrowDown') {
Expand All @@ -231,7 +229,7 @@ describe('Editor', () => {
rows.push({ col1: i, col2: `${i}` });
}

render(<EditorTest gridRows={rows} />);
page.render(<EditorTest gridRows={rows} />);

await userEvent.dblClick(getCellsAtRowIndex(0)[1]);
await userEvent.keyboard('abc');
Expand Down Expand Up @@ -275,31 +273,30 @@ describe('Editor', () => {
}
];

render(
page.render(
<>
<input aria-label="outer-input" value="abc" readOnly />
<DataGrid columns={columns} rows={[{}]} />
</>
);

const outerInput = screen.getByRole('textbox', { name: 'outer-input' });
const outerInput = page.getByRole('textbox', { name: 'outer-input' });
await userEvent.dblClick(getCellsAtRowIndex(0)[0]);
const col1Input = screen.getByRole('textbox', { name: 'col1-input' });
expect(col1Input).toHaveFocus();
const col1Input = page.getByRole('textbox', { name: 'col1-input' });
await expect.element(col1Input).toHaveFocus();
await userEvent.click(outerInput);
expect(outerInput).toHaveFocus();
await expect.element(outerInput).toHaveFocus();
await waitFor(() => {
expect(col1Input).not.toBeInTheDocument();
expect(col1Input.query()).not.toBeInTheDocument();
});
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need a better way

expect(outerInput).toHaveFocus();
await expect.element(outerInput).toHaveFocus();

await userEvent.dblClick(getCellsAtRowIndex(0)[1]);
const col2Input = screen.getByRole('textbox', { name: 'col2-input' });
expect(col2Input).toHaveFocus();
const col2Input = page.getByRole('textbox', { name: 'col2-input' });
await expect.element(col2Input).toHaveFocus();
await userEvent.click(outerInput);
expect(outerInput).toHaveFocus();
expect(col2Input).not.toBeInTheDocument();
expect(outerInput).toHaveFocus();
await expect.element(outerInput).toHaveFocus();
expect(col2Input.query()).not.toBeInTheDocument();
});
});
});
Expand Down
Loading