test fix and iOS close hide

This commit is contained in:
bryanthaboi
2026-08-27 07:11:43 -04:00
parent eed246f645
commit bd3e02986d
9 changed files with 141 additions and 60 deletions
+6 -4
View File
@@ -1394,7 +1394,9 @@ local function buildHeader(imp, m)
-- under the gear and the quit X -- "the settings is covering the logo".
-- Reserving the space on both sides costs a little width and cannot
-- overlap at any window size.
local clusterW = 2 * gear + math.floor(6 * m.s) + m.pad
-- iOS has no quit button (the OS owns app exit), so the cluster is the
-- gear alone and the wordmark gets that width back
local clusterW = (imp.ios and gear or 2 * gear + math.floor(6 * m.s)) + m.pad
local boxX = m.x + clusterW
local boxW = math.max(0, m.w - 2 * clusterW)
if imp.logo and boxW > 0 then
@@ -1428,8 +1430,8 @@ local function buildHeader(imp, m)
-- inboard of it -- but the two are REGISTERED gear first, because the first
-- focusable of the first frame adopts the keyboard ring and that must not be
-- the button that exits the app.
local quitX = rx - gear
rx = quitX - math.floor(6 * m.s)
local quitX = not imp.ios and rx - gear or nil
if quitX then rx = quitX - math.floor(6 * m.s) end
-- Settings gear. It now also owns the CONTROL settings (touch overlay
-- editor, reset rebinds), which used to be buttons stacked in the game
@@ -1441,7 +1443,7 @@ local function buildHeader(imp, m)
chrome.gear.image = imp._gearIcon
btn(imp, rx, by, gear, gear, "gear", "", chrome.gear)
btn(imp, quitX, by, gear, gear, "quit", "", chrome.quit)
if quitX then btn(imp, quitX, by, gear, gear, "quit", "", chrome.quit) end
-- The self-update control lives in the FOOTER next to the BCG mark (small,
-- out of the wordmark's way -- it used to overlap the logo on a phone). It
+3 -1
View File
@@ -303,7 +303,9 @@ Game.save.forcedBike = true
ow = pushOW("ROUTE_17", 4, 10, "down")
ow:flyTo("PALLET_TOWN")
eq(Game.save.forcedBike, nil, "Fly clears BIT_ALWAYS_ON_BIKE")
ow.flyAnim, ow.flyDest, ow.player.inputLocked = nil, nil, false -- undo flyTo
-- undo flyTo, flyFade included: it re-arms flyAnim when it drains
ow.flyAnim, ow.flyDest, ow.flyFade = nil, nil, nil
ow.player.inputLocked = false
Game.save.forcedBike = true
ow:warpToHealPoint()
eq(Game.save.forcedBike, nil, "blackout/escape warps clear BIT_ALWAYS_ON_BIKE")
+8 -5
View File
@@ -35,12 +35,12 @@ local check, eq = S.check, S.eq
-- rng floor: accuracyRoll compares rng(0, 255) against the scaled accuracy,
-- so the lowest roll lands HYPNOSIS (60%) every run
local function freshBattle()
local function freshBattle(rng)
Game.save.options.animations = true
Game.save.party = { Pokemon.new(Data, "SQUIRTLE", 30) }
local tb = BattleState.newWild(Game, "PIDGEY", 10)
tb.queue, tb.nextInsert = {}, 0
tb.rng = function(a) return a end
tb.rng = rng or function(a) return a end
return tb
end
@@ -53,8 +53,8 @@ local function hitRows(tb)
return out
end
local function typeOf(moveId, isPlayer)
local tb = freshBattle()
local function typeOf(moveId, isPlayer, rng)
local tb = freshBattle(rng)
local user = isPlayer and tb.player or tb.enemy
local target = isPlayer and tb.enemy or tb.player
tb:performMove(user, target, { id = moveId, pp = 10 }, false)
@@ -111,7 +111,10 @@ do
eq(typeOf("HYPNOSIS", false), 3, "and type 3 from the foe")
eq(typeOf("GROWL", true), 6, "GROWL is a primary stat drop: type 6")
eq(typeOf("TAIL_WHIP", true), 6, "so is TAIL_WHIP")
eq(typeOf("SAND_ATTACK", false), 3, "the foe's SAND-ATTACK is type 3")
-- StatModifierDownEffect misses on a roll under $40 on the enemy's
-- turn -- engine/battle/effects.asm:552
eq(typeOf("SAND_ATTACK", false, function() return 0x40 end), 3,
"the foe's SAND-ATTACK is type 3")
eq(typeOf("POISONPOWDER", true), 6, "POISONPOWDER is type 6")
eq(typeOf("CONFUSE_RAY", true), 6, "CONFUSE RAY is type 6")
eq(typeOf("DISABLE", true), 6, "DISABLE is type 6")
+31 -15
View File
@@ -90,7 +90,7 @@ end
eq(hooks.onInteract(game, ow, 2, 0), false,
"the wall left of the blackboard stays silent")
-- walk the loop: intro -> prompt -> heading box -> blurb -> prompt again
-- walk the loop: intro -> held prompt -> heading menu over it
stack = {}
hooks.onInteract(game, ow, 3, 0)
local intro = stack[#stack]
@@ -100,36 +100,52 @@ local prompt = stack[#stack]
check(getmetatable(prompt) == TextBox
and pages(prompt):find("heading", 1, true) ~= nil,
"the intro leads into the which-heading prompt")
prompt.onDone()
-- #591: LinkCableHelpText2 ends in `text_end`, so .linkHelpLoop leaves it on
-- screen and runs HandleMenuInput under it
check(prompt.onDone == nil and prompt.stay ~= nil,
"the prompt is held open instead of waiting for A and popping")
prompt.stay.onShown()
local menu = stack[#stack]
check(getmetatable(menu) == Menu, "the prompt opens the heading menu")
check(getmetatable(menu) == Menu, "the held prompt opens the heading menu")
eq(stack[#stack - 1], prompt, "the menu sits on the still-visible prompt box")
eq(#menu.items, 4, "four headings, as in HowToLinkText")
local labels = {}
for i, item in ipairs(menu.items) do labels[i] = item.label end
eq(table.concat(labels, "/"),
"HOW TO LINK/COLOSSEUM/TRADE CENTER/STOP READING",
"the headings read in HowToLinkText order")
eq(menu.items[4].onSelect, nil, "STOP READING just closes the menu")
check(menu.tw == 15 and menu.th == 10 and menu.tx == 0 and menu.ty == 0,
"the box is the asm's 15x10 at the top left")
for i = 1, 3 do
stack = {}
stack = { prompt, menu }
menu.index = i
eq(menu.items[i].keepOpen, true,
labels[i] .. " leaves the menu up, as `jp .linkHelpLoop` does")
menu.items[i].onSelect()
local blurb = stack[#stack]
check(getmetatable(blurb) == TextBox,
labels[i] .. " prints a text box")
check(getmetatable(blurb) == TextBox, labels[i] .. " prints a text box")
local want = Data.text["_LinkCableInfoText" .. i]:match("^[^\n\011\012]+")
check(pages(blurb):find(want, 1, true) ~= nil,
labels[i] .. " prints _LinkCableInfoText" .. i)
check(type(blurb.onDone) == "function",
labels[i] .. " returns to the prompt instead of dropping out")
blurb.onDone()
check(getmetatable(stack[#stack]) == TextBox,
"the prompt comes back after " .. labels[i])
stack[#stack].onDone()
check(getmetatable(stack[#stack]) == Menu,
"the menu comes back after " .. labels[i])
eq(blurb.onDone, nil,
labels[i] .. " pops itself back onto the menu instead of dropping out")
table.remove(stack) -- the blurb pops itself once it has been read
eq(stack[#stack], menu, "the same menu comes back after " .. labels[i])
eq(menu.index, i, "the cursor stays on the heading that was just read")
eq(stack[#stack - 1], prompt, "the held prompt is still under it")
end
-- STOP READING and B share .exit: both close the menu and the prompt box
eq(menu.items[4].keepOpen, nil, "STOP READING closes the menu")
stack = { prompt, menu }
table.remove(stack) -- Menu pops itself before a non-keepOpen onSelect runs
menu.items[4].onSelect()
eq(#stack, 0, "STOP READING closes the menu and the held prompt together")
check(type(menu.onCancel) == "function", "B is watched (PAD_A | PAD_B)")
stack = { prompt, menu }
table.remove(stack)
menu.onCancel()
eq(#stack, 0, "B closes the menu and the held prompt together")
S.finish()
+1 -1
View File
@@ -138,4 +138,4 @@ do
eq(#pushed, 0, "loss does not push after-battle text")
end
print("parity_lance: ok")
S.finish()
+3
View File
@@ -29,8 +29,11 @@ local Bag = require("src.inventory.Bag")
local realTextBox = package.loaded["src.render.TextBox"]
local realBag = package.loaded["src.ui.BagMenu"]
local realParty = package.loaded["src.ui.PartyMenu"]
-- soundOpts only builds an opts table, so the real one runs against the stub.
local soundOpts = require("src.render.TextBox").soundOpts
package.loaded["src.render.TextBox"] = {
new = function(_, text, done) return { textBox = true, text = text, done = done } end,
soundOpts = soundOpts,
}
package.loaded["src.ui.BagMenu"] = nil
package.loaded["src.ui.PartyMenu"] = nil
+5
View File
@@ -50,6 +50,9 @@ check(hideout.noSight == nil or hideout.noSight[TEXT] ~= true,
-- the LIFT KEY half: after the battle -- however it started -- the next talk
-- reveals the ball
-- put the real module back at the bottom: the tier dofiles every parity file
-- into one process, so a stub left behind here breaks every later suite
local realTextBox = package.loaded["src.render.TextBox"]
package.loaded["src.render.TextBox"] = {
new = function(_, text, done) return { text = text, done = done } end,
soundOpts = function() return nil end,
@@ -77,4 +80,6 @@ check(toggles and toggles.ROCKETHIDEOUTB4F_LIFT_KEY == true,
"the LIFT KEY ball is shown")
check(finished, "and the talk hands control back")
package.loaded["src.render.TextBox"] = realTextBox
S.finish()
@@ -8,6 +8,11 @@
-- findPendingRom, answered with the first dump whose SHA-1 mapped to any
-- not-yet-ready version, so the selection was dropped on the floor.
--
-- Since 592daafa the in-launcher Kit.FileBrowser is tried ahead of that scan,
-- so the save-dir scan is now what a build without the Kit browser takes.
-- Kit is stubbed away below to reach it; the last block covers the browser
-- being present.
--
-- Self-contained: `luajit tests/rom_importer_choose_version_test.lua`
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end
@@ -51,6 +56,9 @@ local saved = {
-- to a dialog that may or may not exist on the machine running the suite.
-- The fallback under test is the same one every pickerless device takes.
love.system.getOS = function() return "Unknown" end
-- No Kit browser in this build, so Choose falls through to the save-dir scan.
local savedKit = package.loaded["src.ui.kit.Kit"]
package.loaded["src.ui.kit.Kit"] = { FileBrowser = nil }
love.filesystem.getSaveDirectory = function() return "/tmp/pokemon-love2d" end
love.filesystem.getDirectoryItems = function() return LISTING end
love.filesystem.getInfo = function(name, filter)
@@ -116,6 +124,24 @@ ri:choose("red")
check(ri._started == nil,
"and a version already imported is not extracted a second time")
-- ------- with the Kit browser present it opens instead of scanning
local opened = nil
package.loaded["src.ui.kit.Kit"] = {
FileBrowser = {
open = function(opts) opened = opts end,
},
}
ri = freshImporter()
ri:choose("blue")
check(ri._started == nil and opened ~= nil,
"a build with the Kit browser opens it rather than scanning the save dir")
eq(opened and opened.title, "Select "
.. (GameVersion.info("blue").displayName or "ROM"),
"titled for the version that was chosen")
package.loaded["src.ui.kit.Kit"] = savedKit
for name, fn in pairs(saved) do
if name == "getOS" then love.system.getOS = fn
elseif name == "hash" then love.data.hash = fn
+58 -34
View File
@@ -12,6 +12,9 @@ love = require("tests.love_stub")
-- assertions and the RNG seed come off the shared harness; this file keeps
-- its own tail because the verdict line ("N FAILURES") is what CI greps
local T = require("tests.harness")
-- every suite below is dofile'd in this process, so a suite that reaches for
-- T.finish must raise into runSuites' pcall instead of os.exit(0)-ing the tier
_G.POKEPORT_TEST_CHILD = true
-- this suite has always streamed a line per check, and it is the one a
-- developer watches for progress through ~1600 assertions
T.verbose = true
@@ -437,11 +440,13 @@ check(Damage.compute(ruleset, confused, confused, confMove,
-- (StatModifierDownEffect's side-effect branch skips MoveHitTest)
local MoveEffects = require("src.battle.MoveEffects")
local sideRng = { rng = function() return 0 end }
-- player attacker: effects.asm:552's 25 percent miss is the enemy's branch
local sideUser = { isPlayer = true, stages = {}, mon = {} }
local misted = { stages = {}, mist = true, name = "MISTY", mon = {} }
MoveEffects.secondary.ATTACK_DOWN_SIDE_EFFECT(sideRng, nil, misted)
MoveEffects.secondary.ATTACK_DOWN_SIDE_EFFECT(sideRng, sideUser, misted)
eq(misted.stages.attack, -1, "secondary stat drop pierces MIST")
local misted2 = { stages = {}, mist = true, name = "MISTY", mon = {} }
local mistMsgs = MoveEffects.primary.ATTACK_DOWN1_EFFECT(sideRng, nil, misted2)
local mistMsgs = MoveEffects.primary.ATTACK_DOWN1_EFFECT(sideRng, sideUser, misted2)
check(misted2.stages.attack == nil
and mistMsgs[1]:find("MIST", 1, true) ~= nil,
"primary stat drop still blocked by MIST")
@@ -2644,7 +2649,8 @@ do
-- the text-speed cursor clamps at its ends (.pressedLeftInTextSpeed),
-- MUSIC FILTER cycles OFF/1X/2X/3X, and COLORS / TILT / VIDEO MODE
-- cycle their display modes (SHADER FX activates a pushed screen instead).
do
-- Nested function so LuaJIT's 200-local main-chunk limit is not hit.
(function()
local OptionsMenu = require("src.ui.OptionsMenu")
local OInput = require("src.core.Input")
local PaletteFX = require("src.render.PaletteFX")
@@ -2656,8 +2662,19 @@ do
-- Isolate from earlier save/options writes in this suite
SD.saveOptions(SD.defaultOptions())
local popped = false
local om
-- the rows sit on group pages now, so the stub needs a real push/pop/top
-- stack for a group opener to have anywhere to go
local ostack = { states = {} }
function ostack:push(s) self.states[#self.states + 1] = s end
function ostack:pop()
local s = table.remove(self.states)
if s == om then popped = true end
return s
end
function ostack:top() return self.states[#self.states] end
local og = { data = Data, save = SD.newGame(),
input = OInput, stack = { pop = function() popped = true end },
input = OInput, stack = ostack,
writeOptions = function(self) SD.saveOptions(self.save.options) end,
-- the PERFORMANCE row routes through Game:applyOptions; the
-- stub carries the headless slice of it (the tier record),
@@ -2665,24 +2682,29 @@ do
applyOptions = function(self, o)
require("src.core.Performance").applyOptions(o)
end }
local om = OptionsMenu.new(og)
om = OptionsMenu.new(og)
ostack:push(om)
local OptionRows = require("src.ui.OptionRows")
-- cur is whichever screen the cursor is on: the top level, or the group
-- page seek() walked into
local cur = om
local function press(btn)
OInput.pressed = { [btn] = true }
om:update(1 / 60)
cur:update(1 / 60)
OInput.pressed = {}
end
-- walk the cursor down to a row by id, so a row added to OptionsMenu
-- shifts these blocks instead of silently retargeting them
local function rowAt(s)
local rows = s.view or s.rows
return rows[s.index]
end
-- put the cursor on a row by id, opening its group page first, so a row
-- added to OptionsMenu shifts these blocks instead of silently
-- retargeting them
local function seek(id)
local want = -1
for i, row in ipairs(om.rows) do
if row.id == id then want = i end
end
for _ = 1, #om.rows do
if om.index == want then break end
press("down")
end
return om.index == want
while ostack:top() ~= om do ostack:pop() end
cur = om:focusRow(id) or om
local row = rowAt(cur)
return row ~= nil and row.id == id
end
eq(og.save.options.textSpeed, 3,
"new saves default to MEDIUM text (InitOptions TEXT_DELAY_MEDIUM)")
@@ -2699,9 +2721,11 @@ do
"A switches the battle screen to the WIDE layout")
press("a")
eq(og.save.options.battleLayout, "og", "BATTLE LAYOUT wraps back to OG")
-- the BATTLE page's sixth row sits past the 4-box viewport
check(seek("battleBg"), "cursor reaches BATTLE BG")
eq(cur.scroll, cur.index - OptionRows.VISIBLE,
"viewport scrolls to keep a deep group row on screen")
check(seek("musicVol"), "cursor reaches MUSIC VOL")
eq(om.scroll, om.index - require("src.ui.OptionRows").VISIBLE,
"viewport scrolls to keep MUSIC VOL on screen")
press("left")
eq(og.save.options.musicVol, 6, "left lowers MUSIC VOL")
press("right")
@@ -2733,19 +2757,18 @@ do
eq(og.save.options.tilt, 0, "TILT wraps back to OFF")
check(seek("shaderfx"), "cursor reaches SHADER FX")
-- this row activates a pushed ShaderFXScreen rather than cycling in
-- place like the rest of this suite's rows; `og.stack` above only
-- stubs `pop`, not a real push/top stack, so activate() is not
-- called here -- tests/mod_ui_tests.lua exercises it end to end against
-- a real stack.
check(om.rows[om.index].step == nil, "SHADER FX row has no step()")
check(type(om.rows[om.index].activate) == "function",
-- place like the rest of this suite's rows, so activate() is not called
-- here -- tests/mod_ui_tests.lua exercises it end to end against a real
-- game.
check(rowAt(cur).step == nil, "SHADER FX row has no step()")
check(type(rowAt(cur).activate) == "function",
"SHADER FX row has an activate()")
check(seek("shaderfx2"), "cursor reaches SHADER FX 2")
-- the dual-shader secondary slot: same shared ShaderFXScreen, opened on
-- "secondary" instead -- see the SHADER FX row above for why activate()
-- isn't exercised against this stub stack either.
check(om.rows[om.index].step == nil, "SHADER FX 2 row has no step() either")
check(type(om.rows[om.index].activate) == "function",
-- isn't exercised here either.
check(rowAt(cur).step == nil, "SHADER FX 2 row has no step() either")
check(type(rowAt(cur).activate) == "function",
"SHADER FX 2 row has an activate()")
check(seek("zoom"), "cursor reaches ZOOM")
local ZoomOpt = require("src.render.Zoom")
@@ -2804,10 +2827,10 @@ do
check(seek("dateFormat"), "cursor reaches DATE FORMAT")
check(seek("timeFormat"), "cursor reaches TIME FORMAT")
press("down")
-- CANCEL is appended after the descriptor list rather than living in it, so
-- it lands one past #rows and the window holds the last six boxes. Counted
-- off #rows so the next row added here is not read as a wrap bug.
local cancelRow = #om.rows + 1
-- CANCEL is appended after the top-level view rather than living in it, so
-- it lands one past #view and the window holds the last six boxes. Counted
-- off #view so the next row added here is not read as a wrap bug.
local cancelRow = #om.view + 1
eq(om.index, cancelRow, "CANCEL stays the fixed final row")
eq(om.scroll, cancelRow - 5, "CANCEL keeps the last option boxes on screen")
om:draw() -- smoke: scrolled layout draws under the headless stub
@@ -2826,7 +2849,7 @@ do
require("src.render.Zoom").applyOptions(og.save.options)
require("src.render.TileRenderer").applyOptions(og.save.options)
require("src.core.VideoMode").applyOptions(og.save.options)
end
end)()
end
-- ------------------------------------------------------------------
@@ -3224,7 +3247,8 @@ do
local TileRenderer = require("src.render.TileRenderer")
TileRenderer.setSpinning(true)
local before = TileRenderer.spinBlurActive()
for _ = 1, 8 do TileRenderer.tick(1 / 60) end
-- the blur alternates once per simulated-joypad step, 16 frames (#1831)
for _ = 1, 16 do TileRenderer.tick(1 / 60) end
check(before ~= TileRenderer.spinBlurActive(),
"tick(1/60) advances water/spinner clock at fixed 60Hz")
local mid = TileRenderer.spinBlurActive()