Files
bookorbit/koreader-plugin/spec/bookorbit_focus_highlight_test.lua
T
neon 80244ea4e2 feat(koreader): rebuild the plugin interface and fix four-way annotation sync
Squash of feat/koreader-widget-board. KOReader plugin 2.0.0 gets the widget-board dashboard, redesigned
book details, menus, lists and dialogs, Decor page and reading companions. Highlights, notes and bookmarks
now stay in step across web, Kobo, KOReader and the phones (defects D-00 to D-20, migration 0121), and
reading progress and narration hand off cleanly between devices.
2026-10-20 16:05:27 -06:00

147 lines
6.0 KiB
Lua

-- Proves the focus chrome the catalog's tappable widgets draw for a D-Pad
-- cursor: that it exists at all on a keyboard-only device, that it is an inner
-- border so adding it cannot resize a widget and reflow the page around it,
-- that a touchscreen pays nothing for it, and that no tappable widget is left
-- without it.
local function stubWidget()
local class = {}
class.new = function(_, opts) return opts or {} end
class.extend = function(_, spec)
spec = spec or {}
spec.new = class.new
spec.extend = class.extend
return spec
end
return class
end
local function anyNumber()
return setmetatable({}, { __index = function() return 1 end })
end
package.loaded["ui/bidi"] = { auto = function(text) return text end }
package.loaded["ffi/blitbuffer"] = setmetatable({}, { __index = function() return 0 end })
package.loaded["ui/font"] = { getFace = function() return { size = 10 } end }
package.loaded["ui/geometry"] = stubWidget()
package.loaded["ui/gesturerange"] = stubWidget()
package.loaded["ui/size"] = setmetatable({}, { __index = function() return anyNumber() end })
package.loaded["gettext"] = function(text) return text end
package.loaded["util"] = { fixUtf8 = function(value) return value end }
package.loaded["ffi/util"] = { template = function(value) return value end }
package.loaded["document/documentregistry"] = { hasProvider = function() return true end }
for _, name in ipairs({
"ui/widget/container/centercontainer",
"ui/widget/container/framecontainer",
"ui/widget/container/inputcontainer",
"ui/widget/container/leftcontainer",
"ui/widget/container/rightcontainer",
"ui/widget/horizontalgroup",
"ui/widget/horizontalspan",
"ui/widget/iconwidget",
"ui/widget/imagewidget",
"ui/widget/linewidget",
"ui/widget/overlapgroup",
"ui/widget/progresswidget",
"ui/widget/textboxwidget",
"ui/widget/textwidget",
"ui/widget/verticalgroup",
"ui/widget/verticalspan",
}) do
package.loaded[name] = stubWidget()
end
local touch = false
package.loaded["device"] = {
screen = {
scaleBySize = function(_, value) return value end,
getWidth = function() return 600 end,
getHeight = function() return 800 end,
getDPI = function() return 167 end,
},
hasDPad = function() return true end,
isTouchDevice = function() return touch end,
}
package.path = "koreader-plugin/bookorbit.koplugin/?.lua;" .. package.path
local function loadWidgets()
package.loaded["bookorbit_catalog_widgets"] = nil
package.loaded["bookorbit_catalog_util"] = nil
return require("bookorbit_catalog_widgets")
end
local function assertEqual(actual, expected, label)
if actual ~= expected then
error(string.format("%s: expected %s, got %s", label, tostring(expected), tostring(actual)))
end
end
-- A D-Pad with no touchscreen is the case that has to draw a cursor.
local CatalogWidgets = loadWidgets()
assertEqual(CatalogWidgets.focusNavigation(), true, "a keyboard-only device navigates by focus")
local child = { "content" }
local frame = CatalogWidgets.focusable(child)
assertEqual(frame ~= child, true, "the widget's root is wrapped in a focus frame")
assertEqual(frame[1], child, "the frame carries the original root untouched")
assertEqual(frame.focusable, true, "the frame answers Focus events")
assertEqual(frame.focus_inner_border, true, "the cursor is drawn inside the frame's own bounds")
assertEqual(frame.focus_border_size > 0, true, "with a visible border width")
-- The inner border is the whole point: the dashboard and detail pages size
-- themselves from fixed dimensions, so a cursor that grew a widget would push
-- the rest of the page around every time it moved.
assertEqual(frame.bordersize, 0, "the resting frame adds no border of its own")
assertEqual(frame.padding, 0, "nor any padding")
assertEqual(frame.margin, 0, "nor any margin")
-- A touchscreen never shows the cursor, so it should not pay for the frame.
touch = true
CatalogWidgets = loadWidgets()
assertEqual(CatalogWidgets.focusNavigation(), false, "a touch device does not navigate by focus")
assertEqual(CatalogWidgets.focusable(child), child, "and gets its root back unwrapped")
touch = false
-- Every tappable widget in these two files sets its root as `self[1]`, and each
-- one has to route through the helper: a new widget added without it would be
-- invisible to a D-Pad user, which is exactly the bug this fixes.
local function read(path)
local file = assert(io.open(path, "rb"))
local content = file:read("*a")
file:close()
return content
end
local plugin_dir = "koreader-plugin/bookorbit.koplugin/"
for _, name in ipairs({ "bookorbit_catalog_widgets.lua" }) do
local source = read(plugin_dir .. name)
local roots, wrapped = 0, 0
for assignment in source:gmatch("self%[1%] = ([%w_.]+)") do
roots = roots + 1
if assignment == "CatalogWidgets.focusable" then
wrapped = wrapped + 1
end
end
assertEqual(roots > 0, true, name .. " defines widget roots")
assertEqual(wrapped, roots, "every widget root in " .. name .. " draws a focus cursor")
end
-- The detail page builds every control through one helper, region(), which
-- must wrap what it draws in the focus frame; the only other region is the
-- device status in the header, which takes no taps and joins no focus row.
local view = read(plugin_dir .. "bookorbit_detail_view.lua")
local regions, focusable_regions = 0, 0
for line in view:gmatch("[^\n]+") do
if line:find("Region:new{", 1, true) then
regions = regions + 1
if line:find("CatalogWidgets.focusable(", 1, true) then focusable_regions = focusable_regions + 1 end
end
end
assertEqual(regions, 2, "the detail view builds regions in exactly two places")
assertEqual(focusable_regions, 1, "and the tappable one draws a focus cursor")
assertEqual(view:find("primary_region[1] = CatalogWidgets.focusable(", 1, true) ~= nil, true,
"a button redrawn in place keeps its focus frame")
print("bookorbit_focus_highlight_test.lua: ok")