-- Stand-in for the firmware bindings, so host tests exercise the real ui.lua. -- -- One definition of the binding surface: when a binding is added or renamed, this is -- the file that changes. Tests mutate the returned table to steer the device, and may -- overwrite any stub function outright for a single assertion. local device = { -- Text metrics chosen to match the firmware's default GLCD font, since layout -- assertions are written in real pixels. fontHeight = 8, charWidth = 6, now = 0, rotation = 0, theme = "light", clockSynced = false, tickInterval = 0, appName = "test", textSize = 1, fullscreen = false, timezone = "UTC0", raw = nil, -- pending raw touch reading, {x, y} or nil networks = {}, -- what wifi.scan() returns status = {state = "disconnected", ssid = "", ip = "", rssi = 0}, painted = {}, -- every drawText call, in order calibration = nil, -- last settings.setCalibration() freeHeap = 200000, totalHeap = 320000, largestBlock = 100000, connected = nil, -- last wifi.connect() backed = false, launched = nil, -- last sys.launch()/replace(), {path, arg, replace} saveFails = false, -- make every persisting call report failure } local function saved() return not device.saveFails end function device.install() device.painted = {} -- The widget tree lives in the firmware. Geometry -- layout, hit testing, style -- inheritance -- is asserted in test/ui_layout_test.cpp against the same C++ the panel -- runs, so this fake deliberately implements none of it. What it does keep is the -- structure an app builds, which is what app logic is actually about: a test taps the -- control labelled "Rotation" rather than the pixel it happened to land on. local nodes = {} device.nodes = nodes device.drawn = 0 device.tapTarget = nil node = setmetatable({ reset = function() for index in ipairs(nodes) do nodes[index] = nil end end, create = function(parent, spec) nodes[#nodes + 1] = {parent = parent, label = spec.label, kind = spec.type, interactive = spec.interactive or spec.capture} return #nodes end, attach = function(parent, child) nodes[child].parent = parent end, getParent = function(id) return nodes[id].parent end, getLabel = function(id) return nodes[id].label end, setLabel = function(id, text) nodes[id].label = text end, getCount = function() return #nodes end, layout = function() return true end, -- Any point is inside, because which node a point covers is geometry and geometry is -- not this file's business. Tests choose the target with device.tap(). getRect = function() return 0, 0, 10000, 10000 end, hit = function() return device.tapTarget end, draw = function() device.drawn = device.drawn + 1 end, }, {__index = function() return function() end end}) gui = { color = function(r, g, b) return r * 65536 + g * 256 + b end, -- Rotation swaps the frame, exactly as TFT_eSPI reports it. getWidth = function() return device.rotation % 180 == 0 and 320 or 480 end, getHeight = function() return device.rotation % 180 == 0 and 480 or 320 end, clear = function() end, fillRect = function() end, drawRect = function() end, fillCircle = function() end, drawLine = function() end, roundRect = function() end, drawText = function(label, x, y) device.painted[#device.painted + 1] = {label = label, x = x, y = y} end, setTextSize = function(size) device.textSize = size end, getFontHeight = function() return device.fontHeight * device.textSize end, getTextWidth = function(text) return #text * device.charWidth * device.textSize end, setRotation = function(degrees) device.rotation = degrees end, getRotation = function() return device.rotation end, setFullscreen = function(on) device.fullscreen = on and true or false end, } sys = { getMillis = function() return device.now end, back = function() device.backed = true end, getAppName = function() return device.appName end, setAppName = function(name) device.appName = name end, getMemory = function() return device.freeHeap, device.totalHeap, device.largestBlock end, isClockSynced = function() return device.clockSynced end, setTickInterval = function(ms) assert(ms == 0 or on_tick, "setTickInterval without on_tick") device.tickInterval = ms end, launch = function(path, arg) device.launched = {path = path, arg = arg, replace = false} end, replace = function(path, arg) device.launched = {path = path, arg = arg, replace = true} end, } settings = { getRotation = function() return device.rotation end, setRotation = function(degrees) if degrees % 90 ~= 0 or degrees < 0 or degrees > 270 then return false end device.rotation = degrees return saved() end, getTheme = function() return device.theme end, setTheme = function(name) device.theme = name return saved() end, setCalibration = function(...) device.calibration = {...} return saved() end, getTimezone = function() return device.timezone end, setTimezone = function(tz) device.timezone = tz return saved() end, } input = { isTouched = function() return device.raw ~= nil end, getTouch = function() if not device.raw then return nil end return device.raw[1], device.raw[2] end, getRawTouch = function() if not device.raw then return nil end return device.raw[1], device.raw[2] end, } fs = { readFile = function() return nil end, writeFile = function() return saved() end, exists = function() return false end, listFiles = function() return {} end, listDirs = function() return {} end, } wifi = { scan = function() return device.networks end, getStatus = function() return device.status end, connect = function(ssid, password) device.connected = {ssid, password} return saved() end, forget = function() device.connected = nil return saved() end, } log = {debug = function() end, info = function() end, error = function() end} return device end -- The first node whose text starts with `prefix`, whether or not anything can press it. function device.labelled(prefix) for id, entry in ipairs(device.nodes) do if entry.label and entry.label:sub(1, #prefix) == prefix then return id end end end -- The nearest ancestor of that text that can be pressed: a card's label is a child of the -- button, and it is the button a finger lands on. function device.find(prefix) for id, entry in ipairs(device.nodes) do if entry.label and entry.label:sub(1, #prefix) == prefix then local target = id while target and not device.nodes[target].interactive do target = device.nodes[target].parent end if target then return target end end end end -- Presses the control labelled `prefix`, the way a test means it: by what it says, not by -- where it landed. function device.tap(prefix) local target = device.find(prefix) if not target then error("no control labelled '" .. prefix .. "'", 2) end device.press(target, 0, 0) end -- Presses a node at a point, for the widgets that have no label to aim at. getRect() -- answers the same box here as it does inside the widget, so a custom painter's own -- geometry decides what was hit, exactly as it does on the panel. function device.press(id, x, y) device.tapTarget = id on_touch_down(x, y) on_touch_up(x, y) device.tapTarget = nil end function device.findKind(kind) for id, entry in ipairs(device.nodes) do if entry.kind == kind then return id end end end return device