@marimo-team/frontend 0.25.2-dev53 → 0.25.2-dev61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{CellStatus-Q5giBgg0.js → CellStatus-CRFBVGjI.js} +1 -1
- package/dist/assets/ConnectedDataExplorerComponent-C5UW0p9I.js +1 -0
- package/dist/assets/{ErrorBoundary-CowFEmQK.js → ErrorBoundary-BKmN7zye.js} +2 -2
- package/dist/assets/JsonOutput-nqqzL-n0.js +11 -0
- package/dist/assets/{MarimoErrorOutput-p5jt_udI.js → MarimoErrorOutput-DCu6fm2y.js} +1 -1
- package/dist/assets/{RenderHTML-CqDY0F9e.js → RenderHTML-DRMkieKn.js} +1 -1
- package/dist/assets/RunButton-CPZWjCL9.js +1 -0
- package/dist/assets/{add-cell-with-ai-BKqOYjHI.js → add-cell-with-ai-sH3HbLJO.js} +1 -1
- package/dist/assets/{add-connection-dialog-CL3bFsGA.js → add-connection-dialog-pQcdKIlO.js} +2 -2
- package/dist/assets/{agent-panel-q9X3jIj0.js → agent-panel-Bkcf1sED.js} +1 -1
- package/dist/assets/ai-model-dropdown-D-51nbXZ.js +5 -0
- package/dist/assets/{app-config-button-Bylx9JDv.js → app-config-button-Dr1EwwIk.js} +1 -1
- package/dist/assets/{blob-BTk5Q8u9.js → blob-BIYte8oT.js} +1 -1
- package/dist/assets/cache-panel-BNPF4X5e.js +1 -0
- package/dist/assets/{cell-editor-DfRhvA75.js → cell-editor-Cmqe3QmM.js} +3 -3
- package/dist/assets/{cell-link-DRuyhIKm.js → cell-link-BeZAiE1D.js} +1 -1
- package/dist/assets/{cells-CuKxjgjJ.js → cells-h5u00Nz8.js} +3 -3
- package/dist/assets/{chat-display-BT4wYvSi.js → chat-display-CuZ8niXg.js} +1 -1
- package/dist/assets/chat-panel-CHPPwqcP.js +4 -0
- package/dist/assets/{chat-ui-DsHB8FDP.js → chat-ui-BJAcq-Ln.js} +1 -1
- package/dist/assets/column-preview-DtrgJzVI.js +1 -0
- package/dist/assets/{command-palette-DLjm6e2f.js → command-palette-DUPyVGsq.js} +1 -1
- package/dist/assets/{common-BHV00wKC.js → common-BMeq0Bqi.js} +1 -1
- package/dist/assets/{components-DzAIJKU7.js → components-CWcf8098.js} +1 -1
- package/dist/assets/{config-BaEZVOk2.js → config-CR2d-XsP.js} +1 -1
- package/dist/assets/{datasource-D0736MyK.js → datasource-BLHGFSsT.js} +1 -1
- package/dist/assets/{dependency-graph-panel-B6fAc_9C.js → dependency-graph-panel-C8X3Nn8W.js} +1 -1
- package/dist/assets/{documentation-panel-DQAQba71.js → documentation-panel-C8wrSKlX.js} +1 -1
- package/dist/assets/{download-D-MTWucM.js → download-CUgiCx7s.js} +1 -1
- package/dist/assets/edit-page-DMsRAPsr.js +9 -0
- package/dist/assets/{error-entries-CWxVwEPG.js → error-entries-DQD4A9cx.js} +1 -1
- package/dist/assets/{error-panel-DISYRsce.js → error-panel-BJYkwYEY.js} +1 -1
- package/dist/assets/{feedback-button-B1bYxCrZ.js → feedback-button-DbTjGwcX.js} +2 -2
- package/dist/assets/feedback-button-JqvnTSfN.js +1 -0
- package/dist/assets/file-explorer-panel-BQi88S1o.js +1 -0
- package/dist/assets/{file-name-input-BVXW4Ihy.js → file-name-input-DPJSxW_X.js} +1 -1
- package/dist/assets/{floating-outline-BhY3Wt-J.js → floating-outline-3PlTr0K1.js} +1 -1
- package/dist/assets/{gallery-page-Be8AbY36.js → gallery-page-BqvVmwde.js} +1 -1
- package/dist/assets/{glide-data-editor-D1Fip07J.js → glide-data-editor-CAa14cIr.js} +1 -1
- package/dist/assets/home-page-B_NxFh87.js +4 -0
- package/dist/assets/{hooks-BgpsvfLA.js → hooks-BHoInJWN.js} +1 -1
- package/dist/assets/index-C968xQuz.js +29 -0
- package/dist/assets/index-CF8Gfz1R.css +2 -0
- package/dist/assets/input-DPd57MDP.js +7 -0
- package/dist/assets/{layout-BAJKEADQ.js → layout-CzNq5YtR.js} +5 -5
- package/dist/assets/{logs-panel-CjHa5qIw.js → logs-panel-IKL6F2OX.js} +1 -1
- package/dist/assets/{markdown-renderer-quSCxU-_.js → markdown-renderer-DHvAXF8E.js} +1 -1
- package/dist/assets/{name-cell-input-Cba1m8_c.js → name-cell-input-j4eGLLyd.js} +1 -1
- package/dist/assets/{outline-panel-CWsF1QII.js → outline-panel-CprDWmxn.js} +1 -1
- package/dist/assets/{packages-panel-C-XEdHNc.js → packages-panel-BGqHk9nr.js} +1 -1
- package/dist/assets/{panel-accordion-state-u7avi-RV.js → panel-accordion-state-8xXVhawu.js} +1 -1
- package/dist/assets/{panels-B5iuV0lM.js → panels-Dsf6yEsA.js} +1 -1
- package/dist/assets/{process-output-DbjZ_y5T.js → process-output-DOmeJ1VR.js} +1 -1
- package/dist/assets/{readonly-code-display-Ctjk_xXb.js → readonly-code-display-xvH6MyT6.js} +1 -1
- package/dist/assets/{readonly-python-code-Ctej1jSH.js → readonly-python-code-D4YGmSvB.js} +1 -1
- package/dist/assets/{reveal-component-YHvm3T44.js → reveal-component-CJxdWpbj.js} +1 -1
- package/dist/assets/{run-page-BkMc69OJ.js → run-page-DCu4YGwW.js} +1 -1
- package/dist/assets/{sandbox-panel-fEQNl_1C.js → sandbox-panel-D5bRunTM.js} +1 -1
- package/dist/assets/{scratchpad-panel-CZ1PriSX.js → scratchpad-panel-BSlnM0TK.js} +1 -1
- package/dist/assets/{secrets-panel-Yc5ollBX.js → secrets-panel-DT1Nansx.js} +1 -1
- package/dist/assets/{select-Ww_82o86.js → select-BwRFbZty.js} +1 -1
- package/dist/assets/session-panel-BdN43Lzi.js +1 -0
- package/dist/assets/{snippets-panel-BFXHSCEq.js → snippets-panel-5_jR08Zd.js} +1 -1
- package/dist/assets/spec-D9d9nT0z.js +1 -0
- package/dist/assets/{state-Do1H9fl6.js → state-7os84dnI.js} +1 -1
- package/dist/assets/{state-CdP1z8ku.js → state-BxJ_FX3k.js} +1 -1
- package/dist/assets/switch-C-83e2fG.js +1 -0
- package/dist/assets/{list-filter-CTg19675.js → table-B7KYyTxw.js} +1 -1
- package/dist/assets/{toast-CL5SyrsO.js → toast-BRwEbWb6.js} +1 -1
- package/dist/assets/{tracing-DBfpxL3C.js → tracing-CRkNpBaH.js} +1 -1
- package/dist/assets/tracing-panel-DV2-2TUI.js +2 -0
- package/dist/assets/{tree-actions-DdvPjaze.js → tree-actions-CShqSonp.js} +1 -1
- package/dist/assets/{useAddCell-BbK54Wma.js → useAddCell-DesMfnE4.js} +1 -1
- package/dist/assets/{useCellActionButton-BEZZV5mI.js → useCellActionButton-BO5SFMxA.js} +1 -1
- package/dist/assets/useDateFormatter-CTYCgKB-.js +1 -0
- package/dist/assets/{useDeleteCell-BG8LgAEA.js → useDeleteCell-DSbUjpSC.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-BEjMLLcY.js → useDependencyPanelTab-JIFiQrr2.js} +1 -1
- package/dist/assets/{useInstallPackage-DnijI-KS.js → useInstallPackage-CCQcFc6U.js} +1 -1
- package/dist/assets/{useNotebookActions-Cnznz5jV.js → useNotebookActions-BDD321Hs.js} +1 -1
- package/dist/assets/useNumberFormatter-BP99Zbyy.js +1 -0
- package/dist/assets/{useRunCells-DTPuvtCQ.js → useRunCells-Bx3wXUWO.js} +1 -1
- package/dist/assets/{useSplitCell-BuTWivoM.js → useSplitCell-BRmQP02b.js} +1 -1
- package/dist/assets/{write-secret-modal-DknTu1O1.js → write-secret-modal-BcosCmZy.js} +2 -2
- package/dist/index.html +32 -32
- package/package.json +3 -3
- package/src/components/data-table/TableTopBar.tsx +4 -0
- package/src/components/data-table/__tests__/export-actions.test.tsx +1111 -14
- package/src/components/data-table/__tests__/schemas.test.ts +57 -0
- package/src/components/data-table/data-table.tsx +3 -0
- package/src/components/data-table/export-actions.tsx +365 -81
- package/src/components/data-table/schemas.ts +90 -14
- package/src/components/editor/navigation/__tests__/navigation.test.ts +8 -8
- package/src/plugins/impl/DataTablePlugin.tsx +23 -2
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +65 -8
- package/src/plugins/impl/data-frames/DataFramePlugin.tsx +15 -3
- package/src/stories/dataframe.stories.tsx +7 -0
- package/dist/assets/ConnectedDataExplorerComponent-D5gfkAKK.js +0 -1
- package/dist/assets/JsonOutput-c1E0SCoN.js +0 -11
- package/dist/assets/RunButton-TcDo218m.js +0 -1
- package/dist/assets/ai-model-dropdown-CK8y46Fn.js +0 -5
- package/dist/assets/cache-panel-CKzoUIhe.js +0 -1
- package/dist/assets/chat-panel-C3Bu_7u5.js +0 -4
- package/dist/assets/column-preview-BltuXodG.js +0 -1
- package/dist/assets/edit-page-CMJrCnfU.js +0 -9
- package/dist/assets/feedback-button-BW1-NBBf.js +0 -1
- package/dist/assets/file-explorer-panel-VIG5c9ac.js +0 -1
- package/dist/assets/home-page-Dv2fEKyO.js +0 -4
- package/dist/assets/index-BGdpHNhZ.css +0 -2
- package/dist/assets/index-BdrTweFz.js +0 -29
- package/dist/assets/input-DnyzXg4C.js +0 -7
- package/dist/assets/session-panel-DD5LfQGb.js +0 -1
- package/dist/assets/spec-SOFn9xEK.js +0 -1
- package/dist/assets/switch-CPh_C6Mu.js +0 -1
- package/dist/assets/tracing-panel-CS9W1a0x.js +0 -2
- package/dist/assets/useDateFormatter-CjgScv5a.js +0 -1
- package/dist/assets/useNumberFormatter-C4-_TnkF.js +0 -1
- /package/dist/assets/{context-BFOBKCJB.js → I18nProvider-BFOBKCJB.js} +0 -0
|
@@ -18,6 +18,7 @@ import { downloadByURL } from "@/utils/download";
|
|
|
18
18
|
import { jsonToMarkdown } from "@/utils/json/json-parser";
|
|
19
19
|
import { downloadSizeLimitAtom } from "../download-policy/atoms";
|
|
20
20
|
import { ExportActions, type ExportActionProps } from "../export-actions";
|
|
21
|
+
import type { ExportMetadata } from "../schemas";
|
|
21
22
|
|
|
22
23
|
const mocks = vi.hoisted(() => ({
|
|
23
24
|
handleInstallPackages: vi.fn(),
|
|
@@ -58,13 +59,19 @@ function renderExportActions(
|
|
|
58
59
|
store.set(downloadSizeLimitAtom, sizeLimit);
|
|
59
60
|
store.set(viewStateAtom, { mode, cellAnchor: null });
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
const view = (nextProps: Partial<ExportActionProps>) => (
|
|
62
63
|
<Provider store={store}>
|
|
63
64
|
<TooltipProvider delayDuration={0}>
|
|
64
|
-
<ExportActions downloadAs={downloadAs} {...
|
|
65
|
+
<ExportActions downloadAs={downloadAs} {...nextProps} />
|
|
65
66
|
</TooltipProvider>
|
|
66
|
-
</Provider
|
|
67
|
+
</Provider>
|
|
67
68
|
);
|
|
69
|
+
const result = render(view(props));
|
|
70
|
+
return {
|
|
71
|
+
...result,
|
|
72
|
+
rerenderExportActions: (nextProps: Partial<ExportActionProps>) =>
|
|
73
|
+
result.rerender(view(nextProps)),
|
|
74
|
+
};
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
async function openDialog() {
|
|
@@ -72,6 +79,27 @@ async function openDialog() {
|
|
|
72
79
|
return screen.findByRole("dialog", { name: "Export table" });
|
|
73
80
|
}
|
|
74
81
|
|
|
82
|
+
function selectGeometry(name: string) {
|
|
83
|
+
const select = screen.getByRole("combobox", { name: "Primary geometry" });
|
|
84
|
+
const option = within(select).getByRole("option", {
|
|
85
|
+
name,
|
|
86
|
+
}) as HTMLOptionElement;
|
|
87
|
+
fireEvent.change(select, { target: { value: option.value } });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const geometryMetadata: ExportMetadata = {
|
|
91
|
+
geometry_columns: [
|
|
92
|
+
{ name: "location", encoding: "objects", crs: "EPSG:4326" },
|
|
93
|
+
{ name: "boundary", encoding: "objects", crs: null },
|
|
94
|
+
],
|
|
95
|
+
primary_geometry_column: "location",
|
|
96
|
+
default_geometry_column: "location",
|
|
97
|
+
formats: {
|
|
98
|
+
parquet: { available: true, reason: null, missing_packages: [] },
|
|
99
|
+
geojson: { available: true, reason: null, missing_packages: [] },
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
75
103
|
describe("ExportActions dialog", () => {
|
|
76
104
|
beforeEach(() => {
|
|
77
105
|
vi.clearAllMocks();
|
|
@@ -85,6 +113,423 @@ describe("ExportActions dialog", () => {
|
|
|
85
113
|
vi.unstubAllGlobals();
|
|
86
114
|
});
|
|
87
115
|
|
|
116
|
+
it("selects a primary geometry for GeoParquet without changing other formats", async () => {
|
|
117
|
+
const getExportMetadata = vi.fn().mockResolvedValue(geometryMetadata);
|
|
118
|
+
renderExportActions({ getExportMetadata });
|
|
119
|
+
await openDialog();
|
|
120
|
+
await waitFor(() => {
|
|
121
|
+
expect(
|
|
122
|
+
screen.getByRole("button", { name: "GeoParquet options" }),
|
|
123
|
+
).toBeEnabled();
|
|
124
|
+
});
|
|
125
|
+
expect(screen.getByTestId("export-summary-parquet")).toHaveTextContent(
|
|
126
|
+
"location",
|
|
127
|
+
);
|
|
128
|
+
fireEvent.click(screen.getByRole("button", { name: "GeoParquet options" }));
|
|
129
|
+
selectGeometry("boundary");
|
|
130
|
+
fireEvent.click(
|
|
131
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
132
|
+
);
|
|
133
|
+
await waitFor(() => {
|
|
134
|
+
expect(downloadByURL).toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
expect(downloadAs).toHaveBeenCalledWith({
|
|
137
|
+
format: "parquet",
|
|
138
|
+
geometry_column: "boundary",
|
|
139
|
+
});
|
|
140
|
+
expect(getExportMetadata).toHaveBeenCalledWith({});
|
|
141
|
+
expect(screen.getByText("GeoParquet")).toBeInTheDocument();
|
|
142
|
+
expect(
|
|
143
|
+
screen.getByText("Parquet with geometry and CRS metadata"),
|
|
144
|
+
).toBeInTheDocument();
|
|
145
|
+
expect(screen.getByTestId("export-summary-parquet")).toHaveTextContent(
|
|
146
|
+
"boundary",
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
fireEvent.click(screen.getByRole("button", { name: "Download CSV" }));
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(downloadAs).toHaveBeenCalledWith({ format: "csv" });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("requires a choice when several geometries have no primary", async () => {
|
|
156
|
+
renderExportActions({
|
|
157
|
+
getExportMetadata: vi.fn().mockResolvedValue({
|
|
158
|
+
...geometryMetadata,
|
|
159
|
+
primary_geometry_column: null,
|
|
160
|
+
default_geometry_column: null,
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
await openDialog();
|
|
164
|
+
await screen.findByRole("button", { name: "GeoParquet options" });
|
|
165
|
+
expect(
|
|
166
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
167
|
+
).toBeDisabled();
|
|
168
|
+
expect(
|
|
169
|
+
screen.getByText(
|
|
170
|
+
"Choose a primary geometry column to export GeoParquet.",
|
|
171
|
+
),
|
|
172
|
+
).toBeInTheDocument();
|
|
173
|
+
fireEvent.click(screen.getByRole("button", { name: "GeoParquet options" }));
|
|
174
|
+
selectGeometry("boundary");
|
|
175
|
+
expect(
|
|
176
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
177
|
+
).toBeEnabled();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("shows ineligible geometry sources and keeps ordinary exports available", async () => {
|
|
181
|
+
renderExportActions({
|
|
182
|
+
getExportMetadata: vi.fn().mockResolvedValue({
|
|
183
|
+
...geometryMetadata,
|
|
184
|
+
formats: {
|
|
185
|
+
parquet: {
|
|
186
|
+
available: false,
|
|
187
|
+
reason: "GeoParquet export from Arrow tables is not supported yet.",
|
|
188
|
+
missing_packages: [],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
await openDialog();
|
|
194
|
+
expect(
|
|
195
|
+
await screen.findByText(
|
|
196
|
+
"GeoParquet export from Arrow tables is not supported yet.",
|
|
197
|
+
),
|
|
198
|
+
).toBeInTheDocument();
|
|
199
|
+
expect(
|
|
200
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
201
|
+
).toBeDisabled();
|
|
202
|
+
fireEvent.click(screen.getByRole("button", { name: "Download JSON" }));
|
|
203
|
+
await waitFor(() => {
|
|
204
|
+
expect(downloadAs).toHaveBeenCalledWith({ format: "json" });
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("keeps the geometry choice through a package install retry", async () => {
|
|
209
|
+
const getExportMetadata = vi.fn().mockResolvedValue(geometryMetadata);
|
|
210
|
+
downloadAs
|
|
211
|
+
.mockResolvedValueOnce({
|
|
212
|
+
url: "",
|
|
213
|
+
filename: "",
|
|
214
|
+
error: "GeoParquet export requires pyarrow.",
|
|
215
|
+
code: "missing_packages",
|
|
216
|
+
missing_packages: ["pyarrow"],
|
|
217
|
+
})
|
|
218
|
+
.mockResolvedValueOnce({
|
|
219
|
+
url: "https://example.test/export",
|
|
220
|
+
filename: "table",
|
|
221
|
+
});
|
|
222
|
+
renderExportActions({ getExportMetadata });
|
|
223
|
+
await openDialog();
|
|
224
|
+
await screen.findByRole("button", { name: "GeoParquet options" });
|
|
225
|
+
fireEvent.click(screen.getByRole("button", { name: "GeoParquet options" }));
|
|
226
|
+
selectGeometry("boundary");
|
|
227
|
+
fireEvent.click(
|
|
228
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
229
|
+
);
|
|
230
|
+
expect(
|
|
231
|
+
await screen.findByText("GeoParquet export requires pyarrow."),
|
|
232
|
+
).toBeInTheDocument();
|
|
233
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
234
|
+
expect(
|
|
235
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
236
|
+
).toBeDisabled();
|
|
237
|
+
expect(downloadByURL).not.toHaveBeenCalled();
|
|
238
|
+
fireEvent.click(screen.getByRole("button", { name: "Install pyarrow" }));
|
|
239
|
+
expect(mocks.handleInstallPackages).toHaveBeenCalledWith(
|
|
240
|
+
["pyarrow"],
|
|
241
|
+
expect.any(Function),
|
|
242
|
+
);
|
|
243
|
+
await act(async () => {
|
|
244
|
+
mocks.handleInstallPackages.mock.calls[0][1]();
|
|
245
|
+
});
|
|
246
|
+
await waitFor(() => {
|
|
247
|
+
expect(downloadByURL).toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
expect(downloadAs).toHaveBeenLastCalledWith({
|
|
250
|
+
format: "parquet",
|
|
251
|
+
geometry_column: "boundary",
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("refreshes eligibility after package installation", async () => {
|
|
256
|
+
const getExportMetadata = vi.fn().mockResolvedValue({
|
|
257
|
+
...geometryMetadata,
|
|
258
|
+
formats: {
|
|
259
|
+
parquet: {
|
|
260
|
+
available: false,
|
|
261
|
+
reason: "GeoParquet export requires pyarrow.",
|
|
262
|
+
missing_packages: ["pyarrow"],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
renderExportActions({ getExportMetadata });
|
|
267
|
+
await openDialog();
|
|
268
|
+
await screen.findByRole("button", { name: "GeoParquet options" });
|
|
269
|
+
expect(
|
|
270
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
271
|
+
).toBeDisabled();
|
|
272
|
+
expect(
|
|
273
|
+
screen.getAllByText("GeoParquet export requires pyarrow."),
|
|
274
|
+
).toHaveLength(1);
|
|
275
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
276
|
+
expect(downloadAs).not.toHaveBeenCalled();
|
|
277
|
+
fireEvent.click(
|
|
278
|
+
await screen.findByRole("button", { name: "Install pyarrow" }),
|
|
279
|
+
);
|
|
280
|
+
getExportMetadata.mockResolvedValue(geometryMetadata);
|
|
281
|
+
await act(async () => {
|
|
282
|
+
mocks.handleInstallPackages.mock.calls[0][1]();
|
|
283
|
+
});
|
|
284
|
+
await waitFor(() => {
|
|
285
|
+
expect(downloadByURL).toHaveBeenCalled();
|
|
286
|
+
});
|
|
287
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(2);
|
|
288
|
+
expect(
|
|
289
|
+
screen.queryByText("GeoParquet export requires pyarrow."),
|
|
290
|
+
).not.toBeInTheDocument();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("keeps the install prompt when the package is still missing", async () => {
|
|
294
|
+
const getExportMetadata = vi.fn().mockResolvedValue({
|
|
295
|
+
...geometryMetadata,
|
|
296
|
+
formats: {
|
|
297
|
+
parquet: {
|
|
298
|
+
available: false,
|
|
299
|
+
reason: "GeoParquet export requires pyarrow.",
|
|
300
|
+
missing_packages: ["pyarrow"],
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
renderExportActions({ getExportMetadata });
|
|
305
|
+
await openDialog();
|
|
306
|
+
fireEvent.click(
|
|
307
|
+
await screen.findByRole("button", { name: "Install pyarrow" }),
|
|
308
|
+
);
|
|
309
|
+
await act(async () => {
|
|
310
|
+
mocks.handleInstallPackages.mock.calls[0][1]();
|
|
311
|
+
});
|
|
312
|
+
await waitFor(() => {
|
|
313
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(2);
|
|
314
|
+
});
|
|
315
|
+
expect(
|
|
316
|
+
screen.getByRole("button", { name: "Install pyarrow" }),
|
|
317
|
+
).toBeInTheDocument();
|
|
318
|
+
expect(
|
|
319
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
320
|
+
).toBeDisabled();
|
|
321
|
+
expect(downloadAs).not.toHaveBeenCalled();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("can select a geometry column with an empty name", async () => {
|
|
325
|
+
renderExportActions({
|
|
326
|
+
getExportMetadata: vi.fn().mockResolvedValue({
|
|
327
|
+
...geometryMetadata,
|
|
328
|
+
geometry_columns: [
|
|
329
|
+
geometryMetadata.geometry_columns[0],
|
|
330
|
+
{ name: "", encoding: "objects", crs: null },
|
|
331
|
+
],
|
|
332
|
+
}),
|
|
333
|
+
});
|
|
334
|
+
await openDialog();
|
|
335
|
+
fireEvent.click(
|
|
336
|
+
await screen.findByRole("button", { name: "GeoParquet options" }),
|
|
337
|
+
);
|
|
338
|
+
selectGeometry("(unnamed geometry)");
|
|
339
|
+
fireEvent.click(
|
|
340
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
341
|
+
);
|
|
342
|
+
await waitFor(() => {
|
|
343
|
+
expect(downloadAs).toHaveBeenCalledWith({
|
|
344
|
+
format: "parquet",
|
|
345
|
+
geometry_column: "",
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("refreshes geometry metadata when the source changes", async () => {
|
|
351
|
+
const first = vi.fn().mockResolvedValue(geometryMetadata);
|
|
352
|
+
const second = vi.fn().mockResolvedValue({
|
|
353
|
+
...geometryMetadata,
|
|
354
|
+
geometry_columns: [
|
|
355
|
+
{ name: "region", encoding: "objects", crs: "EPSG:3857" },
|
|
356
|
+
],
|
|
357
|
+
primary_geometry_column: "region",
|
|
358
|
+
default_geometry_column: "region",
|
|
359
|
+
});
|
|
360
|
+
const { rerenderExportActions } = renderExportActions({
|
|
361
|
+
getExportMetadata: first,
|
|
362
|
+
metadataSource: "first",
|
|
363
|
+
});
|
|
364
|
+
await openDialog();
|
|
365
|
+
await screen.findByRole("button", { name: "GeoParquet options" });
|
|
366
|
+
fireEvent.click(screen.getByRole("button", { name: "GeoParquet options" }));
|
|
367
|
+
selectGeometry("boundary");
|
|
368
|
+
|
|
369
|
+
rerenderExportActions({
|
|
370
|
+
getExportMetadata: second,
|
|
371
|
+
metadataSource: "second",
|
|
372
|
+
});
|
|
373
|
+
await waitFor(() => {
|
|
374
|
+
expect(screen.getByTestId("export-summary-parquet")).toHaveTextContent(
|
|
375
|
+
"region",
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
expect(
|
|
379
|
+
screen.getByRole("combobox", { name: "Primary geometry" }),
|
|
380
|
+
).toHaveDisplayValue("region");
|
|
381
|
+
fireEvent.click(
|
|
382
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
383
|
+
);
|
|
384
|
+
await waitFor(() => {
|
|
385
|
+
expect(downloadAs).toHaveBeenCalledWith({
|
|
386
|
+
format: "parquet",
|
|
387
|
+
geometry_column: "region",
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
expect(first).toHaveBeenCalledTimes(1);
|
|
391
|
+
expect(second).toHaveBeenCalledTimes(1);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("keeps a still-valid geometry override when metadata refreshes", async () => {
|
|
395
|
+
const getExportMetadata = vi.fn().mockResolvedValue(geometryMetadata);
|
|
396
|
+
const { rerenderExportActions } = renderExportActions({
|
|
397
|
+
getExportMetadata,
|
|
398
|
+
metadataSource: "first",
|
|
399
|
+
});
|
|
400
|
+
await openDialog();
|
|
401
|
+
fireEvent.click(
|
|
402
|
+
await screen.findByRole("button", { name: "GeoParquet options" }),
|
|
403
|
+
);
|
|
404
|
+
selectGeometry("boundary");
|
|
405
|
+
rerenderExportActions({ getExportMetadata, metadataSource: "second" });
|
|
406
|
+
await waitFor(() => {
|
|
407
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(2);
|
|
408
|
+
});
|
|
409
|
+
expect(
|
|
410
|
+
screen.getByRole("combobox", { name: "Primary geometry" }),
|
|
411
|
+
).toHaveDisplayValue("boundary");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("does not publish an artifact for a structured failure without an error message", async () => {
|
|
415
|
+
downloadAs.mockResolvedValueOnce({
|
|
416
|
+
url: "",
|
|
417
|
+
filename: "",
|
|
418
|
+
code: "invalid_geometry",
|
|
419
|
+
column: "location",
|
|
420
|
+
});
|
|
421
|
+
renderExportActions({
|
|
422
|
+
getExportMetadata: vi.fn().mockResolvedValue(geometryMetadata),
|
|
423
|
+
});
|
|
424
|
+
await openDialog();
|
|
425
|
+
await screen.findByRole("button", { name: "GeoParquet options" });
|
|
426
|
+
fireEvent.click(
|
|
427
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
428
|
+
);
|
|
429
|
+
expect(
|
|
430
|
+
await screen.findByText("The export did not produce a file."),
|
|
431
|
+
).toBeInTheDocument();
|
|
432
|
+
expect(downloadByURL).not.toHaveBeenCalled();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("retries a failed metadata lookup without blocking ordinary formats", async () => {
|
|
436
|
+
const getExportMetadata = vi
|
|
437
|
+
.fn()
|
|
438
|
+
.mockRejectedValueOnce(new Error("Connection lost"))
|
|
439
|
+
.mockResolvedValueOnce(geometryMetadata);
|
|
440
|
+
renderExportActions({ getExportMetadata });
|
|
441
|
+
await openDialog();
|
|
442
|
+
expect(
|
|
443
|
+
await screen.findByText(
|
|
444
|
+
"Could not load geometry options: Connection lost",
|
|
445
|
+
),
|
|
446
|
+
).toBeInTheDocument();
|
|
447
|
+
expect(
|
|
448
|
+
screen.getByRole("button", { name: "Download Parquet" }),
|
|
449
|
+
).toBeDisabled();
|
|
450
|
+
fireEvent.click(screen.getByRole("button", { name: "Download CSV" }));
|
|
451
|
+
await waitFor(() => {
|
|
452
|
+
expect(downloadAs).toHaveBeenCalledWith({ format: "csv" });
|
|
453
|
+
});
|
|
454
|
+
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
|
|
455
|
+
expect(
|
|
456
|
+
await screen.findByRole("button", { name: "GeoParquet options" }),
|
|
457
|
+
).toBeInTheDocument();
|
|
458
|
+
expect(
|
|
459
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
460
|
+
).toBeEnabled();
|
|
461
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(2);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("does not cancel a pending CSV download when geometry metadata is retried", async () => {
|
|
465
|
+
let resolveDownload: (value: {
|
|
466
|
+
url: string;
|
|
467
|
+
filename: string;
|
|
468
|
+
}) => void = () => undefined;
|
|
469
|
+
downloadAs.mockImplementationOnce(
|
|
470
|
+
() =>
|
|
471
|
+
new Promise((resolve) => {
|
|
472
|
+
resolveDownload = resolve;
|
|
473
|
+
}),
|
|
474
|
+
);
|
|
475
|
+
const getExportMetadata = vi
|
|
476
|
+
.fn()
|
|
477
|
+
.mockRejectedValueOnce(new Error("Connection lost"))
|
|
478
|
+
.mockResolvedValueOnce(geometryMetadata);
|
|
479
|
+
renderExportActions({ getExportMetadata });
|
|
480
|
+
await openDialog();
|
|
481
|
+
await screen.findByText("Could not load geometry options: Connection lost");
|
|
482
|
+
|
|
483
|
+
fireEvent.click(screen.getByRole("button", { name: "Download CSV" }));
|
|
484
|
+
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
|
|
485
|
+
await screen.findByRole("button", { name: "GeoParquet options" });
|
|
486
|
+
await act(async () => {
|
|
487
|
+
resolveDownload({
|
|
488
|
+
url: "https://example.test/export",
|
|
489
|
+
filename: "table",
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
expect(downloadByURL).toHaveBeenCalledWith(
|
|
494
|
+
expect.stringContaining("https://example.test/export"),
|
|
495
|
+
"table.csv",
|
|
496
|
+
);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("cancels a pending download when the table source changes", async () => {
|
|
500
|
+
let resolveDownload: (value: {
|
|
501
|
+
url: string;
|
|
502
|
+
filename: string;
|
|
503
|
+
}) => void = () => undefined;
|
|
504
|
+
downloadAs.mockImplementationOnce(
|
|
505
|
+
() =>
|
|
506
|
+
new Promise((resolve) => {
|
|
507
|
+
resolveDownload = resolve;
|
|
508
|
+
}),
|
|
509
|
+
);
|
|
510
|
+
const getExportMetadata = vi.fn().mockResolvedValue(geometryMetadata);
|
|
511
|
+
const { rerenderExportActions } = renderExportActions({
|
|
512
|
+
getExportMetadata,
|
|
513
|
+
metadataSource: "first",
|
|
514
|
+
});
|
|
515
|
+
await openDialog();
|
|
516
|
+
await screen.findByRole("button", { name: "GeoParquet options" });
|
|
517
|
+
|
|
518
|
+
fireEvent.click(screen.getByRole("button", { name: "Download CSV" }));
|
|
519
|
+
rerenderExportActions({ getExportMetadata, metadataSource: "second" });
|
|
520
|
+
await waitFor(() => {
|
|
521
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(2);
|
|
522
|
+
});
|
|
523
|
+
await act(async () => {
|
|
524
|
+
resolveDownload({
|
|
525
|
+
url: "https://example.test/export",
|
|
526
|
+
filename: "table",
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
expect(downloadByURL).not.toHaveBeenCalled();
|
|
531
|
+
});
|
|
532
|
+
|
|
88
533
|
it("shows formats in the approved order with an options toggle where supported", async () => {
|
|
89
534
|
renderExportActions();
|
|
90
535
|
const dialog = await openDialog();
|
|
@@ -98,6 +543,11 @@ describe("ExportActions dialog", () => {
|
|
|
98
543
|
"export-row-parquet",
|
|
99
544
|
"export-row-markdown",
|
|
100
545
|
]);
|
|
546
|
+
for (const format of ["csv", "tsv", "json", "parquet", "markdown"]) {
|
|
547
|
+
expect(
|
|
548
|
+
screen.getByTestId(`export-format-icon-${format}`),
|
|
549
|
+
).toBeInTheDocument();
|
|
550
|
+
}
|
|
101
551
|
for (const label of ["CSV", "TSV", "JSON"]) {
|
|
102
552
|
expect(
|
|
103
553
|
screen.getByRole("button", { name: `${label} options` }),
|
|
@@ -484,11 +934,14 @@ describe("ExportActions dialog", () => {
|
|
|
484
934
|
|
|
485
935
|
fireEvent.click(screen.getByRole("button", { name: "Download Parquet" }));
|
|
486
936
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
937
|
+
expect(
|
|
938
|
+
await screen.findByText("Parquet export requires pyarrow."),
|
|
939
|
+
).toBeInTheDocument();
|
|
940
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
941
|
+
expect(
|
|
942
|
+
screen.getByRole("button", { name: "Download Parquet" }),
|
|
943
|
+
).toBeDisabled();
|
|
944
|
+
fireEvent.click(screen.getByRole("button", { name: "Install pyarrow" }));
|
|
492
945
|
expect(mocks.handleInstallPackages).toHaveBeenCalledWith(
|
|
493
946
|
["pyarrow"],
|
|
494
947
|
expect.any(Function),
|
|
@@ -520,12 +973,16 @@ describe("ExportActions dialog", () => {
|
|
|
520
973
|
|
|
521
974
|
fireEvent.click(screen.getByRole("button", { name: "Download Parquet" }));
|
|
522
975
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
expect(
|
|
976
|
+
expect(
|
|
977
|
+
await screen.findByText(
|
|
978
|
+
"Parquet export isn't available in this notebook",
|
|
979
|
+
),
|
|
980
|
+
).toBeInTheDocument();
|
|
981
|
+
expect(screen.queryByText("pyarrow")).not.toBeInTheDocument();
|
|
982
|
+
expect(
|
|
983
|
+
screen.queryByRole("button", { name: /Install/ }),
|
|
984
|
+
).not.toBeInTheDocument();
|
|
985
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
529
986
|
});
|
|
530
987
|
|
|
531
988
|
it("clears stale failures before a new action and after close", async () => {
|
|
@@ -660,3 +1117,643 @@ describe("ExportActions dialog", () => {
|
|
|
660
1117
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
661
1118
|
});
|
|
662
1119
|
});
|
|
1120
|
+
|
|
1121
|
+
const LONG_GEOMETRY_WKT = `LINESTRING (${Array.from(
|
|
1122
|
+
{ length: 160 },
|
|
1123
|
+
(_, index) => `${index} ${index % 17}`,
|
|
1124
|
+
).join(", ")})`;
|
|
1125
|
+
const jsonRows = [{ row_label: "long", geometry: LONG_GEOMETRY_WKT }];
|
|
1126
|
+
|
|
1127
|
+
describe("ExportActions clipboard geometry", () => {
|
|
1128
|
+
beforeEach(() => {
|
|
1129
|
+
vi.clearAllMocks();
|
|
1130
|
+
downloadAs.mockResolvedValue({
|
|
1131
|
+
url: "https://example.test/export",
|
|
1132
|
+
filename: "geometry",
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
afterEach(() => {
|
|
1137
|
+
vi.unstubAllGlobals();
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it.each([
|
|
1141
|
+
{
|
|
1142
|
+
label: "CSV",
|
|
1143
|
+
sourceFormat: "csv" as const,
|
|
1144
|
+
sourceText: `row_label,geometry\nlong,"${LONG_GEOMETRY_WKT}"\n`,
|
|
1145
|
+
expectedText: `row_label,geometry\nlong,"${LONG_GEOMETRY_WKT}"\n`,
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
label: "TSV",
|
|
1149
|
+
sourceFormat: "tsv" as const,
|
|
1150
|
+
sourceText: `row_label\tgeometry\nlong\t${LONG_GEOMETRY_WKT}\n`,
|
|
1151
|
+
expectedText: `row_label\tgeometry\nlong\t${LONG_GEOMETRY_WKT}\n`,
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
label: "JSON",
|
|
1155
|
+
sourceFormat: "json" as const,
|
|
1156
|
+
sourceText: JSON.stringify(jsonRows),
|
|
1157
|
+
expectedText: JSON.stringify(jsonRows, null, 2),
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
label: "Markdown",
|
|
1161
|
+
sourceFormat: "json" as const,
|
|
1162
|
+
sourceText: JSON.stringify(jsonRows),
|
|
1163
|
+
expectedText: jsonToMarkdown(jsonRows),
|
|
1164
|
+
},
|
|
1165
|
+
])(
|
|
1166
|
+
"copies complete geometry text as $label",
|
|
1167
|
+
async ({ label, sourceFormat, sourceText, expectedText }) => {
|
|
1168
|
+
vi.stubGlobal(
|
|
1169
|
+
"fetch",
|
|
1170
|
+
vi.fn().mockResolvedValue({
|
|
1171
|
+
ok: true,
|
|
1172
|
+
text: vi.fn().mockResolvedValue(sourceText),
|
|
1173
|
+
}),
|
|
1174
|
+
);
|
|
1175
|
+
renderExportActions();
|
|
1176
|
+
await openDialog();
|
|
1177
|
+
|
|
1178
|
+
fireEvent.click(screen.getByRole("button", { name: `Copy ${label}` }));
|
|
1179
|
+
|
|
1180
|
+
await waitFor(() => {
|
|
1181
|
+
expect(copyToClipboard).toHaveBeenCalledWith(expectedText);
|
|
1182
|
+
});
|
|
1183
|
+
expect(downloadAs).toHaveBeenCalledWith({ format: sourceFormat });
|
|
1184
|
+
},
|
|
1185
|
+
);
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
describe("ExportActions GeoJSON", () => {
|
|
1189
|
+
beforeEach(() => {
|
|
1190
|
+
vi.clearAllMocks();
|
|
1191
|
+
downloadAs.mockResolvedValue({
|
|
1192
|
+
url: "https://example.test/export",
|
|
1193
|
+
filename: "table.source",
|
|
1194
|
+
});
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
const metadataWithCrs: ExportMetadata = {
|
|
1198
|
+
...geometryMetadata,
|
|
1199
|
+
geometry_columns: [
|
|
1200
|
+
geometryMetadata.geometry_columns[0],
|
|
1201
|
+
{ name: "boundary", encoding: "objects", crs: "EPSG:3857" },
|
|
1202
|
+
],
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
it("hides GeoJSON for ordinary sources", async () => {
|
|
1206
|
+
renderExportActions({
|
|
1207
|
+
getExportMetadata: vi.fn().mockResolvedValue({
|
|
1208
|
+
geometry_columns: [],
|
|
1209
|
+
primary_geometry_column: null,
|
|
1210
|
+
default_geometry_column: null,
|
|
1211
|
+
formats: {},
|
|
1212
|
+
}),
|
|
1213
|
+
});
|
|
1214
|
+
await openDialog();
|
|
1215
|
+
expect(screen.queryByTestId("export-row-geojson")).not.toBeInTheDocument();
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
it("downloads GeoJSON without PyArrow", async () => {
|
|
1219
|
+
renderExportActions({
|
|
1220
|
+
getExportMetadata: vi.fn().mockResolvedValue({
|
|
1221
|
+
...geometryMetadata,
|
|
1222
|
+
formats: {
|
|
1223
|
+
...geometryMetadata.formats,
|
|
1224
|
+
parquet: {
|
|
1225
|
+
available: false,
|
|
1226
|
+
reason: "GeoParquet export requires pyarrow.",
|
|
1227
|
+
missing_packages: ["pyarrow"],
|
|
1228
|
+
},
|
|
1229
|
+
},
|
|
1230
|
+
}),
|
|
1231
|
+
});
|
|
1232
|
+
await openDialog();
|
|
1233
|
+
fireEvent.click(
|
|
1234
|
+
await screen.findByRole("button", { name: "Download GeoJSON" }),
|
|
1235
|
+
);
|
|
1236
|
+
expect(screen.getByRole("button", { name: "Copy GeoJSON" })).toBeEnabled();
|
|
1237
|
+
await waitFor(() => {
|
|
1238
|
+
expect(downloadByURL).toHaveBeenCalledWith(
|
|
1239
|
+
"https://example.test/export?download=1&filename=table.geojson",
|
|
1240
|
+
"table.geojson",
|
|
1241
|
+
);
|
|
1242
|
+
});
|
|
1243
|
+
expect(downloadAs).toHaveBeenCalledWith({
|
|
1244
|
+
format: "geojson",
|
|
1245
|
+
geometry_column: "location",
|
|
1246
|
+
});
|
|
1247
|
+
expect(copyToClipboard).not.toHaveBeenCalled();
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
it("shares the geometry choice and disables only GeoJSON for a missing CRS", async () => {
|
|
1251
|
+
renderExportActions({
|
|
1252
|
+
getExportMetadata: vi.fn().mockResolvedValue(geometryMetadata),
|
|
1253
|
+
});
|
|
1254
|
+
await openDialog();
|
|
1255
|
+
fireEvent.click(
|
|
1256
|
+
await screen.findByRole("button", { name: "GeoJSON options" }),
|
|
1257
|
+
);
|
|
1258
|
+
expect(
|
|
1259
|
+
screen.getByRole("group", { name: "GeoJSON options" }),
|
|
1260
|
+
).toHaveTextContent(/longitude and latitude/);
|
|
1261
|
+
expect(
|
|
1262
|
+
screen.getByRole("group", { name: "GeoJSON options" }),
|
|
1263
|
+
).toHaveTextContent(/complete WKT/);
|
|
1264
|
+
selectGeometry("boundary");
|
|
1265
|
+
expect(
|
|
1266
|
+
screen.getByRole("button", { name: "Download GeoJSON" }),
|
|
1267
|
+
).toBeDisabled();
|
|
1268
|
+
expect(
|
|
1269
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
1270
|
+
).toBeEnabled();
|
|
1271
|
+
expect(screen.getByRole("button", { name: "Download JSON" })).toBeEnabled();
|
|
1272
|
+
expect(screen.getByRole("button", { name: "Download CSV" })).toBeEnabled();
|
|
1273
|
+
expect(screen.getByTestId("export-row-geojson")).toHaveTextContent(
|
|
1274
|
+
"Declare its source CRS in Python",
|
|
1275
|
+
);
|
|
1276
|
+
fireEvent.click(screen.getByRole("button", { name: "GeoParquet options" }));
|
|
1277
|
+
expect(
|
|
1278
|
+
screen.getByRole("combobox", { name: "Primary geometry" }),
|
|
1279
|
+
).toHaveDisplayValue("boundary");
|
|
1280
|
+
selectGeometry("location");
|
|
1281
|
+
expect(
|
|
1282
|
+
screen.getByRole("button", { name: "Download GeoJSON" }),
|
|
1283
|
+
).toBeEnabled();
|
|
1284
|
+
expect(screen.getByTestId("export-summary-geojson")).toHaveTextContent(
|
|
1285
|
+
"location",
|
|
1286
|
+
);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
it("allows a known-CRS alternative when the default has no CRS", async () => {
|
|
1290
|
+
renderExportActions({
|
|
1291
|
+
getExportMetadata: vi.fn().mockResolvedValue({
|
|
1292
|
+
...geometryMetadata,
|
|
1293
|
+
default_geometry_column: "boundary",
|
|
1294
|
+
primary_geometry_column: "boundary",
|
|
1295
|
+
}),
|
|
1296
|
+
});
|
|
1297
|
+
await openDialog();
|
|
1298
|
+
fireEvent.click(
|
|
1299
|
+
await screen.findByRole("button", { name: "GeoJSON options" }),
|
|
1300
|
+
);
|
|
1301
|
+
expect(
|
|
1302
|
+
screen.getByRole("button", { name: "Download GeoJSON" }),
|
|
1303
|
+
).toBeDisabled();
|
|
1304
|
+
selectGeometry("location");
|
|
1305
|
+
fireEvent.click(screen.getByRole("button", { name: "Download GeoJSON" }));
|
|
1306
|
+
await waitFor(() =>
|
|
1307
|
+
expect(downloadAs).toHaveBeenCalledWith({
|
|
1308
|
+
format: "geojson",
|
|
1309
|
+
geometry_column: "location",
|
|
1310
|
+
}),
|
|
1311
|
+
);
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
it("requires a choice for ambiguous sources", async () => {
|
|
1315
|
+
renderExportActions({
|
|
1316
|
+
getExportMetadata: vi.fn().mockResolvedValue({
|
|
1317
|
+
...metadataWithCrs,
|
|
1318
|
+
primary_geometry_column: null,
|
|
1319
|
+
default_geometry_column: null,
|
|
1320
|
+
}),
|
|
1321
|
+
});
|
|
1322
|
+
await openDialog();
|
|
1323
|
+
fireEvent.click(
|
|
1324
|
+
await screen.findByRole("button", { name: "GeoJSON options" }),
|
|
1325
|
+
);
|
|
1326
|
+
expect(
|
|
1327
|
+
screen.getByRole("button", { name: "Download GeoJSON" }),
|
|
1328
|
+
).toBeDisabled();
|
|
1329
|
+
selectGeometry("boundary");
|
|
1330
|
+
fireEvent.click(screen.getByRole("button", { name: "Download GeoJSON" }));
|
|
1331
|
+
await waitFor(() =>
|
|
1332
|
+
expect(downloadAs).toHaveBeenCalledWith({
|
|
1333
|
+
format: "geojson",
|
|
1334
|
+
geometry_column: "boundary",
|
|
1335
|
+
}),
|
|
1336
|
+
);
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
it("preserves an empty geometry name", async () => {
|
|
1340
|
+
renderExportActions({
|
|
1341
|
+
getExportMetadata: vi.fn().mockResolvedValue({
|
|
1342
|
+
...geometryMetadata,
|
|
1343
|
+
geometry_columns: [{ name: "", encoding: "objects", crs: "EPSG:4326" }],
|
|
1344
|
+
primary_geometry_column: "",
|
|
1345
|
+
default_geometry_column: "",
|
|
1346
|
+
}),
|
|
1347
|
+
});
|
|
1348
|
+
await openDialog();
|
|
1349
|
+
fireEvent.click(
|
|
1350
|
+
await screen.findByRole("button", { name: "Download GeoJSON" }),
|
|
1351
|
+
);
|
|
1352
|
+
await waitFor(() =>
|
|
1353
|
+
expect(downloadAs).toHaveBeenCalledWith({
|
|
1354
|
+
format: "geojson",
|
|
1355
|
+
geometry_column: "",
|
|
1356
|
+
}),
|
|
1357
|
+
);
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
it.each(["missing_crs", "conversion_failed"] as const)(
|
|
1361
|
+
"keeps choices after %s and retries",
|
|
1362
|
+
async (code) => {
|
|
1363
|
+
downloadAs.mockResolvedValueOnce({
|
|
1364
|
+
url: "",
|
|
1365
|
+
filename: "",
|
|
1366
|
+
code,
|
|
1367
|
+
error: "Geometry export failed.",
|
|
1368
|
+
});
|
|
1369
|
+
renderExportActions({
|
|
1370
|
+
getExportMetadata: vi.fn().mockResolvedValue(metadataWithCrs),
|
|
1371
|
+
});
|
|
1372
|
+
await openDialog();
|
|
1373
|
+
fireEvent.click(
|
|
1374
|
+
await screen.findByRole("button", { name: "GeoJSON options" }),
|
|
1375
|
+
);
|
|
1376
|
+
selectGeometry("boundary");
|
|
1377
|
+
fireEvent.click(screen.getByRole("button", { name: "Download GeoJSON" }));
|
|
1378
|
+
expect(await screen.findByRole("alert")).toHaveTextContent(
|
|
1379
|
+
"Geometry export failed.",
|
|
1380
|
+
);
|
|
1381
|
+
expect(downloadByURL).not.toHaveBeenCalled();
|
|
1382
|
+
expect(
|
|
1383
|
+
screen.getByRole("combobox", { name: "Primary geometry" }),
|
|
1384
|
+
).toHaveDisplayValue("boundary");
|
|
1385
|
+
fireEvent.click(screen.getByRole("button", { name: "Download GeoJSON" }));
|
|
1386
|
+
await waitFor(() => expect(downloadByURL).toHaveBeenCalled());
|
|
1387
|
+
expect(downloadAs).toHaveBeenLastCalledWith({
|
|
1388
|
+
format: "geojson",
|
|
1389
|
+
geometry_column: "boundary",
|
|
1390
|
+
});
|
|
1391
|
+
},
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
it("refreshes metadata and keeps the geometry choice after installation", async () => {
|
|
1395
|
+
const getExportMetadata = vi.fn().mockResolvedValue(metadataWithCrs);
|
|
1396
|
+
downloadAs.mockResolvedValueOnce({
|
|
1397
|
+
url: "",
|
|
1398
|
+
filename: "",
|
|
1399
|
+
code: "missing_packages",
|
|
1400
|
+
error: "GeoJSON requires geopandas.",
|
|
1401
|
+
missing_packages: ["geopandas"],
|
|
1402
|
+
});
|
|
1403
|
+
renderExportActions({ getExportMetadata });
|
|
1404
|
+
await openDialog();
|
|
1405
|
+
fireEvent.click(
|
|
1406
|
+
await screen.findByRole("button", { name: "GeoJSON options" }),
|
|
1407
|
+
);
|
|
1408
|
+
selectGeometry("boundary");
|
|
1409
|
+
fireEvent.click(screen.getByRole("button", { name: "Download GeoJSON" }));
|
|
1410
|
+
fireEvent.click(
|
|
1411
|
+
await screen.findByRole("button", { name: "Install geopandas" }),
|
|
1412
|
+
);
|
|
1413
|
+
await act(async () => mocks.handleInstallPackages.mock.calls[0][1]());
|
|
1414
|
+
await waitFor(() => expect(downloadByURL).toHaveBeenCalled());
|
|
1415
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(2);
|
|
1416
|
+
expect(downloadAs).toHaveBeenLastCalledWith({
|
|
1417
|
+
format: "geojson",
|
|
1418
|
+
geometry_column: "boundary",
|
|
1419
|
+
});
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
it.each(["missing-package", "missing-crs", "obsolete-choice"] as const)(
|
|
1423
|
+
"does not retry after installation with %s",
|
|
1424
|
+
async (prerequisite) => {
|
|
1425
|
+
const getExportMetadata = vi.fn().mockResolvedValue(metadataWithCrs);
|
|
1426
|
+
downloadAs.mockResolvedValueOnce({
|
|
1427
|
+
url: "",
|
|
1428
|
+
filename: "",
|
|
1429
|
+
code: "missing_packages",
|
|
1430
|
+
missing_packages: ["geopandas"],
|
|
1431
|
+
});
|
|
1432
|
+
renderExportActions({ getExportMetadata });
|
|
1433
|
+
await openDialog();
|
|
1434
|
+
fireEvent.click(
|
|
1435
|
+
await screen.findByRole("button", { name: "GeoJSON options" }),
|
|
1436
|
+
);
|
|
1437
|
+
selectGeometry("boundary");
|
|
1438
|
+
fireEvent.click(screen.getByRole("button", { name: "Download GeoJSON" }));
|
|
1439
|
+
fireEvent.click(
|
|
1440
|
+
await screen.findByRole("button", { name: "Install geopandas" }),
|
|
1441
|
+
);
|
|
1442
|
+
getExportMetadata.mockResolvedValue(
|
|
1443
|
+
prerequisite === "missing-package"
|
|
1444
|
+
? {
|
|
1445
|
+
...metadataWithCrs,
|
|
1446
|
+
formats: {
|
|
1447
|
+
geojson: {
|
|
1448
|
+
available: false,
|
|
1449
|
+
reason: "GeoJSON requires geopandas.",
|
|
1450
|
+
missing_packages: ["geopandas"],
|
|
1451
|
+
},
|
|
1452
|
+
},
|
|
1453
|
+
}
|
|
1454
|
+
: prerequisite === "missing-crs"
|
|
1455
|
+
? geometryMetadata
|
|
1456
|
+
: {
|
|
1457
|
+
...geometryMetadata,
|
|
1458
|
+
geometry_columns: [geometryMetadata.geometry_columns[0]],
|
|
1459
|
+
},
|
|
1460
|
+
);
|
|
1461
|
+
await act(async () => mocks.handleInstallPackages.mock.calls[0][1]());
|
|
1462
|
+
expect(downloadAs).toHaveBeenCalledTimes(1);
|
|
1463
|
+
expect(downloadByURL).not.toHaveBeenCalled();
|
|
1464
|
+
expect(screen.queryByRole("alert") !== null).toBe(
|
|
1465
|
+
prerequisite === "obsolete-choice",
|
|
1466
|
+
);
|
|
1467
|
+
expect(
|
|
1468
|
+
screen.getByRole("combobox", { name: "Primary geometry" }),
|
|
1469
|
+
).toHaveDisplayValue(
|
|
1470
|
+
prerequisite === "obsolete-choice" ? "location" : "boundary",
|
|
1471
|
+
);
|
|
1472
|
+
expect(
|
|
1473
|
+
screen.getByRole("button", { name: "Download GeoJSON" }),
|
|
1474
|
+
).toHaveProperty("disabled", prerequisite !== "obsolete-choice");
|
|
1475
|
+
},
|
|
1476
|
+
);
|
|
1477
|
+
|
|
1478
|
+
it("rejects an obsolete choice after a source change", async () => {
|
|
1479
|
+
const getExportMetadata = vi.fn().mockResolvedValue(metadataWithCrs);
|
|
1480
|
+
const { rerenderExportActions } = renderExportActions({
|
|
1481
|
+
getExportMetadata,
|
|
1482
|
+
metadataSource: "first",
|
|
1483
|
+
});
|
|
1484
|
+
await openDialog();
|
|
1485
|
+
fireEvent.click(
|
|
1486
|
+
await screen.findByRole("button", { name: "GeoJSON options" }),
|
|
1487
|
+
);
|
|
1488
|
+
selectGeometry("boundary");
|
|
1489
|
+
getExportMetadata.mockResolvedValue({
|
|
1490
|
+
...geometryMetadata,
|
|
1491
|
+
geometry_columns: [geometryMetadata.geometry_columns[0]],
|
|
1492
|
+
});
|
|
1493
|
+
rerenderExportActions({ getExportMetadata, metadataSource: "second" });
|
|
1494
|
+
await waitFor(() =>
|
|
1495
|
+
expect(screen.getByTestId("export-summary-geojson")).toHaveTextContent(
|
|
1496
|
+
"location",
|
|
1497
|
+
),
|
|
1498
|
+
);
|
|
1499
|
+
fireEvent.click(screen.getByRole("button", { name: "Download GeoJSON" }));
|
|
1500
|
+
await waitFor(() =>
|
|
1501
|
+
expect(downloadAs).toHaveBeenCalledWith({
|
|
1502
|
+
format: "geojson",
|
|
1503
|
+
geometry_column: "location",
|
|
1504
|
+
}),
|
|
1505
|
+
);
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
it("keeps a WASM data URL unchanged", async () => {
|
|
1509
|
+
downloadAs.mockResolvedValue({
|
|
1510
|
+
url: "data:application/geo+json;base64,e30=",
|
|
1511
|
+
filename: "table",
|
|
1512
|
+
});
|
|
1513
|
+
renderExportActions({
|
|
1514
|
+
getExportMetadata: vi.fn().mockResolvedValue(geometryMetadata),
|
|
1515
|
+
});
|
|
1516
|
+
await openDialog();
|
|
1517
|
+
fireEvent.click(
|
|
1518
|
+
await screen.findByRole("button", { name: "Download GeoJSON" }),
|
|
1519
|
+
);
|
|
1520
|
+
await waitFor(() =>
|
|
1521
|
+
expect(downloadByURL).toHaveBeenCalledWith(
|
|
1522
|
+
"data:application/geo+json;base64,e30=",
|
|
1523
|
+
"table.geojson",
|
|
1524
|
+
),
|
|
1525
|
+
);
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
it("hides package installation in read mode", async () => {
|
|
1529
|
+
renderExportActions(
|
|
1530
|
+
{
|
|
1531
|
+
getExportMetadata: vi.fn().mockResolvedValue({
|
|
1532
|
+
...geometryMetadata,
|
|
1533
|
+
formats: {
|
|
1534
|
+
geojson: {
|
|
1535
|
+
available: false,
|
|
1536
|
+
reason: "GeoJSON requires geopandas.",
|
|
1537
|
+
missing_packages: ["geopandas"],
|
|
1538
|
+
},
|
|
1539
|
+
},
|
|
1540
|
+
}),
|
|
1541
|
+
},
|
|
1542
|
+
null,
|
|
1543
|
+
"read",
|
|
1544
|
+
);
|
|
1545
|
+
await openDialog();
|
|
1546
|
+
expect(
|
|
1547
|
+
await screen.findByText(
|
|
1548
|
+
"GeoJSON export isn't available in this notebook",
|
|
1549
|
+
),
|
|
1550
|
+
).toBeInTheDocument();
|
|
1551
|
+
expect(
|
|
1552
|
+
screen.queryByRole("button", { name: /Install/ }),
|
|
1553
|
+
).not.toBeInTheDocument();
|
|
1554
|
+
expect(
|
|
1555
|
+
screen.getByRole("button", { name: "Download GeoJSON" }),
|
|
1556
|
+
).toBeDisabled();
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
it("respects the host size limit for geometry sources", () => {
|
|
1560
|
+
renderExportActions(
|
|
1561
|
+
{
|
|
1562
|
+
getExportMetadata: vi.fn().mockResolvedValue(geometryMetadata),
|
|
1563
|
+
sizeBytes: 101,
|
|
1564
|
+
},
|
|
1565
|
+
{ limitBytes: 100, unavailableMessage: "Export is too large." },
|
|
1566
|
+
);
|
|
1567
|
+
expect(screen.getByTestId("export-button")).toBeDisabled();
|
|
1568
|
+
fireEvent.click(screen.getByTestId("export-button"));
|
|
1569
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
1570
|
+
expect(downloadAs).not.toHaveBeenCalled();
|
|
1571
|
+
});
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
describe("ExportActions GeoJSON clipboard", () => {
|
|
1575
|
+
const metadata: ExportMetadata = {
|
|
1576
|
+
...geometryMetadata,
|
|
1577
|
+
geometry_columns: [
|
|
1578
|
+
geometryMetadata.geometry_columns[0],
|
|
1579
|
+
{ name: "boundary", encoding: "objects", crs: "EPSG:3857" },
|
|
1580
|
+
],
|
|
1581
|
+
};
|
|
1582
|
+
const payload = `{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[20,5]},"properties":{"large":9007199254740993,"secondary":${JSON.stringify(LONG_GEOMETRY_WKT)}}}]}`;
|
|
1583
|
+
|
|
1584
|
+
beforeEach(() => {
|
|
1585
|
+
vi.clearAllMocks();
|
|
1586
|
+
downloadAs.mockResolvedValue({
|
|
1587
|
+
url: "https://example.test/export",
|
|
1588
|
+
filename: "table.geojson",
|
|
1589
|
+
});
|
|
1590
|
+
vi.stubGlobal(
|
|
1591
|
+
"fetch",
|
|
1592
|
+
vi.fn().mockResolvedValue({
|
|
1593
|
+
ok: true,
|
|
1594
|
+
text: vi.fn().mockResolvedValue(payload),
|
|
1595
|
+
}),
|
|
1596
|
+
);
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
1600
|
+
|
|
1601
|
+
it("copies the exact FeatureCollection with the selected geometry", async () => {
|
|
1602
|
+
renderExportActions({
|
|
1603
|
+
getExportMetadata: vi.fn().mockResolvedValue(metadata),
|
|
1604
|
+
});
|
|
1605
|
+
await openDialog();
|
|
1606
|
+
fireEvent.click(
|
|
1607
|
+
await screen.findByRole("button", { name: "GeoJSON options" }),
|
|
1608
|
+
);
|
|
1609
|
+
selectGeometry("boundary");
|
|
1610
|
+
fireEvent.click(screen.getByRole("button", { name: "Copy GeoJSON" }));
|
|
1611
|
+
await waitFor(() => expect(copyToClipboard).toHaveBeenCalledWith(payload));
|
|
1612
|
+
expect(downloadAs).toHaveBeenCalledWith({
|
|
1613
|
+
format: "geojson",
|
|
1614
|
+
geometry_column: "boundary",
|
|
1615
|
+
});
|
|
1616
|
+
expect(downloadByURL).not.toHaveBeenCalled();
|
|
1617
|
+
expect(toast).toHaveBeenCalledWith({ title: "Copied to clipboard" });
|
|
1618
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
it.each([
|
|
1622
|
+
{ name: "a missing CRS", metadata: geometryMetadata, choice: "boundary" },
|
|
1623
|
+
{
|
|
1624
|
+
name: "an ambiguous geometry",
|
|
1625
|
+
metadata: {
|
|
1626
|
+
...metadata,
|
|
1627
|
+
primary_geometry_column: null,
|
|
1628
|
+
default_geometry_column: null,
|
|
1629
|
+
},
|
|
1630
|
+
choice: null,
|
|
1631
|
+
},
|
|
1632
|
+
{
|
|
1633
|
+
name: "missing packages",
|
|
1634
|
+
metadata: {
|
|
1635
|
+
...metadata,
|
|
1636
|
+
formats: {
|
|
1637
|
+
geojson: {
|
|
1638
|
+
available: false,
|
|
1639
|
+
reason: "Install geopandas.",
|
|
1640
|
+
missing_packages: ["geopandas"],
|
|
1641
|
+
},
|
|
1642
|
+
},
|
|
1643
|
+
},
|
|
1644
|
+
choice: "location",
|
|
1645
|
+
},
|
|
1646
|
+
])("disables copy for $name", async ({ metadata, choice }) => {
|
|
1647
|
+
renderExportActions({
|
|
1648
|
+
getExportMetadata: vi.fn().mockResolvedValue(metadata),
|
|
1649
|
+
});
|
|
1650
|
+
await openDialog();
|
|
1651
|
+
fireEvent.click(
|
|
1652
|
+
await screen.findByRole("button", { name: "GeoJSON options" }),
|
|
1653
|
+
);
|
|
1654
|
+
if (choice !== null) {
|
|
1655
|
+
selectGeometry(choice);
|
|
1656
|
+
}
|
|
1657
|
+
expect(screen.getByRole("button", { name: "Copy GeoJSON" })).toBeDisabled();
|
|
1658
|
+
fireEvent.click(screen.getByRole("button", { name: "Copy GeoJSON" }));
|
|
1659
|
+
expect(downloadAs).not.toHaveBeenCalled();
|
|
1660
|
+
expect(copyToClipboard).not.toHaveBeenCalled();
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
it("preserves the copy destination and geometry after installation", async () => {
|
|
1664
|
+
downloadAs.mockResolvedValueOnce({
|
|
1665
|
+
url: "",
|
|
1666
|
+
filename: "",
|
|
1667
|
+
code: "missing_packages",
|
|
1668
|
+
missing_packages: ["geopandas"],
|
|
1669
|
+
});
|
|
1670
|
+
const getExportMetadata = vi.fn().mockResolvedValue(metadata);
|
|
1671
|
+
renderExportActions({ getExportMetadata });
|
|
1672
|
+
await openDialog();
|
|
1673
|
+
fireEvent.click(
|
|
1674
|
+
await screen.findByRole("button", { name: "GeoJSON options" }),
|
|
1675
|
+
);
|
|
1676
|
+
selectGeometry("boundary");
|
|
1677
|
+
fireEvent.click(screen.getByRole("button", { name: "Copy GeoJSON" }));
|
|
1678
|
+
fireEvent.click(
|
|
1679
|
+
await screen.findByRole("button", { name: "Install geopandas" }),
|
|
1680
|
+
);
|
|
1681
|
+
expect(copyToClipboard).not.toHaveBeenCalled();
|
|
1682
|
+
await act(async () => mocks.handleInstallPackages.mock.calls[0][1]());
|
|
1683
|
+
await waitFor(() => expect(copyToClipboard).toHaveBeenCalledWith(payload));
|
|
1684
|
+
expect(downloadAs).toHaveBeenLastCalledWith({
|
|
1685
|
+
format: "geojson",
|
|
1686
|
+
geometry_column: "boundary",
|
|
1687
|
+
});
|
|
1688
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(2);
|
|
1689
|
+
expect(downloadByURL).not.toHaveBeenCalled();
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
it("preserves the choice after a backend failure", async () => {
|
|
1693
|
+
downloadAs.mockResolvedValueOnce({
|
|
1694
|
+
url: "",
|
|
1695
|
+
filename: "",
|
|
1696
|
+
code: "conversion_failed",
|
|
1697
|
+
error: "Invalid geometry.",
|
|
1698
|
+
});
|
|
1699
|
+
renderExportActions({
|
|
1700
|
+
getExportMetadata: vi.fn().mockResolvedValue(metadata),
|
|
1701
|
+
});
|
|
1702
|
+
await openDialog();
|
|
1703
|
+
fireEvent.click(
|
|
1704
|
+
await screen.findByRole("button", { name: "GeoJSON options" }),
|
|
1705
|
+
);
|
|
1706
|
+
selectGeometry("boundary");
|
|
1707
|
+
fireEvent.click(screen.getByRole("button", { name: "Copy GeoJSON" }));
|
|
1708
|
+
expect(await screen.findByRole("alert")).toHaveTextContent(
|
|
1709
|
+
"Failed to copy to clipboard",
|
|
1710
|
+
);
|
|
1711
|
+
expect(copyToClipboard).not.toHaveBeenCalled();
|
|
1712
|
+
expect(
|
|
1713
|
+
screen.getByRole("combobox", { name: "Primary geometry" }),
|
|
1714
|
+
).toHaveDisplayValue("boundary");
|
|
1715
|
+
fireEvent.click(screen.getByRole("button", { name: "Copy GeoJSON" }));
|
|
1716
|
+
await waitFor(() => expect(copyToClipboard).toHaveBeenCalledWith(payload));
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
it("reports clipboard rejection in the GeoJSON row", async () => {
|
|
1720
|
+
vi.mocked(copyToClipboard).mockRejectedValueOnce(
|
|
1721
|
+
new Error("Clipboard blocked"),
|
|
1722
|
+
);
|
|
1723
|
+
renderExportActions({
|
|
1724
|
+
getExportMetadata: vi.fn().mockResolvedValue(metadata),
|
|
1725
|
+
});
|
|
1726
|
+
await openDialog();
|
|
1727
|
+
fireEvent.click(
|
|
1728
|
+
await screen.findByRole("button", { name: "Copy GeoJSON" }),
|
|
1729
|
+
);
|
|
1730
|
+
const alert = await screen.findByRole("alert");
|
|
1731
|
+
expect(screen.getByTestId("export-row-geojson")).toContainElement(alert);
|
|
1732
|
+
expect(alert).toHaveTextContent("Clipboard blocked");
|
|
1733
|
+
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
it("does not copy a late response after the source changes", async () => {
|
|
1737
|
+
let resolveText: (value: string) => void = () => undefined;
|
|
1738
|
+
const text = vi.fn().mockReturnValue(
|
|
1739
|
+
new Promise<string>((resolve) => {
|
|
1740
|
+
resolveText = resolve;
|
|
1741
|
+
}),
|
|
1742
|
+
);
|
|
1743
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, text }));
|
|
1744
|
+
const getExportMetadata = vi.fn().mockResolvedValue(metadata);
|
|
1745
|
+
const { rerenderExportActions } = renderExportActions({
|
|
1746
|
+
getExportMetadata,
|
|
1747
|
+
metadataSource: "first",
|
|
1748
|
+
});
|
|
1749
|
+
await openDialog();
|
|
1750
|
+
fireEvent.click(
|
|
1751
|
+
await screen.findByRole("button", { name: "Copy GeoJSON" }),
|
|
1752
|
+
);
|
|
1753
|
+
await waitFor(() => expect(text).toHaveBeenCalled());
|
|
1754
|
+
rerenderExportActions({ getExportMetadata, metadataSource: "second" });
|
|
1755
|
+
await act(async () => resolveText(payload));
|
|
1756
|
+
expect(copyToClipboard).not.toHaveBeenCalled();
|
|
1757
|
+
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
1758
|
+
});
|
|
1759
|
+
});
|