mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 19:54:21 +02:00
CLOSES #1393
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
-- Driver: renders the launcher's FIND MODS tab against a canned index whose
|
||||
-- listings carry the feed's `downloads` object, so the card line, the sort
|
||||
-- popup and the per-mod popup can be eyeballed with real numbers, real
|
||||
-- unknowns and a real zero in the same list.
|
||||
-- SHOT_DIR=/tmp/finddl POKEPORT_DRIVER=tests/drivers/launcher_find_downloads_shot.lua love .
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local RomImporter = require("src.import.RomImporter")
|
||||
|
||||
local dir = os.getenv("SHOT_DIR") or "/tmp/finddl"
|
||||
os.execute('mkdir -p "' .. dir .. '" 2>/dev/null')
|
||||
love.window.setMode(1024, 768, { resizable = true, highdpi = true })
|
||||
U.wait(2)
|
||||
|
||||
local imp = RomImporter.new(function() end, { launcher = true })
|
||||
|
||||
local function mod(id, title, downloads)
|
||||
return {
|
||||
id = id, title = title, author = "someone", version = "1.0.0",
|
||||
summary = "A canned listing for the screenshot.",
|
||||
categories = { "GAMEPLAY" }, tags = {}, games = {},
|
||||
update_check = "ok",
|
||||
downloads = downloads,
|
||||
latest = { version = "1.0.0", zip = {
|
||||
url = "https://example.invalid/" .. id .. ".zip" } },
|
||||
}
|
||||
end
|
||||
|
||||
-- the canned listing IS the fixture: never let the tab go and fetch the
|
||||
-- player's real sources over it
|
||||
-- new() prewarms a real fetch; drop the in-flight handle and the pump
|
||||
imp._refreshFindSources = function() end
|
||||
imp._refreshFind = function() end
|
||||
imp._pumpFindFetch = function() end
|
||||
imp._findFetch = nil
|
||||
imp.findSources = { { feed = "https://example.invalid/data/index.json",
|
||||
base = "https://example.invalid/",
|
||||
label = "example/index" } }
|
||||
imp.findIndex = { schemaVersion = 1, categories = { "GAMEPLAY" }, mods = {
|
||||
mod("busy", "Busy Mod",
|
||||
{ total = 1578, recent = 388, window_days = 30,
|
||||
as_of = "2026-08-18T05:17:00.000Z" }),
|
||||
mod("huge", "Huge Mod",
|
||||
{ total = 1234567, recent = 24000, window_days = 30,
|
||||
as_of = "2026-08-18T05:17:00.000Z" }),
|
||||
mod("young", "Young Mod",
|
||||
{ total = 42, as_of = "2026-08-18T05:17:00.000Z" }),
|
||||
mod("counted-zero", "Counted Zero Mod", { total = 0 }),
|
||||
mod("unknown", "Unknown Mod", nil),
|
||||
} }
|
||||
imp.findLoaded = true
|
||||
imp:_switchTab("find")
|
||||
U.wait(3)
|
||||
|
||||
local pending = nil
|
||||
love.draw = function()
|
||||
imp:draw()
|
||||
if pending then
|
||||
local path = pending
|
||||
pending = nil
|
||||
love.graphics.captureScreenshot(function(imagedata)
|
||||
local f = io.open(path, "wb")
|
||||
if f then f:write(imagedata:encode("png"):getString()) f:close() end
|
||||
end)
|
||||
end
|
||||
end
|
||||
local function shot(name)
|
||||
pending = dir .. "/" .. name
|
||||
for _ = 1, 90 do
|
||||
if not pending then break end
|
||||
imp:update(1 / 60)
|
||||
coroutine.yield()
|
||||
end
|
||||
U.wait(3)
|
||||
local f = io.open(dir .. "/" .. name, "rb")
|
||||
U.log(f and "shot" or "FAIL shot", name)
|
||||
if f then f:close() end
|
||||
end
|
||||
|
||||
local ModUpdate = require("src.mods.ModUpdate")
|
||||
for _, e in ipairs(imp.findIndex.mods) do
|
||||
local s = imp:_findStats(e)
|
||||
U.log((" %-13s %s | trending=%s"):format(e.id,
|
||||
ModUpdate.downloadsShort(s and s.total),
|
||||
tostring(s and ModUpdate.trendingLine(s.recent, s.windowDays))))
|
||||
end
|
||||
|
||||
for _, key in ipairs({ "popularity", "trending", "name" }) do
|
||||
imp.modSort = key
|
||||
imp._findSortCache = nil
|
||||
U.wait(2)
|
||||
shot("find_sort_" .. key .. ".png")
|
||||
local order = {}
|
||||
for _, e in ipairs(imp._findSortCache.list) do order[#order + 1] = e.id end
|
||||
U.log("sort " .. key .. ":", table.concat(order, ", "))
|
||||
end
|
||||
|
||||
imp.modSort = "popularity"
|
||||
imp._sortPopup = "find"
|
||||
U.wait(2)
|
||||
shot("find_sort_popup.png")
|
||||
imp._sortPopup = nil
|
||||
|
||||
imp._findEntry = imp.findIndex.mods[1]
|
||||
U.wait(2)
|
||||
shot("find_entry_counted.png")
|
||||
imp._findEntry = imp.findIndex.mods[5]
|
||||
U.wait(2)
|
||||
shot("find_entry_unknown.png")
|
||||
imp._findEntry = nil
|
||||
|
||||
-- the installed-mods tab shares the persisted sort key; Trending there has
|
||||
-- nothing to sort by, so it must degrade rather than reorder on nothing
|
||||
imp.modSort = "trending"
|
||||
imp:_switchTab("mods")
|
||||
U.wait(3)
|
||||
shot("mods_tab_trending_fallback.png")
|
||||
|
||||
U.log("done")
|
||||
love.event.quit()
|
||||
end
|
||||
@@ -0,0 +1,93 @@
|
||||
-- Driver: opens FIND MODS against the player's real index sources, lets the
|
||||
-- prewarm fetch land, and reports what the panel resolved without asking
|
||||
-- GitHub anything -- the check behind "sorting only covers the pages I
|
||||
-- visited". Shoots the first and last page of the download sort.
|
||||
-- SHOT_DIR=/tmp/findreal POKEPORT_DRIVER=tests/drivers/launcher_find_real_index.lua love .
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local ModUpdate = require("src.mods.ModUpdate")
|
||||
|
||||
local fetched = 0
|
||||
local realBegin = ModUpdate.beginFetchReleases
|
||||
ModUpdate.beginFetchReleases = function(...)
|
||||
fetched = fetched + 1
|
||||
return realBegin(...)
|
||||
end
|
||||
|
||||
local RomImporter = require("src.import.RomImporter")
|
||||
local dir = os.getenv("SHOT_DIR") or "/tmp/findreal"
|
||||
os.execute('mkdir -p "' .. dir .. '" 2>/dev/null')
|
||||
love.window.setMode(1024, 768, { resizable = true, highdpi = true })
|
||||
U.wait(2)
|
||||
|
||||
local imp = RomImporter.new(function() end, { launcher = true })
|
||||
imp:_switchTab("find")
|
||||
for _ = 1, 900 do
|
||||
imp:update(1 / 60)
|
||||
coroutine.yield()
|
||||
if imp.findLoaded and not imp._findFetch then break end
|
||||
end
|
||||
|
||||
local mods = (imp.findIndex and imp.findIndex.mods) or {}
|
||||
U.log("index mods:", #mods)
|
||||
U.log("stale copy:", tostring(imp.findIndex and imp.findIndex.stale))
|
||||
|
||||
local pending = nil
|
||||
love.draw = function()
|
||||
imp:draw()
|
||||
if pending then
|
||||
local path = pending
|
||||
pending = nil
|
||||
love.graphics.captureScreenshot(function(imagedata)
|
||||
local f = io.open(path, "wb")
|
||||
if f then f:write(imagedata:encode("png"):getString()) f:close() end
|
||||
end)
|
||||
end
|
||||
end
|
||||
local function shot(name)
|
||||
pending = dir .. "/" .. name
|
||||
for _ = 1, 90 do
|
||||
if not pending then break end
|
||||
imp:update(1 / 60)
|
||||
coroutine.yield()
|
||||
end
|
||||
U.wait(3)
|
||||
local f = io.open(dir .. "/" .. name, "rb")
|
||||
U.log(f and "shot" or "FAIL shot", name)
|
||||
if f then f:close() end
|
||||
end
|
||||
|
||||
local counted, dated, unknown = 0, 0, 0
|
||||
for _, e in ipairs(mods) do
|
||||
local s = imp:_findStatsCached(e)
|
||||
if s and s.total then counted = counted + 1 else unknown = unknown + 1 end
|
||||
if s and s.latest then dated = dated + 1 end
|
||||
end
|
||||
U.log(("counts=%d dated=%d unknown=%d github requests=%d")
|
||||
:format(counted, dated, unknown, fetched))
|
||||
|
||||
imp.modSort = "popularity"
|
||||
imp._findSortCache = nil
|
||||
U.wait(3)
|
||||
shot("real_page1.png")
|
||||
local sorted = imp._findSortCache and imp._findSortCache.list or {}
|
||||
for i = 1, math.min(3, #sorted) do
|
||||
local s = imp:_findStatsCached(sorted[i])
|
||||
U.log((" #%d %s %s"):format(i, sorted[i].title,
|
||||
ModUpdate.downloadsShort(s and s.total)))
|
||||
end
|
||||
for i = math.max(1, #sorted - 2), #sorted do
|
||||
local s = imp:_findStatsCached(sorted[i])
|
||||
U.log((" #%d %s %s"):format(i, sorted[i].title,
|
||||
ModUpdate.downloadsShort(s and s.total)))
|
||||
end
|
||||
|
||||
-- the last page is the one a page-by-page resolve would have left unsorted
|
||||
imp._pages["find"] = 99
|
||||
U.wait(3)
|
||||
U.log("github requests after sorting:", fetched)
|
||||
shot("real_last_page.png")
|
||||
|
||||
U.log("done")
|
||||
love.event.quit()
|
||||
end
|
||||
@@ -0,0 +1,125 @@
|
||||
-- FIND MODS download counts: the index feed publishes them per listing, so
|
||||
-- the panel reads the one fetch it already made instead of asking GitHub per
|
||||
-- mod (that fan-out is what blew the hourly API limit in _findStatsCached's
|
||||
-- own comment). This pins the resolver's precedence, the unknown-is-not-zero
|
||||
-- rule the card and the sorts both depend on, and the Trending wiring.
|
||||
-- luajit tests/engine/launcher_mod_downloads.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
|
||||
-- No launcher test may reach the network: beginFetchReleases is the only door
|
||||
-- out of _requestFindStats, and counting the calls is the assertion.
|
||||
local ModUpdate = require("src.mods.ModUpdate")
|
||||
local fetched = {}
|
||||
ModUpdate.beginFetchReleases = function(repo, id)
|
||||
fetched[#fetched + 1] = id
|
||||
return { stage = "done" }
|
||||
end
|
||||
|
||||
local RomImporter = require("src.import.RomImporter")
|
||||
local function launcher()
|
||||
return setmetatable({ tab = "find" }, RomImporter)
|
||||
end
|
||||
|
||||
local function entry(id, downloads, github)
|
||||
return { id = id, title = id, downloads = downloads, github = github }
|
||||
end
|
||||
|
||||
-- ------- the feed's counts win, and cost nothing
|
||||
|
||||
do
|
||||
local ri = launcher()
|
||||
local e = entry("counted", { total = 1578, recent = 388, window_days = 30,
|
||||
as_of = "2026-08-18T05:17:00.000Z" },
|
||||
"someone/counted")
|
||||
local stats = ri:_findStats(e)
|
||||
eq(stats.total, 1578, "the feed's total is what the card shows")
|
||||
eq(stats.recent, 388, "the trailing-window count rides along")
|
||||
eq(stats.windowDays, 30, "so does the window it covers")
|
||||
eq(stats.asOf, "2026-08-18T05:17:00.000Z", "and when the index read it")
|
||||
eq(#fetched, 0, "a listing with a published total asks GitHub for nothing")
|
||||
end
|
||||
|
||||
-- A bare number is what a cache written before the object shipped holds; it
|
||||
-- is read back through the same resolver rather than migrated.
|
||||
do
|
||||
local ri = launcher()
|
||||
eq(ri:_findStats(entry("legacy", 4321, "someone/legacy")).total, 4321,
|
||||
"a cached scalar count still resolves")
|
||||
eq(#fetched, 0, "and still costs no request")
|
||||
end
|
||||
|
||||
-- Whole-list, not page-by-page: sorting calls the cached read for every entry
|
||||
-- in the index, so all of them have to resolve from the feed alone. A
|
||||
-- listing the feed dates but does not count still resolves, off `latest`.
|
||||
do
|
||||
local ri = launcher()
|
||||
local dated = { id = "dated", title = "Dated", github = "someone/dated",
|
||||
latest = { version = "1.0.0",
|
||||
published_at = "2026-08-11T17:19:00Z" } }
|
||||
local stats = ri:_findStats(dated)
|
||||
eq(stats.latest, "2026-08-11", "the feed's own release blob dates the row")
|
||||
eq(#fetched, 0, "with no repo fetch to wait on")
|
||||
eq(ModUpdate.datesLine(stats.first, stats.latest), "Updated 2026-08-11",
|
||||
"and the card states what it knows instead of a Released ?")
|
||||
end
|
||||
|
||||
-- ------- unknown is unknown: null falls through to the repo, as any other
|
||||
-- missing feed stat does, and a listing with no repo resolves to no counts
|
||||
-- rather than to zero.
|
||||
|
||||
do
|
||||
local ri = launcher()
|
||||
check(ri:_findStats(entry("nulled", nil, "someone/nulled")) == nil,
|
||||
"a null count is not an answer; the repo is still consulted")
|
||||
eq(#fetched, 1, "which is the fetch the panel already made for dates")
|
||||
|
||||
local bare = launcher()
|
||||
local stats = bare:_findStats(entry("no-repo", nil, nil))
|
||||
check(stats ~= nil and stats.total == nil,
|
||||
"a listing with neither counts nor a repo is resolved-but-unknown")
|
||||
check(stats.recent == nil, "and has nothing to trend on")
|
||||
eq(#fetched, 1, "and queues nothing of its own")
|
||||
end
|
||||
|
||||
-- A real zero is not unknown: the index has seen the releases and counted
|
||||
-- none. The card prints it and the sort ranks it above the unknowns.
|
||||
do
|
||||
local ri = launcher()
|
||||
local stats = ri:_findStats(entry("zero", { total = 0 }, "someone/zero"))
|
||||
eq(stats.total, 0, "a counted zero survives as a number")
|
||||
eq(ModUpdate.downloadsShort(stats.total), "0 downloads",
|
||||
"and prints as zero rather than as a dash")
|
||||
end
|
||||
|
||||
-- ------- the panel wiring around those numbers
|
||||
|
||||
local view = (function()
|
||||
local f = assert(io.open("src/import/LauncherView.lua", "r"))
|
||||
local src = f:read("*a")
|
||||
f:close()
|
||||
return src
|
||||
end)()
|
||||
|
||||
check(view:find('key = "trending"', 1, true) ~= nil,
|
||||
"a Trending sort exists")
|
||||
check(view:find('sortKey == "trending" then return stats and stats.recent or %-1'),
|
||||
"Trending orders by the trailing-window count, unknown last")
|
||||
check(view:find('sortKey == "popularity" then return stats and stats.total or %-1'),
|
||||
"Most downloaded orders by the total, unknown last")
|
||||
-- Trending has no meaning on the installed-mods tab, and the persisted key is
|
||||
-- shared, so that panel has to degrade rather than sort on nothing.
|
||||
check(view:find('if scope == "find" then', 1, true) ~= nil,
|
||||
"Trending is offered only where the feed's counts exist")
|
||||
check(view:find('sortKey == "trending" and scope ~= "find"', 1, true) ~= nil,
|
||||
"the MODS tab falls back when the shared sort key is FIND-only")
|
||||
check(view:find('sortKey or "popularity"', 1, true) ~= nil,
|
||||
"the default sort is unchanged")
|
||||
check(view:find("downloadsShort", 1, true) ~= nil,
|
||||
"the browse card prints the abbreviated count")
|
||||
|
||||
T.finish("launcher_mod_downloads")
|
||||
@@ -114,23 +114,110 @@ do
|
||||
"a feed without release stats parses them as absent")
|
||||
end
|
||||
|
||||
-- release stats a feed can publish: total downloads and first/last dates
|
||||
-- release stats a feed can publish: download counts and first/last dates
|
||||
-- ride along additively, so a feed carrying them stays readable by every
|
||||
-- build that predates them
|
||||
do
|
||||
local withStats = {}
|
||||
for k, v in pairs(NUZLOCKE) do withStats[k] = v end
|
||||
withStats.downloads = 1234
|
||||
withStats.downloads = { total = 1578, recent = 388, window_days = 30,
|
||||
as_of = "2026-08-18T05:17:00.000Z" }
|
||||
withStats.first_release = "2024-05-31"
|
||||
withStats.last_release = "2026-07-01"
|
||||
local index = ModIndex.parse(feed({ withStats }))
|
||||
local m = index.mods[1]
|
||||
eq(m.downloads, 1234, "total downloads are kept")
|
||||
eq(m.downloads.total, 1578, "total downloads are kept")
|
||||
eq(m.downloads.recent, 388, "the trailing-window count is kept")
|
||||
eq(m.downloads.window_days, 30, "the window length is kept")
|
||||
eq(m.downloads.as_of, "2026-08-18T05:17:00.000Z", "the read time is kept")
|
||||
eq(m.first_release, "2024-05-31", "first release date is kept")
|
||||
eq(m.last_release, "2026-07-01", "last release date is kept")
|
||||
withStats.downloads = "9999"
|
||||
m = ModIndex.parse(feed({ withStats })).mods[1]
|
||||
eq(m.downloads, 9999, "numeric-string downloads are coerced")
|
||||
end
|
||||
|
||||
-- ------- download counts: unknown is not zero
|
||||
--
|
||||
-- The feed's `downloads` object has three ways of saying "not known" -- the
|
||||
-- field absent, the field null, and a null count inside it -- and every one
|
||||
-- of them has to stay distinguishable from a real zero, because the browse
|
||||
-- card prints one and sorts the other.
|
||||
do
|
||||
local function jsonWith(downloads)
|
||||
local raw = {}
|
||||
for k, v in pairs(NUZLOCKE) do raw[k] = v end
|
||||
raw.downloads = downloads
|
||||
return feed({ raw })
|
||||
end
|
||||
local function statsForJson(text)
|
||||
return ModIndex.downloadStats(ModIndex.parse(text).mods[1])
|
||||
end
|
||||
local function statsFor(downloads)
|
||||
return statsForJson(jsonWith(downloads))
|
||||
end
|
||||
|
||||
check(statsFor(nil) == nil, "an absent downloads field is unknown")
|
||||
-- Json.encode has no null of its own, so the literal the feed actually
|
||||
-- sends is patched into the text.
|
||||
local nulled = jsonWith({}):gsub('"downloads":%[%]', '"downloads":null', 1)
|
||||
check(nulled:find('"downloads":null', 1, true) ~= nil,
|
||||
"the null feed fixture really contains a null")
|
||||
check(statsForJson(nulled) == nil, "a null downloads field is unknown")
|
||||
check(statsFor({}) == nil, "an object with no counts is unknown")
|
||||
eq(statsFor({ total = 0 }).total, 0, "a real zero total survives")
|
||||
|
||||
-- recent / window_days stay null until the index has more than a day of
|
||||
-- history, even once total is a real number.
|
||||
local young = statsFor({ total = 12, as_of = "2026-08-18T05:17:00.000Z" })
|
||||
eq(young.total, 12, "a total with no window yet is still a total")
|
||||
check(young.recent == nil and young.window_days == nil,
|
||||
"no trailing window means no trending figure, not a zero one")
|
||||
|
||||
-- A cache written before the object shipped stored a bare number; it is
|
||||
-- read back through the same door rather than migrated.
|
||||
local legacy = ModIndex.downloadStats({ downloads = 4321 })
|
||||
eq(legacy.total, 4321, "a bare number reads as the total")
|
||||
check(legacy.recent == nil, "and carries no trending figure")
|
||||
check(ModIndex.downloadStats({}) == nil, "a row with no counts is unknown")
|
||||
check(ModIndex.downloadStats(nil) == nil, "no entry is unknown")
|
||||
end
|
||||
|
||||
-- ------- release dates: the feed already dates every listing it can install
|
||||
--
|
||||
-- Sorting must span the whole index, not the pages a reader happened to
|
||||
-- visit, so the "last updated" date comes off the feed's own `latest` blob
|
||||
-- rather than out of a per-mod repo fetch.
|
||||
do
|
||||
local raw = {}
|
||||
for k, v in pairs(NUZLOCKE) do raw[k] = v end
|
||||
local d = ModIndex.releaseDates(ModIndex.parse(feed({ raw })).mods[1])
|
||||
eq(d.latest, "2026-07-31", "latest release date comes from latest.published_at")
|
||||
check(d.first == nil, "the feed cannot date a first release from that alone")
|
||||
|
||||
raw.first_release = "2024-05-31"
|
||||
raw.last_release = "2026-07-01"
|
||||
d = ModIndex.releaseDates(ModIndex.parse(feed({ raw })).mods[1])
|
||||
eq(d.first, "2024-05-31", "an explicit first_release wins")
|
||||
eq(d.latest, "2026-07-01", "an explicit last_release beats the latest blob")
|
||||
|
||||
local bare = {}
|
||||
for k, v in pairs(NUZLOCKE) do bare[k] = v end
|
||||
bare.latest, bare.update_check = nil, "no installable release"
|
||||
check(ModIndex.releaseDates(ModIndex.parse(feed({ bare })).mods[1]) == nil,
|
||||
"a listing with no releases has no dates")
|
||||
check(ModIndex.releaseDates(nil) == nil, "no entry has no dates")
|
||||
end
|
||||
|
||||
-- ------- cache version: a copy written before a field existed cannot answer
|
||||
-- for it, and the TTL is a whole day
|
||||
do
|
||||
local now = os.time()
|
||||
check(ModIndex.cacheFresh({ checkedAt = now,
|
||||
version = ModIndex.CACHE_VERSION }), "a current cache is fresh")
|
||||
check(not ModIndex.cacheFresh({ checkedAt = now }),
|
||||
"an unstamped cache is refetched rather than trusted for a day")
|
||||
check(not ModIndex.cacheFresh({ checkedAt = now,
|
||||
version = ModIndex.CACHE_VERSION - 1 }), "so is an older stamp")
|
||||
check(not ModIndex.cacheFresh({ checkedAt = now - ModIndex.CACHE_TTL - 1,
|
||||
version = ModIndex.CACHE_VERSION }), "and an expired one")
|
||||
end
|
||||
|
||||
-- schema_version is a contract, not a hint: an unknown one is refused rather
|
||||
|
||||
@@ -208,6 +208,32 @@ eq(ModUpdate.formatCount("12345"), "12,345", "numeric strings are accepted")
|
||||
eq(ModUpdate.formatCount(nil), "0", "nil formats as zero")
|
||||
eq(ModUpdate.formatCount("garbage"), "0", "garbage formats as zero")
|
||||
|
||||
-- abbrevCount: what a browse card has room for
|
||||
eq(ModUpdate.abbrevCount(0), "0", "zero abbreviates to itself")
|
||||
eq(ModUpdate.abbrevCount(999), "999", "below 1000 stays exact")
|
||||
eq(ModUpdate.abbrevCount(1000), "1k", "a round thousand drops its decimal")
|
||||
eq(ModUpdate.abbrevCount(1578), "1.6k", "thousands round to one decimal")
|
||||
eq(ModUpdate.abbrevCount(24000), "24k", "a round figure keeps no .0")
|
||||
eq(ModUpdate.abbrevCount(999999), "1M", "no count ever reads as 1000k")
|
||||
eq(ModUpdate.abbrevCount(1234567), "1.2M", "millions abbreviate too")
|
||||
eq(ModUpdate.abbrevCount("1578"), "1.6k", "numeric strings are accepted")
|
||||
eq(ModUpdate.abbrevCount(nil), "?", "an unknown count is not a zero")
|
||||
eq(ModUpdate.abbrevCount("garbage"), "?", "garbage is unknown, not a zero")
|
||||
|
||||
-- downloadsShort / trendingLine: the card and detail forms
|
||||
eq(ModUpdate.downloadsShort(1578), "1.6k downloads", "the card form abbreviates")
|
||||
eq(ModUpdate.downloadsShort(0), "0 downloads", "a real zero is still stated")
|
||||
eq(ModUpdate.downloadsShort(nil), "? downloads",
|
||||
"an unknown count says so rather than vanishing or reading as zero")
|
||||
eq(ModUpdate.trendingLine(388, 30), "388 in the last 30 days",
|
||||
"the trending line names its window")
|
||||
eq(ModUpdate.trendingLine(388, 12), "388 in the last 12 days",
|
||||
"a short window says so rather than claiming a month")
|
||||
eq(ModUpdate.trendingLine(388, nil), "388 recently",
|
||||
"a count with no window still reads")
|
||||
check(ModUpdate.trendingLine(nil, 30) == nil,
|
||||
"no trailing-window count means no trending line")
|
||||
|
||||
-- statsLine: the shared launcher row line, part by part
|
||||
eq(ModUpdate.statsLine(1234567, "2024-05-31", "2026-07-01"),
|
||||
"1,234,567 downloads across all releases - Released 2024-05-31 - Updated 2026-07-01",
|
||||
|
||||
Reference in New Issue
Block a user