Compare commits
8 Commits
7f52d81bbf
...
8c52f1761e
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c52f1761e | |||
| 805021dcf5 | |||
| 1fdf059261 | |||
| 58554c3c61 | |||
| c4599a2722 | |||
| e179295af3 | |||
| d11b47c809 | |||
| 0441b0c7f5 |
@@ -91,3 +91,4 @@ Il volume `./.segnapunti` persiste lo stato tra i riavvii del container.
|
|||||||
- Qualsiasi modifica al protocollo WebSocket va fatta in `src/websocket-handler.js`.
|
- Qualsiasi modifica al protocollo WebSocket va fatta in `src/websocket-handler.js`.
|
||||||
- Aggiungere test unit in `tests/unit/gameState.test.js` per ogni nuovo action type o regola di gioco.
|
- Aggiungere test unit in `tests/unit/gameState.test.js` per ogni nuovo action type o regola di gioco.
|
||||||
- Il codice deve essere scritto in italiano per commenti e nomi di variabili di dominio (es. `servHome`, `striscia`, `nomi`), ma in inglese per nomi tecnici standard (`state`, `action`, `handler`).
|
- Il codice deve essere scritto in italiano per commenti e nomi di variabili di dominio (es. `servHome`, `striscia`, `nomi`), ma in inglese per nomi tecnici standard (`state`, `action`, `handler`).
|
||||||
|
- Non inserire emoji in codice, UI (label, testi dei bottoni, banner) o commenti, a meno che l'utente non le richieda esplicitamente.
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Banner modalità time out attiva -->
|
<!-- Banner modalità time out attiva -->
|
||||||
<div class="timeout-banner" v-if="state.timeout">⏱ MODALITÀ TIME OUT ATTIVA</div>
|
<div class="timeout-banner" v-if="state.timeout">
|
||||||
|
TIME OUT {{ state.sp.nomi[state.timeoutTeam] }} — {{ timeoutCountdown }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ══════════════════ MODALITÀ MOBILE ══════════════════ -->
|
<!-- ══════════════════ MODALITÀ MOBILE ══════════════════ -->
|
||||||
<template v-if="!isEstesa">
|
<template v-if="!isEstesa">
|
||||||
@@ -67,8 +69,9 @@
|
|||||||
<button class="btn btn-ctrl" @click="openCambiTeam()">
|
<button class="btn btn-ctrl" @click="openCambiTeam()">
|
||||||
Cambi
|
Cambi
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-ctrl btn-timeout" :class="{ active: state.timeout }" @click="sendAction({ type: 'toggleTimeout' })">
|
<button class="btn btn-ctrl btn-timeout" :class="{ active: state.timeout }"
|
||||||
⏱ Time Out
|
@click="state.timeout ? terminaTimeout() : openTimeout()">
|
||||||
|
{{ state.timeout ? timeoutCountdown : 'Time Out' }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" @click="confirmReset = true">
|
<button class="btn btn-danger" @click="confirmReset = true">
|
||||||
Reset
|
Reset
|
||||||
@@ -141,14 +144,15 @@
|
|||||||
|
|
||||||
<!-- Barra azioni secondarie -->
|
<!-- Barra azioni secondarie -->
|
||||||
<div class="e-actions">
|
<div class="e-actions">
|
||||||
<button class="btn e-act e-act--undo" @click="sendAction({ type: 'decPunt' })">↩ Annulla</button>
|
<button class="btn e-act e-act--undo" @click="sendAction({ type: 'decPunt' })">Annulla</button>
|
||||||
<button class="btn e-act" :disabled="!isPunteggioZeroZero" @click="sendAction({ type: 'cambiaPalla' })">Cambio Palla</button>
|
<button class="btn e-act" :disabled="!isPunteggioZeroZero" @click="sendAction({ type: 'cambiaPalla' })">Cambio Palla</button>
|
||||||
<button class="btn e-act" @click="openCambiTeam()">Cambi</button>
|
<button class="btn e-act" @click="openCambiTeam()">Cambi</button>
|
||||||
<button class="btn e-act" @click="sendAction({ type: 'toggleOrder' })">Inverti</button>
|
<button class="btn e-act" @click="sendAction({ type: 'toggleOrder' })">Inverti</button>
|
||||||
<button class="btn e-act" @click="sendAction({ type: 'toggleStriscia' })">Striscia {{ state.visuStriscia ? 'ON' : 'OFF' }}</button>
|
<button class="btn e-act" @click="sendAction({ type: 'toggleStriscia' })">Striscia {{ state.visuStriscia ? 'ON' : 'OFF' }}</button>
|
||||||
<button class="btn e-act" @click="speak()">Voce</button>
|
<button class="btn e-act" @click="speak()">Voce</button>
|
||||||
<button class="btn e-act" @click="openConfig()">Config</button>
|
<button class="btn e-act" @click="openConfig()">Config</button>
|
||||||
<button class="btn e-act btn-timeout" :class="{ active: state.timeout }" @click="sendAction({ type: 'toggleTimeout' })">⏱ Time Out</button>
|
<button class="btn e-act btn-timeout" :class="{ active: state.timeout }"
|
||||||
|
@click="state.timeout ? terminaTimeout() : openTimeout()">{{ state.timeout ? timeoutCountdown : 'Time Out' }}</button>
|
||||||
<button class="btn e-act e-act--danger" @click="confirmReset = true">Reset</button>
|
<button class="btn e-act e-act--danger" @click="confirmReset = true">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,6 +281,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Finestra avvio time out: squadra + scelta video -->
|
||||||
|
<div class="overlay" v-if="showTimeoutModal" @click.self="showTimeoutModal = false">
|
||||||
|
<div class="dialog dialog-timeout">
|
||||||
|
<div class="dialog-title">Time Out</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Squadra che lo ha richiesto</label>
|
||||||
|
<div class="dialog-buttons">
|
||||||
|
<button class="btn btn-set home-bg" :class="{ 'team-sel-active': timeoutTeamSel === 'home' }"
|
||||||
|
@click="timeoutTeamSel = 'home'">{{ state.sp.nomi.home }}</button>
|
||||||
|
<button class="btn btn-set guest-bg" :class="{ 'team-sel-active': timeoutTeamSel === 'guest' }"
|
||||||
|
@click="timeoutTeamSel = 'guest'">{{ state.sp.nomi.guest }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Video (30s nominali)</label>
|
||||||
|
<div class="timeout-video-list">
|
||||||
|
<label class="timeout-video-option" v-for="file in spotList" :key="file">
|
||||||
|
<input type="radio" name="timeoutVideo" :value="file" v-model="timeoutVideoSel" />
|
||||||
|
{{ file }}
|
||||||
|
<span class="video-duration">{{ formatDurata(spotDurations[file]) }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="timeout-video-option">
|
||||||
|
<input type="radio" name="timeoutVideo" :value="null" v-model="timeoutVideoSel" />
|
||||||
|
Nessun video (overlay "TIME OUT")
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-buttons">
|
||||||
|
<button class="btn btn-cancel" @click="showTimeoutModal = false">Annulla</button>
|
||||||
|
<button class="btn btn-confirm" :disabled="!timeoutTeamSel" @click="avviaTimeout()">Avvia</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -304,6 +345,13 @@ export default {
|
|||||||
formGuest: ["1", "2", "3", "4", "5", "6"],
|
formGuest: ["1", "2", "3", "4", "5", "6"],
|
||||||
},
|
},
|
||||||
isLandscape: window.innerWidth > window.innerHeight,
|
isLandscape: window.innerWidth > window.innerHeight,
|
||||||
|
showTimeoutModal: false,
|
||||||
|
timeoutTeamSel: null,
|
||||||
|
timeoutVideoSel: null,
|
||||||
|
spotList: [],
|
||||||
|
spotDurations: {},
|
||||||
|
nowTick: Date.now(),
|
||||||
|
timeoutTicker: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -338,6 +386,17 @@ export default {
|
|||||||
}
|
}
|
||||||
return hasComplete
|
return hasComplete
|
||||||
},
|
},
|
||||||
|
timeoutRemaining() {
|
||||||
|
if (!this.state.timeout || !this.state.timeoutStartedAt) return 30
|
||||||
|
const trascorsi = (this.nowTick - this.state.timeoutStartedAt) / 1000
|
||||||
|
return Math.max(0, Math.ceil(30 - trascorsi))
|
||||||
|
},
|
||||||
|
timeoutCountdown() {
|
||||||
|
const r = this.timeoutRemaining
|
||||||
|
const m = String(Math.floor(r / 60)).padStart(2, '0')
|
||||||
|
const s = String(r % 60).padStart(2, '0')
|
||||||
|
return `${m}:${s}`
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
squadraVincente(val) {
|
squadraVincente(val) {
|
||||||
@@ -346,6 +405,12 @@ export default {
|
|||||||
this.showSetVinto = true
|
this.showSetVinto = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'state.timeout': {
|
||||||
|
immediate: true,
|
||||||
|
handler(attivo) {
|
||||||
|
attivo ? this.avviaTimeoutTicker() : this.fermaTimeoutTicker()
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this._resizeHandler = () => { this.isLandscape = window.innerWidth > window.innerHeight }
|
this._resizeHandler = () => { this.isLandscape = window.innerWidth > window.innerHeight }
|
||||||
@@ -355,6 +420,7 @@ export default {
|
|||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
window.removeEventListener('resize', this._resizeHandler)
|
window.removeEventListener('resize', this._resizeHandler)
|
||||||
window.removeEventListener('orientationchange', this._resizeHandler)
|
window.removeEventListener('orientationchange', this._resizeHandler)
|
||||||
|
this.fermaTimeoutTicker()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
generaReferto,
|
generaReferto,
|
||||||
@@ -458,6 +524,47 @@ export default {
|
|||||||
: this.servHome ? `${home} a ${guest}` : `${guest} a ${home}`
|
: this.servHome ? `${home} a ${guest}` : `${guest} a ${home}`
|
||||||
this.sendWs({ type: 'speak', text })
|
this.sendWs({ type: 'speak', text })
|
||||||
},
|
},
|
||||||
|
avviaTimeoutTicker() {
|
||||||
|
this.fermaTimeoutTicker()
|
||||||
|
this.nowTick = Date.now()
|
||||||
|
this.timeoutTicker = setInterval(() => { this.nowTick = Date.now() }, 250)
|
||||||
|
},
|
||||||
|
fermaTimeoutTicker() {
|
||||||
|
if (this.timeoutTicker) { clearInterval(this.timeoutTicker); this.timeoutTicker = null }
|
||||||
|
},
|
||||||
|
async openTimeout() {
|
||||||
|
this.timeoutTeamSel = null
|
||||||
|
this.timeoutVideoSel = null
|
||||||
|
this.spotList = []
|
||||||
|
this.spotDurations = {}
|
||||||
|
this.showTimeoutModal = true
|
||||||
|
try {
|
||||||
|
const res = await fetch('/spot/list')
|
||||||
|
this.spotList = await res.json()
|
||||||
|
} catch {
|
||||||
|
this.spotList = []
|
||||||
|
}
|
||||||
|
this.spotList.forEach(file => this.probeDurata(file))
|
||||||
|
},
|
||||||
|
probeDurata(file) {
|
||||||
|
const video = document.createElement('video')
|
||||||
|
video.preload = 'metadata'
|
||||||
|
video.onloadedmetadata = () => {
|
||||||
|
this.spotDurations = { ...this.spotDurations, [file]: video.duration }
|
||||||
|
}
|
||||||
|
video.src = '/spot/' + encodeURIComponent(file)
|
||||||
|
},
|
||||||
|
formatDurata(sec) {
|
||||||
|
return typeof sec === 'number' && isFinite(sec) ? `${Math.round(sec)}s` : '…'
|
||||||
|
},
|
||||||
|
avviaTimeout() {
|
||||||
|
if (!this.timeoutTeamSel) return
|
||||||
|
this.sendAction({ type: 'startTimeout', team: this.timeoutTeamSel, video: this.timeoutVideoSel })
|
||||||
|
this.showTimeoutModal = false
|
||||||
|
},
|
||||||
|
terminaTimeout() {
|
||||||
|
this.sendAction({ type: 'stopTimeout' })
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1070,4 +1177,37 @@ export default {
|
|||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal avvio time out */
|
||||||
|
.dialog-timeout {
|
||||||
|
max-height: 85vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.btn-set.team-sel-active {
|
||||||
|
box-shadow: 0 0 0 3px rgba(255,255,255,0.6) inset;
|
||||||
|
}
|
||||||
|
.timeout-video-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 40vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.timeout-video-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
border: 1px solid rgba(255,255,255,0.12);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.video-duration {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 12px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -113,10 +113,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Overlay time out: proietta i video spot a tutto schermo -->
|
<!-- Overlay time out: proietta il video spot a tutto schermo -->
|
||||||
<div v-if="state.timeout" class="timeout-overlay">
|
<div v-if="state.timeout" class="timeout-overlay">
|
||||||
<video v-if="spotList.length" ref="spotVideo" class="timeout-video" playsinline></video>
|
<div class="timeout-countdown">{{ timeoutCountdown }}</div>
|
||||||
<div v-else class="timeout-placeholder">TIME OUT</div>
|
<video v-if="state.timeoutVideo && !mostraPlaceholder" ref="spotVideo" class="timeout-video"
|
||||||
|
:class="{ 'timeout-video--fading': timeoutFading }" playsinline></video>
|
||||||
|
<div v-else class="timeout-placeholder">
|
||||||
|
TIME OUT<br />{{ state.sp.nomi[state.timeoutTeam] }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -129,16 +133,38 @@ export default {
|
|||||||
mixins: [createWsMixin('display')],
|
mixins: [createWsMixin('display')],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
spotList: [], // filename dei video spot da riprodurre
|
mostraPlaceholder: false, // video terminato prima dei 30s: mostra l'immagine "TIME OUT"
|
||||||
spotIndex: 0, // video corrente nella sequenza
|
spotVideoDuration: null, // durata reale del video in riproduzione, nota dopo il loadedmetadata
|
||||||
|
nowTick: Date.now(),
|
||||||
|
timeoutTicker: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.isMobile()) {
|
if (this.isMobile()) {
|
||||||
try { document.documentElement.requestFullscreen() } catch (e) {}
|
try { document.documentElement.requestFullscreen() } catch (e) {}
|
||||||
}
|
}
|
||||||
|
if (this.state.timeout) this.avviaTimeout()
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.fermaTimeoutTicker()
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
timeoutRemaining() {
|
||||||
|
if (!this.state.timeout || !this.state.timeoutStartedAt) return 30
|
||||||
|
const trascorsi = (this.nowTick - this.state.timeoutStartedAt) / 1000
|
||||||
|
return Math.max(0, Math.ceil(30 - trascorsi))
|
||||||
|
},
|
||||||
|
timeoutCountdown() {
|
||||||
|
const r = this.timeoutRemaining
|
||||||
|
const m = String(Math.floor(r / 60)).padStart(2, '0')
|
||||||
|
const s = String(r % 60).padStart(2, '0')
|
||||||
|
return `${m}:${s}`
|
||||||
|
},
|
||||||
|
timeoutFading() {
|
||||||
|
// Video più lungo di 30s: dissolvenza negli ultimi istanti prima dello stop del server.
|
||||||
|
if (!this.spotVideoDuration || this.spotVideoDuration <= 30) return false
|
||||||
|
return this.timeoutRemaining <= 1
|
||||||
|
},
|
||||||
stricciaStrip() {
|
stricciaStrip() {
|
||||||
const currentSet = this.state.sp.striscia.at(-1)
|
const currentSet = this.state.sp.striscia.at(-1)
|
||||||
if (!currentSet) return { home: [], guest: [] }
|
if (!currentSet) return { home: [], guest: [] }
|
||||||
@@ -168,32 +194,41 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
avviaTimeoutTicker() {
|
||||||
|
this.fermaTimeoutTicker()
|
||||||
|
this.nowTick = Date.now()
|
||||||
|
this.timeoutTicker = setInterval(() => { this.nowTick = Date.now() }, 200)
|
||||||
|
},
|
||||||
|
fermaTimeoutTicker() {
|
||||||
|
if (this.timeoutTicker) { clearInterval(this.timeoutTicker); this.timeoutTicker = null }
|
||||||
|
},
|
||||||
async avviaTimeout() {
|
async avviaTimeout() {
|
||||||
this.spotIndex = 0
|
this.mostraPlaceholder = !this.state.timeoutVideo
|
||||||
try {
|
this.spotVideoDuration = null
|
||||||
const res = await fetch('/spot/list')
|
this.avviaTimeoutTicker()
|
||||||
this.spotList = await res.json()
|
if (!this.state.timeoutVideo) return
|
||||||
} catch {
|
|
||||||
this.spotList = []
|
|
||||||
}
|
|
||||||
if (!this.spotList.length) return
|
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
this.caricaSpotCorrente()
|
this.caricaSpotCorrente()
|
||||||
},
|
},
|
||||||
fermaTimeout() {
|
fermaTimeout() {
|
||||||
|
this.fermaTimeoutTicker()
|
||||||
const video = this.$refs.spotVideo
|
const video = this.$refs.spotVideo
|
||||||
if (video) { video.pause(); video.removeAttribute('src'); video.load() }
|
if (video) { video.pause(); video.removeAttribute('src'); video.load() }
|
||||||
this.spotList = []
|
this.mostraPlaceholder = false
|
||||||
|
this.spotVideoDuration = null
|
||||||
},
|
},
|
||||||
caricaSpotCorrente() {
|
caricaSpotCorrente() {
|
||||||
const video = this.$refs.spotVideo
|
const video = this.$refs.spotVideo
|
||||||
if (!video || !this.spotList.length) return
|
if (!video || !this.state.timeoutVideo) return
|
||||||
video.src = '/spot/' + encodeURIComponent(this.spotList[this.spotIndex])
|
video.src = '/spot/' + encodeURIComponent(this.state.timeoutVideo)
|
||||||
// Avanza al prossimo video alla fine (loop: con un solo video torna su sé stesso)
|
video.onloadedmetadata = () => {
|
||||||
video.onended = () => {
|
this.spotVideoDuration = video.duration
|
||||||
this.spotIndex = (this.spotIndex + 1) % this.spotList.length
|
// Riconnessione a metà time out: riallinea il video al punto corretto.
|
||||||
this.caricaSpotCorrente()
|
const trascorsi = this.state.timeoutStartedAt ? (Date.now() - this.state.timeoutStartedAt) / 1000 : 0
|
||||||
|
if (trascorsi > 0.5 && trascorsi < video.duration) video.currentTime = trascorsi
|
||||||
}
|
}
|
||||||
|
// Video più corto dei 30s: a fine riproduzione mostra l'immagine "TIME OUT" fino allo stop del server.
|
||||||
|
video.onended = () => { this.mostraPlaceholder = true }
|
||||||
this.playSpot()
|
this.playSpot()
|
||||||
},
|
},
|
||||||
playSpot() {
|
playSpot() {
|
||||||
@@ -232,7 +267,8 @@ export default {
|
|||||||
.display-page {
|
.display-page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: #000;
|
background: #111;
|
||||||
|
font-family: 'Inter', system-ui, sans-serif;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +291,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.connection-status.disconnected {
|
.connection-status.disconnected {
|
||||||
background: rgba(255, 50, 50, 0.8);
|
background: #c62828;
|
||||||
color: white;
|
color: white;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@@ -268,11 +304,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.connected .dot {
|
.connected .dot {
|
||||||
background: #4caf50;
|
background: #2e7d32;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disconnected .dot {
|
.disconnected .dot {
|
||||||
background: #f44336;
|
background: #ff6b6b;
|
||||||
animation: blink 1s infinite;
|
animation: blink 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,6 +328,8 @@ export default {
|
|||||||
|
|
||||||
.punt {
|
.punt {
|
||||||
font-size: 60vh;
|
font-size: 60vh;
|
||||||
|
font-weight: 900;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -317,13 +355,42 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
transition: opacity 0.8s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeout-video--fading {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeout-countdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
font-size: 4vh;
|
||||||
|
font-weight: 800;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
padding: 6px 18px;
|
||||||
|
border-radius: 12px;
|
||||||
|
z-index: 501;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeout-placeholder {
|
.timeout-placeholder {
|
||||||
color: #fff;
|
color: #ff9800;
|
||||||
font-size: 20vh;
|
font-size: 14vh;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.3;
|
||||||
|
animation: timeout-pulse 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes timeout-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.55; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+18
-2
@@ -22,6 +22,9 @@ export function createInitialState() {
|
|||||||
visuForm: false,
|
visuForm: false,
|
||||||
visuStriscia: true,
|
visuStriscia: true,
|
||||||
timeout: false,
|
timeout: false,
|
||||||
|
timeoutTeam: null,
|
||||||
|
timeoutVideo: null,
|
||||||
|
timeoutStartedAt: null,
|
||||||
modalitaPartita: "3/5",
|
modalitaPartita: "3/5",
|
||||||
sp: {
|
sp: {
|
||||||
striscia: [{ serv: 'h', ris: '', vinc: null }],
|
striscia: [{ serv: 'h', ris: '', vinc: null }],
|
||||||
@@ -165,8 +168,21 @@ export function applyAction(state, action) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case "toggleTimeout": {
|
case "startTimeout": {
|
||||||
s.timeout = !s.timeout
|
const team = action.team
|
||||||
|
if (team !== 'home' && team !== 'guest') break
|
||||||
|
s.timeout = true
|
||||||
|
s.timeoutTeam = team
|
||||||
|
s.timeoutVideo = action.video ?? null
|
||||||
|
s.timeoutStartedAt = action.startedAt ?? null
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "stopTimeout": {
|
||||||
|
s.timeout = false
|
||||||
|
s.timeoutTeam = null
|
||||||
|
s.timeoutVideo = null
|
||||||
|
s.timeoutStartedAt = null
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+78
-10
@@ -46,11 +46,31 @@ button:focus-visible {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: table;
|
display: table;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
/* Linea centrale che separa i due campi, come un tabellone reale */
|
||||||
|
.campo::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
width: 2px;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.hea {
|
.hea {
|
||||||
float: left;
|
float: left;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
font-size: xx-large;
|
font-size: xx-large;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 18px 4px 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-bottom: 3px solid currentColor;
|
||||||
|
opacity: 0.96;
|
||||||
}
|
}
|
||||||
.hea span {
|
.hea span {
|
||||||
/* Bordo di debug: border: 1px solid #f3fb00; */
|
/* Bordo di debug: border: 1px solid #f3fb00; */
|
||||||
@@ -70,6 +90,27 @@ button:focus-visible {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
.mr3, .ml3 {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 3vh;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 4px 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
.mr3 {
|
||||||
|
margin-right: 14px;
|
||||||
|
}
|
||||||
|
.ml3 {
|
||||||
|
margin-left: 14px;
|
||||||
}
|
}
|
||||||
.tal {
|
.tal {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -110,10 +151,11 @@ button:focus-visible {
|
|||||||
max-width: 50vw;
|
max-width: 50vw;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
text-shadow: 0 0 40px currentColor;
|
||||||
}
|
}
|
||||||
.form {
|
.form {
|
||||||
font-size: 5vh;
|
font-size: 5vh;
|
||||||
border-top: #fff dashed 25px;
|
border-top: 2px solid rgba(255, 255, 255, 0.18);
|
||||||
padding-top: 50px;
|
padding-top: 50px;
|
||||||
}
|
}
|
||||||
.formtit {
|
.formtit {
|
||||||
@@ -125,14 +167,23 @@ button:focus-visible {
|
|||||||
font-size: 20vh;
|
font-size: 20vh;
|
||||||
float: left;
|
float: left;
|
||||||
width: 32%;
|
width: 32%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 3px 0;
|
||||||
|
color: #ddd;
|
||||||
|
font-weight: 700;
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.text-bold {
|
||||||
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
.home {
|
.home {
|
||||||
background-color: black;
|
background-color: transparent;
|
||||||
color: yellow;
|
color: #f5c518;
|
||||||
}
|
}
|
||||||
.guest {
|
.guest {
|
||||||
background-color: blue;
|
background-color: transparent;
|
||||||
color: white
|
color: #2196f3;
|
||||||
}
|
}
|
||||||
.striscia {
|
.striscia {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -141,17 +192,28 @@ button:focus-visible {
|
|||||||
right: 10px;
|
right: 10px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: max-content 1fr;
|
grid-template-columns: max-content 1fr;
|
||||||
row-gap: 2px;
|
row-gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
.striscia-nome {
|
.striscia-nome {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding-right: 6px;
|
padding-right: 10px;
|
||||||
|
font-size: 2.2vh;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
.striscia-items {
|
.striscia-items {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
gap: 2px;
|
||||||
}
|
}
|
||||||
.striscia .item {
|
.striscia .item {
|
||||||
width: 25px;
|
width: 25px;
|
||||||
@@ -159,11 +221,17 @@ button:focus-visible {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: 5px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
.striscia .item:not(.item-vuoto) {
|
.striscia .item:not(.item-vuoto) {
|
||||||
background-color: rgb(206, 247, 3);
|
background-color: #f5c518;
|
||||||
color: blue;
|
color: #111;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
.striscia-items.guest-striscia .item:not(.item-vuoto) {
|
||||||
|
background-color: #2196f3;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.campo-config {
|
.campo-config {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { createInitialState, applyAction } from './gameState.js'
|
|||||||
* @param {WebSocketServer} wss - Istanza del server WebSocket.
|
* @param {WebSocketServer} wss - Istanza del server WebSocket.
|
||||||
* @returns {Object} Oggetto con metodi di gestione dello stato.
|
* @returns {Object} Oggetto con metodi di gestione dello stato.
|
||||||
*/
|
*/
|
||||||
|
const DURATA_TIMEOUT_MS = 30000
|
||||||
|
|
||||||
export function setupWebSocketHandler(wss, options = {}) {
|
export function setupWebSocketHandler(wss, options = {}) {
|
||||||
// Stato globale della partita.
|
// Stato globale della partita.
|
||||||
let gameState = options.initialState ?? createInitialState()
|
let gameState = options.initialState ?? createInitialState()
|
||||||
@@ -12,6 +14,17 @@ export function setupWebSocketHandler(wss, options = {}) {
|
|||||||
// Mappa dei ruoli associati ai client connessi.
|
// Mappa dei ruoli associati ai client connessi.
|
||||||
const clients = new Map() // ws -> { role: 'display' | 'controller' }
|
const clients = new Map() // ws -> { role: 'display' | 'controller' }
|
||||||
|
|
||||||
|
// Timer che chiude automaticamente il time out dopo 30s, indipendentemente
|
||||||
|
// dal controller (sopravvive a refresh/disconnessione/standby del tablet).
|
||||||
|
let timeoutTimer = null
|
||||||
|
|
||||||
|
function annullaTimerTimeout() {
|
||||||
|
if (timeoutTimer) {
|
||||||
|
clearTimeout(timeoutTimer)
|
||||||
|
timeoutTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gestisce i messaggi in arrivo dal client
|
* Gestisce i messaggi in arrivo dal client
|
||||||
*/
|
*/
|
||||||
@@ -87,10 +100,16 @@ export function setupWebSocketHandler(wss, options = {}) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Il momento di inizio del time out lo decide il server (non il client),
|
||||||
|
// cosi' gameState.js resta puro/deterministico e testabile.
|
||||||
|
const action = msg.action.type === 'startTimeout'
|
||||||
|
? { ...msg.action, startedAt: Date.now() }
|
||||||
|
: msg.action
|
||||||
|
|
||||||
// Applica l'azione allo stato della partita.
|
// Applica l'azione allo stato della partita.
|
||||||
const previousState = gameState
|
const previousState = gameState
|
||||||
try {
|
try {
|
||||||
gameState = applyAction(gameState, msg.action)
|
gameState = applyAction(gameState, action)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error applying action:', err)
|
console.error('Error applying action:', err)
|
||||||
sendError(ws, 'Failed to apply action')
|
sendError(ws, 'Failed to apply action')
|
||||||
@@ -98,6 +117,20 @@ export function setupWebSocketHandler(wss, options = {}) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Qualsiasi azione che tocca il time out invalida il timer di auto-chiusura
|
||||||
|
// pendente; se il time out e' (ri)partito, ne arma uno nuovo.
|
||||||
|
if (action.type === 'startTimeout' || action.type === 'stopTimeout') {
|
||||||
|
annullaTimerTimeout()
|
||||||
|
}
|
||||||
|
if (action.type === 'startTimeout' && gameState.timeout) {
|
||||||
|
timeoutTimer = setTimeout(() => {
|
||||||
|
timeoutTimer = null
|
||||||
|
gameState = applyAction(gameState, { type: 'stopTimeout' })
|
||||||
|
broadcastState()
|
||||||
|
options.onStateChange?.(gameState)
|
||||||
|
}, DURATA_TIMEOUT_MS)
|
||||||
|
}
|
||||||
|
|
||||||
// Propaga il nuovo stato a tutti i client connessi.
|
// Propaga il nuovo stato a tutti i client connessi.
|
||||||
broadcastState()
|
broadcastState()
|
||||||
options.onStateChange?.(gameState)
|
options.onStateChange?.(gameState)
|
||||||
|
|||||||
@@ -164,6 +164,117 @@ describe('WebSocket Integration (websocket-handler.js)', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// TIME OUT (timer server-side)
|
||||||
|
// =============================================
|
||||||
|
describe('Time out', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
function inviaStartTimeout(controller, overrides = {}) {
|
||||||
|
controller.emit('message', JSON.stringify({
|
||||||
|
type: 'action',
|
||||||
|
action: { type: 'startTimeout', team: 'home', video: 'spot.mp4', ...overrides }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
it('dovrebbe iniettare startedAt lato server (ignorando quello del client)', () => {
|
||||||
|
vi.setSystemTime(50000)
|
||||||
|
const controller = connectAndRegister(wss, 'controller')
|
||||||
|
|
||||||
|
inviaStartTimeout(controller, { startedAt: 12345 })
|
||||||
|
|
||||||
|
const state = handler.getState()
|
||||||
|
expect(state.timeout).toBe(true)
|
||||||
|
expect(state.timeoutStartedAt).toBe(50000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('dovrebbe chiudere automaticamente il time out dopo 30 secondi', () => {
|
||||||
|
const controller = connectAndRegister(wss, 'controller')
|
||||||
|
const display = connectAndRegister(wss, 'display')
|
||||||
|
|
||||||
|
inviaStartTimeout(controller)
|
||||||
|
expect(handler.getState().timeout).toBe(true)
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(29999)
|
||||||
|
expect(handler.getState().timeout).toBe(true)
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(1)
|
||||||
|
const state = handler.getState()
|
||||||
|
expect(state.timeout).toBe(false)
|
||||||
|
expect(state.timeoutTeam).toBe(null)
|
||||||
|
expect(state.timeoutVideo).toBe(null)
|
||||||
|
expect(state.timeoutStartedAt).toBe(null)
|
||||||
|
|
||||||
|
// La chiusura automatica viene broadcastata ai client
|
||||||
|
const msg = lastSent(display)
|
||||||
|
expect(msg.type).toBe('state')
|
||||||
|
expect(msg.state.timeout).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('la chiusura automatica dovrebbe persistere lo stato (onStateChange)', () => {
|
||||||
|
const onStateChange = vi.fn()
|
||||||
|
wss = new MockWebSocketServer()
|
||||||
|
handler = setupWebSocketHandler(wss, { onStateChange })
|
||||||
|
const controller = connectAndRegister(wss, 'controller')
|
||||||
|
|
||||||
|
inviaStartTimeout(controller)
|
||||||
|
onStateChange.mockClear()
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(30000)
|
||||||
|
expect(onStateChange).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onStateChange.mock.calls[0][0].timeout).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('stopTimeout manuale dovrebbe annullare il timer pendente', () => {
|
||||||
|
const controller = connectAndRegister(wss, 'controller')
|
||||||
|
|
||||||
|
inviaStartTimeout(controller)
|
||||||
|
controller.emit('message', JSON.stringify({
|
||||||
|
type: 'action',
|
||||||
|
action: { type: 'stopTimeout' }
|
||||||
|
}))
|
||||||
|
expect(handler.getState().timeout).toBe(false)
|
||||||
|
controller.send.mockClear()
|
||||||
|
|
||||||
|
// Allo scadere dei 30s non deve partire nessun broadcast fantasma
|
||||||
|
vi.advanceTimersByTime(30000)
|
||||||
|
expect(controller.send).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('un nuovo startTimeout dovrebbe riarmare il timer da capo', () => {
|
||||||
|
const controller = connectAndRegister(wss, 'controller')
|
||||||
|
|
||||||
|
inviaStartTimeout(controller)
|
||||||
|
vi.advanceTimersByTime(20000)
|
||||||
|
|
||||||
|
// Riavvio: il timer riparte da zero
|
||||||
|
inviaStartTimeout(controller, { team: 'guest' })
|
||||||
|
vi.advanceTimersByTime(20000)
|
||||||
|
expect(handler.getState().timeout).toBe(true)
|
||||||
|
expect(handler.getState().timeoutTeam).toBe('guest')
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(10000)
|
||||||
|
expect(handler.getState().timeout).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('startTimeout con squadra non valida non dovrebbe armare il timer', () => {
|
||||||
|
const controller = connectAndRegister(wss, 'controller')
|
||||||
|
|
||||||
|
inviaStartTimeout(controller, { team: 'arbitro' })
|
||||||
|
expect(handler.getState().timeout).toBe(false)
|
||||||
|
controller.send.mockClear()
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(30000)
|
||||||
|
expect(controller.send).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// =============================================
|
// =============================================
|
||||||
// BROADCAST MULTI-CLIENT
|
// BROADCAST MULTI-CLIENT
|
||||||
// =============================================
|
// =============================================
|
||||||
|
|||||||
@@ -80,8 +80,11 @@ describe('Game Logic (gameState.js)', () => {
|
|||||||
expect(state.visuStriscia).toBe(true)
|
expect(state.visuStriscia).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('dovrebbe avere timeout false', () => {
|
it('dovrebbe avere timeout false e campi timeout nulli', () => {
|
||||||
expect(state.timeout).toBe(false)
|
expect(state.timeout).toBe(false)
|
||||||
|
expect(state.timeoutTeam).toBe(null)
|
||||||
|
expect(state.timeoutVideo).toBe(null)
|
||||||
|
expect(state.timeoutStartedAt).toBe(null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -484,12 +487,50 @@ describe('Game Logic (gameState.js)', () => {
|
|||||||
expect(s.order).toBe(false)
|
expect(s.order).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('toggleTimeout: false → true → false', () => {
|
})
|
||||||
expect(state.timeout).toBe(false)
|
|
||||||
const s1 = applyAction(state, { type: 'toggleTimeout' })
|
// =============================================
|
||||||
expect(s1.timeout).toBe(true)
|
// TIME OUT (startTimeout, stopTimeout)
|
||||||
const s2 = applyAction(s1, { type: 'toggleTimeout' })
|
// =============================================
|
||||||
expect(s2.timeout).toBe(false)
|
describe('Time out', () => {
|
||||||
|
it('startTimeout dovrebbe attivare il time out con squadra, video e inizio', () => {
|
||||||
|
const s = applyAction(state, { type: 'startTimeout', team: 'home', video: 'spot.mp4', startedAt: 1000 })
|
||||||
|
expect(s.timeout).toBe(true)
|
||||||
|
expect(s.timeoutTeam).toBe('home')
|
||||||
|
expect(s.timeoutVideo).toBe('spot.mp4')
|
||||||
|
expect(s.timeoutStartedAt).toBe(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('startTimeout senza video dovrebbe lasciare timeoutVideo null', () => {
|
||||||
|
const s = applyAction(state, { type: 'startTimeout', team: 'guest', startedAt: 1000 })
|
||||||
|
expect(s.timeout).toBe(true)
|
||||||
|
expect(s.timeoutTeam).toBe('guest')
|
||||||
|
expect(s.timeoutVideo).toBe(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('startTimeout senza squadra valida dovrebbe essere ignorato', () => {
|
||||||
|
for (const team of [undefined, null, 'arbitro', '']) {
|
||||||
|
const s = applyAction(state, { type: 'startTimeout', team, video: 'spot.mp4', startedAt: 1000 })
|
||||||
|
expect(s.timeout).toBe(false)
|
||||||
|
expect(s.timeoutTeam).toBe(null)
|
||||||
|
expect(s.timeoutVideo).toBe(null)
|
||||||
|
expect(s.timeoutStartedAt).toBe(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('stopTimeout dovrebbe azzerare tutti i campi del time out', () => {
|
||||||
|
const attivo = applyAction(state, { type: 'startTimeout', team: 'home', video: 'spot.mp4', startedAt: 1000 })
|
||||||
|
const s = applyAction(attivo, { type: 'stopTimeout' })
|
||||||
|
expect(s.timeout).toBe(false)
|
||||||
|
expect(s.timeoutTeam).toBe(null)
|
||||||
|
expect(s.timeoutVideo).toBe(null)
|
||||||
|
expect(s.timeoutStartedAt).toBe(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('stopTimeout su time out già inattivo non dovrebbe avere effetti', () => {
|
||||||
|
const s = applyAction(state, { type: 'stopTimeout' })
|
||||||
|
expect(s.timeout).toBe(false)
|
||||||
|
expect(s.timeoutTeam).toBe(null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user