Every app directory asks for three or four screenshots. For a desktop app that records calls, that's awkward. The screens worth showing (a transcript, an AI answer, call history) are exactly the ones full of private data, and a drawn mockup that pretends to be the app isn't honest.
What I wanted: the UI that actually ships, pixel for pixel, filled with demo content, captured the same way every time. Here's the setup that did it for Talkaside's Electron front end. It's one Node script of about 150 lines.
1. Take the UI from the release, not the working tree
My working tree is never clean, and the screenshots should match what people download. So the script starts from the release commit:
git archive <release-commit> web/templates web/static | tar -x -C ./rel
The pages are Jinja templates served by Flask. I didn't start Flask (that would also start the app's background services). I rendered the three templates once with plain Jinja and a stub url_for:
def url_for(endpoint, filename='', **kw):
q = '&'.join(f'{k}={v}' for k, v in kw.items())
return f'/static/{filename}' + (f'?{q}' if q else '')
html = env.get_template('index.html').render(url_for=url_for, current_version='1.0.5')
The result is static HTML that points at /static/..., which is easy to serve from anywhere.
2. A mock server inside Electron's main process
The page fetches from the backend as soon as it loads: settings, engine state, devices, licence status, history. A small http.createServer in the Electron main process answers those with fixed JSON:
if (p === '/api/settings') return json(settings);
if (p === '/health') return json({ok: true, engine: 'ready'});
if (p === '/api/engine/state') return json({state: 'stopped'});
if (p === '/api/history') return json({calls, total: calls.length, has_more: false});
if (/^\/api\/history\/\d+$/.test(p)) return json(detail);
return json({});
The settings object is a fixture with every key the UI reads and every API key empty. The history endpoints return three invented calls with plausible titles and summaries.
Then the window is cut off from the network, so nothing leaks out and nothing slow comes in:
win.webContents.session.webRequest.onBeforeRequest((d, done) =>
done({cancel: !d.url.startsWith(base + '/') && !/^(data|blob|devtools):/.test(d.url)}));
I learned this the hard way earlier in the same project. Screen-region capture (CopyFromScreen and friends) grabs whatever is on top of that rectangle at that moment. Mine once grabbed a messenger window. capturePage can't do that.
What went wrong along the way
Each missing mock showed up as visible error text in the UI. That turned out to be useful: the screenshot told me exactly what I'd forgotten.
-
A toast: "Voice catalog offline". The voices endpoint returned
{}, and the app correctly said so. The fix was to hide the toast element in the capture's stylesheet. That's display state only; the content stays real. -
"data.tracks is not iterable" in the call detail. Two endpoints were missing: audio tracks and transcript revisions. Returning
{tracks: []}and{revisions: []}fixed both. -
A call timer stuck at
0:00. Writing the text didn't help, because asetIntervaloverwrote it every second. Setting the app's ownsessionStartto "now minus 12:48" and calling itsupdateTimer()did. -
Electron starting as plain Node. VS Code's terminal sets
ELECTRON_RUN_AS_NODE=1, and thenelectron.exe script.cjsruns the script as Node with noappobject. Run it with that variable removed. -
Scale factor. With an offscreen window,
--force-device-scale-factor=2still gave 1280×800 captures, so the output is in device-independent pixels. For directory listings that's the right size anyway, so I left it.
The result
Four PNGs: an interview answer, a translated call, the call history and a call review. They come from the released UI with no personal data, and one command reproduces them after every release. In the listings I say plainly that the interface is real and the conversation is demo content. That's the difference between a screenshot and a mockup.
I'm building Talkaside, a Windows app for job interviews and calls in another language.
Drafted with AI assistance from this project's code and logs.
Top comments (0)