render-ui-test
Goal: one PNG of real UI, from a test. Least code. Real code path. Glue -> reusable helper.
Deliverable is a test in the project's test framework — not a script, main(), or one-off exec method. A script renders once and is thrown away; a test renders on every run and its content asserts guard the behavior forever (rule 4).
Rules
- Mock at bottom, not UI. Fake lowest data seam (data source, transport, store, clock/env). Everything above = real project code. Never stub UI components -> defeats point.
- One helper does glue.
RenderToImage(buildSubject) -> png. Build deps -> call real render entry -> encode PNG -> write file. Lives in test-support, called from test.
- Reuse. Future tests call helper + swap mock data. No copy-paste.
- Assert content, not pixels. Every test asserts the key thing that should render — the must-show data/text/node count the real logic produces (e.g. user name shows, list has N rows, error banner present). Query the rendered view/tree/markup, not the PNG. This proves mock data flowed through real code into the UI. Eyeball once; content asserts guard forever.
Workflow
- Find render entry — topmost fn app actually calls to make UI for page/component (not leaf widget). Trace route/handler/screen down to view it returns.
- Find lowest data seam — where external data enters. Pick deepest injectable -> max real code above.
- Write helper (see template). One mock impl -> canned data. One render call. One file write.
- Write test — test fn picks subject, overrides mock data, calls helper. This is the deliverable.
- Render — run the test. Project's native offscreen/headless path turns view into pixels. No app launch, no server.
- Eyeball + assert (rule 4) — test writes
out/<name>.png; open it, confirm real component + mock data show. Blank/wrong -> seam too high or entry wrong -> back to 1–2. Then bake the must-show content into asserts before done.
Helper template (pseudocode)
# test-support: reusable glue
RenderToImage(name, buildSubject):
deps = Deps{ data: fakeData() } # mock injected at lowest seam
subject = buildSubject(deps) # real project code runs above
view = render(subject) # real render entry
png = rasterize(view) # offscreen/headless -> bytes
write("out/" + name + ".png", png)
return path, view # path for eyeball, view for content asserts
# the test = deliverable
Test_RendersLoginPage:
fakeData = canned user "Alice", 3 orders # override mock at seam
path, view = RenderToImage("login", buildLoginPage)
# assert the LOGIC rendered the must-show content (query view/tree/markup, not PNG):
assert view.findText("Alice") # mock data reached UI
assert view.count(".order-row") == 3 # real list logic ran
assert view.has("#welcome-banner") # key node present
New test = pick subject + override fakeData() -> call RenderToImage + assert key content. Done.
Query API is whatever the project gives: testing-library getByText, HTML-string contains, render-tree walk, view snapshot.
Keep small
- One mock struct, hardcoded data. No mock libs unless the seam needs them.
- Render entry needs many deps = smell. Inject one data seam, default the rest.
- Pixel/snapshot diff is optional — add it later; content asserts first.
1---2name: render-ui-test3description: Render project UI to PNG from a TEST — not a script, main(), or one-off exec method — with least code, by mocking data at the LOWEST seam so every real project code path runs. Builds a reusable render-glue helper for future tests. Use when asked to render or screenshot a component/page/view to an image, set up snapshot/visual tests, or "see what the UI looks like" without launching the app.4---56# render-ui-test78Goal: one PNG of **real** UI, from a **test**. Least code. Real code path. Glue -> reusable helper.910Deliverable is a test in the project's test framework — not a script, `main()`, or one-off exec method. A script renders once and is thrown away; a test renders on every run and its content asserts **guard** the behavior forever (rule 4).1112## Rules13141. **Mock at bottom, not UI.** Fake lowest data seam (data source, transport, store, clock/env). Everything above = real project code. Never stub UI components -> defeats point.152. **One helper does glue.** `RenderToImage(buildSubject) -> png`. Build deps -> call real render entry -> encode PNG -> write file. Lives in test-support, called from test.163. **Reuse.** Future tests call helper + swap mock data. No copy-paste.174. **Assert content, not pixels.** Every test asserts the key thing that *should* render — the must-show data/text/node count the real logic produces (e.g. user name shows, list has N rows, error banner present). Query the rendered view/tree/markup, not the PNG. This proves mock data flowed through real code into the UI. Eyeball once; content asserts guard forever.1819## Workflow20211. **Find render entry** — topmost fn app actually calls to make UI for page/component (not leaf widget). Trace route/handler/screen down to view it returns.222. **Find lowest data seam** — where external data enters. Pick deepest injectable -> max real code above.233. **Write helper** (see template). One mock impl -> canned data. One render call. One file write.244. **Write test** — test fn picks subject, overrides mock data, calls helper. This is the deliverable.255. **Render** — run the test. Project's native offscreen/headless path turns view into pixels. No app launch, no server.266. **Eyeball + assert (rule 4)** — test writes `out/<name>.png`; open it, confirm real component + mock data show. Blank/wrong -> seam too high or entry wrong -> back to 1–2. Then bake the must-show content into asserts before done.2728## Helper template (pseudocode)2930```31# test-support: reusable glue32RenderToImage(name, buildSubject):33 deps = Deps{ data: fakeData() } # mock injected at lowest seam34 subject = buildSubject(deps) # real project code runs above35 view = render(subject) # real render entry36 png = rasterize(view) # offscreen/headless -> bytes37 write("out/" + name + ".png", png)38 return path, view # path for eyeball, view for content asserts3940# the test = deliverable41Test_RendersLoginPage:42 fakeData = canned user "Alice", 3 orders # override mock at seam43 path, view = RenderToImage("login", buildLoginPage)4445 # assert the LOGIC rendered the must-show content (query view/tree/markup, not PNG):46 assert view.findText("Alice") # mock data reached UI47 assert view.count(".order-row") == 3 # real list logic ran48 assert view.has("#welcome-banner") # key node present49```5051New test = pick subject + override `fakeData()` -> call `RenderToImage` + assert key content. Done.5253Query API is whatever the project gives: testing-library `getByText`, HTML-string `contains`, render-tree walk, view snapshot.5455## Keep small5657- One mock struct, hardcoded data. No mock libs unless the seam needs them.58- Render entry needs many deps = smell. Inject one data seam, default the rest.59- Pixel/snapshot diff is optional — add it later; content asserts first.