---
name: tingly-dev/ui-preview
source: https://app.decimal.ai/s/tingly-dev-ui-preview@2/SKILL.md
source_sha256: 1a9e5c83b315
---

# 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.

## 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 9 `docs/images/` product screenshots + theme previews |
| [`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.