Files
gen1recomp/tests/online_panel_c1.lua
2026-09-24 05:17:32 -04:00

905 lines
36 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = love or require("tests.love_stub")
local OnlinePanel = require("src.import.OnlinePanel")
local Connect = require("src.online.Connect")
local Client = require("src.online.Client")
local Kit = require("src.ui.kit.Kit")
local Layout = require("src.ui.kit.Layout")
local Transition = require("src.ui.kit.Transition")
local Icons = require("src.ui.kit.Icons")
local VirtualKeyboard = require("src.ui.kit.VirtualKeyboard")
local DiscordPresence = require("src.core.DiscordPresence")
local function patch(tbl, fields)
local keys, saved = {}, {}
for k, v in pairs(fields) do
keys[#keys + 1] = k
saved[k] = tbl[k]
tbl[k] = v
end
return function()
for _, k in ipairs(keys) do tbl[k] = saved[k] end
end
end
local function newImp(ready)
return { ready = ready or { red = true }, activeSlot = {}, slots = {},
pulse = 0, _pages = {}, _uiActions = {}, _actAt = {} }
end
local RED_PROFILE = { engine = 1, version = "red", kind = "vanilla",
fingerprint = "abc", rule = { partySize = 1 } }
local function setUp(imp)
local st = OnlinePanel.state(imp)
st.version, st.slotId, st.setupDone = "red", "slot1", true
st.team = { { where = "party", index = 1 } }
st.profiles["red|vanilla|-"] = { profile = RED_PROFILE }
local SAVE = { party = { { species = "PIKACHU", level = 20, moves = {} } } }
st.slotRead = { key = "red|slot1|-",
data = { generation = 1, party = SAVE.party, save = SAVE } }
return st
end
local function drawWith(fn, width, height)
Kit.layout(width or 1280, height or 800)
Kit.beginFrame(-1, -1, false, 0)
local m = Layout.metrics(1200)
local ok, res = pcall(fn, m)
Kit.endFrame()
if not ok then print(res) end
return ok, res, m
end
do
local want = { lock = true, ["lock-open"] = true, mail = true }
for _, name in ipairs(Icons.NAMES) do want[name] = nil end
T.eq(next(want), nil, "the atlas names lock, lock-open and mail")
local f = io.open("assets/launcher/lucide/icons.png", "rb")
local head = f and f:read(24) or ""
if f then f:close() end
local b1, b2, b3, b4 = head:byte(17, 20)
local width = ((b1 or 0) * 16777216) + ((b2 or 0) * 65536)
+ ((b3 or 0) * 256) + (b4 or 0)
T.eq(width, #Icons.NAMES * 96, "and the atlas has one 96px cell per name")
end
T.eq(OnlinePanel.sanitizePin("12a3 4567"), "1234", "a PIN keeps four digits")
T.eq(OnlinePanel.sanitizePin("x"), "", "and nothing else")
T.check(OnlinePanel.pinValid("0000"), "0000 is a valid PIN")
T.check(not OnlinePanel.pinValid("123"), "three digits is not")
T.check(not OnlinePanel.pinValid("12a4"), "letters are not")
do
Connect.reset()
local sentProfiles, sentPresence, connected = nil, nil, nil
local restore = patch(Client, {
state = function() return "online" end,
setProfiles = function(list) sentProfiles = list return list end,
setPresence = function(p) sentPresence = p return true end,
connect = function(opts) connected = opts return true end,
})
local ok = Connect.start({ source = "game", version = "firered",
profiles = { { version = "firered" } },
presence = { where = "game", status = "busy", version = "firered" } })
T.check(ok, "Connect.start while online answers true")
T.eq(connected, nil, "without a second hello")
T.eq(sentProfiles and sentProfiles[1].version, "firered",
"it only swaps the profiles")
T.eq(sentPresence and sentPresence.where, "game", "and the presence")
restore()
end
do
Connect.reset()
local connected, configured = nil, nil
local restoreLinked = patch(Connect, { linked = function() return false end,
storedName = function() return "RED#417" end,
persistName = function() return true end })
local restore = patch(Client, {
state = function() return "offline" end,
configure = function(opts)
if opts and opts.relayAddress then configured = opts.relayAddress end
return configured
end,
connect = function(opts) connected = opts return true end,
})
T.check(Connect.start({ source = "launcher", version = "red",
profiles = { RED_PROFILE },
presence = { where = "launcher", status = "idle", version = "red" },
relayAddress = "127.0.0.1:17999" }), "an offline start connects")
T.eq(configured, "127.0.0.1:17999", "on the relay address it was given")
T.eq(connected and connected.name, "RED#417", "with the stored name")
T.eq(connected and connected.presence and connected.presence.where,
"launcher", "announcing where the player is")
T.eq(connected and connected.profiles and connected.profiles[1], RED_PROFILE,
"and the profiles")
T.eq(Connect.name(), "RED#417", "Connect.name reports it")
restore()
restoreLinked()
end
do
Connect.reset()
local connected = nil
local polls = 0
local sync = {
lobbyTicket = function(_, name) return { name = name } end,
poll = function()
polls = polls + 1
if polls < 2 then return { status = "pending" } end
return { status = "ok", data = { ticket = "tk1", expiresAt = 9e12 } }
end,
release = function() end,
}
local restoreLinked = patch(Connect, { linked = function() return true end,
storedName = function() return "BLUE#222" end,
persistName = function() return true end })
local restore = patch(Client, {
state = function() return "offline" end,
configure = function() return "127.0.0.1:1" end,
connect = function(opts) connected = opts return true end,
})
T.check(Connect.start({ source = "launcher", syncClient = sync }),
"a linked player asks for a ticket first")
T.eq(Connect.state(), "ticket", "and Connect says so")
T.check(Connect.busy(), "which counts as busy")
T.eq(connected, nil, "nothing is dialled yet")
Connect.update(0)
T.eq(connected, nil, "while the ticket is pending")
Connect.update(0)
T.eq(connected and connected.ticket, "tk1", "then the hello carries the ticket")
T.eq(Connect.ticketState(), "ok", "and the ticket is kept")
restore()
restoreLinked()
end
do
local restore = patch(Client, {
upgradeRequired = function() return { text = "Please update." } end,
state = function() return "error" end,
})
T.eq(Connect.error(), "Please update.", "upgrade_required text is the error")
Client.upgradeRequired = function() return {} end
T.eq(Connect.error(), "This build is too old for online play. Please update.",
"with a clear line when the relay sends none")
restore()
local ok, why = Connect.setName("ab")
T.check(not ok, "a two-letter name is refused")
T.eq(why, "Names are 3 to 16 characters.", "saying why")
end
do
local imp = newImp({ red = true, firered = true })
T.eq(OnlinePanel.gameName("firered"), "FireRed", "rows say FireRed")
T.eq(OnlinePanel.gameName("leafgreen"), "LeafGreen", "and LeafGreen")
T.eq(OnlinePanel.whereText({ where = "union" }), "Union Room",
"a Union Room trainer is shown there")
T.eq(OnlinePanel.whereText({ where = "direct" }), "Direct Corner",
"and a Direct Corner one")
local row = OnlinePanel.playerRow(imp, { id = "a1", name = "LEAF", engine = 3,
version = "leafgreen", where = "union", status = "idle", online = true }, "me")
T.eq(row.reason, nil, "an idle FireRed-engine trainer can be invited")
T.eq(row.engine, 3, "on engine 3")
local busy = OnlinePanel.playerRow(imp, { id = "a2", name = "X", engine = 1,
version = "red", where = "game", status = "busy" }, "me")
T.check(busy.reason ~= nil, "a trainer inside a game cannot be")
local gold = OnlinePanel.playerRow(imp, { id = "a3", name = "G", engine = 2,
version = "gold", where = "launcher", status = "idle" }, "me")
T.check(gold.reason ~= nil, "nor one whose game this machine lacks")
T.check(OnlinePanel.entryPasses("gen3", { engine = 3 }), "the Gen 3 chip")
T.check(not OnlinePanel.entryPasses("gen1", { engine = 3 }),
"Gen 1 no longer swallows Gen 3")
local found = false
for _, f in ipairs(OnlinePanel.FILTERS) do
if f.id == "gen3" then found = true end
end
T.check(found, "FILTERS gains a Gen 3 chip")
end
do
local imp = newImp()
local st = setUp(imp)
local ROOM = "r00000000000000aa"
local joins = {}
local pending
local now = 1000
local restore = patch(Client, {
joinRoom = function(room, as, profile, pin)
joins[#joins + 1] = { room = room, as = as, pin = pin }
pending = { id = room, done = false }
return pending
end,
serverTime = function() return now end,
setProfiles = function(list) return list end,
room = function() return nil end,
tournament = function() return nil end,
lobby = function() return {} end,
})
OnlinePanel.go(imp, "play")
T.check(OnlinePanel.startJoin(imp, { room = ROOM, locked = true,
name = "BLUE" }), "joining a locked row")
T.check(OnlinePanel.pinModal(imp) ~= nil, "opens the PIN modal")
T.eq(imp._onlineFocus, OnlinePanel.PIN_FIELD, "with the field focused")
T.eq(#joins, 0, "and sends nothing yet")
OnlinePanel.fieldType(imp, OnlinePanel.PIN_FIELD, "12a")
T.eq(OnlinePanel.pinModal(imp).pin, "12", "the field takes digits only")
T.check(not OnlinePanel.pinSubmit(imp), "two digits do not submit")
T.eq(OnlinePanel.pinModal(imp).error, "A PIN is 4 digits.", "saying why")
OnlinePanel.fieldType(imp, OnlinePanel.PIN_FIELD, "3456")
T.eq(OnlinePanel.pinModal(imp).pin, "1234", "and stops at four")
OnlinePanel.fieldBack(imp, OnlinePanel.PIN_FIELD)
OnlinePanel.fieldType(imp, OnlinePanel.PIN_FIELD, "9")
T.check(OnlinePanel.pinSubmit(imp), "ENTER sends the join")
T.eq(joins[1] and joins[1].pin, "1239", "carrying the PIN")
T.eq(joins[1] and joins[1].room, ROOM, "for the listed room")
T.eq(OnlinePanel.pinModal(imp), nil, "the modal closes")
T.eq(OnlinePanel.screen(imp), "room", "and the Room screen waits")
pending.error, pending.reason, pending.triesLeft, pending.done =
"Wrong PIN.", "bad_pin", 3, true
OnlinePanel.update(imp, 1 / 60)
local mo = OnlinePanel.pinModal(imp)
T.check(mo ~= nil, "a wrong PIN brings the modal back")
T.eq(mo and mo.error, "That PIN didn't match. 3 tries left.",
"with a one-line error")
T.eq(OnlinePanel.screen(imp), "play", "over the list")
T.eq(mo and mo.pin, "1239", "the field keeps the PIN that missed")
T.check(OnlinePanel.pinFailed(imp, { reason = "bad_pin", triesLeft = 1 }),
"a last miss reopens the modal")
T.eq(OnlinePanel.pinModal(imp).error, "That PIN didn't match. 1 try left.",
"saying try, not tries")
OnlinePanel.fieldBack(imp, OnlinePanel.PIN_FIELD)
OnlinePanel.fieldBack(imp, OnlinePanel.PIN_FIELD)
OnlinePanel.fieldBack(imp, OnlinePanel.PIN_FIELD)
OnlinePanel.fieldBack(imp, OnlinePanel.PIN_FIELD)
OnlinePanel.fieldType(imp, OnlinePanel.PIN_FIELD, "0000")
OnlinePanel.pinSubmit(imp)
T.eq(#joins, 2, "a second try goes out")
T.eq(joins[2] and joins[2].pin, "0000", "with the retyped PIN")
pending.error, pending.reason, pending.retryAt, pending.done =
"Locked.", "pin_locked", now + 600000, true
OnlinePanel.update(imp, 1 / 60)
T.check(OnlinePanel.pinLocked(imp), "five misses lock the room")
T.eq(OnlinePanel.pinModal(imp).error, "Too many tries. Try again in 10 min.",
"and the modal says for how long")
OnlinePanel.fieldType(imp, OnlinePanel.PIN_FIELD, "1111")
T.check(not OnlinePanel.pinSubmit(imp), "ENTER stays off while locked")
T.eq(#joins, 2, "nothing more is sent")
now = now + 600001
T.check(not OnlinePanel.pinLocked(imp), "the lockout ends on the relay clock")
local ok = drawWith(function(m)
return require("src.import.online.PinModal").draw(imp, m)
end)
T.check(ok, "the PIN modal draws")
T.check(OnlinePanel.back(imp), "Back closes the modal")
T.eq(OnlinePanel.pinModal(imp), nil, "and it is gone")
restore()
OnlinePanel.home(imp)
end
do
local imp = newImp({ red = true, firered = true })
local st = setUp(imp)
local invites, replies = {}, {}
local restore = patch(Client, {
invite = function(to, activity, detail, profile)
local h = { to = to, activity = activity, state = "sending" }
invites[#invites + 1] = { to = to, activity = activity, detail = detail,
profile = profile, handle = h }
return h
end,
replyInvite = function(id, accept)
replies[#replies + 1] = { id = id, accept = accept }
return true
end,
serverTime = function() return 5000 end,
setProfiles = function(list) return list end,
room = function() return nil end,
tournament = function() return nil end,
})
local gen1 = OnlinePanel.activitiesFor(imp, { engine = 1 })
T.eq(table.concat(gen1, ","), "battle_single,trade",
"a Gen 1 trainer can be asked to battle or trade")
local gen3 = OnlinePanel.activitiesFor(imp, { engine = 3 })
T.eq(table.concat(gen3, ","), "battle_single,battle_double,trade",
"a Gen 3 trainer also gets a double battle")
T.eq(#OnlinePanel.activitiesFor(imp, { engine = 2 }), 0,
"a game this machine lacks offers nothing")
local player = { id = "0badf00d", name = "BLUE#123", engine = 1,
version = "red" }
T.check(OnlinePanel.invitePickerOpen(imp, player), "INVITE opens the picker")
T.check(imp._invitePicker ~= nil, "as a modal")
local ok = drawWith(function(m)
return require("src.import.online.InvitePicker").draw(imp, m)
end)
T.check(ok, "which draws")
T.check(OnlinePanel.startInvite(imp, player, "battle_single"),
"picking Battle sends the invite")
T.eq(imp._invitePicker, nil, "and closes the picker")
T.eq(invites[1] and invites[1].to, "0badf00d", "to that trainer")
T.eq(invites[1] and invites[1].activity, "battle_single", "for a battle")
T.eq(invites[1] and invites[1].profile and invites[1].profile.version, "red",
"with the picked game's profile")
T.eq(st.status, "Invite sent to BLUE#123.", "and a line saying so")
invites[1].handle.id = "i00000000000000a1"
OnlinePanel.inviteClosed(imp, { id = "i00000000000000a1", why = "declined" })
T.check(st.status ~= nil and st.status ~= "Invite sent to BLUE#123.",
"a decline replaces it with one line")
T.eq(st.outgoing, nil, "and clears the outgoing invite")
local fresh = newImp({ red = true })
local fst = OnlinePanel.state(fresh)
fst.version = "red"
T.check(OnlinePanel.startInvite(fresh, player, "battle_single"),
"inviting before any setup")
T.eq(OnlinePanel.screen(fresh), "wizard", "walks the setup wizard first")
T.eq(OnlinePanel.wizard(fresh).kind, "invite", "an invite wizard")
local INV = { id = "i00000000000000b2", activity = "battle_single",
from = { id = "0badf00d", name = "BLUE#123" }, detail = {},
expiresAt = 5000 + 20000 }
OnlinePanel.inviteIn(imp, INV)
OnlinePanel.inviteIn(imp, INV)
T.eq(#st.invites, 1, "a repeated invite_in is one toast")
local toast = OnlinePanel.toast(imp)
T.check(toast ~= nil, "an invite shows a toast")
T.eq(OnlinePanel.inviteLine(toast), "BLUE#123 invites you to a battle.",
"reading who and what")
T.eq(OnlinePanel.inviteLeft(toast), 1, "with a full countdown bar")
local tok, drew = drawWith(function(m)
return require("src.import.online.Toast").draw(imp, m, 80, false)
end)
T.check(tok and drew, "the toast draws on any tab")
T.check(imp._toastRect ~= nil, "and records its rect for occlusion")
T.check(OnlinePanel.toastAction(imp, "accept"), "ACCEPT")
T.eq(replies[1] and replies[1].accept, true, "replies yes")
T.eq(replies[1] and replies[1].id, INV.id, "to that invite")
OnlinePanel.pruneInvites(imp)
T.eq(OnlinePanel.toast(imp), nil, "and the toast goes")
local INV2 = { id = "i00000000000000c3", activity = "trade",
from = { id = "0badf00d", name = "BLUE#123" }, detail = {},
expiresAt = 5000 + 20000 }
OnlinePanel.inviteIn(imp, INV2)
T.check(OnlinePanel.toastAction(imp, "decline"), "DECLINE")
T.eq(replies[2] and replies[2].accept, false, "replies no")
OnlinePanel.pruneInvites(imp)
T.eq(OnlinePanel.toast(imp), nil, "and the toast goes")
local INV3 = { id = "i00000000000000d4", activity = "battle_single",
from = { id = "0badf00d", name = "BLUE#123" }, detail = {},
expiresAt = 4000 }
OnlinePanel.inviteIn(imp, INV3)
OnlinePanel.pruneInvites(imp)
T.eq(OnlinePanel.toast(imp), nil, "an expired invite never shows")
local INV4 = { id = "i00000000000000e5", activity = "battle_single",
from = { id = "0badf00d", name = "BLUE#123" }, detail = {},
expiresAt = 5000 + 20000 }
local unset = newImp({ red = true })
local ust = OnlinePanel.state(unset)
ust.version = "red"
OnlinePanel.inviteIn(unset, INV4)
T.check(OnlinePanel.toastAction(unset, "accept"),
"accepting before any setup")
T.eq(OnlinePanel.wizard(unset) and OnlinePanel.wizard(unset).kind, "accept",
"opens the accept wizard")
T.eq(#replies, 2, "without answering yet")
setUp(unset)
OnlinePanel.wizardTo(unset, "summary")
OnlinePanel.wizardNext(unset)
T.eq(replies[3] and replies[3].id, INV4.id, "the wizard's confirm accepts it")
T.eq(replies[3] and replies[3].accept, true, "with a yes")
restore()
end
do
Kit.layout(1280, 800)
local x, y, w, h = Kit.toastRect(1280, "BLUE invites you to a battle.", 60, 1)
T.check(x + w <= 1280, "the toast sits inside the window")
T.check(x > 640, "top-right")
local sx = Kit.toastRect(1280, "BLUE invites you to a battle.", 60, 0)
T.check(sx >= 1280, "fully slid out starts past the right edge")
local bx = x + w - 20
local by = y + h - 30
Kit.beginFrame(bx, by, true, 0)
Kit.occlude(x, y, w, h)
T.check(not Kit.hit(bx, by, 4, 4), "controls under the toast lose the tap")
local action = Kit.toast({ id = "t", text = "BLUE invites you to a battle.",
W = 1280, top = 60, slide = 1, progress = 0.5 })
T.eq(action, "accept", "while the toast's own ACCEPT takes it")
Kit.occlude(nil)
Kit.endFrame()
Kit.beginFrame(x + 20, by, true, 0)
action = Kit.toast({ id = "t", text = "x", W = 1280, top = 60, slide = 1 })
T.eq(action, "decline", "DECLINE sits on the left")
Kit.endFrame()
Kit.beginFrame(x + 20, by, true, 0)
action = Kit.toast({ id = "t", text = "x", W = 1280, top = 60, slide = 1,
blocked = true })
T.eq(action, nil, "a toast under the loader takes nothing")
Kit.endFrame()
end
do
local armed, reduce = Transition.armed, Transition.reduceMotion
Transition.armed, Transition.reduceMotion = true, false
Transition.start("toast", "in", { duration = 0.2 })
T.check(Transition.get("toast") ~= nil, "the toast slides in")
T.check(not Transition.active(), "without blocking the rest of the launcher")
Transition.reduceMotion = true
Transition.start("toast", "in", { duration = 0.2 })
T.eq(Transition.progress("toast"), 1, "reduce motion snaps it in")
Transition.armed, Transition.reduceMotion = armed, reduce
Transition.reset()
end
do
VirtualKeyboard.active, VirtualKeyboard.mode = true, 4
VirtualKeyboard.maxLen, VirtualKeyboard.text = 4, ""
T.eq(VirtualKeyboard.currentRows()[1][1], "1", "the PIN pad is a number pad")
VirtualKeyboard.textinput("12a3456")
T.eq(VirtualKeyboard.text, "1234", "typing into it keeps four digits")
VirtualKeyboard.active, VirtualKeyboard.mode = false, 1
VirtualKeyboard.maxLen, VirtualKeyboard.text = nil, ""
end
do
local TOKEN = ("0123456789abcdef"):rep(2)
local st = DiscordPresence._state
DiscordPresence.setJoinSecret(TOKEN:upper(), 1, 2)
T.eq(st.joinSecret, TOKEN, "the join secret is the invite token")
DiscordPresence.setJoinSecret("ABC123", 1, 2)
T.eq(st.joinSecret, nil, "a room code is never advertised")
DiscordPresence.setJoinSecret(TOKEN, 1, 2)
DiscordPresence.setJoinCode(nil)
T.eq(st.joinSecret, nil, "the old clear call still clears it")
local returned = nil
local game = { returnToLauncher = function(o) returned = o end,
stack = { top = function() return nil end } }
st.game, st.activity = game, "exploring"
DiscordPresence.handleJoinRequest("i:" .. TOKEN)
T.eq(returned and returned.tab, "online", "a join returns to the ONLINE tab")
T.eq(returned and returned.invite, TOKEN, "carrying the invite token")
returned = nil
DiscordPresence.handleJoinRequest("m:ABC123")
T.eq(returned, nil, "an old code secret joins nothing")
local g3 = { generation = 3, returnToLauncher = function(o) returned = o end }
st.game = g3
package.loaded["src.core.game3.link"] = { link = {} }
DiscordPresence.handleJoinRequest("i:" .. TOKEN)
T.eq(returned, nil, "a Gen 3 game mid-link is left alone")
package.loaded["src.core.game3.link"] = nil
DiscordPresence.handleJoinRequest("i:" .. TOKEN)
T.eq(returned and returned.invite, TOKEN, "and joins once it is free")
st.game, st.activity = nil, "menu"
local handed = nil
DiscordPresence.joinHandler = function(token) handed = token end
DiscordPresence.handleJoinRequest(TOKEN)
T.eq(handed, TOKEN, "the launcher's own handler takes a bare token")
DiscordPresence.joinHandler = nil
local imp = newImp()
OnlinePanel.state(imp)
local restore = patch(Client, {
state = function() return "online" end,
joinRoomByInvite = function(token, as)
handed = { token = token, as = as }
return { done = false }
end,
setProfiles = function(list) return list end,
room = function() return nil end,
tournament = function() return nil end,
lobby = function() return {} end,
})
OnlinePanel._discordInvite = TOKEN
OnlinePanel.update(imp, 1 / 60)
T.eq(type(handed) == "table" and handed.token, TOKEN,
"the panel joins by the token on its next update")
restore()
local minted = {}
local restoreMint = patch(Client, {
inviteToken = function(room) minted[#minted + 1] = room return true end,
serverTime = function() return 1000 end,
})
local ROOM = { room = "r00000000000000ab", stage = "waiting", seats = 2,
players = { { id = "me" } } }
st.enabled = false
OnlinePanel.clearPresence()
OnlinePanel.pushPresence(ROOM)
T.eq(#minted, 0, "no token is minted while Discord is not running")
st.enabled = true
OnlinePanel.pushPresence(ROOM)
T.eq(minted[1], ROOM.room, "a waiting room asks the relay for a token")
OnlinePanel.pushPresence(ROOM)
T.eq(#minted, 1, "once")
OnlinePanel._tokenFor.token = TOKEN
OnlinePanel._tokenFor.expiresAt = 1000 + 1800000
OnlinePanel.pushPresence(ROOM)
T.eq(st.joinSecret, TOKEN, "the token becomes the Discord join secret")
T.eq(st.partyMax, 2, "with the room's seat count")
ROOM.players[2] = { id = "them" }
OnlinePanel.pushPresence(ROOM)
T.eq(st.joinSecret, nil, "a full room stops advertising")
st.enabled = false
restoreMint()
end
do
local imp = newImp()
local st = setUp(imp)
local joined, created = nil, nil
local restore = patch(Client, {
joinTournament = function(opts) joined = opts return { done = false } end,
createTournament = function(opts) created = opts return { done = false } end,
setProfiles = function(list) return list end,
})
T.check(OnlinePanel.joinTournament(imp, { code = "tq2ra3" }, "spectator"),
"a private tournament code joins")
T.eq(joined and joined.code, "TQ2RA3", "upper-cased")
T.eq(joined and joined.tour, nil, "with no id")
T.check(OnlinePanel.joinTournament(imp, { tour = "t00000000000000aa" },
"spectator"), "a listed tournament joins by id")
T.eq(joined and joined.tour, "t00000000000000aa", "by its id")
T.eq(joined and joined.code, nil, "and no code")
st.tourPublic, st.tourPlaying = false, false
OnlinePanel.hostTournament(imp)
T.eq(created and created.public, false, "a private tournament is created")
st.tourPublic, st.tourPlaying = true, true
restore()
end
do
local imp = newImp({ red = true, firered = true })
local st = setUp(imp)
local TOUR = { tour = "t00000000000000aa", code = "TQ2RA3", creator = "me",
stage = "registering", players = { { id = "me", name = "RED" } },
spectators = {}, bracket = {} }
local ROOM = { room = "r00000000000000aa", host = "me", stage = "waiting",
intent = "battle", locked = true, seats = 2,
profile = RED_PROFILE, players = { { id = "me", name = "RED" } },
spectators = {} }
local LOBBY = {}
for i = 1, 20 do
LOBBY[i] = { id = ("%08x"):format(i), name = "T" .. i, online = true,
where = (i % 3 == 0) and "union" or "launcher", status = "idle",
engine = (i % 2 == 0) and 3 or 1,
version = (i % 2 == 0) and "firered" or "red",
room = ("r%016x"):format(i), locked = i % 4 == 0, open = true,
intent = "battle", stage = "waiting", spectators = 0,
profile = RED_PROFILE }
end
local currentTour, currentRoom = nil, ROOM
local restore = patch(Client, {
state = function() return "online" end,
you = function() return { id = "me", name = "RED" } end,
lobby = function() return LOBBY end,
openRooms = function() return LOBBY end,
watchable = function() return LOBBY end,
counts = function() return { players = 21, openRooms = 20 } end,
room = function() return currentRoom end,
tournament = function() return currentTour end,
serverTime = function() return 0 end,
})
local function screenDraw(id)
return drawWith(function(m)
return OnlinePanel.buildOnlinePanel(imp, m.contentX, m.top + 100,
m.contentW, math.max(200, m.h - 200), m)
end)
end
OnlinePanel.go(imp, "play")
st.list = "players"
OnlinePanel.invalidate(imp, "lobby")
OnlinePanel.refresh(imp)
T.check(#OnlinePanel.cache(imp).players > 0, "the Trainers list fills")
Kit.audit = {}
T.check(screenDraw("play"), "Play draws the Trainers list")
local sawInvite = false
for _, row in ipairs(Kit.audit) do
if row.label == "Invite" then sawInvite = true end
end
Kit.audit = nil
T.check(sawInvite, "with an Invite action per trainer")
st.list = "rooms"
T.check(screenDraw("play"), "and the lobby list with locks")
st.routeKey = "R" .. ROOM.room
OnlinePanel.go(imp, "room")
Kit.audit = {}
T.check(screenDraw("room"), "Room draws the players card")
local sawCopy = false
for _, row in ipairs(Kit.audit) do
if row.label == "Copy" then sawCopy = true end
end
Kit.audit = nil
T.check(not sawCopy, "with no code to copy")
currentRoom, currentTour = nil, TOUR
st.routeKey = "T" .. TOUR.tour
OnlinePanel.go(imp, "tournament")
Kit.audit = {}
T.check(screenDraw("tournament"), "the creator's tournament screen draws")
sawCopy = false
for _, row in ipairs(Kit.audit) do
if row.label == "Copy" then sawCopy = true end
end
Kit.audit = nil
T.check(sawCopy, "with the private code and Copy")
TOUR.creator = "someone"
Kit.audit = {}
screenDraw("tournament")
sawCopy = false
for _, row in ipairs(Kit.audit) do
if row.label == "Copy" then sawCopy = true end
end
Kit.audit = nil
T.check(not sawCopy, "a player who did not create it sees no code")
TOUR.creator = "me"
st.private, st.pin = true, "12"
OnlinePanel.startWizard(imp, "hostBattle")
OnlinePanel.wizardTo(imp, "visibility")
T.check(screenDraw("wizard"), "the Private (PIN) step draws")
st.private, st.pin = false, ""
local INV = { id = "i00000000000000f6", activity = "trade",
from = { id = "0badf00d", name = "BLUE#123" }, detail = {},
expiresAt = 20000 }
OnlinePanel.inviteIn(imp, INV)
local Toast = require("src.import.online.Toast")
local PinModal = require("src.import.online.PinModal")
OnlinePanel.pinOpen(imp, { room = ROOM.room, locked = true, name = "BLUE" })
OnlinePanel.home(imp)
OnlinePanel.go(imp, "play")
local clock = os.clock
drawWith(function(m)
OnlinePanel.buildOnlinePanel(imp, m.contentX, m.top + 100, m.contentW, 500, m)
PinModal.draw(imp, m)
Toast.draw(imp, m, 80, false)
end)
local started = clock()
for _ = 1, 25 do
drawWith(function(m)
OnlinePanel.buildOnlinePanel(imp, m.contentX, m.top + 100, m.contentW, 500, m)
PinModal.draw(imp, m)
Toast.draw(imp, m, 80, false)
end)
end
local cost = (clock() - started) / 25
T.check(cost < 0.008, ("Play + PIN modal + toast draw in %.3f ms")
:format(cost * 1000))
OnlinePanel.pinClose(imp)
restore()
end
do
local imp = newImp()
local st = setUp(imp)
local created
local restore = patch(Client, {
createRoom = function(opts) created = opts return { done = false } end,
setProfiles = function(list) return list end,
})
OnlinePanel.startWizard(imp, "hostBattle")
st.private, st.pin = true, "1234"
OnlinePanel.home(imp)
st.tradeRole = "host"
OnlinePanel.startWizard(imp, "tradeRemote")
local has = {}
for _, id in ipairs(OnlinePanel.wizardSteps(imp)) do has[id] = true end
T.check(has.visibility, "hosting a trade asks Public / Private (PIN)")
T.eq(st.private, false, "starting from Public, not the last battle's choice")
st.tradeRole = "join"
has = {}
for _, id in ipairs(OnlinePanel.wizardSteps(imp)) do has[id] = true end
T.check(not has.visibility, "joining a listed trade skips it")
local shown = false
for _, row in ipairs(OnlinePanel.wizardAnswers(imp)) do
if row.step == "visibility" then shown = true end
end
T.check(not shown, "and the summary leaves it out")
st.tradeRole = "host"
OnlinePanel.home(imp)
T.check(OnlinePanel.hostTrade(imp), "a trade is hosted")
T.eq(created and created.private, false, "public")
T.eq(created and created.pin, nil, "with no stale PIN")
st.private, st.pin = true, "4321"
created = nil
OnlinePanel.home(imp)
T.check(OnlinePanel.hostTrade(imp), "a private trade is hosted")
T.eq(created and created.pin, "4321", "with the PIN picked for it")
st.private, st.pin = false, ""
restore()
OnlinePanel.home(imp)
end
do
local imp = newImp()
setUp(imp)
local armed = 0
local was = { enabled = DiscordPresence._state.enabled,
armed = DiscordPresence.launcherArmed }
local restoreClient = patch(Client, {
state = function() return "online" end,
room = function() return nil end,
tournament = function() return nil end,
lobby = function() return {} end,
setProfiles = function(list) return list end,
})
local restorePresence = patch(DiscordPresence, {
ensureLauncher = function()
armed = armed + 1
DiscordPresence.launcherArmed = true
return true
end,
update = function() end,
})
DiscordPresence.launcherArmed = false
for _ = 1, 3 do OnlinePanel.update(imp, 1 / 60) end
T.eq(armed, 1, "back in the launcher while online, presence is re-armed once")
DiscordPresence.launcherArmed = false
for _ = 1, 3 do OnlinePanel.update(imp, 1 / 60) end
T.eq(armed, 1, "and not retried every frame")
restorePresence()
restoreClient()
DiscordPresence._state.enabled = was.enabled
DiscordPresence.launcherArmed = was.armed
end
do
local clock = 100
local restoreTimer = patch(love.timer, { getTime = function() return clock end })
local restore = patch(Client, { serverTime = function() return 1000 end })
local imp = newImp()
OnlinePanel.state(imp)
Transition.reset()
local armed, reduce = Transition.armed, Transition.reduceMotion
Transition.armed, Transition.reduceMotion = true, false
local A = { id = "i000000000000000a", activity = "battle_single",
from = { id = "0000000a", name = "BLUE#102" }, detail = {}, expiresAt = 21000 }
local B = { id = "i000000000000000b", activity = "trade",
from = { id = "0000000b", name = "LEAF#104" }, detail = {}, expiresAt = 21000 }
OnlinePanel.inviteIn(imp, A)
clock = clock + 1
Transition.update(clock)
OnlinePanel.inviteIn(imp, B)
Transition.update(clock)
T.eq(Transition.progress("toast"), 1,
"a queued invite does not re-slide the toast on screen")
T.eq(OnlinePanel.toast(imp).line, OnlinePanel.inviteLine(A),
"the toast line is built once on arrival")
OnlinePanel.declineInvite(imp, OnlinePanel.toast(imp))
OnlinePanel.pruneInvites(imp)
Transition.update(clock)
T.eq(OnlinePanel.toast(imp).id, B.id, "the queued invite shows next")
T.check(Transition.progress("toast") < 1, "and slides in")
Transition.armed, Transition.reduceMotion = armed, reduce
Transition.reset()
restore()
restoreTimer()
end
do
local imp = newImp()
local st = OnlinePanel.state(imp)
st.outgoing = { to = "0badf00d", activity = "battle_single", id = nil }
st.outgoingName = "BLUE#123"
OnlinePanel.inviteClosed(imp, { id = "i0000000000000077", why = "timeout",
to = "0c0ffee0", activity = "trade" })
T.check(st.outgoing ~= nil,
"an unrelated invite closing leaves the one still being sent alone")
OnlinePanel.inviteClosed(imp, { why = "busy", to = "0badf00d",
activity = "battle_single" })
T.eq(st.outgoing, nil, "a refusal for that trainer closes it")
T.eq(st.status, OnlinePanel.inviteClosedText({ why = "busy" }), "with one line")
end
do
local imp = newImp()
setUp(imp)
local now = 1000
local restore = patch(Client, {
serverTime = function() return now end,
joinRoom = function(room) return { id = room, done = false } end,
setProfiles = function(list) return list end,
room = function() return nil end,
tournament = function() return nil end,
lobby = function() return {} end,
})
local ROOM = "r00000000000000bb"
OnlinePanel.pinOpen(imp, { room = ROOM, locked = true, name = "BLUE" })
OnlinePanel.pinFailed(imp, { reason = "pin_locked", retryAt = now + 600000,
pinTarget = { room = ROOM, locked = true, name = "BLUE" } })
OnlinePanel.pinClose(imp)
OnlinePanel.pinOpen(imp, { room = ROOM, locked = true, name = "BLUE" })
T.check(OnlinePanel.pinLocked(imp),
"Cancel then Join on the same locked row keeps the lockout")
OnlinePanel.pinClose(imp)
OnlinePanel.pinOpen(imp, { room = "r00000000000000cc", locked = true })
T.check(not OnlinePanel.pinLocked(imp), "another room is not locked")
OnlinePanel.pinClose(imp)
now = now + 600001
OnlinePanel.pinOpen(imp, { room = ROOM, locked = true })
T.check(not OnlinePanel.pinLocked(imp), "the lockout ends on the relay clock")
T.eq(OnlinePanel._pinLockout[ROOM], nil, "and is forgotten")
OnlinePanel.pinClose(imp)
restore()
end
do
local imp = newImp({ firered = true })
local restore = patch(Client, {
room = function() return { room = "r00000000000000dd", intent = "battle" } end,
tournament = function() return { tour = "t00000000000000dd",
stage = "registering" } end,
})
local function has(list, id)
for _, v in ipairs(list) do if v == id then return true end end
return false
end
local row = OnlinePanel.playerRow(imp, { id = "0badf00d", name = "LEAF",
engine = 3, version = "firered", where = "union" }, "me")
local acts = OnlinePanel.activitiesFor(imp, row)
T.check(not has(acts, "watch") and not has(acts, "tournament"),
"a Union Room trainer is not offered Watch or Join my tournament")
acts = OnlinePanel.activitiesFor(imp, { engine = 3, place = "launcher" })
T.check(has(acts, "watch") and has(acts, "tournament"),
"a launcher trainer still is")
restore()
end
do
local Room = require("src.import.online.Room")
local seen = {}
local restoreKit = patch(Kit, {
tag = function(_, _, _, _, label) seen[#seen + 1] = label end,
text = function(_, text) seen[#seen + 1] = text end,
})
local imp = newImp({ firered = true })
drawWith(function(m)
return Room.playersCard(imp, 0, 0, 600, m, { seats = 4, stage = "battling",
players = { { id = "a", name = "P0", seat = 0 }, { id = "c", name = "P2", seat = 2 },
{ id = "d", name = "P3", seat = 3 } } }, false)
end)
restoreKit()
local at = {}
for i, v in ipairs(seen) do at[v] = at[v] or i end
T.eq(seen[(at.P2 or 1) - 1], "TEAM A", "seat 2 stays on TEAM A after seat 1 leaves")
T.eq(seen[(at.P3 or 1) - 1], "TEAM B", "seat 3 stays on TEAM B")
T.eq(seen[(at["Open seat"] or 1) - 1], "TEAM B", "the empty seat is seat 1")
end
do
local Room = require("src.import.online.Room")
local two = { { id = "a" }, { id = "b" } }
T.eq(Room.stageText({ stage = "waiting", seats = 2, players = { two[1] } }),
"Waiting for a challenger", "one of two seats waits for a challenger")
T.eq(Room.stageText({ stage = "waiting", seats = 2, players = two }),
"Both trainers are picking a team", "a full room no longer waits")
T.eq(Room.stageText({ stage = "ready", seats = 4, engine = 3, players = two }),
"Waiting for trainers", "a readied multi host still waits for seats")
T.eq(Room.stageText({ stage = "waiting", seats = 4, engine = 3,
players = { two[1], two[2], { id = "c" }, { id = "d" } } }),
"Everyone is here", "a full Gen 3 room says so")
T.eq(Room.stageText({ stage = "waiting", seats = 2, intent = "trade",
players = two }), "Both trainers are here", "a full trade room too")
T.eq(Room.stageText({ stage = "battling", seats = 2, players = two }),
"Battle in progress", "later stages pass through")
local Home = require("src.import.online.Home")
T.eq(Home.countsText(1, 1), "1 player online, 1 open lobby", "one reads singular")
T.eq(Home.countsText(2, 0), "2 players online, 0 open lobbies", "others plural")
end
T.finish("online panel c1")