frontispiece / packages / ink-processor / src / data / visible-state.spec.ts
visible-state.spec.ts
Raw
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

import { describe, it, expect } from "vitest"
import { advance, createEmptyVisualInkState, getActiveSection, hideAllVisibleSections, hideOlderLines, makeChoice } from "./visible-state"
import { Compiler } from "inkjs"
import storyText from "../test/test-story.ink?raw"
import { isSectionComplete } from "./story-section"
import { InkStory } from "../inkTypes"

describe("advance", () => {
	it("should advance the story normally", () => {
		const story = new Compiler(storyText).Compile()
		const visualState = createEmptyVisualInkState()
		const newVisualInkState = advance(story, visualState)
		expect(newVisualInkState !== visualState).toBe(true)
		expect(newVisualInkState.sections !== visualState.sections).toBe(true)
		expect(newVisualInkState.sections.length).toEqual(1)
		expect(newVisualInkState.sections[0].activeLine).toEqual(0)
		const secondVisualInkState = advance(story, newVisualInkState)
		expect(newVisualInkState !== secondVisualInkState).toBe(true)
		expect(secondVisualInkState.sections !== newVisualInkState.sections).toBe(true)
		expect(secondVisualInkState.sections[0].lines).toBe(newVisualInkState.sections[0].lines)
		expect(secondVisualInkState.sections[0].activeLine).toEqual(1)
	})

	it("should not advance the story if we've already advanced too far", () => {
		const story = new Compiler(storyText).Compile()
		let visualState = createEmptyVisualInkState()
		while (!isSectionComplete(getActiveSection(visualState))) {
			visualState = advance(story, visualState)
		}
		const newVisualState = advance(story, visualState)
		expect(visualState).toBe(newVisualState)
		expect(newVisualState.sections.length).toEqual(1)
	})
})

describe("makeChoice", () => {
	it("should handle the case where you make a choice before seeing the whole section", () => {
		const story = new Compiler(storyText).Compile()
		const emptyState = createEmptyVisualInkState()

		// make choice should throw an error.
		expect(() => makeChoice(story, emptyState, 0)).toThrowError("choice out of range")

		const newVisualState = advance(story, emptyState)
		expect(newVisualState.sections.length).toBe(1)
		expect(newVisualState.sections[0].activeLine).toBe(0)
	})

	it("should make choices once you've advanced all the way", () => {
		const story = new Compiler(storyText).Compile()
		let visualState = createEmptyVisualInkState()
		while (!isSectionComplete(getActiveSection(visualState))) {
			visualState = advance(story, visualState)
		}
		const newVisualState = makeChoice(story, visualState, 0)
		expect(newVisualState.sections[0].activeChoiceIndex).toBe(0)
	})

	it("should load in new sections if you advance after a choice", () => {
		const story = new Compiler(storyText).Compile()
		let visualState = createEmptyVisualInkState()
		while (!isSectionComplete(getActiveSection(visualState))) {
			visualState = advance(story, visualState)
		}
		let newVisualState = makeChoice(story, visualState, 0)
		expect(newVisualState.sections[0].activeChoiceIndex).toBe(0)
		newVisualState = advance(story, newVisualState);
		expect(newVisualState.sections.length).toBe(2);
		expect(newVisualState.sections[1].activeLine).toBe(0);
		expect(newVisualState.sections[1].activeChoiceIndex).toBeUndefined();
	})

	it("should error out if you make an invalid choice", () => {
		const story = new Compiler(storyText).Compile()
		let visualState = createEmptyVisualInkState()
		while (!isSectionComplete(getActiveSection(visualState))) {
			visualState = advance(story, visualState)
		}
		expect(visualState.sections[0].choices.length).toBe(2)
		expect(() => makeChoice(story, visualState, 3)).toThrowError("choice out of range")
	})
})

describe("hideAllVisibleSections", () => {
	it("should hide any visible sections", () => {
		const story = new Compiler(storyText).Compile()
		let visualState = createEmptyVisualInkState()
		while (!isSectionComplete(getActiveSection(visualState))) {
			visualState = advance(story, visualState)
		}
		visualState = makeChoice(story, visualState, 0)
		visualState = advance(story, visualState)
		
		const newVisualInkState = hideAllVisibleSections(visualState)

		expect(visualState !== newVisualInkState).toBe(true)
		expect(visualState.sections.length).toEqual(newVisualInkState.sections.length)
		for (let i = 0; i < newVisualInkState.sections.length - 1; i++) {
			expect(visualState.sections[i] !== newVisualInkState.sections[i])
			expect(visualState.sections[i].isHidden).toBeFalsy()
			expect(visualState.sections[i].lines).toBe(newVisualInkState.sections[i].lines)
			expect(newVisualInkState.sections[i].isHidden).toBeTruthy()
		}

		// the last section isn't hidden, but rather the lines are
		const lastIndex = newVisualInkState.sections.length - 1
		expect(newVisualInkState.sections[lastIndex].isHidden).toBeFalsy();
		expect(visualState.sections[lastIndex].lines !== newVisualInkState.sections[lastIndex].lines).toBe(true)
		for (let i = 0; i < newVisualInkState.sections[lastIndex].lines.length; i++) {
			const line = newVisualInkState.sections[lastIndex].lines[i]
			if (i <= newVisualInkState.sections[lastIndex].activeLine!) {
				expect(line.isHidden).toBeTruthy()
			} else {
				// make sure the line definition didn't change
				expect(line).toBe(visualState.sections[lastIndex].lines[i])
			}
		}

		// now if we advance, the newly advanced line should not be hidden
		const advancedVisualInkState = advance(story, newVisualInkState)
		for (let i = 0; i < advancedVisualInkState.sections.length - 1; i++) {
			// hidden status didn't change from first hide, so the section should be the same object
			expect(newVisualInkState.sections[i]).toBe(advancedVisualInkState.sections[i])
			expect(newVisualInkState.sections[i].lines).toBe(advancedVisualInkState.sections[i].lines)
			expect(advancedVisualInkState.sections[i].isHidden).toBeTruthy()
		}
		expect(advancedVisualInkState.sections[lastIndex].isHidden).toBeFalsy();
		expect(newVisualInkState.sections[lastIndex] !== advancedVisualInkState.sections[lastIndex]).toBe(true)

		const newLines = newVisualInkState.sections[lastIndex].lines
		const newActiveLine = newVisualInkState.sections[lastIndex].activeLine
		const advancedLines = advancedVisualInkState.sections[lastIndex].lines
		const advancedActiveLine = advancedVisualInkState.sections[lastIndex].activeLine

		expect(newActiveLine! + 1).toEqual(advancedActiveLine)
		expect(newLines[newActiveLine!].isHidden).toBeTruthy()
		expect(advancedLines[newActiveLine!].isHidden).toBeTruthy()
		expect(advancedLines[advancedActiveLine!].isHidden).toBeFalsy()
	})
})

describe("hideOlderLines", () => {
	const advanceAStoryPastTheChoice = (story: InkStory) => {
		let visualState = createEmptyVisualInkState()
		while (!isSectionComplete(getActiveSection(visualState))) {
			visualState = advance(story, visualState)
		}
		visualState = makeChoice(story, visualState, 0)
		visualState = advance(story, visualState)
		return visualState
	}

	it("should hide the whole section if you don't show enough lines to reach it", () => {
		const story = new Compiler(storyText).Compile()
		let visualState = advanceAStoryPastTheChoice(story)
		let newVisualState = hideOlderLines(visualState, 1)
		expect(newVisualState.sections[0].isHidden).toBeTruthy()
		expect(newVisualState.sections !== visualState.sections).toBe(true)

		// since this section didn't change, expect it to be the same.
		expect(newVisualState.sections[1]).toBe(visualState.sections[1])
	})
})