Files
tg-shuttle-score/test/scoreboard.test.js
T

490 lines
22 KiB
JavaScript
Raw Normal View History

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { createScoreboard } from '../src/scoreboard.js'
function fakeBot() {
const calls = []
let nextId = 500
return {
calls,
last: (fn) => [...calls].reverse().find((c) => c.fn === fn),
async sendMessage(chatId, text, opts) {
calls.push({ fn: 'sendMessage', chatId, text, opts })
return { message_id: nextId++, chat: { id: chatId } }
},
async editMessageText(text, opts) {
calls.push({ fn: 'editMessageText', text, opts })
return true
},
async answerCallbackQuery(id, opts) {
calls.push({ fn: 'answerCallbackQuery', id, opts })
return true
},
}
}
const silentLog = { info() {}, warn() {}, error() {} }
const cq = (messageId, action, fromId = 1) => ({
id: 'q',
data: `sc:${action}`,
from: { id: fromId },
message: { chat: { id: -100 }, message_id: messageId },
})
test('/記分 無參數:列出今日出席,依 1→4 點選後自動開局(1、2 一隊,3、4 一隊)', async () => {
const bot = fakeBot()
const sb = createScoreboard(bot, {
dataFile: null,
log: silentLog,
loadAttendees: async () => ['建喵', '柏威', 'Gary', '小念', '蓉蓉'],
})
await sb.start(-100, 1, undefined)
const sent = bot.last('sendMessage')
assert.match(sent.text, /選擇上場人員/)
const buttons = sent.opts.reply_markup.inline_keyboard.flat().map((b) => b.text)
assert.deepEqual(buttons.slice(0, 5), ['建喵', '柏威', 'Gary', '小念', '蓉蓉'])
assert.ok(buttons.includes('✖️ 取消'))
const mid = sent.chatId === -100 && 500
await sb.onCallback(cq(mid, 'pick0'))
await sb.onCallback(cq(mid, 'pick0'))
assert.equal(bot.last('answerCallbackQuery').opts.text, '這位已經選了')
let edit = bot.last('editMessageText')
assert.match(edit.text, /🅰️ 1️⃣ 建喵 2️⃣ —/)
assert.match(edit.text, /🅱️ 4️⃣ — 3️⃣ —/)
assert.ok(edit.opts.reply_markup.inline_keyboard.flat().some((b) => b.text === '1️⃣ 建喵'))
await sb.onCallback(cq(mid, 'pick2'))
edit = bot.last('editMessageText')
assert.ok(edit.opts.reply_markup.inline_keyboard.flat().some((b) => b.text === '▶️ 單打開始'), '2 人時可單打開始')
await sb.onCallback(cq(mid, 'unpick'))
edit = bot.last('editMessageText')
assert.match(edit.text, /2️⃣ —/)
await sb.onCallback(cq(mid, 'pick1'))
await sb.onCallback(cq(mid, 'pick3'))
await sb.onCallback(cq(mid, 'pick4'))
edit = bot.last('editMessageText')
assert.match(edit.text, /羽球記分板/, '第 4 人選完自動變成記分板')
assert.match(edit.text, /🅰️ 建喵 \/ 柏威/)
assert.match(edit.text, /🅱️ 蓉蓉 \/ 小念/, '下排照 web 版 4 3 排列')
const match = sb.store.get(-100, mid)
assert.deepEqual(match.teams.map((t) => t.players), [['建喵', '柏威'], ['小念', '蓉蓉']])
assert.equal(match.phase, undefined)
})
test('/記分 無參數:選 2 人按單打開始;沒人報名時提示', async () => {
const bot = fakeBot()
const sb = createScoreboard(bot, { dataFile: null, log: silentLog, loadAttendees: async () => ['A', 'B', 'C'] })
await sb.start(-100, 1, '')
await sb.onCallback(cq(500, 'pick2'))
await sb.onCallback(cq(500, 'pick0'))
await sb.onCallback(cq(500, 'pickgo'))
assert.deepEqual(sb.store.get(-100, 500).teams.map((t) => t.players), [['C'], ['A']])
const bot2 = fakeBot()
const sb2 = createScoreboard(bot2, { dataFile: null, log: silentLog, loadAttendees: async () => [] })
await sb2.start(-100, 1, undefined)
assert.match(bot2.last('sendMessage').text, /今天還沒有人報名/)
})
test('結束要兩段確認:第一次按只詢問,取消可回復,再按確定才結束', async () => {
const bot = fakeBot()
const sb = createScoreboard(bot, { dataFile: null, log: silentLog })
await sb.start(-100, 1, 'A vs B')
await sb.onCallback(cq(500, 'srv0'))
await sb.onCallback(cq(500, 'pt0'))
await sb.onCallback(cq(500, 'end'))
let edit = bot.last('editMessageText')
assert.match(edit.text, /確定要結束嗎/)
const texts = edit.opts.reply_markup.inline_keyboard.flat().map((b) => b.text)
assert.ok(texts.includes('✅ 確定結束') && texts.includes('↩️ 取消'))
assert.ok(!texts.includes('🏁 結束'))
assert.ok(sb.store.get(-100, 500), '尚未結束')
await sb.onCallback(cq(500, 'endcancel'))
edit = bot.last('editMessageText')
assert.ok(!/確定要結束嗎/.test(edit.text))
assert.ok(edit.opts.reply_markup.inline_keyboard.flat().some((b) => b.text === '🏁 結束'))
await sb.onCallback(cq(500, 'end'))
await sb.onCallback(cq(500, 'pt1'), '按其他按鈕會取消確認')
edit = bot.last('editMessageText')
assert.ok(!/確定要結束嗎/.test(edit.text))
assert.equal(sb.store.get(-100, 500).score[1], 1)
await sb.onCallback(cq(500, 'end'))
await sb.onCallback(cq(500, 'end'))
edit = bot.last('editMessageText')
assert.match(edit.text, /已結束/)
assert.deepEqual(edit.opts.reply_markup.inline_keyboard, [])
assert.equal(sb.store.get(-100, 500), null)
})
test('權限:只有開局者或群組管理員能按,其他人只跳提示;舊資料無 ownerId 不限制', async () => {
const bot = fakeBot()
const admins = new Set([99])
const sb = createScoreboard(bot, {
dataFile: null,
log: silentLog,
isAdmin: async (_chatId, userId) => admins.has(userId),
})
await sb.start(-100, 1, 'A vs B', 1) // 開局者 user 1
assert.equal(sb.store.get(-100, 500).ownerId, 1)
await sb.onCallback(cq(500, 'srv0', 2)) // 路人
assert.equal(bot.last('answerCallbackQuery').opts.text, '只有開局者或群組管理員可以操作記分板')
assert.equal(sb.store.get(-100, 500).server, null, '狀態不變')
await sb.onCallback(cq(500, 'srv0', 99)) // 管理員
assert.equal(sb.store.get(-100, 500).server, 0)
await sb.onCallback(cq(500, 'pt0', 1)) // 開局者
assert.deepEqual(sb.store.get(-100, 500).score, [1, 0])
// 選人階段同樣受限,且開局者會帶到記分板
const sb2 = createScoreboard(bot, { dataFile: null, log: silentLog, loadAttendees: async () => ['A', 'B'], isAdmin: async () => false })
await sb2.start(-100, 1, undefined, 7)
await sb2.onCallback(cq(501, 'pick0', 8))
assert.equal(sb2.store.get(-100, 501).picked.length, 0)
await sb2.onCallback(cq(501, 'pick0', 7))
await sb2.onCallback(cq(501, 'pick1', 7))
await sb2.onCallback(cq(501, 'pickgo', 7))
assert.equal(sb2.store.get(-100, 501).ownerId, 7)
// 舊資料沒有 ownerId → 任何人可按
sb.store.set(-100, 600, { ...sb.store.get(-100, 500), ownerId: undefined })
await sb.onCallback(cq(600, 'pt1', 2))
assert.deepEqual(sb.store.get(-100, 600).score, [1, 1])
})
test('歷史戰績:分出勝負後「確定結束」或「再一局」會存一次;未結算的結束不存;失敗不結束', async () => {
const bot = fakeBot()
const saved = []
let fail = false
const saveHistory = async (m) => {
if (fail) throw new Error('db down')
saved.push({ score: [...m.score], teams: m.teams.map((t) => [...t.players]) })
return saved.length
}
const sb = createScoreboard(bot, { dataFile: null, log: silentLog, saveHistory })
// 未結算就結束 → 不存
await sb.start(-100, 1, 'A vs B')
await sb.onCallback(cq(500, 'srv0'))
await sb.onCallback(cq(500, 'pt0'))
await sb.onCallback(cq(500, 'end'))
assert.match(bot.last('editMessageText').text, /比賽未結算,不會寫入戰績/)
await sb.onCallback(cq(500, 'end'))
assert.equal(saved.length, 0)
assert.match(bot.last('editMessageText').text, /已結束(未結算,未存入戰績)/)
// 結算後再一局 → 存一筆,並開新局
await sb.start(-100, 1, 'A vs B')
await sb.onCallback(cq(501, 'srv0'))
await sb.onCallback(cq(501, 'pt0'))
await sb.onCallback(cq(501, 'pt1'))
await sb.onCallback(cq(501, 'pt1'))
await sb.onCallback(cq(501, 'settle'))
await sb.onCallback(cq(501, 'reset'))
assert.equal(saved.length, 1)
assert.deepEqual(saved[0].score, [1, 2])
assert.match(bot.last('answerCallbackQuery').opts.text, /戰績 #1 已存/)
assert.deepEqual(sb.store.get(-100, 501).score, [0, 0])
assert.equal(sb.store.get(-100, 501).historyId, undefined)
// 新局打到分出勝負 → 確定結束存第二筆;DB 失敗時不結束
const m = sb.store.get(-100, 501)
await sb.onCallback(cq(501, 'srv1'))
m.score = [0, 20]
await sb.onCallback(cq(501, 'pt1'))
assert.equal(m.finished, true)
fail = true
await sb.onCallback(cq(501, 'end'))
assert.match(bot.last('editMessageText').text, /會把這場寫入歷史戰績/)
await sb.onCallback(cq(501, 'end'))
assert.match(bot.last('answerCallbackQuery').opts.text, /寫入歷史戰績失敗/)
assert.ok(sb.store.get(-100, 501), '失敗時不結束')
fail = false
await sb.onCallback(cq(501, 'end'))
assert.equal(saved.length, 2)
assert.deepEqual(saved[1].score, [0, 21])
assert.match(bot.last('editMessageText').text, /已存入歷史戰績 #2/)
assert.equal(sb.store.get(-100, 501), null)
})
test('選人中按取消 → 訊息鎖定並移除狀態', async () => {
const bot = fakeBot()
const sb = createScoreboard(bot, { dataFile: null, log: silentLog, loadAttendees: async () => ['A', 'B'] })
await sb.start(-100, 1, undefined)
await sb.onCallback(cq(500, 'end'))
assert.match(bot.last('editMessageText').text, /已取消選人/)
assert.equal(sb.store.get(-100, 500), null)
})
// 固定亂數(mulberry32
function seeded(seed) {
let a = seed >>> 0
return () => {
a = (a + 0x6d2b79f5) >>> 0
let t = a
t = Math.imul(t ^ (t >>> 15), t | 1)
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}
const TEN = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']
const TEN_SKILL = { A: 9, B: 9, C: 9, D: 9, E: 1, F: 1, G: 1, H: 1, I: 5, J: 5 }
const TEN_ID = Object.fromEntries(TEN.map((name, i) => [name, 11 + i])) // A=11 … J=20
const TEN_MEMBERS = TEN.map((name) => ({ name, skill: TEN_SKILL[name], userId: TEN_ID[name] }))
const buttonTexts = (call) => call.opts.reply_markup.inline_keyboard.flat().map((b) => b.text)
const playersOf = (m) => m.teams.flatMap((t) => t.players)
test('🎲 隨機配對:預設全選 → 可切換 → 依戰績配對預覽 → 開記分板推出 2、3 號場地,計分人從休息者抽', async () => {
const bot = fakeBot()
const todayGames = [{ teams: [['A', 'B'], ['C', 'D']] }, { teams: [['E', 'F'], ['G', 'H']] }] // A~H 剛打完
const sb = createScoreboard(bot, {
dataFile: null,
log: silentLog,
loadAttendees: async () => TEN_MEMBERS,
loadTodayGames: async () => todayGames,
isAdmin: async (_chatId, userId) => userId === 99,
random: seeded(1),
})
await sb.start(-100, 1, undefined, 7) // 開局者 7 不在出席名單
assert.ok(buttonTexts(bot.last('sendMessage')).includes('🎲 隨機配對'))
assert.deepEqual(sb.store.get(-100, 500).skills, TEN_SKILL, '出席名單的 skill 帶進來')
assert.deepEqual(sb.store.get(-100, 500).userIds, TEN_ID, '出席名單的 user id 帶進來')
await sb.onCallback(cq(500, 'rand', 7))
let entry = sb.store.get(-100, 500)
assert.equal(entry.phase, 'roster')
assert.deepEqual(entry.selected, TEN.map(() => true), '預設全選')
let edit = bot.last('editMessageText')
assert.match(edit.text, /已選 10 \/ 10/)
assert.ok(buttonTexts(edit).includes('✅ A'))
assert.ok(buttonTexts(edit).includes('⬜ 全不選'))
await sb.onCallback(cq(500, 'tg8', 7)) // 取消勾 I
assert.equal(sb.store.get(-100, 500).selected[8], false)
edit = bot.last('editMessageText')
assert.match(edit.text, /已選 9 \/ 10/)
assert.ok(buttonTexts(edit).includes('⬜ I'))
assert.ok(buttonTexts(edit).includes('✅ 全選'))
await sb.onCallback(cq(500, 'tg8', 7)) // 勾回
await sb.onCallback(cq(500, 'rall', 7)) // 全不選
assert.equal(sb.store.get(-100, 500).selected.filter(Boolean).length, 0)
await sb.onCallback(cq(500, 'rgo', 7))
assert.equal(bot.last('answerCallbackQuery').opts.text, '至少要選 4 人')
assert.equal(sb.store.get(-100, 500).phase, 'roster')
await sb.onCallback(cq(500, 'rall', 7)) // 全選
await sb.onCallback(cq(500, 'rgo', 7))
entry = sb.store.get(-100, 500)
assert.equal(entry.phase, 'paired')
assert.deepEqual(entry.games.map((g) => g.court), [2, 3])
const active = entry.games.flatMap((g) => [...g.teamA, ...g.teamB])
assert.ok(active.includes('I') && active.includes('J'), '沒打過的 I、J 一定上場')
assert.equal(entry.resting.length, 2)
assert.ok(entry.resting.every((n) => 'ABCDEFGH'.includes(n)), '休息的是剛打完的人')
const scorers = entry.games.map((g) => g.scorer)
assert.ok(scorers.every((s) => s && entry.resting.includes(s)), '計分人從休息者抽')
assert.notEqual(scorers[0], scorers[1], '一人負責一個場地')
edit = bot.last('editMessageText')
assert.match(edit.text, /2️⃣ 號場地 🅰️ .+ vs 🅱️ .+ 📝 計分:/)
assert.match(edit.text, /3️⃣ 號場地/)
assert.match(edit.text, /😴 休息:/)
assert.ok(buttonTexts(edit).includes('✅ 開記分板') && buttonTexts(edit).includes('🔁 重新配對'))
await sb.onCallback(cq(500, 'rgo', 7)) // 重新配對
entry = sb.store.get(-100, 500)
assert.equal(entry.phase, 'paired')
await sb.onCallback(cq(500, 'pgo', 8)) // 路人
assert.equal(bot.last('answerCallbackQuery').opts.text, '只有開局者或群組管理員可以操作記分板')
bot.calls.length = 0
await sb.onCallback(cq(500, 'pgo', 7))
const sent = bot.calls.filter((c) => c.fn === 'sendMessage')
assert.equal(sent.length, 2, '2、3 號場地各一則記分板')
assert.match(sent[0].text, /羽球記分板 2️⃣ 號場地/)
assert.match(sent[1].text, /羽球記分板 3️⃣ 號場地/)
const m2 = sb.store.get(-100, 501)
const m3 = sb.store.get(-100, 502)
assert.equal(m2.court, 2)
assert.equal(m3.court, 3)
assert.deepEqual(m2.teams.map((t) => t.players), [entry.games[0].teamA, entry.games[0].teamB])
assert.equal(m2.server, null)
const scorer2 = entry.games[0].scorer
assert.equal(m2.scorer.name, scorer2)
assert.equal(m2.ownerId, TEN_ID[scorer2], '權限給抽到的計分人,不是開局者')
assert.equal(m3.ownerId, TEN_ID[entry.games[1].scorer])
assert.match(sent[0].text, new RegExp(`📝 計分:${scorer2}`))
assert.equal(sb.store.get(-100, 500), null, '預覽訊息狀態移除')
edit = bot.last('editMessageText')
assert.match(edit.text, /記分板已推出/)
assert.deepEqual(edit.opts.reply_markup.inline_keyboard, [])
// 開局者(7)不是計分人 → 不能按;計分人本人可以
await sb.onCallback(cq(501, 'srv0', 7))
assert.equal(bot.last('answerCallbackQuery').opts.text, '只有開局者或群組管理員可以操作記分板')
assert.equal(m2.server, null)
await sb.onCallback(cq(501, 'swapab', TEN_ID[scorer2]))
assert.deepEqual(sb.store.get(-100, 501).teams.map((t) => t.players), [entry.games[0].teamB, entry.games[0].teamA])
await sb.onCallback(cq(501, 'srv0', 99)) // 管理員也可以
await sb.onCallback(cq(501, 'pt0', TEN_ID[scorer2]))
assert.deepEqual(sb.store.get(-100, 501).score, [1, 0])
})
test('🙋 換人計分:只有計分人 / 管理員能操作、只能在開始計分前;可轉交給場外的人或改成任何人', async () => {
const bot = fakeBot()
const sb = createScoreboard(bot, {
dataFile: null,
log: silentLog,
loadAttendees: async () => TEN_MEMBERS,
loadTodayGames: async () => [],
isAdmin: async (_chatId, userId) => userId === 99,
scorerExclude: new Set(['J']),
random: seeded(3),
})
await sb.start(-100, 1, undefined, 7)
await sb.onCallback(cq(500, 'rand', 7))
await sb.onCallback(cq(500, 'rgo', 7))
const entry = sb.store.get(-100, 500)
assert.ok(entry.games.every((g) => g.scorer !== 'J'), '排除名單不當計分人')
await sb.onCallback(cq(500, 'pgo', 7))
const m = sb.store.get(-100, 501)
const scorer = m.scorer.name
const scorerId = m.ownerId
assert.equal(scorerId, TEN_ID[scorer])
const players = playersOf(m)
// 路人 / 開局者不能開換人
await sb.onCallback(cq(501, 'ho', 7))
assert.equal(bot.last('answerCallbackQuery').opts.text, '只有開局者或群組管理員可以操作記分板')
assert.equal(m.handover, undefined)
// 計分人開換人:候選 = 出席 − 場上 4 人 − 排除名單
await sb.onCallback(cq(501, 'ho', scorerId))
let edit = bot.last('editMessageText')
assert.match(edit.text, new RegExp(`選擇新的計分人.*目前:${scorer}`))
const candidates = m.handover.map((c) => c.name)
assert.ok(candidates.every((n) => !players.includes(n)), '場上的人不列')
assert.ok(!candidates.includes('J'), '排除名單不列')
assert.ok(!candidates.includes(scorer), '現任計分人自己不列')
assert.deepEqual(candidates, TEN.filter((n) => !players.includes(n) && n !== 'J' && n !== scorer))
const texts = buttonTexts(edit)
assert.ok(texts.includes('👥 任何人(不鎖權限)') && texts.includes('↩️ 取消'))
assert.ok(!texts.includes('🅰️ 先發球'))
// 取消 → 回到開局鍵盤
await sb.onCallback(cq(501, 'hocancel', scorerId))
assert.equal(m.handover, undefined)
assert.ok(buttonTexts(bot.last('editMessageText')).includes('🙋 換人計分'))
assert.equal(m.ownerId, scorerId)
// 換人中按別的鍵也會關掉換人面板
await sb.onCallback(cq(501, 'ho', scorerId))
await sb.onCallback(cq(501, 'swapab', scorerId))
assert.equal(m.handover, undefined)
assert.ok(buttonTexts(bot.last('editMessageText')).includes('🅰️ 先發球'))
// 轉交給候選第一位 → 權限換人,原計分人不能再按
await sb.onCallback(cq(501, 'ho', scorerId))
const next = m.handover[0]
await sb.onCallback(cq(501, 'ho0', scorerId))
assert.equal(bot.last('answerCallbackQuery').opts.text, `計分人已改為 ${next.name}`)
assert.equal(m.ownerId, TEN_ID[next.name])
assert.equal(m.scorer.name, next.name)
assert.match(bot.last('editMessageText').text, new RegExp(`📝 計分:${next.name}`))
await sb.onCallback(cq(501, 'srv0', scorerId))
assert.equal(bot.last('answerCallbackQuery').opts.text, '只有開局者或群組管理員可以操作記分板')
assert.equal(m.server, null)
// 管理員改成任何人 → 不鎖權限,開局者也能按
await sb.onCallback(cq(501, 'ho', 99))
await sb.onCallback(cq(501, 'hoany', 99))
assert.equal(m.ownerId, null)
assert.deepEqual(m.scorer, { name: null, userId: null })
assert.match(bot.last('editMessageText').text, /📝 計分:任何人/)
await sb.onCallback(cq(501, 'srv0', 7))
assert.equal(m.server, 0)
// 開始計分後不能換人
await sb.onCallback(cq(501, 'ho', 7))
assert.equal(bot.last('answerCallbackQuery').opts.text, '開始計分後不能換人計分')
assert.equal(m.handover, undefined)
// 沒開換人面板就按候選 → 提示
await sb.onCallback(cq(501, 'ho0', 7))
assert.equal(bot.last('answerCallbackQuery').opts.text, '請重新按「換人計分」')
})
test('🎲 隨機配對:返回 / 取消;4~7 人只推 2 號場地;休息者都被排除 → 任何人;讀不到戰績時照配並提示', async () => {
const bot = fakeBot()
const sb = createScoreboard(bot, {
dataFile: null,
log: silentLog,
loadAttendees: async () => ['A', 'B', 'C', 'D', 'E'],
loadTodayGames: async () => {
throw new Error('db down')
},
scorerExclude: new Set(['A', 'B', 'C', 'D', 'E']),
random: seeded(2),
})
await sb.start(-100, 1, undefined)
await sb.onCallback(cq(500, 'rand'))
await sb.onCallback(cq(500, 'rback'))
assert.equal(sb.store.get(-100, 500).phase, 'pick')
assert.match(bot.last('editMessageText').text, /選擇上場人員/)
await sb.onCallback(cq(500, 'rand'))
await sb.onCallback(cq(500, 'rgo'))
let entry = sb.store.get(-100, 500)
assert.equal(entry.phase, 'paired')
assert.equal(entry.games.length, 1)
assert.equal(entry.games[0].court, 2)
assert.equal(entry.resting.length, 1)
assert.equal(entry.games[0].scorer, null, '休息的人在排除名單 → 沒有計分人')
let edit = bot.last('editMessageText')
assert.match(edit.text, /⚠️ 讀不到今日戰績/)
assert.match(edit.text, /📝 計分:任何人/)
await sb.onCallback(cq(500, 'rback'))
entry = sb.store.get(-100, 500)
assert.equal(entry.phase, 'roster')
assert.equal(entry.games, undefined)
assert.deepEqual(entry.selected, [true, true, true, true, true], '勾選狀態保留')
await sb.onCallback(cq(500, 'rgo'))
bot.calls.length = 0
await sb.onCallback(cq(500, 'pgo'))
const sent = bot.calls.filter((c) => c.fn === 'sendMessage')
assert.equal(sent.length, 1)
const m = sb.store.get(-100, 501)
assert.equal(m.ownerId, null, '沒有計分人 → 不鎖權限')
assert.deepEqual(m.scorer, { name: null, userId: null })
assert.match(sent[0].text, /📝 計分:任何人/)
await sb.onCallback(cq(501, 'srv1', 12345)) // 任何人都能按
assert.equal(m.server, 1)
// 0:0 按上一步 → 退回選先發球,才能換人
await sb.onCallback(cq(501, 'undo', 12345))
assert.equal(m.server, null)
assert.ok(buttonTexts(bot.last('editMessageText')).includes('🙋 換人計分'))
// 換人面板:出席的人都被排除 → 只剩任何人 / 取消
await sb.onCallback(cq(501, 'ho', 12345))
assert.equal(bot.last('answerCallbackQuery').opts.text, '今天沒有其他出席的人可選,可改成「任何人」')
assert.deepEqual(buttonTexts(bot.last('editMessageText')), ['👥 任何人(不鎖權限)', '↩️ 取消'])
await sb.onCallback(cq(501, 'hocancel', 12345))
const bot2 = fakeBot()
const sb2 = createScoreboard(bot2, { dataFile: null, log: silentLog, loadAttendees: async () => ['A', 'B', 'C', 'D'] })
await sb2.start(-100, 1, undefined)
await sb2.onCallback(cq(500, 'rand'))
await sb2.onCallback(cq(500, 'end'))
assert.match(bot2.last('editMessageText').text, /已取消配對/)
assert.equal(sb2.store.get(-100, 500), null)
})