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:
- Any server Claude starts (e.g.,
python3 -m http.server 8765) listens inside the VM only localhost:8765or127.0.0.1:8765from your Chrome will show "connection refused"- There is no way to expose a VM port to your local network
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:
- Execute arbitrary JavaScript in any open tab
- Take screenshots of tabs
- Read page content and accessibility trees
- Click, type, scroll — full browser automation
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:
- It always loads (it's maintained by IANA)
- It has minimal content (easy to replace entirely)
- It's HTTPS, so Canvas and other APIs work normally
- It's not localhost, so Chrome doesn't block it
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:
- Encode the engine JS as base64 (increases size by ~33% but avoids escaping issues)
- Split the base64 string into ~8KB chunks
- 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
- 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:
- Screenshots capture the live canvas rendering
- JS tool calls can change moods:
window._renderer.setMood('cheerful') - Zoom regions can inspect specific areas of the canvas
Practical Gotchas
Chrome Throttles Background Tabs
When a tab isn't in the foreground, Chrome throttles requestAnimationFrame to ~1fps. This means:
- Mood transitions that normally take 400ms can take 3-5 seconds
- To work around this, you can force-complete transitions by directly setting state values:
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:
- Use base64 chunking (as described above)
- Or split logical code sections across multiple calls
- Store shared state on
window.*so subsequent calls can access it
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:
tabs_context_mcp— find or create a tab- Navigate to
example.com(or reuse an existing injection tab) - Inject HTML structure via
document.write() - Build engine if not built:
npx esbuild ... - Base64 encode + chunk the engine
- Inject chunks sequentially onto
window._b64 - Decode + eval:
window.VittoEngine = (new Function(atob(window._b64) + '; return VittoEngine;'))() - Inject UI harness script in 2-3 parts
- Take screenshots, change moods, iterate
Your Local Workflow (Paco)
You don't need any of this! You can:
- Open
vitto animation/vitto-dev-harness.htmldirectly in your browser - It loads the engine from
../gspace1/dist/libs/libs-anim/vitto-avatar/engine/vitto-engine.iife.js - Full 60fps, mouse tracking, all controls work natively
- Prerequisite: Run the esbuild command first so the bundle exists at that path
Or once we wire up the Angular route:
yarn serve:gspace1- Navigate to
localhost:6200/dev/vitto-avatar(or whatever route we create) - Full Angular integration with live reload
Why This Matters
This workflow lets both of us iterate on the same codebase:
- You test locally at 60fps with full interactivity
- Claude tests via Chrome injection with screenshots
- Zero code duplication — both workflows consume the same esbuild bundle
- DRY principle upheld — the engine source lives in one place only
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
- 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.