-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathdata-table-inline-edit.test.tsx
More file actions
218 lines (182 loc) · 9.13 KB
/
Copy pathdata-table-inline-edit.test.tsx
File metadata and controls
218 lines (182 loc) · 9.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* Regression: inline cell editing on an `editable` data-table must be usable
* for per-row data entry (e.g. 生产报工 where each row gets its own actual
* date). Two bugs made it unusable:
*
* A) Clicking an editable cell entered edit mode AND bubbled up to the row's
* onClick → onRowClick, which in ObjectGrid opens the record-detail drawer.
* The edit cell must stopPropagation so the drawer never opens.
* B) The inline editor was a hardcoded text <Input> for every field type, so
* date columns could only be typed by hand. A `type: 'date'` column must
* render a native date picker (<input type="date">).
*/
import { describe, it, expect, vi } from 'vitest';
import { fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import React from 'react';
import { renderComponent } from './test-utils';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`, which is why this carried a raised
// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021).
import '../renderers';
const editableSchema = {
type: 'data-table' as const,
editable: true,
singleClickEdit: true,
columns: [
{ header: '工序', accessorKey: 'name', editable: false },
{ header: '实际开始时间', accessorKey: 'actual_start', type: 'date' },
{ header: '报工数量', accessorKey: 'qty', type: 'number' },
],
data: [{ id: '1', name: '将军柱下料', actual_start: '', qty: '' }],
} as any;
describe('data-table — inline edit is per-row usable', () => {
it('A) clicking an editable cell does NOT fire onRowClick (no detail drawer)', () => {
const onRowClick = vi.fn();
const { container } = renderComponent({ ...editableSchema, onRowClick });
const cell = Array.from(container.querySelectorAll('td')).find((td) =>
td.textContent?.includes('将军柱下料')
) as HTMLElement;
// sanity: the readonly name cell is editable:false, so it should still
// behave like a normal row click.
expect(cell).toBeTruthy();
// Click the editable date cell (3rd column has no text yet → locate by index).
const cells = container.querySelectorAll('tbody td');
const dateCell = cells[1] as HTMLElement; // 实际开始时间
fireEvent.click(dateCell);
expect(onRowClick).not.toHaveBeenCalled();
});
it('A2) clicking a readonly (editable:false) cell still fires onRowClick (sanity)', () => {
const onRowClick = vi.fn();
const { container } = renderComponent({ ...editableSchema, onRowClick });
const nameCell = Array.from(container.querySelectorAll('tbody td')).find((td) =>
td.textContent?.includes('将军柱下料')
) as HTMLElement;
fireEvent.click(nameCell);
expect(onRowClick).toHaveBeenCalledTimes(1);
});
it('B) a date column renders a native date picker, not a free-text input', () => {
const { container } = renderComponent(editableSchema);
const cells = container.querySelectorAll('tbody td');
const dateCell = cells[1] as HTMLElement; // 实际开始时间
fireEvent.click(dateCell);
const input = dateCell.querySelector('input') as HTMLInputElement;
expect(input).toBeTruthy();
expect(input.type).toBe('date');
});
it('B2) a number column renders a numeric input', () => {
const { container } = renderComponent(editableSchema);
const cells = container.querySelectorAll('tbody td');
const qtyCell = cells[2] as HTMLElement; // 报工数量
fireEvent.click(qtyCell);
const input = qtyCell.querySelector('input') as HTMLInputElement;
expect(input).toBeTruthy();
expect(input.type).toBe('number');
});
it('C) editing a cell then clicking away (blur) commits the typed value', () => {
// Regression: switching cells dropped the in-flight value because only
// Enter/Escape committed the edit. Blur must save it too.
const onCellChange = vi.fn();
const { container } = renderComponent({ ...editableSchema, onCellChange });
const cells = container.querySelectorAll('tbody td');
const qtyCell = cells[2] as HTMLElement; // 报工数量
fireEvent.click(qtyCell);
const input = qtyCell.querySelector('input') as HTMLInputElement;
fireEvent.change(input, { target: { value: '42' } });
// Click away (focus leaves the editor) — value must persist, not vanish.
fireEvent.blur(input);
expect(onCellChange).toHaveBeenCalledTimes(1);
expect(onCellChange).toHaveBeenCalledWith(0, 'qty', '42', expect.anything());
});
it('C2) pressing Escape discards the edit and does NOT commit on blur', () => {
const onCellChange = vi.fn();
const { container } = renderComponent({ ...editableSchema, onCellChange });
const cells = container.querySelectorAll('tbody td');
const qtyCell = cells[2] as HTMLElement; // 报工数量
fireEvent.click(qtyCell);
const input = qtyCell.querySelector('input') as HTMLInputElement;
fireEvent.change(input, { target: { value: '99' } });
fireEvent.keyDown(input, { key: 'Escape' });
// The ensuing blur must not resurrect the cancelled value.
fireEvent.blur(input);
expect(onCellChange).not.toHaveBeenCalled();
});
it('D) a failed save surfaces the server error and keeps the pending change', async () => {
// Regression: a rejected save (e.g. a 400 validation failure) was swallowed
// with only console.error — the toolbar stayed stuck, the cell kept the
// unsaved value, and the author got no feedback. The reason must surface.
const onBatchSave = vi.fn().mockRejectedValue({
// Shape the ObjectStack adapter decorates onto thrown errors.
details: { message: 'Invalid task status transition.' },
});
const { container, getByText } = renderComponent({ ...editableSchema, onBatchSave });
// Stage an edit.
const cells = container.querySelectorAll('tbody td');
const qtyCell = cells[2] as HTMLElement; // 报工数量
fireEvent.click(qtyCell);
const input = qtyCell.querySelector('input') as HTMLInputElement;
fireEvent.change(input, { target: { value: '42' } });
fireEvent.blur(input);
// Save All → saveBatch → onBatchSave rejects.
fireEvent.click(getByText(/Save All/i));
expect(onBatchSave).toHaveBeenCalledTimes(1);
// The server's reason is surfaced, not swallowed.
await waitFor(() =>
expect(container.textContent).toContain('Invalid task status transition'),
);
// And the pending change is kept (Save All still offered) — no silent drop
// and no phantom "saved" state.
expect(getByText(/Save All/i)).toBeInTheDocument();
});
it('D2) a successful save does NOT show a save-failure message', async () => {
const onBatchSave = vi.fn().mockResolvedValue(undefined);
const { container, getByText } = renderComponent({ ...editableSchema, onBatchSave });
const cells = container.querySelectorAll('tbody td');
const qtyCell = cells[2] as HTMLElement;
fireEvent.click(qtyCell);
const input = qtyCell.querySelector('input') as HTMLInputElement;
fireEvent.change(input, { target: { value: '7' } });
fireEvent.blur(input);
fireEvent.click(getByText(/Save All/i));
await waitFor(() => expect(onBatchSave).toHaveBeenCalledTimes(1));
expect(container.textContent).not.toContain('Save failed');
});
it('E) a staged editor (e.g. a lookup picker) exits edit mode after Save All', async () => {
// Regression: relational pickers (lookup/master_detail/user/owner) keep their
// widget open on pick — they `stage` the value rather than commit-and-close.
// saveBatch cleared pendingChanges but never cleared `editingCell`, so after
// 全部保存 the saved cell stayed stuck showing the picker widget instead of
// reverting to the resolved display value. Save All must close any open editor.
const onBatchSave = vi.fn().mockResolvedValue(undefined);
// A host-injected editor that mimics a lookup: picking a value STAGES it and
// deliberately keeps the editor open (no commit/close).
const renderCellEditor = ({ column, stage }: any) =>
column.accessorKey === 'qty' ? (
<button data-testid="picker" onClick={() => stage('picked')}>
picker
</button>
) : null;
const { container, getByText, getByTestId, queryByTestId } = renderComponent({
...editableSchema,
onBatchSave,
renderCellEditor,
});
const cells = container.querySelectorAll('tbody td');
const qtyCell = cells[2] as HTMLElement; // uses the injected picker editor
fireEvent.click(qtyCell);
// Editor is open; pick a value — it stages and stays open (no close).
fireEvent.click(getByTestId('picker'));
expect(queryByTestId('picker')).toBeInTheDocument();
// Save All persists the staged value AND must exit edit mode.
fireEvent.click(getByText(/Save All/i));
await waitFor(() => expect(onBatchSave).toHaveBeenCalledTimes(1));
await waitFor(() => expect(queryByTestId('picker')).not.toBeInTheDocument());
});
});