mirror of
https://github.com/jcwimer/wrestlingApp
synced 2026-04-12 16:25:41 +00:00
New stats page, scoreboard, and live scores pages.
This commit is contained in:
435
test/javascript/match_state/engine.test.js
Normal file
435
test/javascript/match_state/engine.test.js
Normal file
@@ -0,0 +1,435 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { getMatchStateConfig } from "match-state-config"
|
||||
import {
|
||||
accumulatedMatchSeconds,
|
||||
activeClockForPhase,
|
||||
adjustClockState,
|
||||
applyChoiceAction,
|
||||
applyMatchAction,
|
||||
buildInitialState,
|
||||
deleteEventFromState,
|
||||
derivedStats,
|
||||
hasRunningClockOrTimer,
|
||||
matchResultDefaults,
|
||||
moveToNextPhase,
|
||||
moveToPreviousPhase,
|
||||
recordProgressiveAction,
|
||||
recomputeDerivedState,
|
||||
scoreboardStatePayload,
|
||||
startAuxiliaryTimerState,
|
||||
startClockState,
|
||||
stopAuxiliaryTimerState,
|
||||
stopClockState,
|
||||
stopAllAuxiliaryTimers,
|
||||
swapEventParticipants,
|
||||
syncClockSnapshot,
|
||||
swapPhaseParticipants
|
||||
} from "match-state-engine"
|
||||
|
||||
function buildEvent(overrides = {}) {
|
||||
return {
|
||||
id: 1,
|
||||
phaseKey: "period_1",
|
||||
phaseLabel: "Period 1",
|
||||
clockSeconds: 120,
|
||||
participantKey: "w1",
|
||||
actionKey: "takedown_3",
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe("match state engine", () => {
|
||||
it("replays takedown and escape into score and control", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
|
||||
state.events = [
|
||||
buildEvent(),
|
||||
buildEvent({
|
||||
id: 2,
|
||||
participantKey: "w2",
|
||||
actionKey: "escape_1",
|
||||
clockSeconds: 80
|
||||
})
|
||||
]
|
||||
|
||||
recomputeDerivedState(config, state)
|
||||
|
||||
expect(state.participantScores).toEqual({ w1: 3, w2: 1 })
|
||||
expect(state.control).toBe("neutral")
|
||||
expect(state.displayControl).toBe("neutral")
|
||||
})
|
||||
|
||||
it("stores non defer choices and applies chosen starting control to later periods", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
|
||||
state.phaseIndex = 2
|
||||
state.events = [
|
||||
buildEvent({
|
||||
id: 1,
|
||||
phaseKey: "choice_1",
|
||||
phaseLabel: "Choice 1",
|
||||
clockSeconds: 0,
|
||||
participantKey: "w1",
|
||||
actionKey: "choice_top"
|
||||
})
|
||||
]
|
||||
|
||||
recomputeDerivedState(config, state)
|
||||
|
||||
expect(state.selections.choice_1).toEqual({ participantKey: "w1", choiceKey: "top" })
|
||||
expect(state.control).toBe("w1_control")
|
||||
expect(state.displayControl).toBe("w1_control")
|
||||
})
|
||||
|
||||
it("ignores defer as a final selection", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
|
||||
state.phaseIndex = 2
|
||||
state.events = [
|
||||
buildEvent({
|
||||
id: 1,
|
||||
phaseKey: "choice_1",
|
||||
phaseLabel: "Choice 1",
|
||||
clockSeconds: 0,
|
||||
participantKey: "w1",
|
||||
actionKey: "choice_defer"
|
||||
})
|
||||
]
|
||||
|
||||
recomputeDerivedState(config, state)
|
||||
|
||||
expect(state.selections).toEqual({})
|
||||
expect(state.control).toBe("neutral")
|
||||
expect(state.displayControl).toBe("neutral")
|
||||
})
|
||||
|
||||
it("derives legacy stats grouped by period", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const stats = derivedStats(config, [
|
||||
buildEvent(),
|
||||
buildEvent({
|
||||
id: 2,
|
||||
participantKey: "w2",
|
||||
actionKey: "escape_1",
|
||||
clockSeconds: 80
|
||||
}),
|
||||
buildEvent({
|
||||
id: 3,
|
||||
phaseKey: "choice_1",
|
||||
phaseLabel: "Choice 1",
|
||||
clockSeconds: 0,
|
||||
participantKey: "w1",
|
||||
actionKey: "choice_defer"
|
||||
})
|
||||
])
|
||||
|
||||
expect(stats.w1).toBe("Period 1: T3\nChoice 1: |Deferred|")
|
||||
expect(stats.w2).toBe("Period 1: E1")
|
||||
})
|
||||
|
||||
it("derives accumulated match time from period clocks", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
|
||||
state.phaseIndex = 2
|
||||
state.clocksByPhase.period_1.remainingSeconds = 42
|
||||
state.clocksByPhase.period_2.remainingSeconds = 75
|
||||
state.clocksByPhase.period_3.remainingSeconds = 120
|
||||
|
||||
const total = accumulatedMatchSeconds(config, state, "period_2")
|
||||
|
||||
expect(total).toBe((120 - 42) + (120 - 75))
|
||||
})
|
||||
|
||||
it("builds scoreboard payload from canonical state and metadata", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
state.participantScores = { w1: 6, w2: 2 }
|
||||
state.phaseIndex = 2
|
||||
|
||||
const payload = scoreboardStatePayload(config, state, {
|
||||
tournamentId: 1,
|
||||
boutNumber: 1001,
|
||||
weightLabel: "106",
|
||||
ruleset: "folkstyle_usa",
|
||||
bracketPosition: "Bracket Round of 64",
|
||||
w1Name: "Wrestler 1",
|
||||
w2Name: "Wrestler 2",
|
||||
w1School: "School A",
|
||||
w2School: "School B"
|
||||
})
|
||||
|
||||
expect(payload.participantScores).toEqual({ w1: 6, w2: 2 })
|
||||
expect(payload.phaseIndex).toBe(2)
|
||||
expect(payload.metadata.boutNumber).toBe(1001)
|
||||
expect(payload.metadata.w1Name).toBe("Wrestler 1")
|
||||
expect(payload.matchResult).toEqual({ finished: false })
|
||||
})
|
||||
|
||||
it("records progressive penalty with linked awarded points", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
state.nextEventId = 1
|
||||
state.nextEventGroupId = 1
|
||||
|
||||
const buildControllerStyleEvent = (participantKey, actionKey, options = {}) => ({
|
||||
id: state.nextEventId++,
|
||||
phaseKey: "period_1",
|
||||
phaseLabel: "Period 1",
|
||||
clockSeconds: 120,
|
||||
participantKey,
|
||||
actionKey,
|
||||
actionGroupId: options.actionGroupId
|
||||
})
|
||||
|
||||
recordProgressiveAction(config, state, "w1", "penalty", buildControllerStyleEvent)
|
||||
recordProgressiveAction(config, state, "w1", "penalty", buildControllerStyleEvent)
|
||||
recordProgressiveAction(config, state, "w1", "penalty", buildControllerStyleEvent)
|
||||
|
||||
expect(state.events.map((eventRecord) => [eventRecord.participantKey, eventRecord.actionKey])).toEqual([
|
||||
["w1", "penalty"],
|
||||
["w2", "plus_1"],
|
||||
["w1", "penalty"],
|
||||
["w2", "plus_1"],
|
||||
["w1", "penalty"],
|
||||
["w2", "plus_2"]
|
||||
])
|
||||
})
|
||||
|
||||
it("applies a normal match action by creating one event", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
|
||||
const applied = applyMatchAction(config, state, config.phaseSequence[0], 118, "w1", "takedown_3")
|
||||
|
||||
expect(applied).toBe(true)
|
||||
expect(state.events).toHaveLength(1)
|
||||
expect(state.events[0]).toMatchObject({
|
||||
phaseKey: "period_1",
|
||||
clockSeconds: 118,
|
||||
participantKey: "w1",
|
||||
actionKey: "takedown_3"
|
||||
})
|
||||
})
|
||||
|
||||
it("applies a progressive action by creating offense and linked award events when earned", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
|
||||
expect(applyMatchAction(config, state, config.phaseSequence[0], 118, "w1", "stalling")).toBe(true)
|
||||
expect(applyMatchAction(config, state, config.phaseSequence[0], 110, "w1", "stalling")).toBe(true)
|
||||
|
||||
expect(state.events.map((eventRecord) => [eventRecord.participantKey, eventRecord.actionKey])).toEqual([
|
||||
["w1", "stalling"],
|
||||
["w1", "stalling"],
|
||||
["w2", "plus_1"]
|
||||
])
|
||||
})
|
||||
|
||||
it("applies a defer choice without storing a final selection", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
const choicePhase = config.phaseSequence[1]
|
||||
|
||||
const result = applyChoiceAction(state, choicePhase, 0, "w1", "defer")
|
||||
|
||||
expect(result).toEqual({ applied: true, deferred: true })
|
||||
expect(state.events).toHaveLength(1)
|
||||
expect(state.events[0].actionKey).toBe("choice_defer")
|
||||
expect(state.selections).toEqual({})
|
||||
})
|
||||
|
||||
it("applies a non defer choice and stores the selection", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
const choicePhase = config.phaseSequence[1]
|
||||
|
||||
const result = applyChoiceAction(state, choicePhase, 0, "w2", "bottom")
|
||||
|
||||
expect(result).toEqual({ applied: true, deferred: false })
|
||||
expect(state.events[0].actionKey).toBe("choice_bottom")
|
||||
expect(state.selections.choice_1).toEqual({ participantKey: "w2", choiceKey: "bottom" })
|
||||
})
|
||||
|
||||
it("deleting a timer-used event restores timer time", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
state.timers.w1.injury.remainingSeconds = 60
|
||||
state.events = [
|
||||
buildEvent({
|
||||
id: 1,
|
||||
participantKey: "w1",
|
||||
actionKey: "timer_used_injury",
|
||||
elapsedSeconds: 20
|
||||
})
|
||||
]
|
||||
|
||||
const deleted = deleteEventFromState(config, state, 1)
|
||||
|
||||
expect(deleted).toBe(true)
|
||||
expect(state.events).toEqual([])
|
||||
expect(state.timers.w1.injury.remainingSeconds).toBe(80)
|
||||
})
|
||||
|
||||
it("swapping a timer-used event moves the used time to the other wrestler", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
state.timers.w1.blood.remainingSeconds = 240
|
||||
state.timers.w2.blood.remainingSeconds = 260
|
||||
state.events = [
|
||||
buildEvent({
|
||||
id: 1,
|
||||
participantKey: "w1",
|
||||
actionKey: "timer_used_blood",
|
||||
elapsedSeconds: 30
|
||||
})
|
||||
]
|
||||
|
||||
const swapped = swapEventParticipants(config, state, 1)
|
||||
|
||||
expect(swapped).toBe(true)
|
||||
expect(state.events[0].participantKey).toBe("w2")
|
||||
expect(state.timers.w1.blood.remainingSeconds).toBe(270)
|
||||
expect(state.timers.w2.blood.remainingSeconds).toBe(230)
|
||||
})
|
||||
|
||||
it("swapping a whole period flips all participants in that period", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
state.events = [
|
||||
buildEvent({
|
||||
id: 1,
|
||||
participantKey: "w1",
|
||||
actionKey: "takedown_3"
|
||||
}),
|
||||
buildEvent({
|
||||
id: 2,
|
||||
participantKey: "w2",
|
||||
actionKey: "escape_1",
|
||||
clockSeconds: 80
|
||||
}),
|
||||
buildEvent({
|
||||
id: 3,
|
||||
phaseKey: "choice_1",
|
||||
phaseLabel: "Choice 1",
|
||||
participantKey: "w1",
|
||||
actionKey: "choice_defer",
|
||||
clockSeconds: 0
|
||||
})
|
||||
]
|
||||
|
||||
const swapped = swapPhaseParticipants(config, state, "period_1")
|
||||
|
||||
expect(swapped).toBe(true)
|
||||
expect(state.events.slice(0, 2).map((eventRecord) => eventRecord.participantKey)).toEqual(["w2", "w1"])
|
||||
expect(state.events[2].participantKey).toBe("w1")
|
||||
})
|
||||
|
||||
it("starts, stops, and adjusts a running match clock", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
const activeClock = state.clocksByPhase.period_1
|
||||
|
||||
expect(startClockState(activeClock, 1_000)).toBe(true)
|
||||
expect(activeClock.running).toBe(true)
|
||||
expect(activeClock.startedAt).toBe(1_000)
|
||||
|
||||
expect(adjustClockState(activeClock, -10, 6_000)).toBe(true)
|
||||
expect(activeClock.remainingSeconds).toBe(105)
|
||||
expect(activeClock.startedAt).toBe(6_000)
|
||||
|
||||
expect(stopClockState(activeClock, 11_000)).toBe(true)
|
||||
expect(activeClock.running).toBe(false)
|
||||
expect(activeClock.remainingSeconds).toBe(100)
|
||||
expect(syncClockSnapshot(activeClock)).toEqual({
|
||||
durationSeconds: 120,
|
||||
remainingSeconds: 100,
|
||||
running: false,
|
||||
startedAt: null
|
||||
})
|
||||
})
|
||||
|
||||
it("starts and stops an auxiliary timer with elapsed time", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const timerState = buildInitialState(config).timers.w1.injury
|
||||
|
||||
expect(startAuxiliaryTimerState(timerState, 2_000)).toBe(true)
|
||||
const result = stopAuxiliaryTimerState(timerState, 17_000)
|
||||
|
||||
expect(result).toEqual({ stopped: true, elapsedSeconds: 15 })
|
||||
expect(timerState.running).toBe(false)
|
||||
expect(timerState.remainingSeconds).toBe(75)
|
||||
})
|
||||
|
||||
it("derives match result defaults from score and overtime context", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
state.participantScores = { w1: 4, w2: 2 }
|
||||
|
||||
const defaults = matchResultDefaults(state, {
|
||||
w1Id: 11,
|
||||
w2Id: 22,
|
||||
currentPhase: config.phaseSequence.find((phase) => phase.key === "sv_1"),
|
||||
accumulationSeconds: 83
|
||||
})
|
||||
|
||||
expect(defaults).toEqual({
|
||||
winnerId: 11,
|
||||
overtimeType: "SV-1",
|
||||
winnerScore: 4,
|
||||
loserScore: 2,
|
||||
pinMinutes: 1,
|
||||
pinSeconds: 23
|
||||
})
|
||||
})
|
||||
|
||||
it("moves between phases and resets control to the new phase base", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
state.control = "w1_control"
|
||||
|
||||
expect(moveToNextPhase(config, state)).toBe(true)
|
||||
expect(state.phaseIndex).toBe(1)
|
||||
|
||||
state.selections.choice_1 = { participantKey: "w2", choiceKey: "bottom" }
|
||||
expect(moveToNextPhase(config, state)).toBe(true)
|
||||
expect(state.phaseIndex).toBe(2)
|
||||
expect(state.control).toBe("w1_control")
|
||||
|
||||
expect(moveToPreviousPhase(config, state)).toBe(true)
|
||||
expect(state.phaseIndex).toBe(1)
|
||||
})
|
||||
|
||||
it("finds the active clock for a timed phase and reports running state", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
const periodOne = config.phaseSequence[0]
|
||||
const choiceOne = config.phaseSequence[1]
|
||||
|
||||
expect(activeClockForPhase(state, periodOne)).toBe(state.clocksByPhase.period_1)
|
||||
expect(activeClockForPhase(state, choiceOne)).toBe(null)
|
||||
expect(hasRunningClockOrTimer(state)).toBe(false)
|
||||
|
||||
state.clocksByPhase.period_1.running = true
|
||||
expect(hasRunningClockOrTimer(state)).toBe(true)
|
||||
})
|
||||
|
||||
it("stops all running auxiliary timers in place", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
state.timers.w1.blood.running = true
|
||||
state.timers.w1.blood.startedAt = 1_000
|
||||
state.timers.w1.blood.remainingSeconds = 300
|
||||
state.timers.w2.injury.running = true
|
||||
state.timers.w2.injury.startedAt = 5_000
|
||||
state.timers.w2.injury.remainingSeconds = 90
|
||||
|
||||
stopAllAuxiliaryTimers(state, 11_000)
|
||||
|
||||
expect(state.timers.w1.blood).toMatchObject({ running: false, startedAt: null, remainingSeconds: 290 })
|
||||
expect(state.timers.w2.injury).toMatchObject({ running: false, startedAt: null, remainingSeconds: 84 })
|
||||
})
|
||||
})
|
||||
94
test/javascript/match_state/presenters.test.js
Normal file
94
test/javascript/match_state/presenters.test.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { getMatchStateConfig } from "match-state-config"
|
||||
import { buildInitialState } from "match-state-engine"
|
||||
import {
|
||||
buttonClassForParticipant,
|
||||
choiceViewModel,
|
||||
displayLabelForParticipant,
|
||||
eventLogSections,
|
||||
humanizeChoice
|
||||
} from "match-state-presenters"
|
||||
|
||||
describe("match state presenters", () => {
|
||||
it("maps assignment to display labels and button classes", () => {
|
||||
const assignment = { w1: "green", w2: "red" }
|
||||
|
||||
expect(displayLabelForParticipant(assignment, "w1")).toBe("Green")
|
||||
expect(displayLabelForParticipant(assignment, "w2")).toBe("Red")
|
||||
expect(buttonClassForParticipant(assignment, "w1")).toBe("btn-success")
|
||||
expect(buttonClassForParticipant(assignment, "w2")).toBe("btn-danger")
|
||||
expect(humanizeChoice("defer")).toBe("Defer")
|
||||
})
|
||||
|
||||
it("builds choice view model with defer blocking another defer", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
const phase = config.phaseSequence[1]
|
||||
state.events = [{
|
||||
id: 1,
|
||||
phaseKey: phase.key,
|
||||
phaseLabel: phase.label,
|
||||
clockSeconds: 0,
|
||||
participantKey: "w1",
|
||||
actionKey: "choice_defer"
|
||||
}]
|
||||
|
||||
const viewModel = choiceViewModel(config, state, phase, {
|
||||
w1: { name: "Wrestler 1" },
|
||||
w2: { name: "Wrestler 2" }
|
||||
})
|
||||
|
||||
expect(viewModel.label).toBe("Choose wrestler and position")
|
||||
expect(viewModel.selectionText).toContain("Green deferred")
|
||||
expect(viewModel.buttons.map((button) => [button.participantKey, button.choiceKey])).toEqual([
|
||||
["w2", "top"],
|
||||
["w2", "bottom"],
|
||||
["w2", "neutral"]
|
||||
])
|
||||
})
|
||||
|
||||
it("builds event log sections with formatted action labels", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
state.events = [
|
||||
{
|
||||
id: 1,
|
||||
phaseKey: "period_1",
|
||||
phaseLabel: "Period 1",
|
||||
clockSeconds: 100,
|
||||
participantKey: "w1",
|
||||
actionKey: "takedown_3"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
phaseKey: "period_1",
|
||||
phaseLabel: "Period 1",
|
||||
clockSeconds: 80,
|
||||
participantKey: "w2",
|
||||
actionKey: "timer_used_blood",
|
||||
elapsedSeconds: 15
|
||||
}
|
||||
]
|
||||
|
||||
const sections = eventLogSections(config, state, (seconds) => `F-${seconds}`)
|
||||
|
||||
expect(sections).toHaveLength(1)
|
||||
expect(sections[0].label).toBe("Period 1")
|
||||
expect(sections[0].items).toEqual([
|
||||
{
|
||||
id: 2,
|
||||
participantKey: "w2",
|
||||
colorLabel: "Red",
|
||||
actionLabel: "Blood Time Used: F-15",
|
||||
clockLabel: "F-80"
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
participantKey: "w1",
|
||||
colorLabel: "Green",
|
||||
actionLabel: "Takedown +3",
|
||||
clockLabel: "F-100"
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
227
test/javascript/match_state/scoreboard_presenters.test.js
Normal file
227
test/javascript/match_state/scoreboard_presenters.test.js
Normal file
@@ -0,0 +1,227 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { getMatchStateConfig } from "match-state-config"
|
||||
import { buildInitialState } from "match-state-engine"
|
||||
import {
|
||||
boardColors,
|
||||
buildRunningTimerSnapshot,
|
||||
emptyBoardViewModel,
|
||||
currentClockText,
|
||||
detectRecentlyStoppedTimer,
|
||||
mainClockRunning,
|
||||
nextTimerBannerState,
|
||||
participantDisplayLabel,
|
||||
participantForColor,
|
||||
populatedBoardViewModel,
|
||||
timerBannerRenderState,
|
||||
timerBannerViewModel,
|
||||
timerIndicatorLabel
|
||||
} from "match-state-scoreboard-presenters"
|
||||
|
||||
describe("match state scoreboard presenters", () => {
|
||||
it("maps colors to participants and labels", () => {
|
||||
const state = {
|
||||
assignment: { w1: "red", w2: "green" },
|
||||
metadata: { w1Name: "Alpha", w2Name: "Bravo" }
|
||||
}
|
||||
|
||||
expect(participantForColor(state, "red")).toBe("w1")
|
||||
expect(participantForColor(state, "green")).toBe("w2")
|
||||
expect(participantDisplayLabel(state, "w1")).toBe("Red Alpha")
|
||||
expect(participantDisplayLabel(state, "w2")).toBe("Green Bravo")
|
||||
})
|
||||
|
||||
it("formats the current clock from running phase state", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
state.phaseIndex = 0
|
||||
state.clocksByPhase.period_1.running = true
|
||||
state.clocksByPhase.period_1.startedAt = 1_000
|
||||
state.clocksByPhase.period_1.remainingSeconds = 120
|
||||
|
||||
expect(currentClockText(config, state, (seconds) => `F-${seconds}`, 11_000)).toBe("F-110")
|
||||
expect(mainClockRunning(config, state)).toBe(true)
|
||||
})
|
||||
|
||||
it("builds timer indicator and banner view models from running timers", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
state.metadata = { w1Name: "Alpha", w2Name: "Bravo" }
|
||||
state.timers.w1.blood.running = true
|
||||
state.timers.w1.blood.startedAt = 1_000
|
||||
state.timers.w1.blood.remainingSeconds = 300
|
||||
|
||||
expect(timerIndicatorLabel(config, state, "w1", (seconds) => `F-${seconds}`, 21_000)).toBe("Blood: F-20")
|
||||
|
||||
const banner = timerBannerViewModel(config, state, { participantKey: "w1", timerKey: "blood", expiresAt: null }, (seconds) => `F-${seconds}`, 21_000)
|
||||
expect(banner).toEqual({
|
||||
color: "green",
|
||||
label: "Green Alpha Blood Running",
|
||||
clockText: "F-20"
|
||||
})
|
||||
})
|
||||
|
||||
it("detects recently stopped timers from the snapshot", () => {
|
||||
const state = {
|
||||
timers: {
|
||||
w1: { blood: { running: false } },
|
||||
w2: { injury: { running: true } }
|
||||
}
|
||||
}
|
||||
const snapshot = {
|
||||
"w1:blood": true,
|
||||
"w2:injury": true
|
||||
}
|
||||
|
||||
expect(detectRecentlyStoppedTimer(state, snapshot)).toEqual({ participantKey: "w1", timerKey: "blood" })
|
||||
expect(buildRunningTimerSnapshot(state)).toEqual({
|
||||
"w1:blood": false,
|
||||
"w2:injury": true
|
||||
})
|
||||
})
|
||||
|
||||
it("builds populated and empty board view models", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
state.metadata = {
|
||||
w1Name: "Alpha",
|
||||
w2Name: "Bravo",
|
||||
w1School: "School A",
|
||||
w2School: "School B",
|
||||
weightLabel: "106"
|
||||
}
|
||||
state.participantScores = { w1: 4, w2: 1 }
|
||||
state.assignment = { w1: "green", w2: "red" }
|
||||
|
||||
const populated = populatedBoardViewModel(
|
||||
config,
|
||||
state,
|
||||
{ w1_stat: "T3", w2_stat: "E1" },
|
||||
1001,
|
||||
(seconds) => `F-${seconds}`
|
||||
)
|
||||
|
||||
expect(populated).toMatchObject({
|
||||
isEmpty: false,
|
||||
redName: "Bravo",
|
||||
redSchool: "School B",
|
||||
redScore: "1",
|
||||
greenName: "Alpha",
|
||||
greenSchool: "School A",
|
||||
greenScore: "4",
|
||||
weightLabel: "Weight 106",
|
||||
boutLabel: "Bout 1001",
|
||||
redStats: "E1",
|
||||
greenStats: "T3"
|
||||
})
|
||||
|
||||
expect(emptyBoardViewModel(1002, "Last result")).toEqual({
|
||||
isEmpty: true,
|
||||
redName: "NO MATCH",
|
||||
redSchool: "",
|
||||
redScore: "0",
|
||||
redTimerIndicator: "",
|
||||
greenName: "NO MATCH",
|
||||
greenSchool: "",
|
||||
greenScore: "0",
|
||||
greenTimerIndicator: "",
|
||||
clockText: "-",
|
||||
phaseLabel: "No Match",
|
||||
weightLabel: "Weight -",
|
||||
boutLabel: "Bout 1002",
|
||||
redStats: "",
|
||||
greenStats: "",
|
||||
lastMatchResult: "Last result"
|
||||
})
|
||||
})
|
||||
|
||||
it("builds next timer banner state for running and recently stopped timers", () => {
|
||||
const runningState = {
|
||||
timers: {
|
||||
w1: { blood: { running: true } },
|
||||
w2: {}
|
||||
}
|
||||
}
|
||||
|
||||
expect(nextTimerBannerState(runningState, {})).toEqual({
|
||||
timerBannerState: {
|
||||
participantKey: "w1",
|
||||
timerKey: "blood",
|
||||
expiresAt: null
|
||||
},
|
||||
previousTimerSnapshot: {
|
||||
"w1:blood": true
|
||||
}
|
||||
})
|
||||
|
||||
const stoppedState = {
|
||||
timers: {
|
||||
w1: { blood: { running: false } },
|
||||
w2: {}
|
||||
}
|
||||
}
|
||||
|
||||
const stoppedResult = nextTimerBannerState(stoppedState, { "w1:blood": true }, 5_000)
|
||||
expect(stoppedResult).toEqual({
|
||||
timerBannerState: {
|
||||
participantKey: "w1",
|
||||
timerKey: "blood",
|
||||
expiresAt: 15_000
|
||||
},
|
||||
previousTimerSnapshot: {
|
||||
"w1:blood": false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it("builds board colors and timer banner render decisions", () => {
|
||||
expect(boardColors(true)).toEqual({
|
||||
red: "#000",
|
||||
center: "#000",
|
||||
green: "#000"
|
||||
})
|
||||
expect(boardColors(false)).toEqual({
|
||||
red: "#c91f1f",
|
||||
center: "#050505",
|
||||
green: "#1cab2d"
|
||||
})
|
||||
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
state.metadata = { w1Name: "Alpha", w2Name: "Bravo" }
|
||||
state.timers.w1.blood.running = true
|
||||
state.timers.w1.blood.startedAt = 1_000
|
||||
state.timers.w1.blood.remainingSeconds = 300
|
||||
|
||||
expect(timerBannerRenderState(
|
||||
config,
|
||||
state,
|
||||
{ participantKey: "w1", timerKey: "blood", expiresAt: null },
|
||||
(seconds) => `F-${seconds}`,
|
||||
11_000
|
||||
)).toEqual({
|
||||
timerBannerState: { participantKey: "w1", timerKey: "blood", expiresAt: null },
|
||||
visible: true,
|
||||
viewModel: {
|
||||
color: "green",
|
||||
label: "Green Alpha Blood Running",
|
||||
clockText: "F-10"
|
||||
}
|
||||
})
|
||||
|
||||
state.clocksByPhase.period_1.running = true
|
||||
state.clocksByPhase.period_1.startedAt = 1_000
|
||||
state.clocksByPhase.period_1.remainingSeconds = 120
|
||||
|
||||
expect(timerBannerRenderState(
|
||||
config,
|
||||
state,
|
||||
{ participantKey: "w1", timerKey: "blood", expiresAt: 20_000 },
|
||||
(seconds) => `F-${seconds}`,
|
||||
11_000
|
||||
)).toEqual({
|
||||
timerBannerState: null,
|
||||
visible: false,
|
||||
viewModel: null
|
||||
})
|
||||
})
|
||||
})
|
||||
307
test/javascript/match_state/scoreboard_state.test.js
Normal file
307
test/javascript/match_state/scoreboard_state.test.js
Normal file
@@ -0,0 +1,307 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import {
|
||||
buildScoreboardContext,
|
||||
connectionPlan,
|
||||
applyMatchPayloadContext,
|
||||
applyMatPayloadContext,
|
||||
applyStatePayloadContext,
|
||||
extractLiveMatchData,
|
||||
matchStorageKey,
|
||||
selectedBoutNumber,
|
||||
storageChangePlan,
|
||||
selectedBoutStorageKey
|
||||
} from "match-state-scoreboard-state"
|
||||
|
||||
describe("match state scoreboard state helpers", () => {
|
||||
it("builds the default scoreboard controller context", () => {
|
||||
expect(buildScoreboardContext({ initialBoutNumber: 1001, matchId: 55 })).toEqual({
|
||||
currentQueueBoutNumber: 1001,
|
||||
currentBoutNumber: 1001,
|
||||
currentMatchId: 55,
|
||||
liveMatchData: {},
|
||||
lastMatchResult: "",
|
||||
state: null,
|
||||
finished: false,
|
||||
timerBannerState: null,
|
||||
previousTimerSnapshot: {}
|
||||
})
|
||||
})
|
||||
|
||||
it("builds tournament-scoped storage keys", () => {
|
||||
expect(selectedBoutStorageKey(4, 2)).toBe("mat-selected-bout:4:2")
|
||||
expect(matchStorageKey(4, 1007)).toBe("match-state:4:1007")
|
||||
expect(matchStorageKey(4, null)).toBe(null)
|
||||
})
|
||||
|
||||
it("builds connection plans by source mode", () => {
|
||||
expect(connectionPlan("localstorage", 11)).toEqual({
|
||||
useStorageListener: true,
|
||||
subscribeMat: true,
|
||||
subscribeMatch: false,
|
||||
matchId: null,
|
||||
loadSelectedBout: true,
|
||||
loadLocalState: true
|
||||
})
|
||||
|
||||
expect(connectionPlan("mat_websocket", 11)).toEqual({
|
||||
useStorageListener: false,
|
||||
subscribeMat: true,
|
||||
subscribeMatch: true,
|
||||
matchId: 11,
|
||||
loadSelectedBout: false,
|
||||
loadLocalState: false
|
||||
})
|
||||
|
||||
expect(connectionPlan("websocket", 11)).toEqual({
|
||||
useStorageListener: false,
|
||||
subscribeMat: false,
|
||||
subscribeMatch: true,
|
||||
matchId: 11,
|
||||
loadSelectedBout: false,
|
||||
loadLocalState: false
|
||||
})
|
||||
})
|
||||
|
||||
it("extracts live match fields from a websocket payload", () => {
|
||||
expect(extractLiveMatchData({
|
||||
w1_stat: "T3",
|
||||
w2_stat: "E1",
|
||||
score: "3-1",
|
||||
win_type: "Decision",
|
||||
winner_name: "Alpha",
|
||||
finished: 1,
|
||||
ignored: "value"
|
||||
})).toEqual({
|
||||
w1_stat: "T3",
|
||||
w2_stat: "E1",
|
||||
score: "3-1",
|
||||
win_type: "Decision",
|
||||
winner_name: "Alpha",
|
||||
finished: 1
|
||||
})
|
||||
})
|
||||
|
||||
it("applies scoreboard state payload context", () => {
|
||||
const context = applyStatePayloadContext(
|
||||
{ currentBoutNumber: 1001, finished: false, state: null },
|
||||
{
|
||||
metadata: { boutNumber: 1002 },
|
||||
matchResult: { finished: true }
|
||||
}
|
||||
)
|
||||
|
||||
expect(context.currentBoutNumber).toBe(1002)
|
||||
expect(context.finished).toBe(true)
|
||||
expect(context.state).toEqual({
|
||||
metadata: { boutNumber: 1002 },
|
||||
matchResult: { finished: true }
|
||||
})
|
||||
})
|
||||
|
||||
it("merges websocket match payload into current scoreboard context", () => {
|
||||
const currentContext = {
|
||||
currentBoutNumber: 1001,
|
||||
finished: false,
|
||||
liveMatchData: { w1_stat: "Old" },
|
||||
state: { metadata: { boutNumber: 1001 } }
|
||||
}
|
||||
|
||||
const nextContext = applyMatchPayloadContext(currentContext, {
|
||||
scoreboard_state: {
|
||||
metadata: { boutNumber: 1003 },
|
||||
matchResult: { finished: true }
|
||||
},
|
||||
w1_stat: "T3",
|
||||
w2_stat: "E1",
|
||||
score: "3-1",
|
||||
finished: 1
|
||||
})
|
||||
|
||||
expect(nextContext.currentBoutNumber).toBe(1003)
|
||||
expect(nextContext.finished).toBe(true)
|
||||
expect(nextContext.liveMatchData).toEqual({
|
||||
w1_stat: "T3",
|
||||
w2_stat: "E1",
|
||||
score: "3-1",
|
||||
finished: 1
|
||||
})
|
||||
expect(nextContext.state).toEqual({
|
||||
metadata: { boutNumber: 1003 },
|
||||
matchResult: { finished: true }
|
||||
})
|
||||
})
|
||||
|
||||
it("updates localstorage scoreboard context from mat payload", () => {
|
||||
const nextContext = applyMatPayloadContext(
|
||||
{
|
||||
sourceMode: "localstorage",
|
||||
currentQueueBoutNumber: null,
|
||||
lastMatchResult: "",
|
||||
currentMatchId: null,
|
||||
currentBoutNumber: null,
|
||||
state: null,
|
||||
liveMatchData: {}
|
||||
},
|
||||
{
|
||||
queue1_bout_number: 1001,
|
||||
last_match_result: "Result text"
|
||||
}
|
||||
)
|
||||
|
||||
expect(nextContext).toMatchObject({
|
||||
currentQueueBoutNumber: 1001,
|
||||
lastMatchResult: "Result text",
|
||||
loadSelectedBout: true,
|
||||
loadLocalState: true,
|
||||
renderNow: true
|
||||
})
|
||||
})
|
||||
|
||||
it("uses the selected mat bout as the localstorage scoreboard fallback", () => {
|
||||
const nextContext = applyMatPayloadContext(
|
||||
{
|
||||
sourceMode: "localstorage",
|
||||
currentQueueBoutNumber: null,
|
||||
lastMatchResult: "",
|
||||
currentMatchId: null,
|
||||
currentBoutNumber: null,
|
||||
state: null,
|
||||
liveMatchData: {}
|
||||
},
|
||||
{
|
||||
queue1_bout_number: 1001,
|
||||
selected_bout_number: 1003,
|
||||
last_match_result: ""
|
||||
}
|
||||
)
|
||||
|
||||
expect(nextContext.currentQueueBoutNumber).toBe(1003)
|
||||
expect(nextContext.loadSelectedBout).toBe(true)
|
||||
expect(nextContext.loadLocalState).toBe(true)
|
||||
})
|
||||
|
||||
it("derives storage change instructions for selected bout and match state keys", () => {
|
||||
const context = { currentBoutNumber: 1001 }
|
||||
|
||||
expect(storageChangePlan(context, "mat-selected-bout:4:2", 4, 2)).toEqual({
|
||||
loadSelectedBout: true,
|
||||
loadLocalState: true,
|
||||
renderNow: true
|
||||
})
|
||||
|
||||
expect(storageChangePlan(context, "match-state:4:1001", 4, 2)).toEqual({
|
||||
loadSelectedBout: false,
|
||||
loadLocalState: true,
|
||||
renderNow: true
|
||||
})
|
||||
|
||||
expect(storageChangePlan(context, "other-key", 4, 2)).toEqual({
|
||||
loadSelectedBout: false,
|
||||
loadLocalState: false,
|
||||
renderNow: false
|
||||
})
|
||||
})
|
||||
|
||||
it("prefers selected bout numbers and falls back to queue bout", () => {
|
||||
expect(selectedBoutNumber({ boutNumber: 1004 }, 1001)).toBe(1004)
|
||||
expect(selectedBoutNumber(null, 1001)).toBe(1001)
|
||||
})
|
||||
|
||||
it("clears websocket scoreboard context when the mat has no active match", () => {
|
||||
const nextContext = applyMatPayloadContext(
|
||||
{
|
||||
sourceMode: "mat_websocket",
|
||||
currentQueueBoutNumber: 1001,
|
||||
currentMatchId: 10,
|
||||
currentBoutNumber: 1001,
|
||||
liveMatchData: { w1_stat: "T3" },
|
||||
state: { metadata: { boutNumber: 1001 } },
|
||||
lastMatchResult: ""
|
||||
},
|
||||
{
|
||||
queue1_bout_number: null,
|
||||
queue1_match_id: null,
|
||||
selected_bout_number: null,
|
||||
selected_match_id: null,
|
||||
last_match_result: "Last result"
|
||||
}
|
||||
)
|
||||
|
||||
expect(nextContext).toMatchObject({
|
||||
currentQueueBoutNumber: null,
|
||||
currentMatchId: null,
|
||||
currentBoutNumber: null,
|
||||
state: null,
|
||||
liveMatchData: {},
|
||||
lastMatchResult: "Last result",
|
||||
resetTimerBanner: true,
|
||||
unsubscribeMatch: true,
|
||||
subscribeMatchId: null,
|
||||
renderNow: true
|
||||
})
|
||||
})
|
||||
|
||||
it("switches websocket scoreboard subscriptions when the selected match changes", () => {
|
||||
const nextContext = applyMatPayloadContext(
|
||||
{
|
||||
sourceMode: "mat_websocket",
|
||||
currentQueueBoutNumber: 1001,
|
||||
currentMatchId: 10,
|
||||
currentBoutNumber: 1001,
|
||||
liveMatchData: { w1_stat: "T3" },
|
||||
state: { metadata: { boutNumber: 1001 } },
|
||||
lastMatchResult: ""
|
||||
},
|
||||
{
|
||||
queue1_bout_number: 1001,
|
||||
queue1_match_id: 10,
|
||||
selected_bout_number: 1002,
|
||||
selected_match_id: 11,
|
||||
last_match_result: ""
|
||||
}
|
||||
)
|
||||
|
||||
expect(nextContext).toMatchObject({
|
||||
currentQueueBoutNumber: 1001,
|
||||
currentMatchId: 11,
|
||||
currentBoutNumber: 1002,
|
||||
state: null,
|
||||
liveMatchData: {},
|
||||
resetTimerBanner: true,
|
||||
subscribeMatchId: 11,
|
||||
renderNow: true
|
||||
})
|
||||
})
|
||||
|
||||
it("keeps current websocket subscription when the selected match is unchanged", () => {
|
||||
const state = { metadata: { boutNumber: 1002 } }
|
||||
const liveMatchData = { w1_stat: "T3" }
|
||||
|
||||
const nextContext = applyMatPayloadContext(
|
||||
{
|
||||
sourceMode: "mat_websocket",
|
||||
currentQueueBoutNumber: 1001,
|
||||
currentMatchId: 11,
|
||||
currentBoutNumber: 1002,
|
||||
liveMatchData,
|
||||
state,
|
||||
lastMatchResult: ""
|
||||
},
|
||||
{
|
||||
queue1_bout_number: 1001,
|
||||
queue1_match_id: 10,
|
||||
selected_bout_number: 1002,
|
||||
selected_match_id: 11,
|
||||
last_match_result: "Result"
|
||||
}
|
||||
)
|
||||
|
||||
expect(nextContext.currentMatchId).toBe(11)
|
||||
expect(nextContext.currentBoutNumber).toBe(1002)
|
||||
expect(nextContext.state).toBe(state)
|
||||
expect(nextContext.liveMatchData).toBe(liveMatchData)
|
||||
expect(nextContext.subscribeMatchId).toBe(null)
|
||||
expect(nextContext.renderNow).toBe(false)
|
||||
expect(nextContext.lastMatchResult).toBe("Result")
|
||||
})
|
||||
})
|
||||
73
test/javascript/match_state/serializers.test.js
Normal file
73
test/javascript/match_state/serializers.test.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { getMatchStateConfig } from "match-state-config"
|
||||
import { buildInitialState } from "match-state-engine"
|
||||
import {
|
||||
buildMatchMetadata,
|
||||
buildPersistedState,
|
||||
buildStorageKey,
|
||||
restorePersistedState
|
||||
} from "match-state-serializers"
|
||||
|
||||
describe("match state serializers", () => {
|
||||
it("builds a tournament and bout scoped storage key", () => {
|
||||
expect(buildStorageKey(12, 1007)).toBe("match-state:12:1007")
|
||||
})
|
||||
|
||||
it("builds match metadata for persistence and scoreboard payloads", () => {
|
||||
expect(buildMatchMetadata({
|
||||
tournamentId: 1,
|
||||
boutNumber: 1001,
|
||||
weightLabel: "106",
|
||||
ruleset: "folkstyle_usa",
|
||||
bracketPosition: "Bracket Round of 64",
|
||||
w1Name: "W1",
|
||||
w2Name: "W2",
|
||||
w1School: "School 1",
|
||||
w2School: "School 2"
|
||||
})).toEqual({
|
||||
tournamentId: 1,
|
||||
boutNumber: 1001,
|
||||
weightLabel: "106",
|
||||
ruleset: "folkstyle_usa",
|
||||
bracketPosition: "Bracket Round of 64",
|
||||
w1Name: "W1",
|
||||
w2Name: "W2",
|
||||
w1School: "School 1",
|
||||
w2School: "School 2"
|
||||
})
|
||||
})
|
||||
|
||||
it("builds persisted state with metadata", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const state = buildInitialState(config)
|
||||
state.participantScores = { w1: 7, w2: 3 }
|
||||
|
||||
const persisted = buildPersistedState(state, { tournamentId: 1, boutNumber: 1001 })
|
||||
|
||||
expect(persisted.participantScores).toEqual({ w1: 7, w2: 3 })
|
||||
expect(persisted.metadata).toEqual({ tournamentId: 1, boutNumber: 1001 })
|
||||
})
|
||||
|
||||
it("restores persisted state over initial defaults", () => {
|
||||
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
||||
const restored = restorePersistedState(config, {
|
||||
participantScores: { w1: 4, w2: 1 },
|
||||
assignment: { w1: "red", w2: "green" },
|
||||
clocksByPhase: {
|
||||
period_1: { remainingSeconds: 30 }
|
||||
},
|
||||
timers: {
|
||||
w1: {
|
||||
injury: { remainingSeconds: 50 }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(restored.participantScores).toEqual({ w1: 4, w2: 1 })
|
||||
expect(restored.assignment).toEqual({ w1: "red", w2: "green" })
|
||||
expect(restored.clocksByPhase.period_1.remainingSeconds).toBe(30)
|
||||
expect(restored.clocksByPhase.period_1.durationSeconds).toBe(120)
|
||||
expect(restored.timers.w1.injury.remainingSeconds).toBe(50)
|
||||
expect(restored.timers.w2.injury.remainingSeconds).toBe(90)
|
||||
})
|
||||
})
|
||||
56
test/javascript/match_state/transport.test.js
Normal file
56
test/javascript/match_state/transport.test.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import {
|
||||
loadJson,
|
||||
performIfChanged,
|
||||
removeKey,
|
||||
saveJson
|
||||
} from "match-state-transport"
|
||||
|
||||
describe("match state transport", () => {
|
||||
it("loads saved json from storage", () => {
|
||||
const storage = {
|
||||
getItem: vi.fn(() => '{"score":3}')
|
||||
}
|
||||
|
||||
expect(loadJson(storage, "match-state:1:1001")).toEqual({ score: 3 })
|
||||
expect(storage.getItem).toHaveBeenCalledWith("match-state:1:1001")
|
||||
})
|
||||
|
||||
it("returns null when stored json is invalid", () => {
|
||||
const storage = {
|
||||
getItem: vi.fn(() => "{not-json")
|
||||
}
|
||||
|
||||
expect(loadJson(storage, "bad")).toBe(null)
|
||||
})
|
||||
|
||||
it("saves and removes json values in storage", () => {
|
||||
const storage = {
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn()
|
||||
}
|
||||
|
||||
expect(saveJson(storage, "key", { score: 5 })).toBe(true)
|
||||
expect(storage.setItem).toHaveBeenCalledWith("key", '{"score":5}')
|
||||
|
||||
expect(removeKey(storage, "key")).toBe(true)
|
||||
expect(storage.removeItem).toHaveBeenCalledWith("key")
|
||||
})
|
||||
|
||||
it("only performs subscription actions when the payload changes", () => {
|
||||
const subscription = {
|
||||
perform: vi.fn()
|
||||
}
|
||||
|
||||
const firstSerialized = performIfChanged(subscription, "send_stat", { new_w1_stat: "T3" }, null)
|
||||
const secondSerialized = performIfChanged(subscription, "send_stat", { new_w1_stat: "T3" }, firstSerialized)
|
||||
const thirdSerialized = performIfChanged(subscription, "send_stat", { new_w1_stat: "T3 E1" }, secondSerialized)
|
||||
|
||||
expect(subscription.perform).toHaveBeenCalledTimes(2)
|
||||
expect(subscription.perform).toHaveBeenNthCalledWith(1, "send_stat", { new_w1_stat: "T3" })
|
||||
expect(subscription.perform).toHaveBeenNthCalledWith(2, "send_stat", { new_w1_stat: "T3 E1" })
|
||||
expect(firstSerialized).toBe('{"new_w1_stat":"T3"}')
|
||||
expect(secondSerialized).toBe(firstSerialized)
|
||||
expect(thirdSerialized).toBe('{"new_w1_stat":"T3 E1"}')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user