---
name: content-local-qa
description: "Stand up a local frontend-content debugging environment against QA — install the Playwright MCP server, boot the content app locally (yarn start on :8080, pointed at the QA gateway), and use a pasted QA bearer token to query QA GraphQL directly and/or authenticate the Playwright browser. Use when the user wants to reproduce/debug a content-review QA issue locally, drive the content app with Playwright, or run GraphQL against QA with a token. Keywords: content app local, playwright mcp, QA token, reproduce GraphQLError, review-queue debugging."
---

# Local content app + QA (token-pasted) debugging setup

Bundled in this skill's directory:
- `qa-gql.mjs` — run any GraphQL query against QA with a pasted token (derives the
  Grass gateway identity/profile + Origin/Referer headers automatically). Proven for
  reproducing a modal's failing query out-of-band.

Resolve this skill's directory absolute path and use it in the `node` commands below.
Treat the token as sensitive: don't echo, log, or commit it. It's short‑lived (~20 min) —
if calls start failing with "jwt expired" / 401, ask for a fresh one.

## Phase 1 — Install the Playwright MCP server

Check first, then add if missing:
```bash
claude mcp list | grep -i playwright || claude mcp add playwright -- npx @playwright/mcp@latest
```
**Critical:** MCP tools load only at **session start**. After adding, tell the user to
**restart Claude Code**; the `browser_*` tools (`mcp__playwright__browser_navigate`, etc.)
become available only in the new session. They're loaded via ToolSearch (`select:mcp__playwright__browser_navigate,...`).

### Phase 1b — (optional) Record the session

Only set this up if the user asks to record/capture the session. It needs two things layered
on top of the base install, and both require the MCP server args to be set **before** the
session starts (same restart caveat as above):

1. **Browser video + trace tools** — add `--caps=devtools` (additive to the default tool set,
   not a replacement). This unlocks `browser_start_video` / `browser_stop_video` (records a
   `.webm` of the browser) and `browser_start_tracing` / `browser_stop_tracing` (records a
   Playwright trace, viewable with `npx playwright show-trace <file>`).
2. **Command log** — add `--save-session`, which dumps every MCP tool call made this session
   (the sequence of `navigate`/`click`/`evaluate`/… calls) as a session file in `--output-dir`.
3. **ffmpeg on the MCP server's PATH** — `browser_start_video`/`browser_stop_video` encode via
   ffmpeg and fail silently (0-byte `.webm`, no error) if it can't be found. Check
   `which ffmpeg` first. If missing system-wide, don't install anything — Playwright already
   ships its own copy at `~/Library/Caches/ms-playwright/ffmpeg-*/ffmpeg-mac` (from
   `npx playwright install`); confirm with
   `find ~/Library/Caches/ms-playwright -iname 'ffmpeg-mac'`. Expose it to just the MCP
   server's subprocess via `-e PATH=...` (scoped to that one process, nothing system-wide):

```bash
claude mcp remove playwright
claude mcp add playwright -e PATH="$HOME/Library/Caches/ms-playwright/ffmpeg-1011:$PATH" -- npx @playwright/mcp@latest --caps=devtools --save-session --output-dir ~/Repos/frontend-content/.playwright-mcp
```
(swap `ffmpeg-1011` for whatever version `find` above reports; omit the `-e PATH=...` entirely
if `which ffmpeg` already succeeded.)

Then **restart Claude Code**. Once `browser_start_video`/`browser_start_tracing` show up via
ToolSearch, kick off recording right after the browser session starts (before the first
`browser_navigate`) and stop it right before wrapping up:
- `browser_start_video` → drive the app normally → `browser_stop_video` (video lands in
  `--output-dir`). **Always pass an explicit `size`** (e.g. `{width: 1280, height: 800}`) — see
  caveat below, omitting it produces a silent 0-byte file.
- Optionally `browser_start_tracing` alongside it for a step-by-step trace.
- `browser_video_show_actions` annotates each subsequent action with a callout on the video
  (nice for a walkthrough); `browser_video_chapter` marks named sections (e.g. "Select YouTube
  service", "Verify Sixty Seconds hidden") — call it right before each meaningful step.
- The command log from `--save-session` and the video/trace files all end up under
  `--output-dir` — point the user there when done.

## Phase 2 — Boot the content app locally

From the `frontend-content` repo root. The login shell may default to an old Node, so
select the pinned version in the SAME command, and run the server in the background:
```bash
export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"; cd ~/Repos/frontend-content && nvm use
# first time only: yarn install
nohup yarn start > /tmp/content-dev.log 2>&1 &   # serves http://localhost:8080
```
- `.env` already sets `GRAPHQL_URL` to the QA gateway, so local runs hit **QA data**.
- Wait for "Serving: http://localhost:8080" in the log; the CLI compiles lazily on first load.
- The local app uses the SAME Auth0 client as content.qaorch.com, so a QA token works for it.

## Phase 3 — The token-pasted approach

Get a token: on content.qaorch.com, DevTools → Network → any `graphql` request → Headers →
`authorization: Bearer …` → copy the part after `Bearer `.

### 3a. Direct QA GraphQL queries (reliable — no browser needed)
Best for reproducing/debugging a specific query out-of-band (e.g. a modal's `ProductQuery`):
```bash
export QA_TOKEN='eyJ...'
node "<skill-dir>/qa-gql.mjs" \
  --file ~/Repos/frontend-content/modules/contentReview/src/data/queries/product/ProductQuery.gql \
  --vars '{"id":"<productId>","reviewQueueId":<rqId>}'
```
It prints whether `data.product` is null and lists each error's `message` / `path` / `code` /
`service` — which is how the participations `GraphQLError` was root‑caused. Use `--raw` for the
full response, `--query '<gql>'` for inline queries. Override `GRAPHQL_URL` to
`https://qa-graphql-gateway.theorchard.io/graphql` to target the local app's gateway instead of Grass.

### 3b. Authenticate the Playwright browser
The Playwright MCP launches a **fresh browser profile** with no session, so it lands on the
Auth0 login page. Two ways in:

- **Interactive (reliable):** navigate to `http://localhost:8080` (or `https://content.qaorch.com`),
  raise the window (`osascript -e 'tell application "Google Chrome" to activate'`), and ask the
  user to log in **once** in that Playwright window. Then drive the app normally.
- **Token injection (experimental, skips login):** on the app origin, write the token into the
  Auth0 SPA cache via `browser_evaluate`, then reload:
  ```js
  () => {
    const CLIENT='Kx5FmaNXiCoUKC2pfqsLA8mcKHQusHSE', AUD='https://workstation.qaorch.com/api',
          SCOPE='openid profile email offline_access', T='<PASTE_TOKEN>';
    const exp=JSON.parse(atob(T.split('.')[1])).exp;
    localStorage.setItem(`@@auth0spajs@@::${CLIENT}::${AUD}::${SCOPE}`,
      JSON.stringify({ body:{ access_token:T, scope:SCOPE, expires_in:1200, token_type:'Bearer',
        audience:AUD, client_id:CLIENT }, expiresAt:exp }));
    return 'injected';
  }
  ```
  If the app still redirects to login (the SDK also wants an id_token/`@@user@@` entry that can't
  be fabricated from the access token alone), fall back to the interactive login — it's a one‑time
  step per browser session.

## Hard-won caveats (relay these)
- **`browser_start_video` silently produces a 0-byte `.webm` without an explicit `size`:** even
  with ffmpeg correctly on PATH (see above) and correct start/stop ordering, omitting the `size`
  param (e.g. `{width: 1280, height: 800}`) makes the encoder write nothing — no error surfaces
  from the tool call. Always pass `size` explicitly. After stopping, verify with
  `ls -la <output-dir>/*.webm` before telling the user recording succeeded.
- **Reading QA state without flipping it:** opening a product modal fires an on-open effect that
  copies attributes/suggestions into edits. To see a product's true stored state, read the **first
  `ProductQuery`** on modal open (via `browser_network_request`), or query out-of-band with `qa-gql.mjs`.
- **Downstream 401/403 on direct calls:** some subgraphs (`ows-track`, `ows-product-review`) reject
  a direct call even with a valid token, because the gateway forwards service creds a manual call
  can't reproduce. When `qa-gql.mjs` hits those, read the data via the app's own request in the
  Playwright browser instead (`browser_network_request` on the app's `ProductQuery`).
- **Grass gateway** (`content.qaorch.com`) requires Origin/Referer + `orchard-identity/profile`
  headers — `qa-gql.mjs` sets these from the JWT automatically.
- **Real QA data:** the local app reads/writes real QA. Approvals, moves, inserts are real mutations
  — confirm before any write, and prefer read-only queries for debugging.
