Skip to content

Commit 14e3ce5

Browse files
committed
test(steami_screen): add basic mock tests
Add initial mock test coverage for the steami_screen driver. ## Added - FakeDisplay backend to simulate a display device - Basic property tests: center, radius, max_chars - Core drawing API tests: clear, show, pixel, line, rect, text - Text helpers: title, subtitle, value - Widgets: bar, menu, face - Advanced widgets smoke tests: graph, gauge, compass, watch - Geometry helpers via public API: circle (outline and fill) - Edge cases: empty subtitle, unknown face, invalid position fallback Tests validate that high-level API correctly delegates to the display backend by inspecting recorded calls. ## Notes - Tests access the backend via `dev._d` due to runner scope limitations - No hardware dependency (pure mock) ## Remaining work - Validate exact layout/positions (not just call presence) - Test scaling behavior (text scale, value rendering) - Add snapshot/visual tests (e.g. Pillow backend) - Add hardware tests for real displays (SSD1327, GC9A01) - Improve coverage of edge cases (bounds, clipping, long text)
1 parent 02f4d70 commit 14e3ce5

3 files changed

Lines changed: 384 additions & 2 deletions

File tree

lib/steami_screen/steami_screen/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from steami_screen.colors import rgb_to_gray4, rgb_to_rgb8, rgb_to_rgb565
2-
from steami_screen.gc9a01 import GC9A01Display
3-
from steami_screen.screen import (
2+
from steami_screen.device import (
43
BLACK,
54
BLUE,
65
DARK,
@@ -12,6 +11,7 @@
1211
YELLOW,
1312
Screen,
1413
)
14+
from steami_screen.gc9a01 import GC9A01Display
1515
from steami_screen.sssd1327 import SSD1327Display
1616

1717
__all__ = [

tests/scenarios/steami_screen.yaml

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
driver: steami_screen
2+
driver_class: Screen
3+
4+
mock_init: |
5+
class FakeDisplay:
6+
def __init__(self):
7+
self.calls = []
8+
self.width = 128
9+
self.height = 128
10+
11+
def fill(self, color):
12+
self.calls.append(("fill", color))
13+
14+
def pixel(self, x, y, color):
15+
self.calls.append(("pixel", x, y, color))
16+
17+
def text(self, text, x, y, color):
18+
self.calls.append(("text", text, x, y, color))
19+
20+
def line(self, x1, y1, x2, y2, color):
21+
self.calls.append(("line", x1, y1, x2, y2, color))
22+
23+
def fill_rect(self, x, y, w, h, color):
24+
self.calls.append(("fill_rect", x, y, w, h, color))
25+
26+
def rect(self, x, y, w, h, color):
27+
self.calls.append(("rect", x, y, w, h, color))
28+
29+
def show(self):
30+
self.calls.append(("show",))
31+
32+
def clear_calls(self):
33+
self.calls = []
34+
35+
dev = Screen(FakeDisplay())
36+
37+
tests:
38+
- name: "Center is computed correctly"
39+
action: script
40+
script: |
41+
result = dev.center == (64, 64)
42+
expect_true: true
43+
mode: [mock]
44+
45+
- name: "Radius is computed correctly"
46+
action: script
47+
script: |
48+
result = dev.radius == 64
49+
expect_true: true
50+
mode: [mock]
51+
52+
- name: "Max chars matches screen width"
53+
action: script
54+
script: |
55+
result = dev.max_chars == 16
56+
expect_true: true
57+
mode: [mock]
58+
59+
- name: "clear calls backend fill"
60+
action: script
61+
script: |
62+
d = dev._d
63+
d.clear_calls()
64+
dev.clear()
65+
result = len(d.calls) == 1 and d.calls[0][0] == "fill"
66+
expect_true: true
67+
mode: [mock]
68+
69+
- name: "show calls backend show"
70+
action: script
71+
script: |
72+
d = dev._d
73+
d.clear_calls()
74+
dev.show()
75+
result = len(d.calls) == 1 and d.calls[0][0] == "show"
76+
expect_true: true
77+
mode: [mock]
78+
79+
- name: "pixel calls backend pixel"
80+
action: script
81+
script: |
82+
d = dev._d
83+
d.clear_calls()
84+
dev.pixel(10, 20)
85+
result = d.calls == [("pixel", 10, 20, (255, 255, 255))]
86+
expect_true: true
87+
mode: [mock]
88+
89+
- name: "line calls backend line"
90+
action: script
91+
script: |
92+
d = dev._d
93+
d.clear_calls()
94+
dev.line(1, 2, 30, 40)
95+
result = d.calls == [("line", 1, 2, 30, 40, (255, 255, 255))]
96+
expect_true: true
97+
mode: [mock]
98+
99+
- name: "rect outline uses backend rect"
100+
action: script
101+
script: |
102+
d = dev._d
103+
d.clear_calls()
104+
dev.rect(5, 6, 20, 10)
105+
result = d.calls == [("rect", 5, 6, 20, 10, (255, 255, 255))]
106+
expect_true: true
107+
mode: [mock]
108+
109+
- name: "rect fill uses backend fill_rect"
110+
action: script
111+
script: |
112+
d = dev._d
113+
d.clear_calls()
114+
dev.rect(5, 6, 20, 10, fill=True)
115+
result = d.calls == [("fill_rect", 5, 6, 20, 10, (255, 255, 255))]
116+
expect_true: true
117+
mode: [mock]
118+
119+
- name: "text at CENTER draws backend text"
120+
action: script
121+
script: |
122+
d = dev._d
123+
d.clear_calls()
124+
dev.text("Hi")
125+
result = len(d.calls) >= 1 and d.calls[0][0] == "text"
126+
expect_true: true
127+
mode: [mock]
128+
129+
- name: "text at explicit coordinates uses given position"
130+
action: script
131+
script: |
132+
d = dev._d
133+
d.clear_calls()
134+
dev.text("Hi", at=(12, 34))
135+
result = d.calls == [("text", "Hi", 12, 34, (255, 255, 255))]
136+
expect_true: true
137+
mode: [mock]
138+
139+
- name: "text scale 2 still renders text"
140+
action: script
141+
script: |
142+
d = dev._d
143+
d.clear_calls()
144+
dev.text("Hi", scale=2)
145+
text_calls = [c for c in d.calls if c[0] == "text"]
146+
result = len(text_calls) > 0
147+
expect_true: true
148+
mode: [mock]
149+
150+
- name: "title draws text near north position"
151+
action: script
152+
script: |
153+
d = dev._d
154+
d.clear_calls()
155+
dev.title("Hello")
156+
text_calls = [c for c in d.calls if c[0] == "text"]
157+
result = len(text_calls) == 1 and text_calls[0][1] == "Hello"
158+
expect_true: true
159+
mode: [mock]
160+
161+
- name: "subtitle with one line draws one text call"
162+
action: script
163+
script: |
164+
d = dev._d
165+
d.clear_calls()
166+
dev.subtitle("Bottom")
167+
text_calls = [c for c in d.calls if c[0] == "text"]
168+
result = len(text_calls) == 1 and text_calls[0][1] == "Bottom"
169+
expect_true: true
170+
mode: [mock]
171+
172+
- name: "subtitle with multiple lines draws multiple text calls"
173+
action: script
174+
script: |
175+
d = dev._d
176+
d.clear_calls()
177+
dev.subtitle("Line1", "Line2", "Line3")
178+
text_calls = [c for c in d.calls if c[0] == "text"]
179+
labels = [c[1] for c in text_calls]
180+
result = len(text_calls) == 3 and labels == ["Line1", "Line2", "Line3"]
181+
expect_true: true
182+
mode: [mock]
183+
184+
- name: "subtitle with no lines does nothing"
185+
action: script
186+
script: |
187+
d = dev._d
188+
d.clear_calls()
189+
dev.subtitle()
190+
result = len(d.calls) == 0
191+
expect_true: true
192+
mode: [mock]
193+
194+
- name: "value draws main value"
195+
action: script
196+
script: |
197+
d = dev._d
198+
d.clear_calls()
199+
dev.value(42)
200+
text_calls = [c for c in d.calls if c[0] == "text"]
201+
result = len(text_calls) > 0
202+
expect_true: true
203+
mode: [mock]
204+
205+
- name: "value with label draws label and value"
206+
action: script
207+
script: |
208+
d = dev._d
209+
d.clear_calls()
210+
dev.value(42, label="Temp")
211+
text_calls = [c for c in d.calls if c[0] == "text"]
212+
labels = [c[1] for c in text_calls]
213+
result = "Temp" in labels
214+
expect_true: true
215+
mode: [mock]
216+
217+
- name: "value with unit draws extra text"
218+
action: script
219+
script: |
220+
d = dev._d
221+
d.clear_calls()
222+
dev.value(42, unit="C")
223+
text_calls = [c for c in d.calls if c[0] == "text"]
224+
labels = [c[1] for c in text_calls]
225+
result = "C" in labels
226+
expect_true: true
227+
mode: [mock]
228+
229+
- name: "bar draws background and fill"
230+
action: script
231+
script: |
232+
d = dev._d
233+
d.clear_calls()
234+
dev.bar(50, max_val=100)
235+
fill_calls = [c for c in d.calls if c[0] == "fill_rect"]
236+
result = len(fill_calls) == 2
237+
expect_true: true
238+
mode: [mock]
239+
240+
- name: "bar with zero value only draws background"
241+
action: script
242+
script: |
243+
d = dev._d
244+
d.clear_calls()
245+
dev.bar(0, max_val=100)
246+
fill_calls = [c for c in d.calls if c[0] == "fill_rect"]
247+
result = len(fill_calls) == 1
248+
expect_true: true
249+
mode: [mock]
250+
251+
- name: "menu draws all visible items"
252+
action: script
253+
script: |
254+
d = dev._d
255+
d.clear_calls()
256+
dev.menu(["A", "B", "C"], selected=1)
257+
text_calls = [c for c in d.calls if c[0] == "text"]
258+
labels = [c[1] for c in text_calls]
259+
result = len(text_calls) == 3 and "> B" in labels
260+
expect_true: true
261+
mode: [mock]
262+
263+
- name: "face happy draws filled pixels"
264+
action: script
265+
script: |
266+
d = dev._d
267+
d.clear_calls()
268+
dev.face("happy")
269+
fill_calls = [c for c in d.calls if c[0] == "fill_rect"]
270+
result = len(fill_calls) > 0
271+
expect_true: true
272+
mode: [mock]
273+
274+
- name: "face custom bitmap draws filled pixels"
275+
action: script
276+
script: |
277+
d = dev._d
278+
d.clear_calls()
279+
dev.face((0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xFF))
280+
fill_calls = [c for c in d.calls if c[0] == "fill_rect"]
281+
result = len(fill_calls) > 0
282+
expect_true: true
283+
mode: [mock]
284+
285+
- name: "face unknown expression does nothing"
286+
action: script
287+
script: |
288+
d = dev._d
289+
d.clear_calls()
290+
dev.face("unknown")
291+
result = len(d.calls) == 0
292+
expect_true: true
293+
mode: [mock]
294+
295+
- name: "graph with one point draws axes and value without crashing"
296+
action: script
297+
script: |
298+
d = dev._d
299+
d.clear_calls()
300+
dev.graph([12], min_val=0, max_val=100)
301+
result = len(d.calls) > 0
302+
expect_true: true
303+
mode: [mock]
304+
305+
- name: "graph with multiple points draws line segments"
306+
action: script
307+
script: |
308+
d = dev._d
309+
d.clear_calls()
310+
dev.graph([10, 20, 15, 30], min_val=0, max_val=40)
311+
line_calls = [c for c in d.calls if c[0] == "line"]
312+
result = len(line_calls) > 0
313+
expect_true: true
314+
mode: [mock]
315+
316+
- name: "gauge draws arc and labels"
317+
action: script
318+
script: |
319+
d = dev._d
320+
d.clear_calls()
321+
dev.gauge(50, min_val=0, max_val=100)
322+
pixel_calls = [c for c in d.calls if c[0] == "pixel"]
323+
text_calls = [c for c in d.calls if c[0] == "text"]
324+
result = len(pixel_calls) > 0 and len(text_calls) > 0
325+
expect_true: true
326+
mode: [mock]
327+
328+
- name: "compass draws needle and labels"
329+
action: script
330+
script: |
331+
d = dev._d
332+
d.clear_calls()
333+
dev.compass(90)
334+
text_calls = [c for c in d.calls if c[0] == "text"]
335+
line_calls = [c for c in d.calls if c[0] == "line"]
336+
result = len(text_calls) >= 4 and len(line_calls) > 0
337+
expect_true: true
338+
mode: [mock]
339+
340+
- name: "watch draws clock face"
341+
action: script
342+
script: |
343+
d = dev._d
344+
d.clear_calls()
345+
dev.watch(10, 15, 30)
346+
text_calls = [c for c in d.calls if c[0] == "text"]
347+
line_calls = [c for c in d.calls if c[0] == "line"]
348+
result = len(text_calls) >= 4 and len(line_calls) > 0
349+
expect_true: true
350+
mode: [mock]
351+
352+
- name: "Resolve invalid cardinal falls back to center"
353+
action: script
354+
script: |
355+
d = dev._d
356+
d.clear_calls()
357+
dev.text("X", at="INVALID")
358+
result = len(d.calls) == 1 and d.calls[0][0] == "text"
359+
expect_true: true
360+
mode: [mock]
361+
362+
- name: "circle outline draws pixels"
363+
action: script
364+
script: |
365+
d = dev._d
366+
d.clear_calls()
367+
dev.circle(64, 64, 10)
368+
pixel_calls = [c for c in d.calls if c[0] == "pixel"]
369+
result = len(pixel_calls) > 0
370+
expect_true: true
371+
mode: [mock]
372+
373+
- name: "circle fill draws lines"
374+
action: script
375+
script: |
376+
d = dev._d
377+
d.clear_calls()
378+
dev.circle(64, 64, 10, fill=True)
379+
line_calls = [c for c in d.calls if c[0] == "line"]
380+
result = len(line_calls) > 0
381+
expect_true: true
382+
mode: [mock]

0 commit comments

Comments
 (0)