@mechris3/glassbox
v0.1.48
Published
Debugger-style browser automation test runner with live execution control via CDP
Maintainers
Readme
@mechris3/glassbox
Debugger-style browser automation test runner using Chrome DevTools Protocol (CDP) directly. No Puppeteer, no Playwright — just raw WebSocket + JSON.
Features:
- Live execution control: breakpoints, pause/resume, step-through
- Visual dashboard with real-time action streaming
- Headless batch runner for CI
- Diagnostic data collection (console errors, network, performance)
- MCP server for AI agent integration
- Browser and profile auto-detection
Quick Start
# Install
npm install @mechris3/glassbox
# Scaffold a project (creates glassbox.config.ts + directories)
npx glassbox init
# Write journeys and page objects (see below), then:
npx glassbox serve # Dashboard UI at http://localhost:3001
npx glassbox run # Headless batch run (for CI)glassbox init creates this structure in your current directory:
my-tests/
├── package.json ← dependency + scripts
├── glassbox.config.ts ← created by init
├── journeys/ ← put your journey files here
└── page-objects/ ← put your page objects hereThe generated package.json includes convenience scripts:
npm run serve # Start the visual dashboard at http://localhost:3001
npm run run # Run all journeys headless (exit 0 = pass, 1 = fail)
npm run run:headed # Run with a visible browser windowAfter running glassbox init, install dependencies and you're ready:
npm install
npm run serveYou can set this up as a standalone project or as a subfolder within an existing frontend project:
# Standalone
my-e2e-tests/
├── package.json
├── glassbox.config.ts
├── journeys/
└── page-objects/
# Inside an existing project
my-app/
├── src/
├── package.json
└── e2e/
├── glassbox.config.ts
├── journeys/
└── page-objects/Either way, run glassbox commands from the directory containing your glassbox.config.ts.
Prerequisites
- Node.js 20+
- A Chromium-based browser (Chrome, Brave, Edge)
Environment Variables
Config files, hooks, journeys, and page objects are all TypeScript running in Node — process.env works everywhere:
// page-objects/login.page.ts — credentials from env
async login() {
const user = process.env.TEST_USERNAME ?? '[email protected]';
const pass = process.env.TEST_PASSWORD ?? 'password123';
await this.fill('[data-testid="email"]', user);
await this.fill('[data-testid="password"]', pass);
await this.click('[data-testid="submit"]');
}
// glassbox.config.ts — headless in CI
browser: { headless: process.env.CI === 'true' }
// helpers/before-each.ts — API key for test data service
const apiKey = process.env.TEST_API_KEY;
await fetch('http://...', { headers: { Authorization: `Bearer ${apiKey}` } });Consumer Usage
Page Objects
Extend BasePage — no constructor needed. The following methods are available via this:
| Method | Description |
|--------|-------------|
| Interaction | |
| click(selector) | Click an element |
| hover(selector) | Hover over an element |
| focus(selector) | Focus an element |
| pressKey(key) | Press a key ('Enter', 'Tab', 'Escape', etc.) |
| fill(selector, value) | Clear and set an input's value |
| type(selector, value, {delay?}) | Type character by character |
| uploadFile(selector, filePath) | Upload a file to a file input |
| selectByIndex(selector, index) | Select dropdown option by index |
| selectByValue(selector, value) | Select dropdown option by value |
| selectByText(selector, text) | Select dropdown option by visible text |
| Queries | |
| getText(selector) | Get element's text content |
| getInputValue(selector) | Get input's current value |
| getAttribute(selector, attr) | Get an attribute value |
| countElements(selector) | Count matching elements |
| isVisible(selector) | Check if element is visible |
| isDisabled(selector) | Check if element is disabled |
| Waiting | |
| waitForSelector(selector, {timeout?}) | Wait for element to appear |
| waitForHidden(selector, {timeout?}) | Wait for element to disappear |
| waitForText(selector, expected, {timeout?}) | Wait until text content matches |
| waitForCount(selector, expected, {timeout?}) | Wait until element count matches |
| waitForTimeout(ms) | Fixed delay (use sparingly) |
| Navigation | |
| goto(url) | Navigate (relative URLs resolve against target) |
| clickAndWaitForNavigation(selector) | Click and wait for page load |
| getCurrentUrl() | Get the current page URL |
| Advanced | |
| evaluate(script) | Execute arbitrary JS in the page |
| clearSession(origin?) | Clear cookies and storage |
| readClipboard() | Read text from clipboard |
Example:
// page-objects/login.page.ts
import { BasePage } from '@mechris3/glassbox';
export class LoginPage extends BasePage {
private selectors = {
username: '[data-testid="username"]',
password: '[data-testid="password"]',
submit: '[data-testid="submit"]',
};
async login(user: string, pass: string) {
await this.fill(this.selectors.username, user);
await this.fill(this.selectors.password, pass);
await this.click(this.selectors.submit);
}
async verifyLoggedIn(expectedName: string) {
const name = await this.getText('[data-testid="display-name"]');
if (name !== expectedName) throw new Error(`Expected "${expectedName}", got "${name}"`);
}
}Journeys
Extend Journey — use this.page() to create page objects:
// journeys/login.journey.ts
import { Journey } from '@mechris3/glassbox';
import { LoginPage } from '../page-objects/login.page';
export class LoginJourney extends Journey {
loginPage = this.page(LoginPage);
async execute() {
await this.loginPage.login('testuser', 'password123');
await this.loginPage.verifyLoggedIn('testuser');
}
}Config
All paths in the config are relative to the config file's directory:
my-tests/
├── glassbox.config.ts
├── journeys/
│ └── login.journey.ts
├── page-objects/
│ └── login.page.ts
└── helpers/
└── before-each.ts// glassbox.config.ts
import { defineConfig } from '@mechris3/glassbox';
export default defineConfig({
journeys: './journeys/**/*.journey.ts',
browser: {
defaultViewport: { width: 1280, height: 720 },
headless: false,
},
server: { port: 3001 },
testData: {
beforeEach: './helpers/before-each.ts',
},
});Best Practices
Journeys are pure orchestration
A journey's execute() method should contain only calls to page object methods — no conditionals, no assertions, no direct selectors:
// Good — reads like a user story
async execute() {
await this.loginPage.loginAs('[email protected]', 'alice123');
await this.dashboardPage.verifyLoaded();
await this.dashboardPage.navigateToProjects();
await this.projectsPage.verifyProjectCount(6);
}
// Bad — logic leaking into journey
async execute() {
await this.fill('[data-testid="email"]', '[email protected]');
const count = await this.getText('[data-testid="count"]');
if (parseInt(count) !== 6) throw new Error('wrong');
}Page objects own everything
- Selectors — store in a private
selectorsobject, never expose them - Interaction —
loginAs(),addTask(),filterByStatus()— intention-revealing names - Validation —
verifyLoaded(),verifyErrorMessage(),verifyTaskCount()— throw descriptive errors on failure
export class ProjectsPage extends BasePage {
private selectors = {
projectCard: '[data-testid="project-card"]',
filterActive: '[data-testid="filter-active"]',
count: '[data-testid="project-count"]',
};
async filterByActive() {
await this.click(this.selectors.filterActive);
}
async verifyProjectCount(expected: number) {
const count = await this.getText(this.selectors.count);
if (count !== String(expected)) {
throw new Error(`Expected ${expected} projects, got ${count}`);
}
}
}Selector strategy
- Always use
[data-testid="..."]— never CSS classes, tag names, or DOM structure - For repeated items, include an identifier:
[data-testid="task-item-${id}"] - If an element doesn't have a
data-testid, add one to your app first
File organisation
glassbox/
├── page-objects/
│ ├── login.page.ts ← one class per file
│ ├── dashboard.page.ts
│ └── projects.page.ts
├── journeys/
│ ├── login-and-browse.journey.ts
│ └── create-task.journey.ts
└── helpers/
└── before-each.tsLifecycle Hooks
Run code before/after journeys for test data setup and cleanup:
// glassbox.config.ts
export default defineConfig({
hooks: {
globalSetup: './helpers/global-setup.ts', // Once before all journeys
beforeEach: './helpers/before-each.ts', // Before each journey
afterEach: './helpers/after-each.ts', // After each journey (even on failure)
globalTeardown: './helpers/global-teardown.ts', // Once after all journeys
},
});Hook files export a default async function. Use them for any setup/teardown your tests need — resetting databases, seeding test data, clearing caches, etc:
// helpers/before-each.ts
import type { HookContext } from '@mechris3/glassbox';
export default async function(context: HookContext) {
// Call your test API to reset state (can be any URL, not necessarily the target app)
await fetch('http://localhost:4000/api/test-data/reset', { method: 'POST' });
// Or seed specific data
await fetch('http://localhost:4000/api/test-data/seed', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ users: ['testuser'], projects: 3 }),
});
}targetUrl comes from the dashboard settings panel (the URL field) or the --url flag when using the CLI. It's the URL of the app you're testing.
HookContext contains:
| Property | Available in | Description |
|----------|-------------|-------------|
| targetUrl | All hooks | The app URL being tested |
| journeyName | beforeEach, afterEach | Display name of the current journey |
| journeyPath | beforeEach, afterEach | File path of the current journey |
| journeyStatus | afterEach only | 'passed' or 'failed' |
CLI Commands
Run all commands from the directory containing your glassbox.config.ts:
cd my-tests/ # where your glassbox.config.ts livesDashboard (visual UI):
npx glassbox serve # Start dashboard at http://localhost:3001Open the URL in your browser to see journeys, run them, set breakpoints, pause/step, and inspect results visually.
Headless runner (CI / terminal):
npx glassbox run # Run all journeys, exit 0/1
npx glassbox run login # Only journeys matching "login"
npx glassbox run --url http://localhost:3000 # Specify app URL
npx glassbox run --headed # Show the browser while running
npx glassbox run --no-fail-fast # Don't stop at first failureOther:
npx glassbox init # Scaffold config + directories in current dir
npx glassbox mcp # Start MCP server for AI agent integrationMCP Server (AI Integration)
The MCP server lets AI agents (Kiro, Claude) debug failed journeys by querying diagnostic data.
Add to your .kiro/settings/mcp.json:
{
"mcpServers": {
"glassbox": {
"command": "npx",
"args": ["glassbox", "mcp"],
"env": { "GLASSBOX_URL": "http://localhost:3001" },
"cwd": "/absolute/path/to/your/glassbox-tests"
}
}
}cwd must be an absolute path to the directory containing your glassbox.config.ts and node_modules/. Kiro does not resolve relative paths in MCP configs.
Requires glassbox serve to be running. The AI gets these tools:
| Tool | Purpose |
|------|---------|
| getDiagnosticSummary | Summary of diagnostic counts (call first) |
| getConsoleErrors | console.error/warn with stack traces |
| getExceptions | Unhandled JS exceptions with source locations |
| getNetworkFailures | Failed HTTP requests (4xx/5xx/network errors) |
| getNetworkLog | All XHR/Fetch requests with status codes |
| getTimeline | Chronological event stream |
| getPerformanceMetrics | Heap size, DOM nodes, layout recalculations |
| listJourneys | Available journey files |
| runJourney | Run by name/ID (browser stays open on failure) |
| getDomSnapshot | Inspect page HTML or a specific element |
| getScreenshot | Capture page screenshot (base64 PNG) |
| getAccessibilityTree | Full ARIA tree for understanding page structure |
| resetDiagnostics | Clear data + close live session before re-run |
The AI can identify journeys by name ("login"), ID ("login-and-add-todos"), or full path. After a failure, the browser stays open so getDomSnapshot and getScreenshot can inspect the failed state.
License
Proprietary. All rights reserved.
