Skip to content

Commit 0d3073e

Browse files
committed
chore: review comments
1 parent 64041a0 commit 0d3073e

3 files changed

Lines changed: 198 additions & 3 deletions

File tree

packages/pluggableWidgets/datagrid-web/src/features/data-export/__tests__/ExportController.spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,15 @@ describe("ExportController export callbacks", () => {
7979
await controller.exportData(jest.fn());
8080

8181
expect(onAfter).toHaveBeenCalledTimes(1);
82-
expect(onAfter).toHaveBeenCalledWith(expect.objectContaining({ status: "success" }));
82+
expect(onAfter).toHaveBeenCalledWith(
83+
expect.objectContaining({
84+
status: "success",
85+
gridName: "test-grid",
86+
columnTitles: "Col1,Col2",
87+
chunkSize: 100,
88+
exportedItemCount: 10
89+
})
90+
);
8391
});
8492

8593
it("calls onAfterExport with status 'aborted' when request ends with aborted status", async () => {
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
jest.mock("mendix", () => ({}), { virtual: true });
2+
3+
import { act, renderHook } from "@testing-library/react";
4+
import { actionValue, list } from "@mendix/widget-plugin-test-utils";
5+
import { TaskProgressService } from "@mendix/widget-plugin-grid/main";
6+
import { IColumnGroupStore } from "../../../helpers/state/ColumnGroupStore";
7+
import { useDataExport } from "../useDataExport";
8+
import { getExportRegistry } from "../registry";
9+
import type { AfterExportArgs, BeforeExportArgs } from "../ExportController";
10+
import Big from "big.js";
11+
12+
function makeMockProgress(): TaskProgressService {
13+
return {
14+
inProgress: false,
15+
lengthComputable: false,
16+
loaded: 0,
17+
total: 0,
18+
onloadstart: jest.fn(),
19+
onprogress: jest.fn(),
20+
onloadend: jest.fn()
21+
};
22+
}
23+
24+
function makeColumnsStore(): IColumnGroupStore {
25+
return {
26+
loaded: true,
27+
availableColumns: [],
28+
visibleColumns: [],
29+
columnFilters: [],
30+
swapColumns: jest.fn(),
31+
setIsResizing: jest.fn()
32+
};
33+
}
34+
35+
const GRID_NAME = "test-grid";
36+
37+
const BEFORE_ARGS: BeforeExportArgs = {
38+
gridName: GRID_NAME,
39+
columnTitles: "Col1,Col2",
40+
chunkSize: 100,
41+
fileName: "export.xlsx",
42+
sheetName: "Sheet1",
43+
startTime: new Date("2026-01-01T00:00:00Z")
44+
};
45+
46+
const AFTER_ARGS: AfterExportArgs = {
47+
...BEFORE_ARGS,
48+
exportedItemCount: 42,
49+
status: "success",
50+
endTime: new Date("2026-01-01T00:01:00Z")
51+
};
52+
53+
describe("useDataExport subscription wiring", () => {
54+
afterEach(() => {
55+
jest.clearAllMocks();
56+
getExportRegistry().clear();
57+
});
58+
59+
function renderExportHook(overrides?: {
60+
onBeforeExport?: ReturnType<typeof actionValue>;
61+
onAfterExport?: ReturnType<typeof actionValue>;
62+
}) {
63+
const columnsStore = makeColumnsStore();
64+
const progress = makeMockProgress();
65+
return renderHook(() =>
66+
useDataExport(
67+
{
68+
name: GRID_NAME,
69+
datasource: list(0),
70+
columns: [],
71+
onBeforeExport: overrides?.onBeforeExport,
72+
onAfterExport: overrides?.onAfterExport
73+
},
74+
columnsStore,
75+
progress
76+
)
77+
);
78+
}
79+
80+
it("calls onBeforeExport.execute with correct payload when canExecute is true", () => {
81+
const action = actionValue(true);
82+
renderExportHook({ onBeforeExport: action });
83+
84+
const controller = getExportRegistry().get(GRID_NAME)!;
85+
act(() => {
86+
controller.emit("beforeexport", BEFORE_ARGS);
87+
});
88+
89+
expect(action.execute).toHaveBeenCalledTimes(1);
90+
expect(action.execute).toHaveBeenCalledWith({
91+
gridName: GRID_NAME,
92+
columnTitles: "Col1,Col2",
93+
chunkSize: new Big(100),
94+
fileName: "export.xlsx",
95+
sheetName: "Sheet1",
96+
startTime: BEFORE_ARGS.startTime
97+
});
98+
});
99+
100+
it("does not call onBeforeExport.execute when canExecute is false", () => {
101+
const action = actionValue(false);
102+
renderExportHook({ onBeforeExport: action });
103+
104+
const controller = getExportRegistry().get(GRID_NAME)!;
105+
act(() => {
106+
controller.emit("beforeexport", BEFORE_ARGS);
107+
});
108+
109+
expect(action.execute).not.toHaveBeenCalled();
110+
});
111+
112+
it("calls onAfterExport.execute with correct payload on success", () => {
113+
const action = actionValue(true);
114+
renderExportHook({ onAfterExport: action });
115+
116+
const controller = getExportRegistry().get(GRID_NAME)!;
117+
act(() => {
118+
controller.emit("afterexport", AFTER_ARGS);
119+
});
120+
121+
expect(action.execute).toHaveBeenCalledTimes(1);
122+
expect(action.execute).toHaveBeenCalledWith({
123+
gridName: GRID_NAME,
124+
columnTitles: "Col1,Col2",
125+
chunkSize: new Big(100),
126+
fileName: "export.xlsx",
127+
sheetName: "Sheet1",
128+
exportedItemCount: new Big(42),
129+
status: "success",
130+
startTime: AFTER_ARGS.startTime,
131+
endTime: AFTER_ARGS.endTime
132+
});
133+
});
134+
135+
it("does not call onAfterExport.execute when canExecute is false", () => {
136+
const action = actionValue(false);
137+
renderExportHook({ onAfterExport: action });
138+
139+
const controller = getExportRegistry().get(GRID_NAME)!;
140+
act(() => {
141+
controller.emit("afterexport", AFTER_ARGS);
142+
});
143+
144+
expect(action.execute).not.toHaveBeenCalled();
145+
});
146+
147+
it("unsubscribes on unmount — no calls after the component is removed", () => {
148+
const action = actionValue(true);
149+
const { unmount } = renderExportHook({ onBeforeExport: action });
150+
151+
const controller = getExportRegistry().get(GRID_NAME)!;
152+
unmount();
153+
154+
act(() => {
155+
controller.emit("beforeexport", BEFORE_ARGS);
156+
});
157+
158+
expect(action.execute).not.toHaveBeenCalled();
159+
});
160+
161+
it("reads the latest ActionValue from ref without resubscribing", () => {
162+
const firstAction = actionValue(true);
163+
const secondAction = actionValue(true);
164+
const columnsStore = makeColumnsStore();
165+
const progress = makeMockProgress();
166+
167+
const { rerender } = renderHook(
168+
({ onBeforeExport }: { onBeforeExport: ReturnType<typeof actionValue> }) =>
169+
useDataExport(
170+
{ name: GRID_NAME, datasource: list(0), columns: [], onBeforeExport, onAfterExport: undefined },
171+
columnsStore,
172+
progress
173+
),
174+
{ initialProps: { onBeforeExport: firstAction } }
175+
);
176+
177+
rerender({ onBeforeExport: secondAction });
178+
179+
const controller = getExportRegistry().get(GRID_NAME)!;
180+
act(() => {
181+
controller.emit("beforeexport", BEFORE_ARGS);
182+
});
183+
184+
expect(firstAction.execute).not.toHaveBeenCalled();
185+
expect(secondAction.execute).toHaveBeenCalledTimes(1);
186+
});
187+
});

packages/pluggableWidgets/datagrid-web/src/features/data-export/useDataExport.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function useDataExport(
5050
}, [columnsStore.visibleColumns, entry]);
5151

5252
useEffect(() => {
53-
return entry?.controller.on("beforeexport", args => {
53+
return entry.controller.on("beforeexport", args => {
5454
const action = onBeforeExportRef.current;
5555
if (action?.canExecute) {
5656
action.execute({
@@ -66,7 +66,7 @@ export function useDataExport(
6666
}, [entry]);
6767

6868
useEffect(() => {
69-
return entry?.controller.on("afterexport", args => {
69+
return entry.controller.on("afterexport", args => {
7070
const action = onAfterExportRef.current;
7171
if (action?.canExecute) {
7272
action.execute({

0 commit comments

Comments
 (0)