Skip to content
Back to skills

Ui Preview

BSecurity

Capture headless-Chrome screenshots of the tingly-box frontend (running locally in mock mode) so frontend changes can be visually verified in environments without a real browser. Use when the user asks to "preview", "screenshot", "see the page", "show me the UI", "verify visually", or when frontend layout / component / styling changes need a sanity-check before review. Works in restricted/cloud sandboxes where Playwright's normal Chromium install is blocked.

  • 348 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 26, 2026
testingpythongoshellbashreactnodetestinggitapifrontend

Works with

  • claude code
  • cli
  • api
  • mcp

Security analysis

B81/100
  • mediumUses curl or wget to download content
  • highPerforms destructive filesystem operations
  • mediumInstalls packages at runtime which could introduce malicious dependencies
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 4 files and shows the line behind each finding

Scanned October 2, 2026

npx -y skills add tingly-dev/tingly-box --skill ui-preview --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ui Preview?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Ui Preview
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/tingly-dev-ui-preview/badge)](https://www.skillsdirectory.com/skills/tingly-dev-ui-preview)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: ui-preview
description: Capture headless-Chrome screenshots of the tingly-box frontend (running locally in mock mode) so frontend changes can be visually verified in environments without a real browser. Use when the user asks to "preview", "screenshot", "see the page", "show me the UI", "verify visually", or when frontend layout / component / styling changes need a sanity-check before review. Works in restricted/cloud sandboxes where Playwright's normal Chromium install is blocked.
---

# Headless UI preview (Playwright + Chrome for Testing)

Designed for the remote-execution container where:
- `cdn.playwright.dev` / `dl.google.com` are blocked — use Chrome for Testing via `storage.googleapis.com`.
- Ubuntu's `chromium-browser` is a snap stub and won't run.
- MCP servers can't be registered mid-session — use Playwright's Node API directly.

## Setup (run once per fresh container)

> ⚠️ **This project uses pnpm** (`frontend/pnpm-lock.yaml` is the source of truth).
> Do **NOT** run `npm i` here — npm ignores the pnpm lockfile and re-resolves
> transitive deps, which pulls **broken** versions (e.g. `es-toolkit@1.47.0` vs the
> pinned `1.46.1`) that crash the whole SPA at load. See *Troubleshooting* below.

`playwright` is already a committed `devDependency` in `frontend/package.json`, so
a plain install provides it — no ad-hoc add/revert needed. `@emotion/react` /
`@emotion/styled` are likewise already declared.

```bash
# 1. Install deps from the lockfile (also fixes a node_modules previously
#    polluted by npm — wipe it first with `rm -rf node_modules` if you suspect that).
cd frontend
pnpm install --frozen-lockfile

# 2. Download Chrome for Testing (Playwright's own Chromium download is blocked)
mkdir -p /tmp/chrome && cd /tmp/chrome
curl -fsSL -o chrome.zip \
  "https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.96/linux64/chrome-linux64.zip"
unzip -q chrome.zip          # → /tmp/chrome/chrome-linux64/chrome
```

**Do NOT** modify `frontend/package.json` / `frontend/pnpm-lock.yaml` for tooling
anymore — everything needed is already committed. If a previous session left them
dirty (or npm polluted the tree), reset with:
```bash
cd <repo> && git checkout -- frontend/package.json frontend/pnpm-lock.yaml
pnpm -C frontend install --frozen-lockfile
```

## Dev server

```bash
cd frontend
USE_MOCK=true node_modules/.bin/vite --mode mock --port 3000 &
until curl -fs http://localhost:3000 >/dev/null; do sleep 1; done
```

`USE_MOCK=true` must be a shell env var (read by `vite.config.ts` before `.env.mock`).

## Auth seeding

Every script must seed `localStorage.user_auth_token` via `addInitScript` or the app
redirects to the login screen:

```js
await page.addInitScript(() => {
    localStorage.setItem('user_auth_token', 'mock-token-for-screenshots');
});
```

## First-run onboarding guides

Mock mode defaults these **off** (`src/mocks/mockConfig.ts`), so a fresh browser
context no longer pops the "Direct Routing Guide" dialog and steals focus/clicks —
no Escape-key dismissal needed before interacting with the page. If a script
specifically needs to screenshot or assert the first-run guide itself, force it on
per-navigation with a query param (no code change, no shared state between runs):

```js
await page.goto('http://localhost:3000/agent/openai?mockOnboarding=on', { waitUntil: 'networkidle' });
```

`?mockOnboarding=off` is the (redundant) explicit default, useful when a test wants
to assert the off state regardless of what a prior run left in localStorage.

## Data profile: populated vs. newcomer

The onboarding flag above only controls dialogs. What data the mock backend
"has" is a separate switch (`src/mocks/mockConfig.ts`):

```js
await page.goto('http://localhost:3000/credentials?mockData=newcomer', { waitUntil: 'networkidle' });
```

- `?mockData=populated` (default): the full demo data set.
- `?mockData=newcomer`: a fresh install. It has no credentials, quota, usage,
  request logs or Claude Code profiles, and the built-in rules have no services.
  Use it to check that each page lands sensibly before the user has added
  anything. It also turns the first-run guides on, unless `?mockOnboarding=off`
  is given.

The profile is kept in `sessionStorage` for the tab, so in-app navigation stays
in the chosen world. Pass `?mockData=populated` to switch back, or use a fresh
browser context.

## Scripts

All scripts live here and are run from `frontend/`. They use `createRequire(cwd)` to
resolve `playwright` from `frontend/node_modules` regardless of the script's location:

```js
import { createRequire } from 'module';
const { chromium } = createRequire('file://' + process.cwd() + '/')('playwright');
```

| Script | Mode | Purpose |
|--------|------|---------|
| [`screenshot.mjs`](screenshot.mjs) | mock | **Ad-hoc template** — copy here and customise; do NOT commit |
| [`docs-screenshots.mjs`](docs-screenshots.mjs) | mock | All `docs/images/` product screenshots (`<group>-<index>-<name>.png`, sidebar order) + theme previews; GIF frames listed in `docs/images/gif-frames.txt` |
| [`regression-credentials.mjs`](regression-credentials.mjs) | mock | Assertion-based regression for `/credentials` Add API Key flow |
| [`scenario-routing-graph.mjs`](scenario-routing-graph.mjs) | real backend | Codex routing graph screenshots (requires running Go server) |

Each script is self-documenting — see its file header for usage, outputs, and known issues.

## Shrinking screenshots (optional)

Screenshots are captured with Playwright's plain `page.screenshot()` — the skill
deliberately does **not** compress images internally, because optimizers depend on
host tooling that is not guaranteed in the run environment. If a PNG is large and
you want to trim it before committing, run any optimizer you have available on the
captured file afterwards. None of these are required:

- `pngquant --quality 80-95 --strip --force --out foo.png foo.png` (lossy, smallest PNGs)
- `oxipng -o 4 --strip safe foo.png` (lossless)
- Node: `sharp(file).png({ quality: 80, palette: true }).toFile(out)`
- Python: `Pillow` → `img.save(out, optimize=True)`

Prefer keeping the original capture when quality matters more than size.

## After capturing

1. `SendUserFile` the PNGs so the user can review them.
2. For ad-hoc `screenshot.mjs`: delete it before committing — the stop hook will flag it.
3. `docs/` is gitignored; force-add images: `git add -f docs/images/`.
4. Free the port: `fuser -k 3000/tcp` or `pkill -f "vite --mode mock"`.

## Troubleshooting

**Blank white page / `root` is empty, console shows
`TypeError: require_isUnsafeProperty is not a function`**
(stack points at `es-toolkit/dist/compat/object/get.js` ← `recharts`).

Cause: `node_modules` was installed/resolved by **npm** instead of pnpm, so a
transitive dep (`es-toolkit`) drifted to a version whose CJS→ESM interop esbuild
mis-bundles. It breaks the React vendor chunk, so **every** route renders blank —
not just the page you're testing. Tweaking `optimizeDeps` (include/exclude/
`keepNames`) does **not** fix it.

Fix — reinstall the pinned tree with pnpm:
```bash
cd frontend
rm -rf node_modules node_modules/.vite        # drop the npm-polluted tree + dep cache
pnpm install --frozen-lockfile
# verify the pinned (working) version is what's on disk:
ls node_modules/.pnpm | grep es-toolkit       # expect es-toolkit@1.46.1, NOT 1.47.0
pnpm add -D playwright                         # re-add the tool
```
Then restart the dev server. If you edited `vite.config.ts` while chasing this,
revert it — the config is not the problem.

**Dev-server process exits immediately (e.g. exit 144) on `--force`**: usually a
follow-on of the crash above (the unhandled client error). Once the pnpm tree is
correct it starts cleanly; `--force` is only needed once to drop a stale
`node_modules/.vite` optimize cache.

Files in this skill

  • SKILL.md7.8 KB
  • regression-credentials.mjs6.2 KB
  • scenario-routing-graph.mjs7 KB
  • screenshot.mjs2.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.