Files
bookorbit/koreader-plugin/spec/bookorbit_menu_context_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

204 lines
8.8 KiB
Lua

-- Proves KOReader's side of BookOrbit: a trimmed Tools entry, the menu bar
-- tab that acts as a button (and its opt-out), the icon path the tab bar can
-- resolve, and where each of the menu sheet's actions goes.
package.loaded["gettext"] = function(text)
return text
end
package.loaded["ffi/util"] = {
template = function(text, ...)
local values = { ... }
return (text:gsub("%%(%d+)", function(index)
return tostring(values[tonumber(index)])
end))
end,
}
local ticks = {}
package.loaded["device"] = { model = "test-device" }
package.loaded["ui/widget/infomessage"] = { new = function(_, opts) return opts end }
package.loaded["ui/widget/inputdialog"] = {}
package.loaded["ui/network/manager"] = {
runWhenConnected = function(_, callback) callback() end,
}
package.loaded["ui/widget/notification"] = {}
package.loaded["ui/uimanager"] = {
nextTick = function(_, callback) table.insert(ticks, callback) end,
tickAfterNext = function(_, callback) table.insert(ticks, callback) end,
show = function() end,
}
package.loaded["ffi/sha2"] = { md5 = function(value) return value end }
package.loaded["libs/libkoreader-lfs"] = { currentdir = function() return "/opt/koreader" end }
package.loaded["bookorbit_api"] = {}
package.loaded["bookorbit_highlight_diagnostics"] = {}
package.loaded["bookorbit_sweep"] = {
syncStatus = function()
return { lastSweepAt = 0, matched = 0, unmatched = 0 }
end,
}
package.path = "koreader-plugin/bookorbit.koplugin/?.lua;" .. package.path
local MainMenu = require("bookorbit_main_menu")
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
local function itemText(item)
if item.text then return item.text end
if item.text_func then return item:text_func() end
end
local function texts(items)
local out = {}
for _, item in ipairs(items) do table.insert(out, itemText(item)) end
return table.concat(out, "|")
end
local function newPlugin(opts)
opts = opts or {}
local calls = {}
local plugin = {
settings = { menu_bar_icon = opts.menu_bar_icon },
path = opts.path or "plugins/bookorbit.koplugin",
ui = opts.reader and { document = { file = "/books/Book.epub" }, menu = {} } or { menu = {} },
isLoggedIn = function() return opts.logged_out ~= true end,
calls = calls,
}
MainMenu.install(plugin)
for _, name in ipairs({ "browseCatalog", "openSignIn", "openBookSheet", "openSettings", "openSyncStatus", "startSweep", "applyUpdate", "customizeHome" }) do
plugin[name] = function(_, ...) table.insert(calls, { name, ... }) end
end
return plugin, calls
end
local function menuItems(plugin)
local menu_items = {}
plugin:addToMainMenu(menu_items)
return menu_items
end
local function runTicks()
while #ticks > 0 do table.remove(ticks, 1)() end
end
-- The file browser's Tools entry: open BookOrbit, or its settings.
local plugin, calls = newPlugin()
local items = menuItems(plugin)
assertEqual(texts(items.bookorbit.sub_item_table), "Open BookOrbit|Settings", "the Tools entry is two rows in the file browser")
assertEqual(items.bookorbit.sorting_hint, "tools", "the entry still falls back to the Tools menu")
-- In a book it adds This book.
local reader = newPlugin({ reader = true })
assertEqual(texts(menuItems(reader).bookorbit.sub_item_table), "Open BookOrbit|This book|Settings", "a book adds This book")
-- The menu bar tab is a button: never remembered, never a list.
local tab = items.bookorbit_tab
assertEqual(tab ~= nil, true, "the menu bar tab is offered by default")
assertEqual(tab.remember, false, "the tab is never remembered, so opening the menu never fires it")
assertEqual(#tab, 0, "the tab holds no rows")
assertEqual(type(tab.callback), "function", "the tab acts on a tap")
-- Its icon is the shipped SVG, reached from KOReader's icon folders by a
-- climb to the root, with the extension the tab bar adds stripped.
assertEqual(tab.icon:sub(1, 9), "../../../", "the icon name climbs out of the icon folders")
assertEqual(tab.icon:find("opt/koreader/plugins/bookorbit.koplugin/assets/bookorbit.tab", 1, true) ~= nil, true, "a relative plugin path is made absolute")
assertEqual(tab.icon:sub(-4) ~= ".svg", true, "the extension is left to the tab bar")
local user_plugin = newPlugin({ path = "/mnt/onboard/.adds/koreader/plugins/bookorbit.koplugin" })
assertEqual(user_plugin:menuBarIconName():find("../mnt/onboard/.adds/koreader/plugins/bookorbit.koplugin/assets/bookorbit.tab", 1, true) ~= nil, true,
"an absolute plugin path is used as it is")
-- Turned off, the tab is not registered at all (an unordered item would show
-- up as a NEW: row in the first menu).
local hidden = newPlugin({ menu_bar_icon = false })
assertEqual(menuItems(hidden).bookorbit_tab, nil, "no tab when the icon is turned off")
-- Tapping the tab: BookOrbit in the file browser, This book in a book, and
-- sign-in when signed out, each after the menu's own close.
plugin.ui.menu.onCloseFileManagerMenu = function() table.insert(calls, { "close_fm_menu" }) end
tab.callback()
assertEqual(calls[1][1], "close_fm_menu", "the KOReader menu closes first")
assertEqual(#calls, 1, "nothing opens before the close has painted")
runTicks()
assertEqual(calls[2][1], "browseCatalog", "the file browser's tab opens BookOrbit")
local book, book_calls = newPlugin({ reader = true })
book.ui.menu.onCloseReaderMenu = function() table.insert(book_calls, { "close_reader_menu" }) end
menuItems(book).bookorbit_tab.callback()
runTicks()
assertEqual(book_calls[1][1], "close_reader_menu", "the reader menu closes first")
assertEqual(book_calls[2][1], "openBookSheet", "in a book the tab opens This book")
local out, out_calls = newPlugin({ logged_out = true })
menuItems(out).bookorbit_tab.callback()
runTicks()
assertEqual(out_calls[1][1], "openSignIn", "signed out, the tab opens sign-in")
-- Open BookOrbit from Tools signs in first when needed.
out_calls[1] = nil
out:openBookOrbit()
assertEqual(out_calls[#out_calls][1], "openSignIn", "Open BookOrbit asks for a sign-in when signed out")
-- The sheet's actions close it, then act.
local sheet_plugin, sheet_calls = newPlugin()
local events = {}
local catalog = {
goDashboard = function() table.insert(events, "home") end,
onCloseAllMenus = function() table.insert(events, "exit") end,
onSearchButtonTap = function() table.insert(events, "search") end,
boardOpenLink = function(_, link) table.insert(events, "link:" .. link) end,
showBookActions = function() table.insert(events, "list-options") end,
}
local closes, refreshes = 0, 0
local page = {
close = function() closes = closes + 1 end,
refresh = function() refreshes = refreshes + 1 end,
sheet_data = {},
}
package.loaded["bookorbit_menu_sheet"] = { data = function() return { reloaded = true } end }
local function run(action)
sheet_plugin:onMenuSheetAction(page, catalog, action)
runTicks()
end
run({ kind = "home" })
run({ kind = "exit" })
run({ kind = "search" })
run({ kind = "link", link = "authors" })
run({ kind = "list-options" })
assertEqual(table.concat(events, ","), "home,exit,search,link:authors,list-options", "catalog actions reach the catalog")
assertEqual(closes, 5, "every action closes the sheet first")
-- Sync now keeps the sheet open: its tile shows the run, then the outcome.
run({ kind = "sync" })
assertEqual(closes, 5, "Sync now leaves the sheet open")
assertEqual(sheet_calls[1][1], "startSweep", "Sync now starts a library sync")
local sweep = sheet_calls[1][2]
assertEqual(sweep.quiet, true, "the sync reports to the sheet, not a dialog")
assertEqual(page.sheet_data.sync.running, true, "the tile says the sync is running")
sweep.on_progress("Matching books")
assertEqual(page.sheet_data.sync.text, "Matching books", "the short progress line is shown")
sweep.on_done(nil)
assertEqual(page.sheet_data.reloaded, true, "the sheet's numbers are reloaded when the sync ends")
assertEqual(page.sheet_data.sync.done, true, "the tile says the sync finished")
assertEqual(refreshes, 3, "the sheet redraws at start, on progress and at the end")
plugin.errorLabel = function() return "fallback" end
sheet_plugin.errorLabel = plugin.errorLabel
sweep.on_done("not_started")
assertEqual(page.sheet_data.sync.err, "Another sync is running", "a sync that did not start says why")
table.remove(sheet_calls, 1)
run({ kind = "settings" })
run({ kind = "status" })
run({ kind = "about" })
run({ kind = "customize" })
local names = {}
for _, call in ipairs(sheet_calls) do table.insert(names, call[1]) end
assertEqual(table.concat(names, ","), "openSettings,openSyncStatus,openSettings,customizeHome", "plugin actions reach the plugin")
assertEqual(sheet_calls[3][3], "about", "Help & about opens settings on About")
print("bookorbit_menu_context_test.lua: ok")