252 lines
6.8 KiB
JavaScript
252 lines
6.8 KiB
JavaScript
|
|
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();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|