Claude Chrome Testing Workflow

Documentation for Developers

Claude + Chrome: Visual Testing Workflow

The Problem

Claude runs inside a lightweight Linux VM. Your Chrome browser runs on your Mac. There is no network bridge between them. This means:

This is not a bug — it's a security boundary by design. The VM is sandboxed.

The Solution: Chrome JavaScript Injection

Claude has access to your Chrome browser via the "Claude in Chrome" extension. This extension provides tools to:

The key insight: we don't need to serve files. We inject code directly into an existing page.

Prompt to Claude


How It Works (Step by Step)

Step 1: Open a "Host" Page

Navigate to a simple, publicly reachable website. We use https://example.com/ because:

Step 2: Replace the Page with document.write()

Using the Chrome JavaScript tool, we execute:

document.write("<html>...our entire HTML structure...</html>");

This completely replaces example.com's DOM with our custom page. The CSS, canvas element, layout — everything gets injected in this single call.

Important: The HTML must be properly escaped for JavaScript string context. Single quotes in CSS (like font-family: 'Segoe UI') will break if the outer string uses single quotes. Use JSON-style double-quote escaping or template strings.

Step 3: Inject Large Scripts via Base64 Chunking

The Chrome JS tool has a practical size limit per call (~8-10KB of text). Our VittoEngine bundle is ~35KB. To handle this:

  1. Encode the engine JS as base64 (increases size by ~33% but avoids escaping issues)
  2. Split the base64 string into ~8KB chunks
  3. Inject each chunk sequentially, accumulating on a window variable:
// First chunk
window._b64 = "SGVsbG8gV29ybGQ...first 8000 chars...";

// Subsequent chunks
window._b64 += "...next 8000 chars...";
window._b64 += "...next 8000 chars...";
// ... repeat until all chunks loaded
  1. Decode and evaluate the complete base64 string:
window.VittoEngine = (new Function(
  atob(window._b64) + "; return VittoEngine;"
))();

Why new Function() instead of plain eval()? — eval() in Chrome's extension context doesn't always expose var declarations to the global scope. new Function() creates a proper scope where we can explicitly return the global we need.

Step 4: Inject the UI Script

The UI harness code (mood buttons, sliders, inspector) is smaller (~9KB) and can be injected in 2-3 Chrome JS tool calls directly as executable JavaScript. No base64 needed — just split at natural boundaries (e.g., after mood grid setup, after slider creation, after inspector).

All references to the renderer and state must be stored on window.* (e.g., window._renderer, window._sliderEls) since each Chrome JS tool call runs in a separate evaluation context.

Step 5: Take Screenshots and Interact

Once everything is injected:

Practical Gotchas

Chrome Throttles Background Tabs

When a tab isn't in the foreground, Chrome throttles requestAnimationFrame to ~1fps. This means:

var target = VittoEngine.MOOD_DEFINITIONS['sleepy'];
var s = window._renderer.getState();
s.tT = 1.0; // Skip transition
for (var k of VittoEngine.MOOD_NUMERIC_KEYS) {
  if (target[k] !== undefined) s[k] = target[k];
}
s.bg = [...target.bg];

Size Limits per JS Call

Keep each Chrome JS tool call under ~4000 characters of JavaScript. For larger payloads:

The Tab Persists Until Navigated Away

Once injected, the page stays alive as long as the tab isn't refreshed or navigated. You CAN interact with it from your side too (click mood buttons, drag sliders, move mouse over the canvas).

However: If you refresh the tab, everything disappears (it goes back to example.com). The injection is purely in-memory.

What YOU See vs What Claude Sees

You (Mac) Claude (VM)
localhost:8765 Connection refused Would work inside VM only
example.com tab Vitto harness (after injection) Can screenshot + interact
Dev harness HTML file Opens locally in your browser Can't serve to your browser

Preparing the Engine for Injection

Before a Chrome testing session, the engine must be built:

# From the monorepo root (your machine or Claude's VM)
cd /path/to/gspace1
npx esbuild libs/libs-anim/vitto-avatar/src/lib/engine/index.ts \
  --bundle --format=iife --global-name=VittoEngine \
  --outfile=dist/libs/libs-anim/vitto-avatar/engine/vitto-engine.iife.js \
  --minify --sourcemap

Then to prepare base64 chunks:

# Encode the built bundle
base64 dist/libs/libs-anim/vitto-avatar/engine/vitto-engine.iife.js > /tmp/engine.b64

# Split into 8KB chunks
split -b 8000 /tmp/engine.b64 /tmp/b64chunk_

# Or with Python for more control:
python3 -c "
import base64
with open('dist/libs/libs-anim/vitto-avatar/engine/vitto-engine.iife.js', 'rb') as f:
    encoded = base64.b64encode(f.read()).decode()
chunk_size = 8000
for i in range(0, len(encoded), chunk_size):
    chunk = encoded[i:i+chunk_size]
    with open(f'/tmp/b64chunk_{i // chunk_size}.txt', 'w') as out:
        out.write(chunk)
    print(f'Chunk {i // chunk_size}: {len(chunk)} chars')
"

For Future Sessions: Quick Reference

When Claude needs to visually test the Vitto avatar in Chrome:

  1. tabs_context_mcp — find or create a tab
  2. Navigate to example.com (or reuse an existing injection tab)
  3. Inject HTML structure via document.write()
  4. Build engine if not built: npx esbuild ...
  5. Base64 encode + chunk the engine
  6. Inject chunks sequentially onto window._b64
  7. Decode + eval: window.VittoEngine = (new Function(atob(window._b64) + '; return VittoEngine;'))()
  8. Inject UI harness script in 2-3 parts
  9. Take screenshots, change moods, iterate

Your Local Workflow (Paco)

You don't need any of this! You can:

  1. Open vitto animation/vitto-dev-harness.html directly in your browser
  2. It loads the engine from ../gspace1/dist/libs/libs-anim/vitto-avatar/engine/vitto-engine.iife.js
  3. Full 60fps, mouse tracking, all controls work natively
  4. Prerequisite: Run the esbuild command first so the bundle exists at that path

Or once we wire up the Angular route:

  1. yarn serve:gspace1
  2. Navigate to localhost:6200/dev/vitto-avatar (or whatever route we create)
  3. Full Angular integration with live reload

Why This Matters

This workflow lets both of us iterate on the same codebase:

I asked: "is there a way for me to see what you are seeing?

Answer:

Yes! Two ways:
Right now, in your Chrome — the injection I did is in YOUR browser. Look for a tab titled "Vitto Dev Harness" on example.com. That's the tab I was injecting into. It should still have the full harness with the canvas, mood buttons, sliders, and everything running. It's a different tab from the 127.0.0.1:8765 one you screenshotted.
You should have these tabs open:

example.com — this is the one (titled "Vitto Dev Harness")
127.0.0.1:8765 — dead, ignore this one
editor.rive.app/... — the old Rive editor
Two other 127.0.0.1 tabs — also dead

But the better long-term option is to open the HTML file directly. From your Mac:

First make sure the bundle exists by running from the gspace1 root:

```bash
npx esbuild libs/libs-anim/vitto-avatar/src/lib/engine/index.ts --bundle --format=iife --global-name=VittoEngine --outfile=dist/libs/libs-anim/vitto-avatar/engine/vitto-engine.iife.js --minify --sourcemap
  1. Then just open the file in Chrome:
vitto animation/vitto-dev-harness.html
That HTML file loads the engine via a relative path (../gspace1/dist/libs/...), so as long as both folders are siblings on your machine, it'll work natively at 60fps with full mouse tracking, mood buttons, sliders — everything.