mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 20:20:19 +02:00
269 lines
11 KiB
Lua
269 lines
11 KiB
Lua
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
love = love or require("tests.love_stub")
|
|
|
|
love.graphics.setLineJoin = love.graphics.setLineJoin or function() end
|
|
love.graphics.newShader = love.graphics.newShader or function() return {} end
|
|
|
|
local RomImporter = require("src.import.RomImporter")
|
|
local LauncherView = require("src.import.LauncherView")
|
|
local TouchSkin = require("src.core.TouchSkin")
|
|
|
|
local function read(path)
|
|
local f = assert(io.open(path, "r"))
|
|
local src = f:read("*a")
|
|
f:close()
|
|
return src
|
|
end
|
|
|
|
local function window(w, h)
|
|
love.graphics.getDimensions = function() return w, h end
|
|
love.graphics.getPixelDimensions = function() return w, h end
|
|
end
|
|
|
|
local function launcher()
|
|
return RomImporter.new(function() end, { launcher = true })
|
|
end
|
|
|
|
eq(RomImporter.skinUrlName("https://example.com/pads/gbc.zip"), "gbc.zip",
|
|
"a direct .zip keeps its name")
|
|
eq(RomImporter.skinUrlName("https://example.com/pads/Neon.deltaskin"),
|
|
"Neon.deltaskin", "and so does a .deltaskin")
|
|
eq(RomImporter.skinUrlName("https://example.com/overlay.cfg"), "overlay.cfg",
|
|
"a bare RetroArch cfg is kept as a cfg")
|
|
eq(RomImporter.skinUrlName("https://example.com/pads/gbc.zip?raw=1"), "gbc.zip",
|
|
"a query string is not part of the name")
|
|
eq(RomImporter.skinUrlName("https://example.com/pads/gbc.zip#frag"), "gbc.zip",
|
|
"nor is a fragment")
|
|
eq(RomImporter.skinUrlName("https://example.com/download"), "download.zip",
|
|
"an extension-less link is treated as an archive")
|
|
eq(RomImporter.skinUrlName("https://example.com/a b/../pad.tar"), "pad.zip",
|
|
"an unknown extension is replaced, and the name is sanitized")
|
|
check(RomImporter.skinUrlName("https://example.com/"):match("^[%w%._%-]+$"),
|
|
"the download name can never escape the skins folder")
|
|
|
|
local name, payload = RomImporter.wrapSkinPayload("overlay.cfg",
|
|
"overlays = 1\noverlay0_descs = 0\n")
|
|
eq(name, "overlay.zip", "a downloaded .cfg is wrapped into an archive")
|
|
eq(payload:sub(1, 2), "PK", "which is a real zip")
|
|
check(payload:find("overlays = 1", 1, true) ~= nil,
|
|
"carrying the cfg text inside it")
|
|
check(payload:find("overlay.cfg", 1, true) ~= nil,
|
|
"under the name RetroArch parsing expects")
|
|
|
|
local zipName, zipData = RomImporter.wrapSkinPayload("pad.zip", "PK\3\4stuff")
|
|
eq(zipName, "pad.zip", "a zip is passed through untouched")
|
|
eq(zipData, "PK\3\4stuff", "bytes and all")
|
|
eq(select(1, RomImporter.wrapSkinPayload("pad.deltaskin", "PK\3\4x")),
|
|
"pad.deltaskin", "and so is a .deltaskin")
|
|
local pkName, pkData = RomImporter.wrapSkinPayload("overlay.cfg", "PK\3\4real")
|
|
eq(pkName, "overlay.zip", "a .cfg link that serves zip bytes is renamed, not refused")
|
|
eq(pkData, "PK\3\4real", "and its bytes are left alone")
|
|
|
|
local imp = launcher()
|
|
check(not imp:_addSkinFromUrl(""), "an empty link is refused")
|
|
check(imp._skinNotice and not imp._skinNotice.ok, "with a visible error")
|
|
eq(imp._skinFetch, nil, "and no download is started")
|
|
check(not imp:_addSkinFromUrl("file:///etc/passwd"),
|
|
"a non-http link is refused")
|
|
eq(imp._skinFetch, nil, "and still starts nothing")
|
|
check(not imp:_addSkinFromUrl("skins/local.zip"),
|
|
"a bare path is not a link either")
|
|
|
|
local Fetch = require("src.net.Fetch")
|
|
local realDownload, realPoll, realRelease = Fetch.download, Fetch.poll,
|
|
Fetch.release
|
|
local asked
|
|
Fetch.download = function(url, dest) asked = { url = url, dest = dest } return 7 end
|
|
Fetch.poll = function() return { status = "pending", progress = 0.5 } end
|
|
Fetch.release = function() end
|
|
|
|
imp = launcher()
|
|
imp.skinUrl = "https://example.com/pads/neon.deltaskin"
|
|
check(imp:_addSkinFromUrl(), "a good link starts a download")
|
|
check(imp._skinFetch ~= nil, "and parks the job on the importer")
|
|
eq(asked.url, "https://example.com/pads/neon.deltaskin", "the url is fetched")
|
|
check(asked.dest:find("neon.deltaskin", 1, true) ~= nil,
|
|
"into a file named after the link")
|
|
check(asked.dest:find("%.%.") == nil, "with no traversal in the path")
|
|
check(not imp:_addSkinFromUrl("https://example.com/other.zip"),
|
|
"a second add while one is in flight is ignored")
|
|
|
|
imp:_pumpSkinFetch()
|
|
check(imp._skinFetch ~= nil, "a pending download stays in flight")
|
|
|
|
local installed
|
|
imp._installSkinData = function(_, n, d) installed = { name = n, data = d } return "neon" end
|
|
Fetch.poll = function()
|
|
return { status = "ok", path = "skins/_download/neon.deltaskin" }
|
|
end
|
|
love.filesystem.write("skins/_download/neon.deltaskin", "PK\3\4payload")
|
|
imp:_pumpSkinFetch()
|
|
eq(imp._skinFetch, nil, "a finished download is released")
|
|
check(installed ~= nil, "and its bytes go to the installer")
|
|
eq(installed.name, "neon.deltaskin", "under the downloaded name")
|
|
eq(love.filesystem.read("skins/_download/neon.deltaskin"), nil,
|
|
"the temporary download is cleaned up")
|
|
eq(imp.skinUrl, "", "and the field is cleared for the next one")
|
|
|
|
imp = launcher()
|
|
imp._installSkinData = function() return nil end
|
|
Fetch.download = function() return 8 end
|
|
Fetch.poll = function() return { status = "error", err = "404" } end
|
|
imp:_addSkinFromUrl("https://example.com/missing.zip")
|
|
imp:_pumpSkinFetch()
|
|
eq(imp._skinFetch, nil, "a failed download is released too")
|
|
check(imp._skinNotice and not imp._skinNotice.ok, "and reported")
|
|
check(tostring(imp._skinNotice.text):find("404", 1, true) ~= nil,
|
|
"with the reason attached")
|
|
|
|
Fetch.download, Fetch.poll, Fetch.release = realDownload, realPoll, realRelease
|
|
|
|
imp = launcher()
|
|
eq(imp:_installSkinData("pad.zip", ""), nil, "an empty payload is refused")
|
|
check(imp._skinNotice and not imp._skinNotice.ok, "and says so")
|
|
eq(imp:_installSkinData("notes.txt", "hello"), nil, "a non-archive is refused")
|
|
|
|
love.filesystem.write("skins/warny.zip/overlay.cfg", [[
|
|
overlays = 1
|
|
overlay0_name = "warny"
|
|
overlay0_normalized = true
|
|
overlay0_descs = 2
|
|
overlay0_desc0 = "a,0.5,0.5,rect,0.05,0.05"
|
|
]])
|
|
imp = launcher()
|
|
eq(imp:_installSkinData("warny.zip", "PK\3\4stub"), "warny", "a skin installs")
|
|
check(imp._skinNotice.ok, "with an ok notice")
|
|
check(tostring(imp._skinNotice.text):find("missing desc", 1, true) ~= nil,
|
|
"that repeats what the importer had to complain about")
|
|
|
|
love.filesystem.write("skins/vecty.deltaskin/info.json", [[
|
|
{ "name": "Vecty", "gameTypeIdentifier": "com.rileytestut.delta.game.gbc",
|
|
"representations": { "iphone": { "standard": { "portrait": {
|
|
"assets": { "resizable": "iphone_portrait.pdf" },
|
|
"mappingSize": {"width":320,"height":480},
|
|
"items": [ { "inputs": ["a"], "frame": {"x":0,"y":0,"width":32,"height":32} } ]
|
|
} } } } }
|
|
]])
|
|
imp = launcher()
|
|
eq(imp:_installSkinData("vecty.deltaskin", "PK\3\4stub"), nil,
|
|
"a PDF-only Delta skin does not install silently")
|
|
check(imp._skinNotice and not imp._skinNotice.ok, "the tab reports the refusal")
|
|
check(tostring(imp._skinNotice.text):find("PDF artwork", 1, true) ~= nil,
|
|
"and says why, instead of listing a skin with no buttons")
|
|
|
|
local function dropped(fileName)
|
|
return { getFilename = function() return fileName end,
|
|
open = function() return false end }
|
|
end
|
|
|
|
local routed
|
|
local function routeDrop(tab, fileName)
|
|
routed = nil
|
|
local drop = launcher()
|
|
drop.tab = tab
|
|
drop._installSkinZip = function() routed = "skin" end
|
|
drop._installMod = function() routed = "mod" end
|
|
drop.startData = function() routed = "rom" end
|
|
drop:filedropped(dropped(fileName))
|
|
return routed
|
|
end
|
|
|
|
eq(routeDrop("mods", "pad.deltaskin"), "skin",
|
|
"a dropped .deltaskin installs as a skin from any tab")
|
|
eq(routeDrop("skins", "pad.deltaskin"), "skin", "and from the skins tab")
|
|
eq(routeDrop("skins", "Neon.DeltaSkin"), "skin", "whatever its case")
|
|
eq(routeDrop("skins", "pad.zip"), "skin", "a zip on the skins tab is still a skin")
|
|
eq(routeDrop("mods", "pad.zip"), "mod", "and a mod anywhere else")
|
|
|
|
check(TouchSkin.saveTo(TouchSkin.newSkin("uxskin"), "uxskin") ~= nil,
|
|
"a skin to list")
|
|
imp = launcher()
|
|
local entries = imp:_ensureSkins(true)
|
|
check(#entries > 0, "the installed skins are listed")
|
|
local byId = {}
|
|
for _, entry in ipairs(entries) do
|
|
byId[entry.id] = entry
|
|
check(type(entry.format) == "string",
|
|
entry.id .. " reports the format it was parsed from")
|
|
end
|
|
eq(byId.uxskin and byId.uxskin.format, "native",
|
|
"a skin.lua skin is badged as the native format")
|
|
|
|
imp = launcher()
|
|
eq(imp:_exportSkin("no-such-skin", "native"), nil, "exporting a ghost fails")
|
|
check(imp._skinNotice and not imp._skinNotice.ok, "with an error notice")
|
|
|
|
local first = entries[1]
|
|
imp = launcher()
|
|
local path = imp:_exportSkin(first.id, "delta")
|
|
check(path ~= nil and path:match("%.deltaskin$") ~= nil,
|
|
"a bundled skin exports as a .deltaskin")
|
|
check(imp._skinNotice.ok, "and the tab reports where it landed")
|
|
check(tostring(imp._skinNotice.text):find(path, 1, true) ~= nil,
|
|
"naming the path, which is the whole mobile story")
|
|
check(imp._skinExport ~= nil and imp._skinExport.path == path,
|
|
"the export is remembered so Show file can reveal it")
|
|
path = imp:_exportSkin(first.id, "retroarch")
|
|
check(path ~= nil and path:match("%.zip$") ~= nil,
|
|
"and as a RetroArch .zip")
|
|
path = imp:_exportSkin(first.id, "native")
|
|
check(path ~= nil and path:match("%.zip$") ~= nil, "and as a gen1recomp .zip")
|
|
|
|
window(420, 900)
|
|
imp = launcher()
|
|
imp.tab = "skins"
|
|
LauncherView.draw(imp)
|
|
LauncherView.draw(imp)
|
|
check(true, "the skins tab draws with the URL row")
|
|
imp._skinActions = { id = entries[1].id }
|
|
LauncherView.draw(imp)
|
|
check(imp._skinActions ~= nil, "the actions sheet stays up while it draws")
|
|
imp._skinFetch = { name = "neon.zip" }
|
|
LauncherView.draw(imp)
|
|
imp._skinFetch = nil
|
|
|
|
window(320, 640)
|
|
LauncherView.draw(imp)
|
|
LauncherView.draw(imp)
|
|
check(true, "and on a phone-width window, where Paste gives up its room")
|
|
|
|
local view = read("src/import/LauncherView.lua")
|
|
local rom = read("src/import/RomImporter.lua")
|
|
|
|
check(view:find('"skins-url"', 1, true) ~= nil,
|
|
"the skins tab carries an add-by-URL field")
|
|
check(view:find('"skins-url-add"', 1, true) ~= nil, "with a button to submit it")
|
|
check(view:find('"skins-url-paste"', 1, true) ~= nil,
|
|
"and a paste button, because a phone cannot type a URL")
|
|
check(view:find("_addSkinFromUrl", 1, true) ~= nil,
|
|
"which reaches the importer's downloader")
|
|
check(view:find("Loader.inline", 1, true) ~= nil,
|
|
"and the row shows progress while it runs")
|
|
check(view:find("SKIN_FORMAT_LABEL", 1, true) ~= nil,
|
|
"rows carry a format badge")
|
|
check(view:find("buildSkinActionsModal", 1, true) ~= nil,
|
|
"the gear opens an actions sheet")
|
|
check(view:find("_exportSkin", 1, true) ~= nil, "which can export the skin")
|
|
check(view:find("skinact-exp-delta", 1, true) ~= nil,
|
|
"including as a Delta skin")
|
|
local modals = view:match("local function modalUp%(imp%)(.-)\nend")
|
|
check(modals and modals:find("_skinActions", 1, true) ~= nil,
|
|
"the sheet raises the modal shield like every other popup")
|
|
check(view:find("imp.onOpenSkinStudio(imp.modScope or \"red\", id)", 1, true)
|
|
~= nil, "and still hands the studio a real game version")
|
|
|
|
check(rom:find("deltaskin", 1, true) ~= nil,
|
|
"the desktop file picker offers .deltaskin")
|
|
check(rom:find("_pumpSkinFetch", 1, true) ~= nil,
|
|
"the skin download is pumped from update()")
|
|
local update = rom:match("function RomImporter:update%(dt%)(.-)\nend\n")
|
|
check(update and update:find("_pumpSkinFetch", 1, true) ~= nil,
|
|
"from inside update itself, not just declared")
|
|
check(TouchSkin.ARCHIVE_EXTS.deltaskin == true,
|
|
"and the installer accepts the extension")
|
|
|
|
T.finish("launcher_skins_ux")
|