mirror of
https://github.com/jcwimer/wrestlingApp
synced 2026-04-11 16:01:56 +00:00
316 lines
12 KiB
JavaScript
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 })
|
|
})
|
|
})
|