Files
gen1recomp/tests/engine/game3_start_menu_scroll_test.lua
T
1jamie 26506b0677 fix(emerald): fix party TM descriptions, summary 5th move, and UI issues
- Fix Emerald party menu TM learnability descriptions (sDescriptionStringTable indexing)
- Fix summary menu 5th move slot display and move learning in RSE
- Fix pause/start menu viewport scrolling and scroll arrow indicators
- Fix berry planting bag pocket routing and screen veil fading in Emerald
- Fix Route 101 Birch rescue positioning and truck sequence unfreeze
- Add comprehensive regression tests for all fixes
2026-09-28 22:03:03 -05:00

221 lines
7.7 KiB
Lua

local T = require("tests.harness")
local check, eq = T.check, T.eq
local function romTextPlain(key) return key end
package.loaded["src.core.game3.rom_text"] = {
plain = romTextPlain, box = romTextPlain, ascii = romTextPlain, has = function() return true end,
key = function(n, i, j) return n end, at = function(n, i, j) return n end,
count = function() return 0 end, list = function() return {} end,
lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end,
}
local StartMenu = require("src.ui.game3.start_menu")
local Window = require("src.ui.game3.window")
local RseData = require("src.ui.game3.rse.start_menu_data")
-- Mock love for draw test
local loveStub = require("tests.love_stub")
_G.love = _G.love or loveStub
-- 1. Test standard 8-or-fewer entries (Vanilla Emerald without MODS)
do
StartMenu.resetCursor()
eq(StartMenu.cursor, 1, "Cursor is 1 after reset")
eq(StartMenu._scrollOffset, 0, "Scroll offset is 0 after reset")
local fakeSession = { name = "MAY", version = "emerald", bag = {}, party = {}, dex = {} }
local fakeGame = { modStatus = { available = {} } } -- No mods -> 8 vanilla items (Dex, Mon, Bag, Nav, Name, Save, Option, Exit)
StartMenu.show({ session = fakeSession, game = fakeGame })
check(StartMenu.isOpen(), "StartMenu is open")
eq(#StartMenu.ENTRIES, 8, "Vanilla Emerald has 8 entries with pokedex/pokemon/pokenav flags")
local tpl = StartMenu.contentTemplate()
eq(tpl.h, 18, "8 items window height is (8*2)+2 = 18 tiles")
eq(tpl.top + tpl.h, 19, "Top(1) + Height(18) = 19 <= 19 (fits in 20-tile screen)")
eq(StartMenu._scrollOffset, 0, "Scroll offset stays 0")
-- Move down through all 8 items
for i = 2, 8 do
StartMenu.move(1)
eq(StartMenu.cursor, i, "Cursor moved to " .. i)
eq(StartMenu._scrollOffset, 0, "Scroll offset stays 0 for <= 8 items")
end
-- Wrap down from 8 to 1
StartMenu.move(1)
eq(StartMenu.cursor, 1, "Cursor wrapped to 1")
eq(StartMenu._scrollOffset, 0, "Scroll offset stays 0")
-- Wrap up from 1 to 8
StartMenu.move(-1)
eq(StartMenu.cursor, 8, "Cursor wrapped to 8")
eq(StartMenu._scrollOffset, 0, "Scroll offset stays 0")
StartMenu.close(true)
end
-- 2. Test 9 entries (Emerald with MODS menu entry)
do
StartMenu.resetCursor()
local fakeSession = { name = "MAY", version = "emerald", bag = {}, party = {}, dex = {} }
local fakeGame = {
modStatus = {
available = {
{ id = "test_mod", name = "Test Mod", enabled = true },
},
},
}
StartMenu.show({ session = fakeSession, game = fakeGame })
check(StartMenu.isOpen(), "StartMenu is open with mods")
eq(#StartMenu.ENTRIES, 9, "Emerald with MODS has 9 entries")
-- Window template must be capped at 8 visible entries
local tpl = StartMenu.contentTemplate()
eq(tpl.h, 18, "Window height is capped at (8*2)+2 = 18 tiles despite 9 entries")
eq(tpl.top + tpl.h, 19, "Window bottom frame is within row 19 (no viewport clipping)")
eq(StartMenu._scrollOffset, 0, "Initial scroll offset is 0")
-- Navigate down to item 8
for i = 2, 8 do
StartMenu.move(1)
eq(StartMenu.cursor, i, "Cursor is " .. i)
eq(StartMenu._scrollOffset, 0, "Scroll offset is 0 while on items 1..8")
end
-- Navigate down to item 9 (EXIT) -> should scroll
StartMenu.move(1)
eq(StartMenu.cursor, 9, "Cursor is on item 9 (EXIT)")
eq(StartMenu._scrollOffset, 1, "Scroll offset shifted to 1 to show items 2..9")
-- Navigate down from item 9 -> wraps to item 1 (POKéDEX) and resets scroll
StartMenu.move(1)
eq(StartMenu.cursor, 1, "Cursor wrapped to item 1 (POKéDEX)")
eq(StartMenu._scrollOffset, 0, "Scroll offset reset to 0 on wrap to top")
-- Navigate up from item 1 -> wraps to item 9 (EXIT) and shifts scroll to bottom
StartMenu.move(-1)
eq(StartMenu.cursor, 9, "Cursor wrapped to item 9 (EXIT)")
eq(StartMenu._scrollOffset, 1, "Scroll offset shifted to 1 on wrap to bottom")
-- Navigate up to item 8 (MODS) -> scroll stays 1, cursor row is 7
StartMenu.move(-1)
eq(StartMenu.cursor, 8, "Cursor moved to item 8 (MODS)")
eq(StartMenu._scrollOffset, 1, "Scroll offset stays 1")
-- Navigate up until item 1
for i = 7, 2, -1 do
StartMenu.move(-1)
eq(StartMenu.cursor, i, "Cursor is " .. i)
end
eq(StartMenu._scrollOffset, 1, "Scroll offset stays 1 until item 1 is reached")
StartMenu.move(-1)
eq(StartMenu.cursor, 1, "Cursor is 1")
eq(StartMenu._scrollOffset, 0, "Scroll offset adjusted to 0 when cursor hits row 1")
-- Verify draw executes cleanly when scrolled
StartMenu.move(-1) -- Move to 9 (scrolled)
local ok, err = pcall(StartMenu.draw)
check(ok, "StartMenu.draw() executes cleanly while scrolled: " .. tostring(err))
StartMenu.close(true)
end
-- 3. Test arbitrary large list (e.g. 14 modded entries)
do
StartMenu.resetCursor()
StartMenu.ENTRIES = {}
for i = 1, 14 do
table.insert(StartMenu.ENTRIES, { id = "item" .. i, label = "ITEM " .. i })
end
StartMenu._data = RseData
StartMenu.open = true
StartMenu.cursor = 1
StartMenu.clampScroll()
eq(StartMenu._scrollOffset, 0, "Scroll is 0")
local tpl = StartMenu.contentTemplate()
eq(tpl.h, 18, "Height is still 18 tiles max")
-- Step down 10 times (to item 11)
for i = 2, 11 do
StartMenu.move(1)
end
eq(StartMenu.cursor, 11, "Cursor is 11")
eq(StartMenu._scrollOffset, 3, "Scroll offset is 11 - 8 = 3 (showing items 4..11)")
-- Step up 5 times (to item 6)
for _ = 1, 5 do
StartMenu.move(-1)
end
eq(StartMenu.cursor, 6, "Cursor is 6")
eq(StartMenu._scrollOffset, 3, "Scroll offset remains 3 (item 6 is visible in 4..11)")
-- Direct cursor assignment & clampScroll
StartMenu.cursor = 14
StartMenu.clampScroll()
eq(StartMenu._scrollOffset, 6, "clampScroll sets scroll to 14 - 8 = 6 for item 14")
StartMenu.cursor = 2
StartMenu.clampScroll()
eq(StartMenu._scrollOffset, 1, "clampScroll sets scroll to 1 for item 2")
StartMenu.close(true)
end
-- 4. Test scroll arrow indicators during draw
do
local ListMenu = require("src.ui.game3.list_menu")
local capturedArrows = {}
local origDrawScrollArrows = ListMenu.drawScrollArrows
ListMenu.drawScrollArrows = function(tpl, showUp, showDown, t)
capturedArrows[#capturedArrows + 1] = { up = showUp, down = showDown }
end
StartMenu.resetCursor()
StartMenu.ENTRIES = {}
for i = 1, 9 do
table.insert(StartMenu.ENTRIES, { id = "item" .. i, label = "ITEM " .. i })
end
StartMenu._data = RseData
StartMenu.open = true
-- At top (scroll = 0): down arrow should be visible, up arrow hidden
StartMenu.cursor = 1
StartMenu.clampScroll()
capturedArrows = {}
StartMenu.draw()
eq(#capturedArrows, 1, "drawScrollArrows called once")
eq(capturedArrows[1].up, false, "Up arrow hidden at top")
eq(capturedArrows[1].down, true, "Down arrow shown at top to indicate more items below")
-- At bottom (scroll = 1): up arrow should be visible, down arrow hidden
StartMenu.cursor = 9
StartMenu.clampScroll()
capturedArrows = {}
StartMenu.draw()
eq(#capturedArrows, 1, "drawScrollArrows called once")
eq(capturedArrows[1].up, true, "Up arrow shown at bottom to indicate more items above")
eq(capturedArrows[1].down, false, "Down arrow hidden at bottom")
-- In middle of 14 items: both up and down arrows visible
StartMenu.ENTRIES = {}
for i = 1, 14 do
table.insert(StartMenu.ENTRIES, { id = "item" .. i, label = "ITEM " .. i })
end
StartMenu.cursor = 6
StartMenu._scrollOffset = 3
capturedArrows = {}
StartMenu.draw()
eq(#capturedArrows, 1, "drawScrollArrows called once in middle")
eq(capturedArrows[1].up, true, "Up arrow shown in middle")
eq(capturedArrows[1].down, true, "Down arrow shown in middle")
ListMenu.drawScrollArrows = origDrawScrollArrows
StartMenu.close(true)
end
T.finish("game3_start_menu_scroll_test")