---
name: fluent-ui-test
description: Browser-based UI testing for Fluent Commerce OMS App and Store App using Chrome DevTools MCP. Verifies manifest deployments render correctly, pages load, components display data, and user actions are functional. Triggers on "test UI", "verify manifest in browser", "browser test", "OMS test", "devtools test", "does the page work", "check OMS", "verify page renders".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: [--app oms|store|servicepoint] [--route /orders] [--entity-id <id>] [--screenshot] [--login]
cursor-tier: auto
---

# Browser-Based UI Testing for Fluent Commerce

Verify that Mystique manifests and pages actually work in a live Fluent Commerce UX app (OMS App, Store App) using the Chrome DevTools MCP browser automation tools.

This skill bridges the gap between offline manifest validation (`/fluent-mystique-validate`) and live visual confirmation. It automates: login, navigation, component rendering checks, data loading verification, user action availability, and screenshot capture.

**Token optimization:** This skill uses a helper library (`BROWSER_OPTIMIZATIONS.md`) that reduces browser interaction token costs by ~90%. Instead of calling `take_snapshot` (20-40K tokens) for every check, it uses `pageSummary()` (~500 tokens) and pre-computed selectors from the manifest. See `BROWSER_OPTIMIZATIONS.md` for the full helper library, decision tree, and manifest-to-selector guide.

## Pre-flight: Feedback Check

Before starting, check for past learnings from this skill:

1. Check if `accounts/<PROFILE>/feedback/fluent-ui-test.jsonl` exists
2. If yes, read last 20 records
3. Filter to: FAILURE or USER_CORRECTED outcomes, confidence = CONFIRMED or PROMOTED
4. Extract top 3 relevant learnings — use as internal guidance during execution
5. Do NOT show raw feedback to the user — silently adjust approach based on past issues

## Ownership Boundary

This skill owns browser-based UI verification of deployed manifests in live Fluent UX apps.

Other skills own:
- Offline manifest JSON validation: `/fluent-mystique-validate`
- Manifest construction: `/fluent-mystique-builder`
- Manifest complexity analysis: `/fluent-mystique-analyze`
- API-level E2E testing (order lifecycle, events): `/fluent-e2e-test`
- Manifest deployment via Settings API: `/fluent-settings`

## Prerequisites

### Required
1. **Chrome DevTools MCP configured** in `.mcp.json` (with optional screencast recording):
   ```json
   {
     "chrome-devtools": {
       "command": "npx",
       "args": [
         "-y", "chrome-devtools-mcp@latest",
         "--save-video"
       ],
       "env": {
         "FLUENT_UI_BASE_URL": "https://<account>.test.apps.engineering.fluentcommerce.com"
       }
     }
   }
   ```
   Note: `--save-video` is optional but recommended. Without it, the skill still works using screenshots. With it, you also get continuous screencast recording.
2. **Chrome DevTools MCP connected** — the `navigate_page`, `take_snapshot`, `take_screenshot`, `evaluate_script` tools must be available. If not visible in the tool list, the IDE needs a restart.
3. **Valid Fluent credentials** — resolved automatically via profile-first strategy (see Credential Resolution below). No manual env vars needed if `FLUENT_PROFILE` is set.
4. **Manifest deployed** — the manifest must already be deployed to the target environment as a setting (via `fluent module install --include settings` or GraphQL `createSetting`/`updateSetting`).

### Optional: Chrome MCP Extension (manual debugging fallback)

If Chrome DevTools MCP is unavailable, the skill can fall back to Chrome MCP Extension (`chrome-manual` server) for basic browser testing. Chrome MCP Extension attaches to an existing Chrome session with cookies/auth intact.

Enable with: `npx @fluentcommerce/ai-skills mcp-setup --profile <PROFILE> --with-chrome`

**Tool mapping (Chrome DevTools → Chrome MCP Extension):**

| Chrome DevTools Tool | Chrome MCP Extension Equivalent | Notes |
|---|---|---|
| `navigate_page` | `navigate` | Same purpose |
| `take_snapshot` | `get_interactive_elements` | Interactive elements only, not full DOM snapshot |
| `take_screenshot` | `screenshot` | Chrome supports element-specific |
| `click` | `click` | Chrome uses CSS selectors |
| `type_text` | `fill_form` / `keyboard_input` | Different API surface |
| `evaluate_script` | `execute_script` | Same purpose |
| `list_network_requests` | `get_network_requests` | Chrome captures full POST payloads |

**Limitations:** Chrome MCP Extension does NOT support `--save-video` or headless mode. Use Chrome DevTools MCP for automated CI/CD testing.

### Credential Resolution

Resolve credentials using this priority chain. The **default path** (Priority 1) reads directly from the profile that's already configured in `.mcp.json` — zero extra setup needed. Override sources exist for when you need a different user or want to skip the login form.

| Priority | Source | How to read |
|----------|--------|-------------|
| 1 (default) | **CLI profile from `.mcp.json`** | Read `FLUENT_PROFILE` from any MCP server's env in `.mcp.json` → resolve username + password from profile files. This is the normal path — no extra config. |
| 2 (override) | Explicit creds in `.mcp.json` `chrome-devtools.env` | `FLUENT_UI_USERNAME` + `FLUENT_UI_PASSWORD` — overrides profile user for browser testing |
| 3 (role file) | `.fluent-ui-credentials.json` in workspace root | Role-based entries (`--role admin`, `--role store`). For testing same page as different users. |
| 4 (token) | `FLUENT_UI_TOKEN` in `.mcp.json` or shell env | Skip form login entirely — inject token via `evaluate_script` |
| 5 (env vars) | Shell env `FLUENT_USERNAME` + `FLUENT_PASSWORD` | Legacy fallback |
| 6 (fallback) | Ask user | Prompt for credentials |

**Resolution steps:**
1. Read `.mcp.json` → find `FLUENT_PROFILE` from any MCP server's `env` (typically `fluent-mcp-extn` or `fluent-mcp`). If found:
   a. Read `~/.fluentcommerce/<FLUENT_PROFILE>/profile.json` → extract `"user"` field (default username)
   b. If `FLUENT_PROFILE_RETAILER` is also set, check `~/.fluentcommerce/<FLUENT_PROFILE>/retailer.<ref>.json` → if it has a `"user"` field, use that instead
   c. Read `~/.fluentcommerce/<FLUENT_PROFILE>/user.<resolved-username>.json` → extract `"username"` and `"password"`
   d. If found → use these credentials. Done.
2. Check `.mcp.json` for `FLUENT_UI_USERNAME` + `FLUENT_UI_PASSWORD` in `chrome-devtools.env`. If both set → use them (overrides profile user).
3. Check for `.fluent-ui-credentials.json` in workspace root. If exists → read it (see Role File below).
4. Check `.mcp.json` for `FLUENT_UI_TOKEN` in `chrome-devtools.env` (or shell env). If set → use Token Injection (skip form login entirely).
5. Check shell env vars `FLUENT_USERNAME` and `FLUENT_PASSWORD`. If both set → use them.
6. If no source provides credentials → ask the user.

#### Default: Profile from `.mcp.json` (Priority 1 — zero config)

The `.mcp.json` already has `FLUENT_PROFILE` configured for the MCP servers. The skill reads it and resolves credentials automatically:

```
.mcp.json → FLUENT_PROFILE=<PROFILE>
  → ~/.fluentcommerce/<PROFILE>/profile.json → user: "<username>"
  → ~/.fluentcommerce/<PROFILE>/user.<username>.json → username + password
```

No extra env vars, no extra files. The profile already has the credentials.

If `FLUENT_PROFILE_RETAILER` is set, the retailer override is checked first (`retailer.<ref>.json` → `user` field), then the resolved user's credential file is read.

#### Override: Explicit Creds in `.mcp.json` (Priority 2)

When you want to test as a **different user** than the profile default, add explicit credentials to the chrome-devtools config:

```json
{
  "chrome-devtools": {
    "env": {
      "FLUENT_UI_BASE_URL": "https://acme.test.apps.engineering.fluentcommerce.com",
      "FLUENT_UI_USERNAME": "store_user@acme.com",
      "FLUENT_UI_PASSWORD": "store-password"
    }
  }
}
```

These override the profile user. To switch back to the profile default, remove `FLUENT_UI_USERNAME`/`FLUENT_UI_PASSWORD` and restart.

#### Role File: `.fluent-ui-credentials.json` (Priority 3 — multi-role testing)

For testing the same page as different users without editing `.mcp.json` each time:

```json
{
  "default": {
    "username": "operations_user@acme.com",
    "password": "ops-password"
  },
  "roles": {
    "admin": {
      "username": "admin_user@acme.com",
      "password": "admin-password"
    },
    "store": {
      "username": "store_user@acme.com",
      "password": "store-password"
    }
  }
}
```

Use `--role admin` or say "test as store user". Reads `roles.<name>`. No restart needed.

This file MUST be gitignored (already added to `.gitignore`).

#### Token Injection (Priority 4 — skip form login)

For CI/CD, SSO bypass, or quick switching with pre-generated tokens:

```json
{
  "chrome-devtools": {
    "env": {
      "FLUENT_UI_BASE_URL": "https://acme.test.apps.engineering.fluentcommerce.com",
      "FLUENT_UI_TOKEN": "eyJhbGciOiJSUzI1NiIs..."
    }
  }
}
```

**Injection method** (via `evaluate_script` after navigating to app domain):
```javascript
localStorage.setItem('token', '<FLUENT_UI_TOKEN>');
localStorage.setItem('accessToken', '<FLUENT_UI_TOKEN>');
```

Then `navigate_page` to the target route — the app reads the token from localStorage and skips login.

**Token expiry:** If the app redirects to login after injection, the token is stale. The skill detects this (login form in snapshot) and falls through to form-based login.

**IMPORTANT:** Never store or log the resolved password or token. Never echo it. Use credentials only for `type_text` into the password field or `evaluate_script` token injection. The Chrome DevTools MCP browser session persists cookies — login only happens once per session. `.mcp.json` and `.fluent-ui-credentials.json` are gitignored — credentials are not committed.

### Prerequisite Check (Phase 0)

Before starting, verify:
1. Check for Chrome DevTools MCP tools first (search for `navigate_page`)
2. If Chrome DevTools not available, check for Chrome MCP Extension tools (search for `navigate` from chrome-manual server)
3. The target UX app URL is reachable
4. Resolve credentials using the Credential Resolution chain above
5. **Login persistence (one-time setup tip):** Read `.mcp.json` → check if `chrome-devtools.args` contains `"--storage-state"`. If absent, mention once: _"Add `\"--storage-state=./chrome-session.json\"` to `chrome-devtools.args` in `.mcp.json` to persist login across sessions (~15s + ~10K tokens saved per run). See `docs/chrome-devtools-mcp-reference.md` §9."_ Don't repeat this if the user has already been told.

If neither browser MCP is connected:
```
-> BLOCKED: No browser MCP connected. Install Chrome DevTools MCP (recommended) or Chrome MCP Extension. Restart IDE to pick up .mcp.json changes.
```

### Phase 0a: Manifest Selector Cache (once per session, before browser)

Pre-analyze manifests to get the exact labels for all UI elements. This eliminates "guess the label" misses (e.g., using "Ref" when the column is actually "Order #") and replaces the first `take_snapshot` entirely.

```bash
# Check if cache exists
ls accounts/<PROFILE>/manifests/selector-cache-slim.json 2>/dev/null

# If missing, generate it (~2 seconds, reads local manifest JSONs)
node fluent-ai-skills/tools/manifest-selector-map.mjs --profile <PROFILE>
```

Then read `selector-cache-slim.json` once (~15K tokens). It provides:
- **navigation[]** — exact sidebar labels + paths (use with `__fcTest.findNav()`)
- **actionButtons[]** — user action button labels per page (use with `__fcTest.findButton()`)
- **cardTitles[]** — exact card headings per page (use with `__fcTest.findCard()`)
- **listPages[]** — exact column headers per page (use with `__fcTest.findTableColumn()`)

**Auto-refresh:** If the cache is missing or older than 24 hours, regenerate: `node fluent-ai-skills/tools/manifest-selector-map.mjs --profile <PROFILE>`. Skills check freshness automatically in pre-flight.

**Skip if:** Manifest files don't exist under `accounts/<PROFILE>/manifests/`, or this is a first-time walkthrough where element discovery is the goal.

### Phase 0b: Inject Helper Library (MANDATORY — use initScript)

**Use `initScript` on `navigate_page`** to auto-inject `__fcTest` during navigation. This eliminates the separate injection round trip.

```
navigate_page:
  url: <target URL>
  initScript: <full IIFE from BROWSER_OPTIMIZATIONS.md "Layer 1: Helper Library">
```

The `initScript` runs before page scripts load, so `__fcTest` is available immediately. Pass it on **every `navigate_page` call** (it's idempotent — checks `if (window.__fcTest) return`).

**Combine queries with batch()** to minimize round trips:
```javascript
// ONE evaluate_script call replaces 3-4 separate calls:
() => JSON.stringify(window.__fcTest.batch([
  { action: 'pageSummary' },
  { action: 'getActionButtons' },
  { action: 'highlight', args: ['Cancel Order', 'VERIFIED'] }
]))
```

**Target: maximum 2-3 MCP calls per page** (navigate + combined evaluate + optional screenshot).

**Fallback:** If `initScript` doesn't work (rare), inject via a single `evaluate_script` with the full IIFE. Verify: `window.__fcTest ? 'ready' : 'not_injected'`.

**MUI-aware finders** (added from manifest selector cache integration):
- `findCard(title)` — find card by heading text (walks MUI Card DOM)
- `findButton(label)` — find MUI button by label
- `findTab(label)` — find MUI tab by label
- `findNav(label)` — find sidebar/nav item by label
- `findInput(label)` — find input by associated label
- `findTableColumn(header)` — find column by header text
- `findTableRow(col, val)` — find row by cell value in column
- `clickNav(l)`, `clickButton(l)`, `clickTab(l)`, `clickTableRow(c,v)` — find + click
- `fillInput(label, value)` — find input + set value with React-compatible events
- `navigateTo(path)` — hash-based SPA navigation
- `batch([ops])` — multiple operations in one `evaluate_script` call
- `pageState()` — ultra-compact page summary (~150 tokens vs ~30K snapshot)

**The helper provides:**
- `pageSummary()` — compact page state (~500 tokens vs 30K from `take_snapshot`)
- `scanComponents()` — detect MUI components + custom SDK components (~400 tokens)
- `verifyComponent(alias)` — check if specific Mystique component rendered (~100 tokens)
- `find(text, role)` — locate elements without snapshots (~100 tokens)
- `exists(text, role)` — check element visibility (~100 tokens)
- `getActionButtons()` — list workflow action buttons (~200 tokens)
- `highlight(selectorOrText, label)` — click telemetry with pulsing border (~100 tokens)
- `annotate(text, type)` — step annotation banner (~100 tokens)
- `getTimeline()` — action timeline for reports (~200 tokens)
- `diffState(before, after)` — compare two `pageState()` snapshots, returns only changes (~200 tokens)
- `isLoggedIn()` — detect login vs app state without snapshot (~100 tokens)
- `loginForm(user, pass)` — programmatic login, replaces 4 MCP calls (~100 tokens)
- `inject(type, opts)` — DOM injection preview: card, column, tab, row, chip, section (~100 tokens each)
- `clearInjected()` — remove all injected preview elements (~50 tokens)

**For custom SDK components:** Use `verifyComponent("fc.custom.alias")` to check existence. Use `scanComponents()` for a full inventory of MUI-based and custom components. See `BROWSER_OPTIMIZATIONS.md` "Component Compatibility Matrix" for which function handles which component type.

**Decision tree (when to use what):** See `BROWSER_OPTIMIZATIONS.md` "Decision Tree" section. Rule of thumb: start with `pageSummary()`, escalate to `scanComponents()` for custom pages, use `take_snapshot` only as last resort.

## Fluent UX App URL Patterns

| App | Setting Key | URL Path | Purpose |
|-----|-------------|----------|---------|
| OMS | `fc.mystique.manifest.oms` | `/oms/` | Order Management System |
| Store App | `fc.mystique.manifest.store` | `/store/` | Store Operations |

**Base URL pattern:** `https://<account>.<env>.apps.engineering.fluentcommerce.com/<app>/`

Where:
- `<account>` — account name (e.g., `acme`, `acmedev`)
- `<env>` — `test`, `sandbox`, `staging`, or `prod` (omitted for prod)
- `<app>` — `oms`, `store`, or `servicepoint`

## Protocol Phases

### Phase 1: Login (optimized — 2-3 MCP calls max)

The Fluent UX apps use Fluent's authentication system. On first navigation, the app redirects to a login page. The Chrome DevTools MCP profile persists cookies — login only happens once per session.

**Steps (2-3 calls total):**

1. `navigate_page` to the app URL with `initScript` = helper library IIFE (from `__fcTest.initscript.js` or `BROWSER_OPTIMIZATIONS.md` Layer 1). This auto-injects `__fcTest` on the landing page — login or app.

2. `evaluate_script` — detect session state + login if needed in ONE call:
   ```javascript
   () => {
     var status = window.__fcTest.isLoggedIn();
     if (status.loggedIn) {
       return JSON.stringify({ login: 'skipped', page: window.__fcTest.pageSummary() });
     }
     if (status.hasForm) {
       var result = window.__fcTest.loginForm(USERNAME, PASSWORD);
       return JSON.stringify({ login: result, page: null });
     }
     return JSON.stringify({ login: 'unknown', page: window.__fcTest.pageSummary() });
   }
   ```
   - **Session reuse** (`loggedIn=true`): Skip login entirely — already in app. **Done in 2 calls.**
   - **Login form** (`hasForm=true`): `loginForm()` fills username + password + clicks submit programmatically.
   - **Unknown state**: Check `pageSummary()` — may be loading, redirect in progress, or unexpected page.

3. If login was submitted: `wait_for` redirect (2-5 seconds), then `evaluate_script` → `pageSummary()` to verify:
   - Pass: `sidebar` non-empty OR `title` matches app route
   - Fail: still shows login form → credentials incorrect or MFA required → `take_snapshot` for debugging

**Best case: 2 MCP calls** (session reuse). **Worst case: 3 MCP calls** (form login + verify). Old flow was 6 calls.

**Chrome lock error:** If `navigate_page` fails with "browser is already running", run `chrome-lock-recover.ps1`:
```bash
powershell -ExecutionPolicy Bypass -File "<skills-dir>/fluent-ui-test/chrome-lock-recover.ps1"
```
This kills only MCP profile Chrome processes (never the user's personal browser), clears lock files, and retries.

**IMPORTANT:** Never store or log credentials. Use the credentials resolved in Phase 0 (Credential Resolution). Never echo the password.

### Phase 2: Navigate to Target Page

After login, navigate to the specific page being tested.

**Common OMS routes:**
| Route | Page | Components typically present |
|-------|------|-----------------------------|
| `/oms/orders` | Orders list | `fc.list`, search/filter, pagination |
| `/oms/orders/:id` | Order detail | `fc.page`, `fc.tabs`, `fc.card.attribute`, status badge |
| `/oms/fulfilments` | Fulfilments list | `fc.list`, status filters |
| `/oms/fulfilments/:id` | Fulfilment detail | `fc.page`, items table, workflow actions |
| `/oms/locations` | Locations list | `fc.list`, location search |
| `/oms/inventory` | Inventory positions | `fc.list`, catalogue filters |
| `/oms/returns` | Returns list | `fc.list`, return status |
| `/oms/settings` | Settings | `fc.settingForm` |

**Navigation methods (choose the fastest):**

| Scenario | Method | MCP calls |
|---|---|---|
| **First page after login** | Already there from Phase 1 | 0 |
| **Different page, same app** | `evaluate_script → navigateTo('/orders/123')` | 1 |
| **Different app** (OMS → Store) | `navigate_page(new_url, initScript=HELPER)` | 1 |
| **Fresh manifest test** (need full reload) | `navigate_page(about:blank)` + `navigate_page(url, initScript=HELPER)` | 2 |

**Steps:**
1. **SPA navigation (PREFERRED)** — if already in the same app, use hash navigation in a single `evaluate_script`:
   ```javascript
   () => {
     window.__fcTest.navigateTo('/orders/123/2/ORDER-REF');
     return JSON.stringify(window.__fcTest.pageState());
   }
   ```
   1 MCP call, no page reload, helper stays injected. Use for all in-app page changes.

2. **Full navigation** — only when switching apps or testing fresh manifest loads:
   - `navigate_page` to the full URL with `initScript` (e.g., `https://.../oms/orders`)
   - If you see **"App not found"**, include `/oms` or `/store` in the base URL and retry.
   - Skip the `about:blank` bounce unless explicitly testing fresh manifest loads.

3. `evaluate_script` → `pageSummary()` to verify page loaded:
   - If `pageSummary().loading` is `true`, wait 2-3 seconds and retry
   - If unexpected state (no title, alerts), escalate to `take_snapshot`
4. Verify: `pageSummary().title` is not empty, `alerts` is empty, URL matches route

**For entity detail pages:** You need a real entity ID. Use `graphql.query` or `entity.get` to find one first.

### Phase 3: Verify Page Rendering

**Preferred:** Use `pageSummary()` for initial verification (~500 tokens). Escalate to `take_snapshot` (~30K tokens) only when pageSummary shows unexpected state or doesn't provide enough detail.

```javascript
// Via evaluate_script — get compact page state
JSON.stringify(window.__fcTest.pageSummary())
```

**What to check using pageSummary():**

| Check | pageSummary() Field | Pass Criteria | Escalate to take_snapshot if... |
|-------|-------------------|---------------|-------------------------------|
| Page loads | `title` | Non-empty title | Title empty or unexpected |
| Components render | `tabs`, `cards`, `tables` | Expected arrays non-empty | Key components missing |
| Data loads | `tables[].rowCount`, `loading` | `rowCount > 0`, `loading: false` | `loading: true` persists after retry |
| Navigation works | `sidebar` | Expected links present | Links missing or all inactive |
| User actions visible | `buttons` | Expected action buttons listed | Buttons missing (escalate for DOM details) |
| No errors | `alerts` | Empty array | Alerts present — escalate + `list_console_messages` |

**Fallback checks (only when pageSummary insufficient):**

| Check | Pass Criteria | Fail Indicators |
|-------|---------------|-----------------|
| Page loads | Snapshot shows headings, navigation, content | Empty `#root`, error boundary, "Something went wrong" |
| Components render | Expected component types present (lists, cards, tabs) | Missing sections, "undefined" text, blank areas |
| Data loads | Actual entity data visible (refs, statuses, dates) | "Loading..." persists, "No data" when data exists, spinner stuck |
| Navigation works | Sidebar/header links present and functional | Missing nav items, broken links |
| User actions visible | Action buttons for current entity status | Missing actions that should be available |
| No console errors | `list_console_messages` shows no red errors | React errors, GraphQL errors, manifest parse errors |

**Component verification by page type:**

**List pages** (`fc.list`):
- Table/list renders with column headers
- Row count > 0 (unless genuinely empty)
- Pagination controls present
- Search/filter inputs functional
- Click on row navigates to detail

**Detail pages** (`fc.page`):
- Page heading shows entity ref/ID
- Status badge displays current status
- Tabs render with correct labels
- Attribute cards show key-value pairs
- Timeline/activity section loads

**Custom pages** (manifest-specific):
- Page section headers match manifest configuration
- Custom components render without errors
- Data bindings resolve (no `{{undefined}}` or `{{null}}` display)
- Conditional components show/hide based on data

### Phase 4: User Action Verification (MANDATORY after workflow-deploy)

If the test follows a workflow deployment, this phase is mandatory. For manifest-only tests, it remains optional.

1. Navigate to an entity detail page
2. **Preferred:** `evaluate_script` → `JSON.stringify(window.__fcTest.getActionButtons())` to list visible buttons (~200 tokens)
   - **Fallback:** `take_snapshot` if helper not injected or buttons not found (~30K tokens)
3. Verify expected actions are present for the entity's current workflow status
4. **DO NOT click destructive actions** unless explicitly testing them with test data
5. Cross-reference with `/fluent-transition-api` to confirm expected actions match. See `/fluent-workflow-builder` diagnostic checklist (UA01-UA17) for troubleshooting

   **Module alignment check:** Before verifying button visibility:
   - OMS App uses `module: "adminconsole"` (the OMS App's module alias, legacy alias) for Transition API (NOT `"oms"`) — the OMS manifest has `orchestrationAlias: "adminconsole"` which overrides the module name
   - Verify workflow `context[].modules` includes `"adminconsole"` for OMS buttons
   - For child entities (CREDIT_MEMO, FULFILMENT), user actions must be in the parent workflow for UI button visibility (the UI uses the entity's `workflowRef` as `flexType`, which for child entities = parent workflow). Note: the Transition API itself accepts both parent and child workflow names as `flexType`
   - Console warning "Error building user action lookups" = Transition API call failed

#### Click Telemetry for User Action Verification

When verifying user action buttons, use click telemetry to visually highlight each button being inspected. This makes screenshots and screencast recordings self-documenting.

**Step 4a: Verify helper library is injected** (should already be done in Phase 0b)

```javascript
// Via evaluate_script — check helper is ready (injected in Phase 0b)
window.__fcTest ? 'ready' : 'not_injected'
```

If `'not_injected'`, inject the helper library from `BROWSER_OPTIMIZATIONS.md` now. The helper includes keyframes, timeline tracker, highlight, and annotate functions — no separate initialization needed.

**Step 4b: Highlight each action button** using the helper library

For each action button found via `getActionButtons()` or `pageSummary().buttons`:

```
1. evaluate_script → window.__fcTest.highlight("Button Label", "VERIFY: BUTTON LABEL")
   Returns {found: true, rect: {...}} or {found: false}
2. wait_for 300ms — let highlight render
3. take_screenshot — capture the highlighted button as evidence
4. evaluate_script → window.__fcTest.clearHighlight()
```

**Example** (replace `Cancel Order` with actual button text):
```javascript
// Via evaluate_script — highlight + log in one call (~100 tokens)
JSON.stringify(window.__fcTest.highlight("Cancel Order", "VERIFY: CANCEL ORDER"))
```

**Cleanup:**
```javascript
window.__fcTest.clearHighlight()
```

**If button not found** (`{found: false}`): Check with `__fcTest.exists("Cancel Order", "button")` first. If false, the button is not in the DOM — escalate to `take_snapshot` for full investigation, then check Transition API.

**Step 4c: Include timeline in test results**

After all button verifications, extract the timeline:

```javascript
// Via evaluate_script — get action timeline (~200 tokens)
JSON.stringify(window.__fcTest.getTimeline())
```

Append to the test results report:
```
### Action Timeline
| # | Elapsed | Action | Target | Label |
|---|---------|--------|--------|-------|
| 1 | +0.3s | verify | cancel-btn | VERIFY: Cancel Order |
| 2 | +1.1s | verify | approve-btn | VERIFY: Approve |
```

### Phase 5: Annotate & Screenshot

Before taking the final screenshot, inject a step annotation overlay so the evidence is self-documenting (especially useful when `--save-video` is recording).

**Step 5a: Inject annotation overlay**
```javascript
// Via evaluate_script — one-line annotation (~100 tokens vs ~800 tokens inline)
window.__fcTest.annotate("Orders list — verifying data load", "VERIFY")
```
Replace the description text with a concise description of the page/step being verified. The second parameter is the badge type (e.g., `"VERIFY"`, `"STEP"`, `"PASS"`, `"FAIL"`).

**Step 5b: Screenshot**
1. `take_screenshot` to capture the annotated page state
2. Save the screenshot:
   - **With feature context** (status.json exists for current work): save to `features/<slug>/assets/`
     ```bash
     mkdir -p accounts/<PROFILE>/features/<slug>/assets
     # Save screenshot as: ui-test-<route-slug>.png (e.g., ui-test-order-list.png)
     ```
     This enables relative linking from spec.md/plan.md: `![Order list verification](assets/ui-test-order-list.png)`
   - **Without feature context**: save to `.tmp/ui-test/`
     ```bash
     mkdir -p .tmp/ui-test
     # Save screenshot as: ui-test-<route-slug>.png
     ```

**Step 5c: Remove annotation**
```javascript
// Via evaluate_script — clean up all overlays
window.__fcTest.clearAll()
```

**Step 5d: Report results**
1. Report results:

```
## UI Test Results

**App:** OMS
**Route:** /oms/orders
**URL:** https://acme.test.apps.engineering.fluentcommerce.com/oms/orders

### Checks
- [x] Login successful
- [x] Page loads without errors
- [x] List component renders with data
- [x] Pagination controls present
- [x] Search/filter functional
- [ ] Screenshot captured

### Issues Found
- None

### Evidence
- DOM snapshot: [attached]
- Screenshot: [attached]
```

**Step 5e: Thought-bubble annotations for NEW components**

When verifying a newly deployed custom SDK component or manifest addition, use the thought-bubble annotation pattern to clearly mark what's new vs. existing. Annotations must be OUTSIDE the component — never inside the card.

- **Glow border** on the new component: `border: 2px solid #1a73e8; animation: pulseGlow 2s ease-in-out infinite;`
- **"NEW" badge** inline on the heading: `background:#1a73e8; color:white; font-size:9px; padding:2px 6px; border-radius:8px;`
- **Thought bubble** positioned below the component (not inside): yellow `#fff8e1` / `#ffb300` with CSS triangle arrow pointing up

Full injection template and CSS: see `/fluent-ui-record` skill ("Thought Bubble Annotations" section).
OMS native styling reference (Lato font, MuiCard hierarchy, computed styles): see `knowledge/frontend/mystique-patterns.md` ("OMS DOM Structure and Styling Reference" section).

## Manifest Cache Handling

Phase 2 already performs an `about:blank` bounce before every navigation, which clears cached SPA state. This is usually sufficient for fresh manifest loads.

**If the page still shows stale content after Phase 2 navigation:**
1. `press_key("F5")` for a hard refresh (clears browser HTTP cache for the page)
2. If still stale, the setting may have a CDN cache layer — wait 1-2 minutes and retry

**Verify the correct manifest is loaded:**
1. `list_network_requests` — check for `fc.mystique.manifest.*` API calls and inspect response content
2. Or: use `graphql.query` / `setting.get` to fetch the setting value directly and compare

## Troubleshooting

| Symptom | Cause | Fix |
|---------|-------|-----|
| Blank page after login | Manifest missing or invalid JSON | Check `fc.mystique.manifest.<app>` setting exists and is valid JSON |
| "Something went wrong" banner | React error boundary triggered | Check `list_console_messages` for the specific error |
| Components missing | Component alias not registered | Verify alias exists in OOTB registry or custom plugins are deployed |
| Data not loading | GraphQL query error | Check console for GraphQL errors, verify query permissions |
| "App not found" page | Wrong app base path | Use app-specific URL (for OMS include `/oms`) before route/hash |
| Console: `Manifest contains an invalid route` | Manifest JSON shape or `lobValue` encoding issue | Re-validate with `/fluent-mystique-validate`; ensure `lobValue` is raw JSON (not stringified) |
| Console: `Error building user action lookups` | Transition API request failed (module/flexType/flexVersion/route context) | Verify Transition call includes `module`, `flexType`, `flexVersion`; check OMS uses `module: "adminconsole"` |
| User action dropdown empty | Attribute source/settings contract mismatch | Verify `source` uses lowercase `settings.<KEY>`, setting value uses `{ "active": [...] }`, and setting context matches target retailer |
| Login redirect loop | Session expired or invalid credentials | Clear cookies, try fresh login |
| Actions not showing | Workflow status mismatch or missing userActions | Use `/fluent-transition-api` to verify expected actions |
| Old manifest showing | Cache not cleared | Hard refresh, wait for cache TTL, or redeploy with different setting value |

## Screencast Recording Awareness

When Chrome DevTools MCP is configured with `--save-video`, every browser interaction is continuously recorded. This means:

1. **Annotations appear in the recording** — DOM overlays injected via `evaluate_script` are captured in the screencast
2. **Login flow is recorded** — the recording includes everything from first navigation
3. **Recording persists after MCP session ends** — find recordings in `.tmp/chrome-devtools/`

To view the recording:
```bash
# List recordings
ls .tmp/chrome-devtools/*

# Open in default player (Windows)
start .tmp/chrome-devtools/recording.webm
```

**Tip:** For polished recording evidence, use `/fluent-ui-record` instead — it orchestrates before/after capture with DOM annotations and generates an HTML evidence report.

## Testing Localhost Plugins (Local Dev Server)

When developing custom SDK components, the primary workflow is running a local dev server (`yarn start` on `localhost:3001`) and pointing the Fluent manifest at it. This lets you iterate on component code with hot-reload — no CDN deploy needed.

### When to Use

- During active component development (the default workflow for `/fluent-frontend-build dev`)
- After scaffolding a new SDK project (`/fluent-mystique-scaffold`)
- After adding or modifying components in an existing project (`/fluent-mystique-component`)
- For rapid iteration on field components, layout components, or any custom UI

### Pre-Conditions

1. Dev server running on `http://localhost:3001/` — **check first before starting a new one** (see below)
2. Manifest `plugins[]` includes `{ "type": "url", "src": "http://localhost:3001/" }` — trailing `/` is REQUIRED
3. Manifest deployed to the environment (via `fluent module install --include settings` or GraphQL)

### Dev Server Detection (MANDATORY before starting)

Before attempting to start a dev server, check if one is already running. This prevents port conflicts when multiple agents or terminals are working in parallel.

```bash
curl -s http://localhost:3001/version.json
```

| Result | Meaning | Action |
|--------|---------|--------|
| Valid JSON response (`{ "bundle": "..." }`) | Server already running | **Skip `yarn start`** — reuse existing instance |
| Connection refused / timeout | No server running | Start with `yarn start` in the SDK project directory |
| HTML error page or unexpected content | Wrong process on port 3001 | Kill the process (`lsof -i :3001` / `netstat -ano | findstr 3001`), then start `yarn start` |

### Localhost Verification Protocol

**Phase 2a (replaces standard Phase 2 navigation for localhost tests):**

1. `navigate_page` to the target app (e.g., `https://acme.test.apps.engineering.fluentcommerce.com/oms/`)
2. Login if needed (standard Phase 1)
3. `list_network_requests` — look for a request to `localhost:3001/version.json`
   - **If present**: The host app discovered the local plugin. Check that the response contains `{ "bundle": "bundle.js" }` or similar
   - **If absent**: The manifest `plugins[]` is not wired correctly, or the dev server is not running
4. `list_console_messages` — check for:
   - CORS errors → dev server needs `Access-Control-Allow-Origin: *` header (SDK template includes this)
   - `net::ERR_CONNECTION_REFUSED` → dev server not running or wrong port
   - React/Mystique registration errors → component code issue
5. Navigate to the page that uses the custom component
6. `take_snapshot` to verify the component renders (not blank, not "undefined")
7. `take_screenshot` for visual evidence

### Iteration Loop with Localhost

```
Edit source code (src/components/ or src/fields/)
         ↓
Webpack auto-rebuilds (1-3 seconds)
         ↓
Browser hot-reloads (HMR) — no manual refresh needed
         ↓
take_snapshot + list_console_messages to verify
         ↓
If broken → check console, fix code, repeat
If working → take_screenshot for evidence
```

**Key check:** After each code change, run `list_console_messages` to catch runtime errors that may not be visually obvious (e.g., hook errors, missing data, type coercion issues).

### Multi-App Plugin Deployment

If the same custom component is used across multiple Fluent apps (OMS App, Store App), the localhost plugin must be added to the **root manifest** of each app:

| App | Root Manifest Setting | Add plugin to |
|-----|----------------------|---------------|
| OMS App | `fc.mystique.manifest.oms` | `plugins[]` array in root manifest |
| Store App | `fc.mystique.manifest.store` | `plugins[]` array in root manifest |

**Important:** `plugins[]` go ONLY in the root manifest (`fc.mystique.manifest.<app>`), NEVER in fragments. Fragments contain only `routes[]` — they inherit plugins from the root manifest. If your component is used in multiple apps, add the plugin entry to each app's root manifest.

### Cleanup After Local Testing

**CRITICAL:** Before ending a local dev session:
1. Stop the dev server (`Ctrl+C` or kill the background process)
2. Remove `{ "type": "url", "src": "http://localhost:3001/" }` from ALL root manifest `plugins[]` where it was added
3. Redeploy the cleaned manifest settings
4. If switching to production CDN, replace with `{ "type": "url", "src": "https://cdn.example.com/my-plugin/" }`

A localhost URL left in a deployed manifest breaks the UI for all users who don't have the dev server running.

## Integration with Other Skills

### After `/fluent-mystique-builder`
Build manifest → `/fluent-mystique-validate` → deploy via `fluent module install --include settings` or GraphQL → **`/fluent-ui-test`** to verify in browser

### After `/fluent-mystique-scaffold`
Scaffold SDK component → `yarn install` → `/fluent-frontend-build dev` (start dev server) → wire manifest `plugins[]` to localhost → deploy manifest → **`/fluent-ui-test`** to verify custom component renders → iterate → `/fluent-frontend-build full` for production

### After `/fluent-frontend-build dev`
Dev server running on localhost:3001 → manifest wired with localhost plugin → **`/fluent-ui-test`** to verify component renders → iterate with hot-reload → re-test after changes

### After `/fluent-workflow-deploy`
Deploy workflow changes → **`/fluent-ui-test`** to verify status transitions, user actions, and page behavior reflect the new workflow

### For Before/After Evidence
If you need a visual evidence report (not just pass/fail), use **`/fluent-ui-record`** instead. It captures baseline screenshots, waits for the change to be deployed, captures after screenshots, and generates a self-contained HTML report with diffs and annotations.

## Defect Triage + Iteration Loop (MANDATORY on FAIL)

When a UI test fails, do not stop at reporting. Run a fix loop until PASS or a confirmed blocker:

1. Classify failure source:
   - **Manifest/config** -> `/fluent-mystique-builder` then `/fluent-mystique-validate`
   - **SDK component/plugin** -> `/fluent-mystique-component` or `/fluent-mystique-scaffold`
   - **Workflow/user action** -> `/fluent-workflow-builder` or `/fluent-transition-api`
   - **Data/env/auth** -> fix seed data/profile/credentials first
2. Capture reproducible evidence (route, entity ref, screenshot, console/network error)
3. Apply fix and redeploy changed artifact(s) when required:
   - **Code-only with localhost plugin active (`http://localhost:3001/`)** -> no manifest redeploy needed; wait for rebuild and hard refresh
   - **Manifest/plugin URL/workflow change** -> redeploy changed artifact(s) before re-test
4. Re-run the same `/fluent-ui-test` scenario
5. Repeat until PASS, or emit `BLOCKED` with concrete cause and recovery path

This loop is the default behavior for end-to-end validation and regression hardening.

## Handoff Protocol

| Signal | Meaning |
|--------|---------|
| `-> PASS: <app> <route>` | Page renders correctly, all checks pass |
| `-> FAIL: <app> <route> <reason>` | Page has issues, details in report |
| `-> BLOCKED: Chrome DevTools MCP not connected` | Cannot run, IDE restart needed |
| `-> BLOCKED: Login failed` | Cannot authenticate, check credentials |
| `-> NEXT: /fluent-mystique-validate` | Manifest issues found, validate offline first |

## Error Reporting

| Severity | Code | Example |
|----------|------|---------|
| CRITICAL | `PREREQ_MISSING` | Chrome DevTools MCP not connected |
| HIGH | `LOGIN_FAILED` | Authentication failed |
| HIGH | `PAGE_ERROR` | React error boundary triggered |
| MEDIUM | `COMPONENT_MISSING` | Expected component not rendered |
| LOW | `DATA_STALE` | Cached manifest showing, refresh needed |

## Session Tracking

Track all browser test executions:
```json
{
  "skill": "fluent-ui-test",
  "type": "browser_test",
  "app": "oms",
  "route": "/orders",
  "result": "PASS|FAIL",
  "checks": { "login": true, "pageLoad": true, "dataLoad": true },
  "issues": [],
  "timestamp": "<ISO>"
}
```

## Post-Execution: Feedback Capture

After completing this skill (whether success or failure), write a feedback record:

1. **Classify outcome:** `SUCCESS` (completed cleanly), `PARTIAL_SUCCESS` (completed after fixing issues), `FAILURE` (could not complete), `BLOCKED` (prereq missing), `USER_CORRECTED` (user overrode approach)
2. **Build record:** Create a `feedback-record-v1` JSON object with:
   - `skill`: `"fluent-ui-test"`
   - `feature`: feature slug if applicable, null otherwise
   - `phases`: trace of each phase with PASS/FAIL/SKIP
   - `learnings`: any gotchas discovered during execution
   - `userCorrections`: any corrections the user provided
3. **Redact secrets:** Strip tokens, passwords, API keys. Keep business identifiers.
4. **Append** one JSON line to `accounts/<PROFILE>/feedback/fluent-ui-test.jsonl`
5. **Update** `accounts/<PROFILE>/feedback/index.json` counters (create with `mkdir -p` if missing)

**Skip capture if:** The execution was trivially simple (e.g., user asked a question, no actual write operation happened).

## Known Pitfalls
<!-- feedback-promoted: managed section, updated by feedback loop -->

_(No promoted learnings yet.)_

<!-- end feedback-promoted -->
