Skip to content

Commit d015b07

Browse files
committed
T3C-55 json download error
1 parent f2fe650 commit d015b07

File tree

4 files changed

+283
-91
lines changed

4 files changed

+283
-91
lines changed

next-client/src/app/api/report/download/[uri]/route.ts

Lines changed: 0 additions & 64 deletions
This file was deleted.

next-client/src/components/report/Report.tsx

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
OutlineStateAction,
4141
useOutlineState,
4242
} from "../outline/hooks/useOutlineState";
43+
import { downloadReportData } from "@/lib/report/downloadUtils";
4344

4445
const ToolBarFrame = ({
4546
children,
@@ -265,7 +266,7 @@ function Report({
265266
{state.children.map((themeNode) => (
266267
<Theme key={themeNode.data.id} node={themeNode} />
267268
))}
268-
<Appendix filename={reportData.title} reportUri={reportUri} />
269+
<Appendix filename={reportData.title} reportData={reportData} />
269270
</Col>
270271
}
271272
ToolBar={
@@ -566,37 +567,15 @@ export function ReportOverview({ topics }: { topics: schema.Topic[] }) {
566567
}
567568

568569
function Appendix({
569-
reportUri,
570+
reportData,
570571
filename,
571572
}: {
572-
reportUri: string;
573+
reportData: schema.UIReportData;
573574
filename: string;
574575
}) {
575-
const handleDownload = async () => {
576-
console.log(reportUri);
576+
const handleDownload = () => {
577577
try {
578-
const fetchUrl = `/api/report/download/${encodeURIComponent(reportUri)}`;
579-
const response = await fetch(fetchUrl, {
580-
headers: {
581-
"Content-Type": "application/json",
582-
},
583-
});
584-
if (!response.ok) {
585-
toast.error("An error occured: could not download report data");
586-
return;
587-
}
588-
const blob = await response.blob();
589-
const downloadUrl = window.URL.createObjectURL(blob);
590-
591-
const link = document.createElement("a");
592-
link.href = downloadUrl;
593-
link.download = filename + "-" + Date.now();
594-
595-
document.body.appendChild(link);
596-
link.click();
597-
document.body.removeChild(link);
598-
599-
window.URL.revokeObjectURL(downloadUrl);
578+
downloadReportData(reportData, filename);
600579
} catch (error) {
601580
toast.error(
602581
`Failed to download report data: ${(error as Error).message}`,
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2+
import { downloadReportData } from "./downloadUtils";
3+
import * as schema from "tttc-common/schema";
4+
5+
// Mock DOM APIs
6+
const mockCreateElement = vi.fn();
7+
const mockClick = vi.fn();
8+
const mockAppendChild = vi.fn();
9+
const mockRemoveChild = vi.fn();
10+
const mockCreateObjectURL = vi.fn();
11+
const mockRevokeObjectURL = vi.fn();
12+
13+
beforeEach(() => {
14+
// Reset all mocks
15+
vi.clearAllMocks();
16+
17+
// Mock document methods
18+
const mockLink = {
19+
href: "",
20+
download: "",
21+
click: mockClick,
22+
};
23+
24+
mockCreateElement.mockReturnValue(mockLink);
25+
mockCreateObjectURL.mockReturnValue("blob:mock-url");
26+
27+
// Mock global objects
28+
Object.defineProperty(global, "document", {
29+
value: {
30+
createElement: mockCreateElement,
31+
body: {
32+
appendChild: mockAppendChild,
33+
removeChild: mockRemoveChild,
34+
},
35+
},
36+
writable: true,
37+
});
38+
39+
Object.defineProperty(global, "window", {
40+
value: {
41+
URL: {
42+
createObjectURL: mockCreateObjectURL,
43+
revokeObjectURL: mockRevokeObjectURL,
44+
},
45+
},
46+
writable: true,
47+
});
48+
49+
Object.defineProperty(global, "Blob", {
50+
value: vi.fn((content, options) => ({
51+
content,
52+
type: options?.type,
53+
})),
54+
writable: true,
55+
});
56+
});
57+
58+
afterEach(() => {
59+
vi.restoreAllMocks();
60+
});
61+
62+
describe("downloadReportData", () => {
63+
const mockReportData: schema.UIReportData = {
64+
title: "Test Report",
65+
description: "A test report",
66+
date: "2025-01-01",
67+
topics: [],
68+
questionAnswers: [],
69+
};
70+
71+
it("should create a download link with correct filename and timestamp", () => {
72+
const filename = "test-report";
73+
const mockTimestamp = 1234567890;
74+
75+
vi.spyOn(Date, "now").mockReturnValue(mockTimestamp);
76+
77+
downloadReportData(mockReportData, filename);
78+
79+
expect(mockCreateElement).toHaveBeenCalledWith("a");
80+
81+
const mockLink = mockCreateElement.mock.results[0].value;
82+
expect(mockLink.download).toBe(`${filename}-${mockTimestamp}.json`);
83+
expect(mockLink.href).toBe("blob:mock-url");
84+
});
85+
86+
it("should create a blob with correct JSON content and type", () => {
87+
const filename = "test-report";
88+
const mockTimestamp = 1234567890;
89+
90+
vi.spyOn(Date, "now").mockReturnValue(mockTimestamp);
91+
92+
downloadReportData(mockReportData, filename);
93+
94+
expect(global.Blob).toHaveBeenCalledWith(
95+
[expect.stringContaining('"title": "Test Report"')],
96+
{ type: "application/json" },
97+
);
98+
99+
// Check that the blob content includes the proper download schema structure
100+
const blobCall = (global.Blob as any).mock.calls[0];
101+
const jsonContent = blobCall[0][0];
102+
const parsedContent = JSON.parse(jsonContent);
103+
104+
expect(parsedContent).toEqual([
105+
"v0.2",
106+
{
107+
data: ["v0.2", mockReportData],
108+
downloadTimestamp: mockTimestamp,
109+
},
110+
]);
111+
});
112+
113+
it("should create object URL from blob and set it as link href", () => {
114+
const filename = "test-report";
115+
116+
downloadReportData(mockReportData, filename);
117+
118+
expect(mockCreateObjectURL).toHaveBeenCalledWith(
119+
expect.objectContaining({
120+
type: "application/json",
121+
}),
122+
);
123+
124+
const mockLink = mockCreateElement.mock.results[0].value;
125+
expect(mockLink.href).toBe("blob:mock-url");
126+
});
127+
128+
it("should append link to document body, click it, then remove it", () => {
129+
const filename = "test-report";
130+
131+
downloadReportData(mockReportData, filename);
132+
133+
const mockLink = mockCreateElement.mock.results[0].value;
134+
135+
expect(mockAppendChild).toHaveBeenCalledWith(mockLink);
136+
expect(mockClick).toHaveBeenCalledOnce();
137+
expect(mockRemoveChild).toHaveBeenCalledWith(mockLink);
138+
});
139+
140+
it("should clean up object URL after download", () => {
141+
const filename = "test-report";
142+
143+
downloadReportData(mockReportData, filename);
144+
145+
expect(mockRevokeObjectURL).toHaveBeenCalledWith("blob:mock-url");
146+
});
147+
148+
it("should format JSON with proper indentation", () => {
149+
const filename = "test-report";
150+
151+
downloadReportData(mockReportData, filename);
152+
153+
const blobCall = (global.Blob as any).mock.calls[0];
154+
const jsonContent = blobCall[0][0];
155+
156+
// Check that JSON is formatted with 2-space indentation
157+
expect(jsonContent).toContain('[\n "v0.2",');
158+
expect(jsonContent).toContain(' "data": [');
159+
expect(jsonContent).toContain("\n]");
160+
});
161+
162+
it("should handle complex report data correctly", () => {
163+
const complexReportData: schema.UIReportData = {
164+
title: "Complex Report",
165+
description: "A complex test report with special characters & symbols",
166+
date: "2025-01-01",
167+
topics: [
168+
{
169+
id: "topic-1",
170+
topicName: "Topic 1",
171+
topicShortDescription: "Short description",
172+
subtopics: [],
173+
},
174+
],
175+
questionAnswers: [
176+
{
177+
question: "What is the purpose?",
178+
answer: "Testing complex data",
179+
},
180+
],
181+
};
182+
183+
const filename = "complex-report";
184+
185+
expect(() => downloadReportData(complexReportData, filename)).not.toThrow();
186+
187+
const blobCall = (global.Blob as any).mock.calls[0];
188+
const jsonContent = blobCall[0][0];
189+
const parsedContent = JSON.parse(jsonContent);
190+
191+
expect(parsedContent[1].data[1]).toEqual(complexReportData);
192+
});
193+
194+
it("should throw error with descriptive message on failure", () => {
195+
const filename = "test-report";
196+
197+
// Force JSON.stringify to throw an error
198+
const circularObj = {} as any;
199+
circularObj.self = circularObj;
200+
const badReportData = circularObj as schema.UIReportData;
201+
202+
expect(() => downloadReportData(badReportData, filename)).toThrow(
203+
/Failed to download report data:/,
204+
);
205+
});
206+
207+
it("should handle empty filename gracefully", () => {
208+
const filename = "";
209+
const mockTimestamp = 1234567890;
210+
211+
vi.spyOn(Date, "now").mockReturnValue(mockTimestamp);
212+
213+
downloadReportData(mockReportData, filename);
214+
215+
const mockLink = mockCreateElement.mock.results[0].value;
216+
expect(mockLink.download).toBe(`-${mockTimestamp}.json`);
217+
});
218+
219+
it("should handle filenames with special characters", () => {
220+
const filename = "report-with-special_chars@123";
221+
const mockTimestamp = 1234567890;
222+
223+
vi.spyOn(Date, "now").mockReturnValue(mockTimestamp);
224+
225+
downloadReportData(mockReportData, filename);
226+
227+
const mockLink = mockCreateElement.mock.results[0].value;
228+
expect(mockLink.download).toBe(`${filename}-${mockTimestamp}.json`);
229+
});
230+
});

0 commit comments

Comments
 (0)