import { expect } from "@esm-bundle/chai"; import sinon from "sinon"; import { Persistence } from "../../src/core/Persistence.js"; describe("Core: Persistence", () => { let persistence; let mockDB; let mockStore; let mockTransaction; let mockRequest; let globalObj; beforeEach(() => { persistence = new Persistence(); // Mock IndexedDB mockStore = { put: sinon.stub(), get: sinon.stub(), delete: sinon.stub(), }; mockTransaction = { objectStore: sinon.stub().returns(mockStore), }; mockDB = { objectStoreNames: { contains: sinon.stub().returns(false), }, createObjectStore: sinon.stub(), transaction: sinon.stub().returns(mockTransaction), }; // Use window or self for browser environment globalObj = typeof window !== "undefined" ? window : typeof self !== "undefined" ? self : globalThis; }); const createMockRequest = () => { // Mock indexedDB.open - create a new request each time mockRequest = { onerror: null, onsuccess: null, onupgradeneeded: null, result: mockDB, }; // Mock indexedDB using defineProperty since it's read-only Object.defineProperty(globalObj, "indexedDB", { value: { open: sinon.stub().returns(mockRequest), }, writable: true, configurable: true, }); return mockRequest; }; it("CoA 1: init should create database and object stores", async () => { const request = createMockRequest(); // Start init, which will call indexedDB.open const initPromise = persistence.init(); // Trigger upgrade first (happens synchronously during open) if (request.onupgradeneeded) { request.onupgradeneeded({ target: { result: mockDB } }); } // Then trigger success if (request.onsuccess) { request.onsuccess({ target: { result: mockDB } }); } await initPromise; expect(globalObj.indexedDB.open.calledWith("AetherShardsDB", 6)).to.be.true; expect(mockDB.createObjectStore.calledWith("Runs", { keyPath: "id" })).to.be .true; expect(mockDB.createObjectStore.calledWith("Roster", { keyPath: "id" })).to .be.true; expect(mockDB.createObjectStore.calledWith("Campaign", { keyPath: "id" })) .to.be.true; expect(persistence.db).to.equal(mockDB); }); it("CoA 2: saveRun should store run data with active_run id", async () => { persistence.db = mockDB; const runData = { seed: 12345, depth: 5, squad: [] }; const mockPutRequest = { onsuccess: null, onerror: null, }; mockStore.put.returns(mockPutRequest); const savePromise = persistence.saveRun(runData); mockPutRequest.onsuccess(); await savePromise; expect(mockDB.transaction.calledWith(["Runs"], "readwrite")).to.be.true; expect(mockStore.put.calledOnce).to.be.true; const savedData = mockStore.put.firstCall.args[0]; expect(savedData.id).to.equal("active_run"); expect(savedData.seed).to.equal(12345); }); it("CoA 3: loadRun should retrieve active_run data", async () => { persistence.db = mockDB; const savedData = { id: "active_run", seed: 12345, depth: 5 }; const mockGetRequest = { onsuccess: null, onerror: null, result: savedData, }; mockStore.get.returns(mockGetRequest); const loadPromise = persistence.loadRun(); mockGetRequest.onsuccess(); const result = await loadPromise; expect(mockDB.transaction.calledWith(["Runs"], "readonly")).to.be.true; expect(mockStore.get.calledWith("active_run")).to.be.true; expect(result).to.deep.equal(savedData); }); it("CoA 4: clearRun should delete active_run", async () => { persistence.db = mockDB; const mockDeleteRequest = { onsuccess: null, onerror: null, }; mockStore.delete.returns(mockDeleteRequest); const deletePromise = persistence.clearRun(); mockDeleteRequest.onsuccess(); await deletePromise; expect(mockDB.transaction.calledWith(["Runs"], "readwrite")).to.be.true; expect(mockStore.delete.calledWith("active_run")).to.be.true; }); it("CoA 5: saveRoster should wrap roster data with id", async () => { persistence.db = mockDB; const rosterData = { roster: [], graveyard: [] }; const mockPutRequest = { onsuccess: null, onerror: null, }; mockStore.put.returns(mockPutRequest); const savePromise = persistence.saveRoster(rosterData); mockPutRequest.onsuccess(); await savePromise; expect(mockDB.transaction.calledWith(["Roster"], "readwrite")).to.be.true; expect(mockStore.put.calledOnce).to.be.true; const savedData = mockStore.put.firstCall.args[0]; expect(savedData.id).to.equal("player_roster"); expect(savedData.data).to.deep.equal(rosterData); }); it("CoA 6: loadRoster should extract data from stored object", async () => { persistence.db = mockDB; const storedData = { id: "player_roster", data: { roster: [], graveyard: [] }, }; const mockGetRequest = { onsuccess: null, onerror: null, result: storedData, }; mockStore.get.returns(mockGetRequest); const loadPromise = persistence.loadRoster(); mockGetRequest.onsuccess(); const result = await loadPromise; expect(mockDB.transaction.calledWith(["Roster"], "readonly")).to.be.true; expect(mockStore.get.calledWith("player_roster")).to.be.true; expect(result).to.deep.equal(storedData.data); }); it("CoA 7: loadRoster should return null if no data exists", async () => { persistence.db = mockDB; const mockGetRequest = { onsuccess: null, onerror: null, result: undefined, }; mockStore.get.returns(mockGetRequest); const loadPromise = persistence.loadRoster(); mockGetRequest.onsuccess(); const result = await loadPromise; expect(result).to.be.null; }); it("CoA 8: saveRun should auto-init if db not initialized", async () => { const request = createMockRequest(); const runData = { seed: 12345 }; const mockPutRequest = { onsuccess: sinon.stub(), onerror: null, }; mockStore.put.returns(mockPutRequest); // Start saveRun, which will trigger init const savePromise = persistence.saveRun(runData); // Trigger upgrade and success for init if (request.onupgradeneeded) { request.onupgradeneeded({ target: { result: mockDB } }); } if (request.onsuccess) { request.onsuccess({ target: { result: mockDB } }); } // Wait a bit for init to complete, then trigger put success await new Promise((resolve) => setTimeout(resolve, 10)); mockPutRequest.onsuccess(); await savePromise; expect(persistence.db).to.equal(mockDB); expect(mockStore.put.calledOnce).to.be.true; }); it("CoA 9: saveCampaign should store campaign data with campaign_data id", async () => { persistence.db = mockDB; const campaignData = { completedMissions: ["MISSION_TUTORIAL_01", "MISSION_TEST_01"], }; const mockPutRequest = { onsuccess: null, onerror: null, }; mockStore.put.returns(mockPutRequest); const savePromise = persistence.saveCampaign(campaignData); mockPutRequest.onsuccess(); await savePromise; expect(mockDB.transaction.calledWith(["Campaign"], "readwrite")).to.be.true; expect(mockStore.put.calledOnce).to.be.true; const savedData = mockStore.put.firstCall.args[0]; expect(savedData.id).to.equal("campaign_data"); expect(savedData.data).to.deep.equal(campaignData); }); it("CoA 10: loadCampaign should extract data from stored object", async () => { persistence.db = mockDB; const storedData = { id: "campaign_data", data: { completedMissions: ["MISSION_TUTORIAL_01", "MISSION_TEST_01"], }, }; const mockGetRequest = { onsuccess: null, onerror: null, result: storedData, }; mockStore.get.returns(mockGetRequest); const loadPromise = persistence.loadCampaign(); mockGetRequest.onsuccess(); const result = await loadPromise; expect(mockDB.transaction.calledWith(["Campaign"], "readonly")).to.be.true; expect(mockStore.get.calledWith("campaign_data")).to.be.true; expect(result).to.deep.equal(storedData.data); }); it("CoA 11: loadCampaign should return null if no data exists", async () => { persistence.db = mockDB; const mockGetRequest = { onsuccess: null, onerror: null, result: undefined, }; mockStore.get.returns(mockGetRequest); const loadPromise = persistence.loadCampaign(); mockGetRequest.onsuccess(); const result = await loadPromise; expect(result).to.be.null; }); });