Skip to content

Commit b5b1d31

Browse files
fix: code coverage
1 parent fc2fccc commit b5b1d31

3 files changed

Lines changed: 312 additions & 0 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
import { IntlProvider } from '@edx/frontend-platform/i18n';
3+
import { fireEvent, render, screen } from '@testing-library/react';
4+
5+
import SaveErrorAlert from './SaveErrorAlert';
6+
7+
const wrapper = ({ children }: { children: React.ReactNode; }) => (
8+
<IntlProvider locale="en" messages={{}}>{children}</IntlProvider>
9+
);
10+
11+
describe('SaveErrorAlert', () => {
12+
it('stays closed when only a draft error is present and delete-error state is omitted', () => {
13+
render(
14+
<SaveErrorAlert
15+
draftError="Delete failed"
16+
isError={false}
17+
isUpdateError={false}
18+
/>,
19+
{ wrapper },
20+
);
21+
22+
expect(screen.queryByText('Error saving changes')).not.toBeInTheDocument();
23+
});
24+
25+
it('opens for delete errors and reopens when a new delete failure arrives', () => {
26+
const { rerender } = render(
27+
<SaveErrorAlert
28+
draftError="First delete failure"
29+
isError={false}
30+
isUpdateError={false}
31+
isDeleteError
32+
/>,
33+
{ wrapper },
34+
);
35+
36+
expect(screen.getByText('Error saving changes')).toBeInTheDocument();
37+
fireEvent.click(screen.getByRole('button', { name: /dismiss/i }));
38+
expect(screen.queryByText('Error saving changes')).not.toBeInTheDocument();
39+
40+
rerender(
41+
<SaveErrorAlert
42+
draftError="Second delete failure"
43+
isError={false}
44+
isUpdateError={false}
45+
isDeleteError
46+
/>,
47+
);
48+
49+
expect(screen.getByText('Error saving changes')).toBeInTheDocument();
50+
expect(screen.getByText('Second delete failure. Please try again.')).toBeInTheDocument();
51+
});
52+
});
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import React from 'react';
2+
import { IntlProvider } from '@edx/frontend-platform/i18n';
3+
import { fireEvent, render, screen } from '@testing-library/react';
4+
5+
import TableBody from './TableBody';
6+
import { TreeTableContext } from './TreeTableContext';
7+
8+
jest.mock('./CreateRow', () => () => (
9+
<tr data-testid="create-row">
10+
<td>Create row</td>
11+
</tr>
12+
));
13+
14+
jest.mock('./EditRow', () => ({
15+
initialValue,
16+
handleUpdateRow,
17+
cancelEditRow,
18+
}: {
19+
initialValue: string;
20+
handleUpdateRow: (value: string) => void;
21+
cancelEditRow: () => void;
22+
}) => (
23+
<tr data-testid="edit-row">
24+
<td>{initialValue}</td>
25+
<td>
26+
<button onClick={() => handleUpdateRow('updated root')}>save edit</button>
27+
</td>
28+
<td>
29+
<button onClick={cancelEditRow}>cancel edit</button>
30+
</td>
31+
</tr>
32+
));
33+
34+
jest.mock('./NestedRows', () => ({
35+
parentRowValue,
36+
isCreating,
37+
onSaveNewChildRow,
38+
onCancelCreation,
39+
}: {
40+
parentRowValue: string;
41+
isCreating: boolean;
42+
onSaveNewChildRow: (value: string, parentRowValue?: string) => void;
43+
onCancelCreation: () => void;
44+
}) => (
45+
<tr data-testid={`nested-rows-${parentRowValue}`}>
46+
<td>{parentRowValue}</td>
47+
<td>{String(isCreating)}</td>
48+
<td>
49+
<button onClick={() => onSaveNewChildRow('new child', parentRowValue)}>save child</button>
50+
</td>
51+
<td>
52+
<button onClick={onCancelCreation}>cancel child</button>
53+
</td>
54+
</tr>
55+
));
56+
57+
const wrapper = ({ children }: { children: React.ReactNode; }) => (
58+
<IntlProvider locale="en" messages={{}}>{children}</IntlProvider>
59+
);
60+
61+
const baseContextValue = (): any => ({
62+
treeData: [],
63+
columns: [{ accessorKey: 'value', header: 'Tag name', cell: () => 'unused' }],
64+
pageCount: -1,
65+
pagination: { pageIndex: 0, pageSize: 10 },
66+
handlePaginationChange: jest.fn(),
67+
isLoading: false,
68+
isCreatingTopRow: false,
69+
draftError: '',
70+
createRowMutation: {},
71+
updateRowMutation: {},
72+
toast: { show: false, message: '', variant: 'success' as const },
73+
setToast: jest.fn(),
74+
setIsCreatingTopRow: jest.fn(),
75+
exitDraftWithoutSave: jest.fn(),
76+
handleCreateRow: jest.fn(),
77+
creatingParentId: null,
78+
setCreatingParentId: jest.fn(),
79+
setDraftError: jest.fn(),
80+
validate: jest.fn(() => true),
81+
handleUpdateRow: jest.fn(),
82+
editingRowId: null,
83+
setEditingRowId: jest.fn(),
84+
table: null,
85+
});
86+
87+
const makeCell = (id: string, content: string) => ({
88+
id,
89+
column: { columnDef: { cell: () => content } },
90+
getContext: () => ({}),
91+
});
92+
93+
const makeRow = ({
94+
id,
95+
value,
96+
depth = 0,
97+
subRows = [],
98+
}: {
99+
id: number;
100+
value: string;
101+
depth?: number;
102+
subRows?: any[];
103+
}) => ({
104+
id: String(id),
105+
depth,
106+
original: { id, value },
107+
subRows,
108+
getVisibleCells: () => [makeCell(`${id}-cell`, `${value} cell`)],
109+
});
110+
111+
const renderTableBody = (contextValue = baseContextValue()) => render(
112+
<TreeTableContext.Provider value={contextValue as any}>
113+
<table>
114+
<TableBody />
115+
</table>
116+
</TreeTableContext.Provider>,
117+
{ wrapper },
118+
);
119+
120+
describe('TableBody', () => {
121+
it('returns null when no table instance is available in context', () => {
122+
const { container } = render(
123+
<table>
124+
<TableBody />
125+
</table>,
126+
{ wrapper },
127+
);
128+
129+
expect(container.querySelector('tbody')).toBeNull();
130+
});
131+
132+
it('shows an empty-state row when the table has no rows', () => {
133+
const contextValue = baseContextValue();
134+
contextValue.table = {
135+
getRowModel: () => ({ rows: [] }),
136+
};
137+
138+
renderTableBody(contextValue);
139+
140+
expect(screen.getByText('No results found')).toBeInTheDocument();
141+
});
142+
143+
it('shows a loading row when table data is still loading', () => {
144+
const contextValue = baseContextValue();
145+
contextValue.isLoading = true;
146+
contextValue.table = {
147+
getRowModel: () => ({ rows: [] }),
148+
};
149+
150+
renderTableBody(contextValue);
151+
152+
expect(screen.getByRole('status')).toBeInTheDocument();
153+
});
154+
155+
it('renders top-level creation and nested-row callbacks from context', () => {
156+
const contextValue = baseContextValue();
157+
const rootRow = makeRow({ id: 1, value: 'root tag' });
158+
159+
contextValue.isCreatingTopRow = true;
160+
contextValue.creatingParentId = 1;
161+
contextValue.table = {
162+
getRowModel: () => ({ rows: [rootRow] }),
163+
};
164+
165+
renderTableBody(contextValue);
166+
167+
expect(screen.getByTestId('create-row')).toBeInTheDocument();
168+
expect(screen.getByText('root tag cell')).toBeInTheDocument();
169+
expect(screen.getByTestId('nested-rows-root tag')).toHaveTextContent('true');
170+
171+
fireEvent.click(screen.getByRole('button', { name: 'save child' }));
172+
expect(contextValue.handleCreateRow).toHaveBeenCalledWith('new child', 'root tag');
173+
174+
fireEvent.click(screen.getByRole('button', { name: 'cancel child' }));
175+
expect(contextValue.setDraftError).toHaveBeenCalledWith('');
176+
expect(contextValue.setCreatingParentId).toHaveBeenCalledWith(null);
177+
expect(contextValue.exitDraftWithoutSave).toHaveBeenCalled();
178+
});
179+
180+
it('renders edit mode for the matching row and wires save and cancel through context', () => {
181+
const contextValue = baseContextValue();
182+
const rootRow = makeRow({ id: 1, value: 'root tag' });
183+
184+
contextValue.editingRowId = '1:root tag';
185+
contextValue.table = {
186+
getRowModel: () => ({ rows: [rootRow] }),
187+
};
188+
189+
renderTableBody(contextValue);
190+
191+
expect(screen.getByTestId('edit-row')).toBeInTheDocument();
192+
193+
fireEvent.click(screen.getByRole('button', { name: 'save edit' }));
194+
expect(contextValue.handleUpdateRow).toHaveBeenCalledWith('updated root', 'root tag');
195+
196+
fireEvent.click(screen.getByRole('button', { name: 'cancel edit' }));
197+
expect(contextValue.setEditingRowId).toHaveBeenCalledWith(null);
198+
expect(contextValue.exitDraftWithoutSave).toHaveBeenCalled();
199+
});
200+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React, { useContext, useEffect } from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
4+
import { TreeTableContext } from './TreeTableContext';
5+
6+
const ContextProbe = () => {
7+
const context = useContext(TreeTableContext);
8+
9+
useEffect(() => {
10+
context.handlePaginationChange({ pageIndex: 1, pageSize: 25 });
11+
context.setToast((prevToast) => ({ ...prevToast, show: true }));
12+
context.setIsCreatingTopRow(true);
13+
context.exitDraftWithoutSave();
14+
context.handleCreateRow('new tag', 'parent tag');
15+
context.setCreatingParentId(123);
16+
context.setDraftError('Draft error');
17+
context.handleUpdateRow('updated tag', 'original tag');
18+
context.setEditingRowId(456);
19+
}, [context]);
20+
21+
return (
22+
<>
23+
<span data-testid="page-count">{String(context.pageCount)}</span>
24+
<span data-testid="pagination">
25+
{`${context.pagination.pageIndex}:${context.pagination.pageSize}`}
26+
</span>
27+
<span data-testid="is-loading">{String(context.isLoading)}</span>
28+
<span data-testid="is-creating-top-row">{String(context.isCreatingTopRow)}</span>
29+
<span data-testid="draft-error">{context.draftError}</span>
30+
<span data-testid="create-row-mutation">{JSON.stringify(context.createRowMutation)}</span>
31+
<span data-testid="update-row-mutation">{JSON.stringify(context.updateRowMutation)}</span>
32+
<span data-testid="toast">{JSON.stringify(context.toast)}</span>
33+
<span data-testid="creating-parent-id">{String(context.creatingParentId)}</span>
34+
<span data-testid="editing-row-id">{String(context.editingRowId)}</span>
35+
<span data-testid="table-is-null">{String(context.table === null)}</span>
36+
<span data-testid="validate-result">{String(context.validate('tag value', 'hard'))}</span>
37+
</>
38+
);
39+
};
40+
41+
describe('TreeTableContext', () => {
42+
it('provides safe default values and no-op handlers when no provider is present', () => {
43+
render(<ContextProbe />);
44+
45+
expect(screen.getByTestId('page-count')).toHaveTextContent('-1');
46+
expect(screen.getByTestId('pagination')).toHaveTextContent('0:0');
47+
expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
48+
expect(screen.getByTestId('is-creating-top-row')).toHaveTextContent('false');
49+
expect(screen.getByTestId('draft-error')).toBeEmptyDOMElement();
50+
expect(screen.getByTestId('create-row-mutation')).toHaveTextContent('{}');
51+
expect(screen.getByTestId('update-row-mutation')).toHaveTextContent('{}');
52+
expect(screen.getByTestId('toast')).toHaveTextContent(
53+
'{"show":false,"message":"","variant":"success"}',
54+
);
55+
expect(screen.getByTestId('creating-parent-id')).toHaveTextContent('null');
56+
expect(screen.getByTestId('editing-row-id')).toHaveTextContent('null');
57+
expect(screen.getByTestId('table-is-null')).toHaveTextContent('true');
58+
expect(screen.getByTestId('validate-result')).toHaveTextContent('true');
59+
});
60+
});

0 commit comments

Comments
 (0)