1
0
mirror of https://github.com/jcwimer/wrestlingApp synced 2026-04-11 16:01:56 +00:00
Files
wrestlingdev.com/test/javascript/controllers/match_state_controller.test.js

316 lines
12 KiB
JavaScript

import { beforeEach, describe, expect, it, vi } from "vitest"
import { getMatchStateConfig } from "match-state-config"
import { buildInitialState } from "match-state-engine"
import MatchStateController from "../../../app/assets/javascripts/controllers/match_state_controller.js"
function buildController() {
const controller = new MatchStateController()
controller.element = {
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
querySelector: vi.fn(() => null)
}
controller.application = {
getControllerForElementAndIdentifier: vi.fn()
}
controller.matchIdValue = 22
controller.tournamentIdValue = 8
controller.boutNumberValue = 1001
controller.weightLabelValue = "106"
controller.bracketPositionValue = "Bracket Round of 64"
controller.rulesetValue = "folkstyle_usa"
controller.w1IdValue = 11
controller.w2IdValue = 12
controller.w1NameValue = "Alpha"
controller.w2NameValue = "Bravo"
controller.w1SchoolValue = "School A"
controller.w2SchoolValue = "School B"
controller.hasW1StatFieldTarget = true
controller.hasW2StatFieldTarget = true
controller.w1StatFieldTarget = { value: "" }
controller.w2StatFieldTarget = { value: "" }
controller.hasMatchResultsPanelTarget = true
controller.matchResultsPanelTarget = { querySelector: vi.fn(() => ({})) }
return controller
}
describe("match state controller", () => {
beforeEach(() => {
vi.restoreAllMocks()
global.window = {
localStorage: {
getItem: vi.fn(() => null),
setItem: vi.fn(),
removeItem: vi.fn()
},
setInterval: vi.fn(() => 123),
clearInterval: vi.fn(),
setTimeout: vi.fn((fn) => {
fn()
return 1
}),
clearTimeout: vi.fn(),
confirm: vi.fn(() => true)
}
})
it("connect initializes state, restores persistence, renders, and subscribes", () => {
const controller = buildController()
controller.initializeState = vi.fn()
controller.loadPersistedState = vi.fn()
controller.syncClockFromActivePhase = vi.fn()
controller.hasRunningClockOrTimer = vi.fn(() => true)
controller.startTicking = vi.fn()
controller.render = vi.fn()
controller.setupSubscription = vi.fn()
controller.connect()
expect(controller.element.addEventListener).toHaveBeenCalledWith("click", controller.boundHandleClick)
expect(controller.initializeState).toHaveBeenCalledTimes(1)
expect(controller.loadPersistedState).toHaveBeenCalledTimes(1)
expect(controller.syncClockFromActivePhase).toHaveBeenCalledTimes(1)
expect(controller.startTicking).toHaveBeenCalledTimes(1)
expect(controller.render).toHaveBeenCalledWith({ rebuildControls: true })
expect(controller.setupSubscription).toHaveBeenCalledTimes(1)
})
it("applyAction recomputes and rerenders when a match action is accepted", () => {
const controller = buildController()
controller.config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
controller.state = buildInitialState(controller.config)
controller.recomputeDerivedState = vi.fn()
controller.render = vi.fn()
controller.applyAction({
dataset: {
participantKey: "w1",
actionKey: "takedown_3"
}
})
expect(controller.recomputeDerivedState).toHaveBeenCalledTimes(1)
expect(controller.render).toHaveBeenCalledWith({ rebuildControls: true })
})
it("applyChoice rerenders on defer and advances on a committed choice", () => {
const controller = buildController()
controller.config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
controller.state = buildInitialState(controller.config)
controller.state.phaseIndex = 1
controller.recomputeDerivedState = vi.fn()
controller.render = vi.fn()
controller.advancePhase = vi.fn()
controller.applyChoice({
dataset: {
participantKey: "w1",
choiceKey: "defer"
}
})
expect(controller.recomputeDerivedState).toHaveBeenCalledTimes(1)
expect(controller.render).toHaveBeenCalledWith({ rebuildControls: true })
expect(controller.advancePhase).not.toHaveBeenCalled()
controller.recomputeDerivedState.mockClear()
controller.render.mockClear()
controller.applyChoice({
dataset: {
participantKey: "w1",
choiceKey: "top"
}
})
expect(controller.advancePhase).toHaveBeenCalledTimes(1)
})
it("updateStatFieldsAndBroadcast writes hidden fields and pushes both channel payloads", () => {
const controller = buildController()
controller.derivedStats = vi.fn(() => ({ w1: "Period 1: T3", w2: "Period 1: E1" }))
controller.pushDerivedStatsToChannel = vi.fn()
controller.pushScoreboardStateToChannel = vi.fn()
controller.updateStatFieldsAndBroadcast()
expect(controller.w1StatFieldTarget.value).toBe("Period 1: T3")
expect(controller.w2StatFieldTarget.value).toBe("Period 1: E1")
expect(controller.lastDerivedStats).toEqual({ w1: "Period 1: T3", w2: "Period 1: E1" })
expect(controller.pushDerivedStatsToChannel).toHaveBeenCalledTimes(1)
expect(controller.pushScoreboardStateToChannel).toHaveBeenCalledTimes(1)
})
it("pushes derived stats and scoreboard payloads through the match subscription with dedupe", () => {
const controller = buildController()
controller.matchSubscription = { perform: vi.fn() }
controller.lastDerivedStats = { w1: "Period 1: T3", w2: "Period 1: E1" }
controller.scoreboardStatePayload = vi.fn(() => ({ participantScores: { w1: 3, w2: 1 } }))
controller.pushDerivedStatsToChannel()
controller.pushScoreboardStateToChannel()
controller.pushDerivedStatsToChannel()
controller.pushScoreboardStateToChannel()
expect(controller.matchSubscription.perform).toHaveBeenCalledTimes(2)
expect(controller.matchSubscription.perform).toHaveBeenNthCalledWith(1, "send_stat", {
new_w1_stat: "Period 1: T3",
new_w2_stat: "Period 1: E1"
})
expect(controller.matchSubscription.perform).toHaveBeenNthCalledWith(2, "send_scoreboard", {
scoreboard_state: { participantScores: { w1: 3, w2: 1 } }
})
})
it("starts, stops, and adjusts the active match clock", () => {
const controller = buildController()
controller.config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
controller.state = buildInitialState(controller.config)
controller.render = vi.fn()
controller.startTicking = vi.fn()
controller.stopTicking = vi.fn()
controller.startClock()
expect(controller.activeClock().running).toBe(true)
expect(controller.startTicking).toHaveBeenCalledTimes(1)
expect(controller.render).toHaveBeenCalledTimes(1)
controller.stopClock()
expect(controller.activeClock().running).toBe(false)
expect(controller.stopTicking).toHaveBeenCalledTimes(1)
const beforeAdjustment = controller.activeClock().remainingSeconds
controller.adjustClock(-1)
expect(controller.activeClock().remainingSeconds).toBe(beforeAdjustment - 1)
})
it("stopping an auxiliary timer records a timer-used event", () => {
const controller = buildController()
controller.config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
controller.state = buildInitialState(controller.config)
controller.state.timers.w1.blood.running = true
controller.state.timers.w1.blood.startedAt = 1_000
controller.state.timers.w1.blood.remainingSeconds = 300
controller.render = vi.fn()
vi.spyOn(Date, "now").mockReturnValue(6_000)
controller.stopAuxiliaryTimer("w1", "blood")
expect(controller.state.timers.w1.blood.running).toBe(false)
expect(controller.state.events).toHaveLength(1)
expect(controller.state.events[0]).toMatchObject({
participantKey: "w1",
actionKey: "timer_used_blood",
elapsedSeconds: 5
})
expect(controller.render).toHaveBeenCalledTimes(1)
})
it("deletes, swaps events, and swaps whole phases through button dataset ids", () => {
const controller = buildController()
controller.config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
controller.state = buildInitialState(controller.config)
controller.state.events = [
{
id: 1,
phaseKey: "period_1",
phaseLabel: "Period 1",
clockSeconds: 120,
participantKey: "w1",
actionKey: "takedown_3"
},
{
id: 2,
phaseKey: "period_1",
phaseLabel: "Period 1",
clockSeconds: 100,
participantKey: "w2",
actionKey: "escape_1"
}
]
controller.recomputeDerivedState = vi.fn()
controller.render = vi.fn()
controller.swapEvent({ dataset: { eventId: "1" } })
expect(controller.state.events[0].participantKey).toBe("w2")
controller.swapPhase({ dataset: { phaseKey: "period_1" } })
expect(controller.state.events.map((eventRecord) => eventRecord.participantKey)).toEqual(["w1", "w1"])
controller.deleteEvent({ dataset: { eventId: "2" } })
expect(controller.state.events.map((eventRecord) => eventRecord.id)).toEqual([1])
expect(controller.recomputeDerivedState).toHaveBeenCalledTimes(3)
expect(controller.render).toHaveBeenCalledTimes(3)
})
it("delegated click dispatches dynamic state buttons", () => {
const controller = buildController()
controller.applyAction = vi.fn()
const button = {
dataset: { matchStateButton: "score-action" }
}
controller.handleDelegatedClick({
target: {
closest: vi.fn(() => button)
}
})
expect(controller.applyAction).toHaveBeenCalledWith(button)
})
it("applyMatchResultDefaults forwards derived defaults to the nested match-score controller", () => {
const controller = buildController()
controller.config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
controller.state = buildInitialState(controller.config)
controller.state.participantScores = { w1: 4, w2: 2 }
controller.currentPhase = vi.fn(() => controller.config.phaseSequence[0])
controller.accumulatedMatchSeconds = vi.fn(() => 69)
const nested = { applyDefaultResults: vi.fn() }
controller.application.getControllerForElementAndIdentifier.mockReturnValue(nested)
controller.applyMatchResultDefaults()
expect(nested.applyDefaultResults).toHaveBeenCalledTimes(1)
expect(nested.applyDefaultResults.mock.calls[0][0]).toMatchObject({
winnerId: 11,
winnerScore: 4,
loserScore: 2
})
})
it("resetMatch reinitializes state and clears persistence when confirmed", () => {
const controller = buildController()
controller.initializeState = vi.fn()
controller.syncClockFromActivePhase = vi.fn()
controller.clearPersistedState = vi.fn()
controller.render = vi.fn()
controller.stopTicking = vi.fn()
controller.resetMatch()
expect(window.confirm).toHaveBeenCalledTimes(1)
expect(controller.stopTicking).toHaveBeenCalledTimes(1)
expect(controller.initializeState).toHaveBeenCalledTimes(1)
expect(controller.syncClockFromActivePhase).toHaveBeenCalledTimes(1)
expect(controller.clearPersistedState).toHaveBeenCalledTimes(1)
expect(controller.render).toHaveBeenCalledWith({ rebuildControls: true })
})
it("loadPersistedState restores saved state and clears invalid saved state", () => {
const controller = buildController()
controller.config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
controller.buildInitialState = vi.fn(() => ({ fresh: true }))
controller.clearPersistedState = vi.fn()
window.localStorage.getItem.mockReturnValueOnce('{"participantScores":{"w1":3,"w2":1}}')
controller.loadPersistedState()
expect(controller.state.participantScores).toEqual({ w1: 3, w2: 1 })
window.localStorage.getItem.mockReturnValue("{bad-json")
controller.loadPersistedState()
expect(controller.clearPersistedState).toHaveBeenCalledTimes(1)
expect(controller.state).toEqual({ fresh: true })
})
})