diff --git a/src/components/questions/QuestionsTable.test.tsx b/src/components/questions/QuestionsTable.test.tsx
index 2137e31c..661b20bd 100644
--- a/src/components/questions/QuestionsTable.test.tsx
+++ b/src/components/questions/QuestionsTable.test.tsx
@@ -158,6 +158,57 @@ describe("QuestionsTable analytics", () => {
});
});
+ it("exports valid JSON that round-trips through import", async () => {
+ localStorage.setItem("leetcode-patterns-completed", JSON.stringify([2, 0]));
+ localStorage.setItem("leetcode-patterns-starred", JSON.stringify([1]));
+ localStorage.setItem("leetcode-patterns-notes", JSON.stringify({ 0: "my note" }));
+ const user = userEvent.setup();
+ render();
+ await user.click(screen.getByRole("button", { name: /Export/ }));
+
+ const blob = vi.mocked(URL.createObjectURL).mock.calls[0][0] as Blob;
+ const text = await blob.text();
+ const exported = JSON.parse(text);
+ expect(exported).toMatchObject({ completed: [0, 2], starred: [1], notes: { 0: "my note" } });
+
+ localStorage.clear();
+ const importContainer = screen.getByRole("button", { name: /Import/ }).closest("div")!;
+ const fileInput = importContainer.querySelector("input[type='file']")! as HTMLInputElement;
+ await user.upload(fileInput, new File([text], "progress.json", { type: "application/json" }));
+ await waitFor(() => {
+ expect(JSON.parse(localStorage.getItem("leetcode-patterns-completed")!)).toEqual([0, 2]);
+ expect(JSON.parse(localStorage.getItem("leetcode-patterns-starred")!)).toEqual([1]);
+ expect(JSON.parse(localStorage.getItem("leetcode-patterns-notes")!)).toEqual({ 0: "my note" });
+ });
+ });
+
+ it("exports completed and starred sorted numerically regardless of completion order", async () => {
+ const user = userEvent.setup();
+ render();
+ const checkboxCell = (i: number) =>
+ screen.getAllByRole("checkbox", { name: /^Mark .+ as (complete|incomplete)$/ })[i].closest("td")!;
+ await user.click(checkboxCell(2));
+ await user.click(checkboxCell(0));
+ await waitFor(() => {
+ expect(JSON.parse(localStorage.getItem("leetcode-patterns-completed")!)).toHaveLength(2);
+ });
+ await user.click(screen.getByRole("button", { name: /Export/ }));
+
+ const blob = vi.mocked(URL.createObjectURL).mock.calls[0][0] as Blob;
+ const exported = JSON.parse(await blob.text());
+ expect(exported.completed).toEqual([0, 2]);
+ });
+
+ it("exports with a trailing newline", async () => {
+ const user = userEvent.setup();
+ render();
+ await user.click(screen.getByRole("button", { name: /Export/ }));
+
+ const blob = vi.mocked(URL.createObjectURL).mock.calls[0][0] as Blob;
+ const text = await blob.text();
+ expect(text.endsWith("}\n")).toBe(true);
+ });
+
it("tracks hide_completed when toggling", async () => {
const user = userEvent.setup();
render();