// This program has been developed by students from the bachelor Computer Science at Utrecht // University within the Software Project course. // © Copyright Utrecht University (Department of Information and Computing Sciences) // SaveLoadPanel.all.test.tsx import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx'; import SaveLoadPanel from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.tsx'; import { makeProjectBlob } from '../../../../../src/utils/SaveLoad.ts'; import { mockReactFlow } from "../../../../setupFlowTests.ts"; // optional helper if present // helper to read Blob contents in tests (works in Node/Jest env) async function blobToText(blob: Blob): Promise { if (typeof (blob as any).text === "function") return await (blob as any).text(); if (typeof (blob as any).arrayBuffer === "function") { const buf = await (blob as any).arrayBuffer(); return new TextDecoder().decode(buf); } return await new Promise((resolve, reject) => { const fr = new FileReader(); fr.onload = () => resolve(String(fr.result)); fr.onerror = () => reject(fr.error); fr.readAsText(blob); }); } beforeAll(() => { // if you have a mockReactFlow helper used in other tests, call it if (typeof mockReactFlow === "function") mockReactFlow(); }); beforeEach(() => { // clear and seed the zustand store to a known empty state act(() => { const { setNodes, setEdges } = useFlowStore.getState(); setNodes([]); setEdges([]); }); // Ensure URL.createObjectURL exists so jest.spyOn works if (!URL.createObjectURL) URL.createObjectURL = jest.fn(); }); afterEach(() => { jest.restoreAllMocks(); }); describe("SaveLoadPanel - combined tests", () => { test("makeProjectBlob creates a valid JSON blob", async () => { const nodes = [ { id: "n1", type: "start", position: { x: 0, y: 0 }, data: { label: "Start" }, } as any, ]; const edges: any[] = []; const blob = makeProjectBlob("my-project", nodes, edges); expect(blob).toBeInstanceOf(Blob); const text = await blobToText(blob); const parsed = JSON.parse(text); expect(parsed.name).toBe("my-project"); expect(typeof parsed.savedAt).toBe("string"); expect(Array.isArray(parsed.nodes)).toBe(true); expect(Array.isArray(parsed.edges)).toBe(true); expect(parsed.nodes).toEqual(nodes); expect(parsed.edges).toEqual(edges); }); test("onSave creates a blob URL and sets anchor href", async () => { // Seed the store so onSave has nodes to save act(() => { useFlowStore.getState().setNodes([ { id: "start", type: "start", position: { x: 0, y: 0 }, data: { label: "start" } } as any, ]); useFlowStore.getState().setEdges([]); }); // Ensure createObjectURL exists and spy it if (!URL.createObjectURL) URL.createObjectURL = jest.fn(); const createObjectURLSpy = jest.spyOn(URL, "createObjectURL").mockReturnValue("blob:fake-url"); render(); const saveAnchor = screen.getByText(/Save Graph/i) as HTMLAnchorElement; await act(async () => { fireEvent.click(saveAnchor); }); expect(createObjectURLSpy).toHaveBeenCalledTimes(1); const blobArg = createObjectURLSpy.mock.calls[0][0]; expect(blobArg).toBeInstanceOf(Blob); expect(saveAnchor.getAttribute("href")).toBe("blob:fake-url"); const text = await blobToText(blobArg as Blob); const parsed = JSON.parse(text); expect(parsed.name).toBeDefined(); expect(parsed.nodes).toBeDefined(); expect(parsed.edges).toBeDefined(); createObjectURLSpy.mockRestore(); }); test("onLoad with invalid JSON does not update store", async () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const file = new File(["not json"], "bad.json", { type: "application/json" }); file.text = jest.fn(() => Promise.resolve(`{"bad json`)); window.alert = jest.fn(); render(); const input = document.querySelector('input[type="file"]') as HTMLInputElement; act(() => { fireEvent.change(input, { target: { files: [file] } }); }); await waitFor(() => { expect(window.alert).toHaveBeenCalledTimes(1); const nodesAfter = useFlowStore.getState().nodes; expect(nodesAfter).toHaveLength(0); }); // Clean up the spy consoleSpy.mockRestore(); }); test("onLoad resolves to null when no file is chosen (user cancels) and does not update store", async () => { render(); const input = document.querySelector('input[type="file"]') as HTMLInputElement; expect(input).toBeTruthy(); // Click Load to set resolver const loadButton = screen.getByLabelText(/load graph/i); await act(async () => { fireEvent.click(loadButton); // simulate user cancelling: change with empty files fireEvent.change(input, { target: { files: [] } }); await Promise.resolve(); }); await waitFor(() => { const nodesAfter = useFlowStore.getState().nodes; const edgesAfter = useFlowStore.getState().edges; expect(nodesAfter).toHaveLength(0); expect(edgesAfter).toHaveLength(0); expect(input.value).toBe(""); }); }); });