import { expect } from "@esm-bundle/chai"; import sinon from "sinon"; import { narrativeManager, NarrativeManager } from "../../src/managers/NarrativeManager.js"; describe("Manager: NarrativeManager", () => { let manager; beforeEach(() => { // Create a fresh instance for testing manager = new NarrativeManager(); }); it("CoA 1: Should initialize with empty state", () => { expect(manager.currentSequence).to.be.null; expect(manager.currentNode).to.be.null; expect(manager.history).to.be.instanceof(Set); expect(manager.history.size).to.equal(0); }); it("CoA 2: startSequence should load sequence and set first node", () => { const sequenceData = { id: "TEST_SEQUENCE", nodes: [ { id: "1", type: "DIALOGUE", text: "Hello", next: "2" }, { id: "2", type: "DIALOGUE", text: "World", next: "END" }, ], }; const updateSpy = sinon.spy(manager, "broadcastUpdate"); manager.startSequence(sequenceData); expect(manager.currentSequence).to.equal(sequenceData); expect(manager.currentNode.id).to.equal("1"); expect(manager.history.has("TEST_SEQUENCE")).to.be.true; expect(updateSpy.called).to.be.true; }); it("CoA 3: startSequence should handle invalid data gracefully", () => { const consoleError = sinon.stub(console, "error"); manager.startSequence(null); expect(manager.currentSequence).to.be.null; manager.startSequence({}); expect(manager.currentSequence).to.be.null; consoleError.restore(); }); it("CoA 4: next should advance to next node in sequence", () => { const sequenceData = { id: "TEST_SEQUENCE", nodes: [ { id: "1", type: "DIALOGUE", text: "First", next: "2" }, { id: "2", type: "DIALOGUE", text: "Second", next: "END" }, ], }; manager.startSequence(sequenceData); expect(manager.currentNode.id).to.equal("1"); manager.next(); expect(manager.currentNode.id).to.equal("2"); }); it("CoA 5: next should end sequence when reaching END", () => { const endSpy = sinon.spy(manager, "endSequence"); const sequenceData = { id: "TEST_SEQUENCE", nodes: [{ id: "1", type: "DIALOGUE", text: "Last", next: "END" }], }; manager.startSequence(sequenceData); manager.next(); expect(endSpy.called).to.be.true; expect(manager.currentSequence).to.be.null; }); it("CoA 6: next should not advance on CHOICE nodes", () => { const consoleWarn = sinon.stub(console, "warn"); const sequenceData = { id: "TEST_SEQUENCE", nodes: [ { id: "1", type: "CHOICE", text: "Choose", choices: [{ text: "Option 1", next: "2" }], }, ], }; manager.startSequence(sequenceData); manager.next(); expect(consoleWarn.called).to.be.true; expect(manager.currentNode.id).to.equal("1"); consoleWarn.restore(); }); it("CoA 7: makeChoice should advance based on choice selection", () => { const sequenceData = { id: "TEST_SEQUENCE", nodes: [ { id: "1", type: "CHOICE", choices: [ { text: "Option A", next: "2" }, { text: "Option B", next: "3" }, ], }, { id: "2", type: "DIALOGUE", text: "Result A", next: "END" }, { id: "3", type: "DIALOGUE", text: "Result B", next: "END" }, ], }; manager.startSequence(sequenceData); manager.makeChoice(1); // Select Option B expect(manager.currentNode.id).to.equal("3"); }); it("CoA 8: makeChoice should dispatch trigger events", () => { const triggerSpy = sinon.spy(); manager.addEventListener("narrative-trigger", triggerSpy); const sequenceData = { id: "TEST_SEQUENCE", nodes: [ { id: "1", type: "CHOICE", choices: [ { text: "Option", next: "2", trigger: { action: "GAIN_REPUTATION", value: 10 }, }, ], }, { id: "2", type: "DIALOGUE", next: "END" }, ], }; manager.startSequence(sequenceData); manager.makeChoice(0); expect(triggerSpy.called).to.be.true; expect(triggerSpy.firstCall.args[0].detail.action).to.deep.equal({ action: "GAIN_REPUTATION", value: 10, }); }); it("CoA 9: _advanceToNode should process node triggers", () => { const triggerSpy = sinon.spy(); manager.addEventListener("narrative-trigger", triggerSpy); const sequenceData = { id: "TEST_SEQUENCE", nodes: [ { id: "1", type: "DIALOGUE", next: "2" }, { id: "2", type: "DIALOGUE", next: "END", trigger: { action: "UNLOCK_MISSION" }, }, ], }; manager.startSequence(sequenceData); manager.next(); expect(triggerSpy.called).to.be.true; }); it("CoA 10: _advanceToNode should auto-advance ACTION nodes", () => { const sequenceData = { id: "TEST_SEQUENCE", nodes: [ { id: "1", type: "DIALOGUE", next: "2" }, { id: "2", type: "ACTION", next: "3", trigger: { action: "DO_SOMETHING" } }, { id: "3", type: "DIALOGUE", text: "After action", next: "END" }, ], }; manager.startSequence(sequenceData); manager.next(); // Should skip ACTION node and go to 3 expect(manager.currentNode.id).to.equal("3"); }); it("CoA 11: endSequence should dispatch narrative-end event", () => { const endSpy = sinon.spy(); manager.addEventListener("narrative-end", endSpy); const sequenceData = { id: "TEST_SEQUENCE", nodes: [{ id: "1", type: "DIALOGUE", next: "END" }], }; manager.startSequence(sequenceData); manager.endSequence(); expect(endSpy.called).to.be.true; expect(manager.currentSequence).to.be.null; expect(manager.currentNode).to.be.null; }); it("CoA 12: broadcastUpdate should dispatch narrative-update event", () => { const updateSpy = sinon.spy(); manager.addEventListener("narrative-update", updateSpy); const sequenceData = { id: "TEST_SEQUENCE", nodes: [{ id: "1", type: "DIALOGUE", text: "Test", next: "END" }], }; manager.startSequence(sequenceData); expect(updateSpy.called).to.be.true; const eventDetail = updateSpy.firstCall.args[0].detail; expect(eventDetail.node).to.equal(manager.currentNode); expect(eventDetail.active).to.be.true; }); it("CoA 13: _advanceToNode should handle missing nodes gracefully", () => { const consoleError = sinon.stub(console, "error"); const endSpy = sinon.spy(manager, "endSequence"); const sequenceData = { id: "TEST_SEQUENCE", nodes: [{ id: "1", type: "DIALOGUE", next: "MISSING_NODE" }], }; manager.startSequence(sequenceData); manager.next(); expect(consoleError.called).to.be.true; expect(endSpy.called).to.be.true; consoleError.restore(); }); });