@seedgrid/fe-backup 2026.7.0
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/README.md +86 -0
- package/dist/backup.d.ts +47 -0
- package/dist/backup.d.ts.map +1 -0
- package/dist/backup.js +3 -0
- package/dist/export-status.d.ts +6 -0
- package/dist/export-status.d.ts.map +1 -0
- package/dist/export-status.js +28 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/messages.d.ts +308 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +202 -0
- package/dist/module.d.ts +3 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +23 -0
- package/dist/module.json +15 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +11 -0
- package/dist/server/create-backup-server.d.ts +19 -0
- package/dist/server/create-backup-server.d.ts.map +1 -0
- package/dist/server/create-backup-server.js +19 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2 -0
- package/package.json +43 -0
- package/scaffold/README.md +69 -0
- package/scaffold/src/app/api/backup/exports/[jobId]/download/route.ts +87 -0
- package/scaffold/src/app/backup/exports/(create)/new/page.test.tsx +182 -0
- package/scaffold/src/app/backup/exports/(create)/new/page.tsx +168 -0
- package/scaffold/src/app/backup/exports/(list)/page.test.tsx +166 -0
- package/scaffold/src/app/backup/exports/(list)/page.tsx +286 -0
- package/scaffold/src/modules/backup/components/ExportStatusBadge.test.tsx +75 -0
- package/scaffold/src/modules/backup/components/ExportStatusBadge.tsx +54 -0
- package/scaffold/src/modules/backup/config.ts +27 -0
- package/scaffold/src/modules/backup/index.ts +19 -0
- package/scaffold/src/modules/backup/services/backup-api.test.ts +122 -0
- package/scaffold/src/modules/backup/services/backup-api.ts +205 -0
- package/scaffold/src/modules/backup/services/backup-service.test.ts +125 -0
- package/scaffold/src/modules/backup/services/backup-service.ts +34 -0
- package/scaffold/src/modules/backup/types.ts +17 -0
- package/scaffold/src/modules/backup/useBackupAppShellSections.test.tsx +61 -0
- package/scaffold/src/modules/backup/useBackupAppShellSections.tsx +50 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// seedgrid:managed
|
|
2
|
+
|
|
3
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { backupService } from "@/modules/backup/services/backup-service";
|
|
8
|
+
|
|
9
|
+
vi.mock("next/navigation", () => ({
|
|
10
|
+
useRouter: () => ({ push: vi.fn() }),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("@/modules/backup/services/backup-service", () => ({
|
|
14
|
+
backupService: {
|
|
15
|
+
createExportJob: vi.fn(),
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("@/modules/security", () => ({
|
|
20
|
+
RouteGuard: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
21
|
+
PageFrame: ({ children, actions }: { children: React.ReactNode; actions?: React.ReactNode }) => (
|
|
22
|
+
<div>
|
|
23
|
+
{actions}
|
|
24
|
+
{children}
|
|
25
|
+
</div>
|
|
26
|
+
),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock("@/i18n", () => ({
|
|
30
|
+
useI18n: () => ({ t: (key: string) => key }),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock("@seedgrid/fe-components", () => ({
|
|
34
|
+
SgButton: ({ label, onClick, loading, disabled }: any) => (
|
|
35
|
+
<button onClick={onClick} disabled={disabled || loading}>
|
|
36
|
+
{label}
|
|
37
|
+
</button>
|
|
38
|
+
),
|
|
39
|
+
SgRadioGroup: ({ title, source, value, onChange }: any) => (
|
|
40
|
+
<fieldset>
|
|
41
|
+
<legend>{title}</legend>
|
|
42
|
+
{source.map((opt: any) => (
|
|
43
|
+
<label key={opt.value}>
|
|
44
|
+
<input
|
|
45
|
+
type="radio"
|
|
46
|
+
name={title}
|
|
47
|
+
value={opt.value}
|
|
48
|
+
checked={value === opt.value}
|
|
49
|
+
onChange={() => onChange(opt.value)}
|
|
50
|
+
/>
|
|
51
|
+
{opt.label}
|
|
52
|
+
</label>
|
|
53
|
+
))}
|
|
54
|
+
</fieldset>
|
|
55
|
+
),
|
|
56
|
+
SgStack: ({ children }: any) => <div>{children}</div>,
|
|
57
|
+
SgToggleSwitch: ({ id, label, checked, onChange }: any) => (
|
|
58
|
+
<label htmlFor={id}>
|
|
59
|
+
<input
|
|
60
|
+
id={id}
|
|
61
|
+
type="checkbox"
|
|
62
|
+
checked={checked}
|
|
63
|
+
onChange={(e) => onChange(e.target.checked)}
|
|
64
|
+
/>
|
|
65
|
+
{label}
|
|
66
|
+
</label>
|
|
67
|
+
),
|
|
68
|
+
sgWhistle: { success: vi.fn(), error: vi.fn() },
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
const mockCreateExportJob = vi.mocked(backupService.createExportJob);
|
|
72
|
+
|
|
73
|
+
describe("NewExportPage", () => {
|
|
74
|
+
beforeEach(() => vi.clearAllMocks());
|
|
75
|
+
|
|
76
|
+
it("renders format and scope radio groups", async () => {
|
|
77
|
+
const { default: Page } = await import("./page");
|
|
78
|
+
render(<Page />);
|
|
79
|
+
|
|
80
|
+
expect(
|
|
81
|
+
screen.getByText("backup.exports.create.format_label")
|
|
82
|
+
).toBeInTheDocument();
|
|
83
|
+
expect(
|
|
84
|
+
screen.getByText("backup.exports.create.scope_label")
|
|
85
|
+
).toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("submits with CSV_ZIP and DOMAIN by default", async () => {
|
|
89
|
+
mockCreateExportJob.mockResolvedValue({
|
|
90
|
+
jobId: "new-job",
|
|
91
|
+
status: "REQUESTED",
|
|
92
|
+
format: "CSV_ZIP",
|
|
93
|
+
scope: "DOMAIN",
|
|
94
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const { default: Page } = await import("./page");
|
|
98
|
+
render(<Page />);
|
|
99
|
+
|
|
100
|
+
const submitBtn = screen.getByText("backup.exports.create.submit");
|
|
101
|
+
await userEvent.click(submitBtn);
|
|
102
|
+
|
|
103
|
+
await waitFor(() =>
|
|
104
|
+
expect(mockCreateExportJob).toHaveBeenCalledWith(
|
|
105
|
+
expect.objectContaining({ format: "CSV_ZIP", scope: "DOMAIN" })
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("submits with SQL_DUMP_ZIP when that format is selected", async () => {
|
|
111
|
+
mockCreateExportJob.mockResolvedValue({
|
|
112
|
+
jobId: "new-job",
|
|
113
|
+
status: "REQUESTED",
|
|
114
|
+
format: "SQL_DUMP_ZIP",
|
|
115
|
+
scope: "DOMAIN",
|
|
116
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const { default: Page } = await import("./page");
|
|
120
|
+
render(<Page />);
|
|
121
|
+
|
|
122
|
+
const sqlDumpRadio = screen.getByDisplayValue("SQL_DUMP_ZIP");
|
|
123
|
+
await userEvent.click(sqlDumpRadio);
|
|
124
|
+
|
|
125
|
+
const submitBtn = screen.getByText("backup.exports.create.submit");
|
|
126
|
+
await userEvent.click(submitBtn);
|
|
127
|
+
|
|
128
|
+
await waitFor(() =>
|
|
129
|
+
expect(mockCreateExportJob).toHaveBeenCalledWith(
|
|
130
|
+
expect.objectContaining({ format: "SQL_DUMP_ZIP" })
|
|
131
|
+
)
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("includes attachments when the toggle is checked", async () => {
|
|
136
|
+
mockCreateExportJob.mockResolvedValue({
|
|
137
|
+
jobId: "new-job",
|
|
138
|
+
status: "REQUESTED",
|
|
139
|
+
format: "CSV_ZIP",
|
|
140
|
+
scope: "DOMAIN",
|
|
141
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const { default: Page } = await import("./page");
|
|
145
|
+
render(<Page />);
|
|
146
|
+
|
|
147
|
+
const toggle = screen.getByRole("checkbox");
|
|
148
|
+
await userEvent.click(toggle);
|
|
149
|
+
|
|
150
|
+
const submitBtn = screen.getByText("backup.exports.create.submit");
|
|
151
|
+
await userEvent.click(submitBtn);
|
|
152
|
+
|
|
153
|
+
await waitFor(() =>
|
|
154
|
+
expect(mockCreateExportJob).toHaveBeenCalledWith(
|
|
155
|
+
expect.objectContaining({ includeAttachments: true })
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("disables the submit button while submitting", async () => {
|
|
161
|
+
let resolveCreate!: () => void;
|
|
162
|
+
mockCreateExportJob.mockReturnValue(
|
|
163
|
+
new Promise((res) => {
|
|
164
|
+
resolveCreate = () => res({
|
|
165
|
+
jobId: "x", status: "REQUESTED", format: "CSV_ZIP", scope: "DOMAIN",
|
|
166
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
167
|
+
});
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const { default: Page } = await import("./page");
|
|
172
|
+
render(<Page />);
|
|
173
|
+
|
|
174
|
+
const submitBtn = screen.getByText("backup.exports.create.submit");
|
|
175
|
+
await userEvent.click(submitBtn);
|
|
176
|
+
|
|
177
|
+
expect(submitBtn).toBeDisabled();
|
|
178
|
+
|
|
179
|
+
resolveCreate();
|
|
180
|
+
await waitFor(() => expect(submitBtn).not.toBeDisabled());
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
SgButton,
|
|
7
|
+
SgRadioGroup,
|
|
8
|
+
SgStack,
|
|
9
|
+
SgToggleSwitch,
|
|
10
|
+
sgWhistle,
|
|
11
|
+
} from "@seedgrid/fe-components";
|
|
12
|
+
import { useRouter } from "next/navigation";
|
|
13
|
+
|
|
14
|
+
import { useI18n } from "@/i18n";
|
|
15
|
+
import { PageFrame, RouteGuard } from "@/modules/security";
|
|
16
|
+
|
|
17
|
+
import { backupConfig } from "@/modules/backup/config";
|
|
18
|
+
import { extractBackupApiMessage } from "@/modules/backup/services/backup-api";
|
|
19
|
+
import { backupService } from "@/modules/backup/services/backup-service";
|
|
20
|
+
import type { BackupFormat, ExportRequest, ExportScope } from "@/modules/backup/types";
|
|
21
|
+
|
|
22
|
+
// `as const` (em vez de `: Array<{ labelKey: string }>`): mantem labelKey/hintKey
|
|
23
|
+
// como literais, que sao chaves validas de i18n — t() so aceita a uniao de
|
|
24
|
+
// chaves conhecidas, entao um `string` alargado seria recusado (TS2345).
|
|
25
|
+
const FORMAT_OPTIONS = [
|
|
26
|
+
{
|
|
27
|
+
value: "CSV_ZIP",
|
|
28
|
+
labelKey: "backup.exports.create.format_csv_zip",
|
|
29
|
+
hintKey: "backup.exports.create.format_csv_zip_hint",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
value: "SQL_DUMP_ZIP",
|
|
33
|
+
labelKey: "backup.exports.create.format_sql_dump_zip",
|
|
34
|
+
hintKey: "backup.exports.create.format_sql_dump_zip_hint",
|
|
35
|
+
},
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
const SCOPE_OPTIONS = [
|
|
39
|
+
{
|
|
40
|
+
value: "DOMAIN",
|
|
41
|
+
labelKey: "backup.exports.create.scope_domain",
|
|
42
|
+
hintKey: "backup.exports.create.scope_domain_hint",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
value: "RAW",
|
|
46
|
+
labelKey: "backup.exports.create.scope_raw",
|
|
47
|
+
hintKey: "backup.exports.create.scope_raw_hint",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: "CUSTOM",
|
|
51
|
+
labelKey: "backup.exports.create.scope_custom",
|
|
52
|
+
hintKey: "backup.exports.create.scope_custom_hint",
|
|
53
|
+
},
|
|
54
|
+
] as const;
|
|
55
|
+
|
|
56
|
+
export default function NewExportPage() {
|
|
57
|
+
return (
|
|
58
|
+
<RouteGuard permissions={["JOB_CREATE"]}>
|
|
59
|
+
<NewExportScreen />
|
|
60
|
+
</RouteGuard>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function NewExportScreen() {
|
|
65
|
+
const router = useRouter();
|
|
66
|
+
const { t } = useI18n();
|
|
67
|
+
|
|
68
|
+
const [format, setFormat] = useState<BackupFormat>("CSV_ZIP");
|
|
69
|
+
const [scope, setScope] = useState<ExportScope>("DOMAIN");
|
|
70
|
+
const [includeAttachments, setIncludeAttachments] = useState(false);
|
|
71
|
+
const [submitting, setSubmitting] = useState(false);
|
|
72
|
+
|
|
73
|
+
const formatRadioSource = FORMAT_OPTIONS.map((opt) => ({
|
|
74
|
+
label: t(opt.labelKey),
|
|
75
|
+
value: opt.value,
|
|
76
|
+
description: t(opt.hintKey),
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
const scopeRadioSource = SCOPE_OPTIONS.map((opt) => ({
|
|
80
|
+
label: t(opt.labelKey),
|
|
81
|
+
value: opt.value,
|
|
82
|
+
description: t(opt.hintKey),
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
async function handleSubmit() {
|
|
86
|
+
setSubmitting(true);
|
|
87
|
+
|
|
88
|
+
const request: ExportRequest = {
|
|
89
|
+
format,
|
|
90
|
+
scope,
|
|
91
|
+
includeAttachments,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
await backupService.createExportJob(request);
|
|
96
|
+
sgWhistle.success({ message: t("backup.exports.create.success") });
|
|
97
|
+
router.push(backupConfig.routes.exports);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
sgWhistle.error({
|
|
100
|
+
message:
|
|
101
|
+
extractBackupApiMessage(error) ?? t("backup.exports.create.error"),
|
|
102
|
+
});
|
|
103
|
+
} finally {
|
|
104
|
+
setSubmitting(false);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<PageFrame
|
|
110
|
+
title={t("backup.exports.create.title")}
|
|
111
|
+
description={t("backup.exports.create.description")}
|
|
112
|
+
actions={
|
|
113
|
+
<SgStack direction="row" gap={12}>
|
|
114
|
+
<SgButton
|
|
115
|
+
appearance="ghost"
|
|
116
|
+
severity="secondary"
|
|
117
|
+
label={t("backup.common.cancel")}
|
|
118
|
+
onClick={() => router.push(backupConfig.routes.exports)}
|
|
119
|
+
disabled={submitting}
|
|
120
|
+
/>
|
|
121
|
+
<SgButton
|
|
122
|
+
appearance="solid"
|
|
123
|
+
severity="primary"
|
|
124
|
+
shape="rounded"
|
|
125
|
+
label={t("backup.exports.create.submit")}
|
|
126
|
+
loading={submitting}
|
|
127
|
+
onClick={() => void handleSubmit()}
|
|
128
|
+
/>
|
|
129
|
+
</SgStack>
|
|
130
|
+
}
|
|
131
|
+
>
|
|
132
|
+
<div className="max-w-2xl">
|
|
133
|
+
<div className="rounded-lg border border-slate-200 bg-white p-6">
|
|
134
|
+
<SgStack gap={24}>
|
|
135
|
+
<SgRadioGroup
|
|
136
|
+
title={t("backup.exports.create.format_label")}
|
|
137
|
+
source={formatRadioSource}
|
|
138
|
+
value={format}
|
|
139
|
+
orientation="vertical"
|
|
140
|
+
selectionStyle="highlight"
|
|
141
|
+
onChange={(val) => setFormat(val as BackupFormat)}
|
|
142
|
+
/>
|
|
143
|
+
|
|
144
|
+
<hr className="border-slate-100" />
|
|
145
|
+
|
|
146
|
+
<SgRadioGroup
|
|
147
|
+
title={t("backup.exports.create.scope_label")}
|
|
148
|
+
source={scopeRadioSource}
|
|
149
|
+
value={scope}
|
|
150
|
+
orientation="vertical"
|
|
151
|
+
selectionStyle="highlight"
|
|
152
|
+
onChange={(val) => setScope(val as ExportScope)}
|
|
153
|
+
/>
|
|
154
|
+
|
|
155
|
+
<hr className="border-slate-100" />
|
|
156
|
+
|
|
157
|
+
<SgToggleSwitch
|
|
158
|
+
id="backup-include-attachments"
|
|
159
|
+
label={t("backup.exports.create.attachments_label")}
|
|
160
|
+
checked={includeAttachments}
|
|
161
|
+
onChange={setIncludeAttachments}
|
|
162
|
+
/>
|
|
163
|
+
</SgStack>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</PageFrame>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// seedgrid:managed
|
|
2
|
+
|
|
3
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { backupService } from "@/modules/backup/services/backup-service";
|
|
8
|
+
import type { ExportResponse } from "@/modules/backup/types";
|
|
9
|
+
|
|
10
|
+
vi.mock("next/navigation", () => ({
|
|
11
|
+
useRouter: () => ({ push: vi.fn() }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("@/modules/backup/services/backup-service", () => ({
|
|
15
|
+
backupService: {
|
|
16
|
+
listExportJobs: vi.fn(),
|
|
17
|
+
deleteExportJob: vi.fn(),
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("@/modules/security", () => ({
|
|
22
|
+
RouteGuard: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
23
|
+
PageFrame: ({ children, actions }: { children: React.ReactNode; actions?: React.ReactNode }) => (
|
|
24
|
+
<div>
|
|
25
|
+
{actions}
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock("@/i18n", () => ({
|
|
32
|
+
useI18n: () => ({ t: (key: string) => key }),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("@seedgrid/fe-components", () => ({
|
|
36
|
+
SgButton: ({ label, onClick, children, "aria-label": ariaLabel }: any) => (
|
|
37
|
+
<button onClick={onClick} aria-label={ariaLabel}>
|
|
38
|
+
{label ?? children}
|
|
39
|
+
</button>
|
|
40
|
+
),
|
|
41
|
+
// ExportStatusBadge (renderizado numa coluna do datatable) usa SgBadge; sem
|
|
42
|
+
// ele no mock, <SgBadge> fica undefined e o componente quebra no render.
|
|
43
|
+
SgBadge: ({ value }: any) => <span>{value}</span>,
|
|
44
|
+
SgConfirmationDialog: ({ open, onConfirm, onCancel }: any) =>
|
|
45
|
+
open ? (
|
|
46
|
+
<div>
|
|
47
|
+
<button onClick={onConfirm}>confirm</button>
|
|
48
|
+
<button onClick={onCancel}>cancel</button>
|
|
49
|
+
</div>
|
|
50
|
+
) : null,
|
|
51
|
+
SgDatatable: ({ value, columns, emptyMessage }: any) =>
|
|
52
|
+
value.length === 0 ? (
|
|
53
|
+
<div>{emptyMessage}</div>
|
|
54
|
+
) : (
|
|
55
|
+
<table>
|
|
56
|
+
<tbody>
|
|
57
|
+
{value.map((row: any, i: number) => (
|
|
58
|
+
<tr key={i}>
|
|
59
|
+
{columns.map((col: any, j: number) => (
|
|
60
|
+
<td key={j}>{col.body ? col.body(row) : row[col.field]}</td>
|
|
61
|
+
))}
|
|
62
|
+
</tr>
|
|
63
|
+
))}
|
|
64
|
+
</tbody>
|
|
65
|
+
</table>
|
|
66
|
+
),
|
|
67
|
+
SgStack: ({ children }: any) => <div>{children}</div>,
|
|
68
|
+
sgWhistle: { success: vi.fn(), error: vi.fn() },
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
const mockListExportJobs = vi.mocked(backupService.listExportJobs);
|
|
72
|
+
const mockDeleteExportJob = vi.mocked(backupService.deleteExportJob);
|
|
73
|
+
|
|
74
|
+
function makeJob(overrides: Partial<ExportResponse> = {}): ExportResponse {
|
|
75
|
+
return {
|
|
76
|
+
jobId: "job-abc-123",
|
|
77
|
+
status: "DONE",
|
|
78
|
+
format: "CSV_ZIP",
|
|
79
|
+
scope: "DOMAIN",
|
|
80
|
+
progress: 100,
|
|
81
|
+
downloadable: true,
|
|
82
|
+
createdAt: "2024-01-15T10:30:00",
|
|
83
|
+
...overrides,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("ExportsListPage", () => {
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
vi.clearAllMocks();
|
|
90
|
+
mockListExportJobs.mockResolvedValue([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("shows the empty message when there are no export jobs", async () => {
|
|
94
|
+
const { default: Page } = await import("./page");
|
|
95
|
+
render(<Page />);
|
|
96
|
+
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(
|
|
99
|
+
screen.getByText("backup.exports.list.empty")
|
|
100
|
+
).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("renders export rows when the service returns data", async () => {
|
|
105
|
+
const jobs = [
|
|
106
|
+
makeJob({ jobId: "job-abc-123", status: "DONE" }),
|
|
107
|
+
makeJob({ jobId: "job-def-456", status: "RUNNING" }),
|
|
108
|
+
];
|
|
109
|
+
mockListExportJobs.mockResolvedValue(jobs);
|
|
110
|
+
|
|
111
|
+
const { default: Page } = await import("./page");
|
|
112
|
+
render(<Page />);
|
|
113
|
+
|
|
114
|
+
await waitFor(() => {
|
|
115
|
+
expect(screen.getByText(/job-abc/)).toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("calls deleteExportJob when the confirmation dialog is confirmed", async () => {
|
|
120
|
+
const job = makeJob({ jobId: "job-to-delete" });
|
|
121
|
+
mockListExportJobs.mockResolvedValue([job]);
|
|
122
|
+
mockDeleteExportJob.mockResolvedValue(undefined);
|
|
123
|
+
|
|
124
|
+
const { default: Page } = await import("./page");
|
|
125
|
+
render(<Page />);
|
|
126
|
+
|
|
127
|
+
await waitFor(() =>
|
|
128
|
+
expect(screen.getByText(/job-to-d/)).toBeInTheDocument()
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const deleteBtn = screen.getByRole("button", { name: "backup.exports.list.action.delete" });
|
|
132
|
+
await userEvent.click(deleteBtn);
|
|
133
|
+
|
|
134
|
+
const confirmBtn = screen.getByText("confirm");
|
|
135
|
+
await userEvent.click(confirmBtn);
|
|
136
|
+
|
|
137
|
+
await waitFor(() =>
|
|
138
|
+
expect(mockDeleteExportJob).toHaveBeenCalledWith("job-to-delete")
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("closes the confirmation dialog when cancelled", async () => {
|
|
143
|
+
const job = makeJob({ jobId: "job-cancel-test" });
|
|
144
|
+
mockListExportJobs.mockResolvedValue([job]);
|
|
145
|
+
|
|
146
|
+
const { default: Page } = await import("./page");
|
|
147
|
+
render(<Page />);
|
|
148
|
+
|
|
149
|
+
// A coluna do jobId exibe row.jobId.slice(0, 8) — "job-cancel-test" vira
|
|
150
|
+
// "job-canc" (8 chars). O regex tem que casar a forma truncada.
|
|
151
|
+
await waitFor(() =>
|
|
152
|
+
expect(screen.getByText(/job-canc/)).toBeInTheDocument()
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const deleteBtn = screen.getByRole("button", { name: "backup.exports.list.action.delete" });
|
|
156
|
+
await userEvent.click(deleteBtn);
|
|
157
|
+
|
|
158
|
+
expect(screen.getByText("confirm")).toBeInTheDocument();
|
|
159
|
+
|
|
160
|
+
const cancelBtn = screen.getByText("cancel");
|
|
161
|
+
await userEvent.click(cancelBtn);
|
|
162
|
+
|
|
163
|
+
expect(screen.queryByText("confirm")).not.toBeInTheDocument();
|
|
164
|
+
expect(mockDeleteExportJob).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
});
|