itk-corteo
v1.0.0
Published
A lightweight “guided demo” layer for React apps:
Readme
ITK-Corteo (Demo Mode)
A lightweight “guided demo” layer for React apps:
- Record real user interactions (click/input/select/accordion + URL changes)
- Play them back with a spotlight, coachmarks, and draggable controls
- Load demo configs from static JSON via a simple manifest
In this repo the package name is
itk-corteo.
Installation
bun add itk-corteoPeer deps: react, react-dom, react-router-dom, theme-ui.
Quick Start
- Serve your demo configs (a
manifest.json+ one or more*.jsonconfigs).
Example structure (matches this repo’s playground):
public/
demos/
manifest.json
test-form-demo.json- Wrap your app with
DemoMode.
import { DemoMode } from 'itk-corteo';
export function App() {
return (
<DemoMode enabled={true} configSrc="/demos">
{/* your app */}
</DemoMode>
);
}- Open the command palette with Ctrl/Cmd + Shift + D, then select a demo.
Components
DemoMode
The top-level provider + UI shell (command palette + player overlay + recorder UI).
export interface DemoModeProps {
configSrc: string;
enabled: boolean;
children: React.ReactNode;
}enabled=falsemakesDemoModea no-op (renders children only).configSrcis the base URL for demo assets;DemoModefetches:${configSrc}/manifest.json- each
${configSrc}/{file}listed in the manifest
DemoRecorder
Optional, but recommended in development: adds a “recording HUD” and save dialog.
import { DemoMode, DemoRecorder } from 'itk-corteo';
<DemoMode enabled={true} configSrc="/demos">
<DemoRecorder />
{/* your app */}
</DemoMode>;Recording Demos
- Open the command palette (Ctrl/Cmd + Shift + D) and click Start Recording.
- Interactions are captured globally (document-level capture listeners):
clickinput(debounced)select(native<select>and zenith-style custom options)accordion(elements witharia-expandedordata-*accordion*attributes)- URL changes via History API +
popstate
When you press Save, the recorder downloads a *.json DemoConfig file.
Playing Demos
During playback you’ll see:
- A spotlight cutout around the current target element
- An optional coachmark (shown only when the step has
titleordescription) - Draggable controls (they reset to top-center when a new demo starts)
Keyboard shortcuts during playback:
- ArrowRight: next step
- ArrowLeft: previous step
- Space: play/pause
- Escape: stop
Loading configs (Manifest)
configSrc is expected to contain a manifest.json:
{
"demos": [
{ "id": "test-form-demo", "file": "test-form-demo.json" }
]
}Only configs that pass validateDemoConfig() are included.
Configuration Schema
DemoConfig
export interface DemoConfig {
id: string;
name: string;
version: string;
steps: DemoStep[];
description?: string;
createdAt?: string;
updatedAt?: string;
}DemoStep
export type StepAction =
| 'click'
| 'input'
| 'select'
| 'accordion'
| 'navigate'
| 'wait'
| 'custom';
export interface DemoStep {
id: string;
title?: string;
description?: string;
target: TargetConfig;
action: StepAction;
value?: string;
url?: string;
expectedUrlAfter?: string;
captures?: CapturedRequest[];
apiMode?: ApiMode;
onNext?: () => Promise<void>;
onBack?: () => Promise<void>;
sensitive?: boolean;
}Notes:
navigateis best-effort and useshistory.pushState.waittreatsvalueas milliseconds.customrunsonNextwhen executing the step.
Selector strategies
A target is an ordered list of strategies; higher priority strategies are tried first:
testidlabelformFieldcontextualnthMatchcss
Examples:
{ "type": "testid", "value": "submit" }{ "type": "label", "value": "Company Name" }{ "type": "contextual", "parent": "[data-testid='person-0']", "selector": "input" }{ "type": "css", "selector": "button[type='submit']" }API Interception
The repo includes a small fetch interceptor core (src/core/apiInterceptor.core.ts) supporting:
- passthrough: call real
fetch(optionally capture requests) - replay: return a mocked
Responsefor captured{ method, url }matches - custom: run a custom hook and then call real
fetch
Captured requests are represented as:
export interface CapturedRequest {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
url: string;
requestBody?: unknown;
responseBody?: unknown;
responseStatus?: number;
timestamp?: number;
}Multi-App Demos (Module Federation)
Demo/recording state is persisted to sessionStorage under keys prefixed with itk-corteo:.
If multiple micro-frontends share the same browser session and bundle this library, they can resume playback/recording state after navigation.
Local Development
bun install
bun run devbun run devstarts the playground Vite server on http://localhost:3000- In this repo, the playground mounts:
<DemoMode configSrc="/demos" enabled={true}>- demo assets live in
playground/demos/
Troubleshooting
- No demos show up: ensure
${configSrc}/manifest.jsonis reachable and lists the right files. - A demo config is ignored: it must pass
validateDemoConfig()(id/name/version + at least 1 step + valid target strategies). - Keyboard shortcuts don’t work:
enabledmust be true, and focus must not be inside an input/textarea.
Zenith-UI Targeting Guide
Context: Most teams don't use testIds when implementing Zenith components. This section explains how the recorder targets elements when testIds are missing and how to make your Zenith components targetable.
The Core Problem
Zenith components support testIds, but teams often don't use them:
// What teams write (no testId):
<TextInput labelText="Email" value={email} />
// What you should write (with testId):
<TextInput labelText="Email" value={email} testId="email-input" />Result: The recorder uses fallback strategies that work with Zenith's architecture.
How Targeting Works
The selector engine tries 6 strategies in priority order until one succeeds:
1. testid (Best - Recommended)
- Looks for
data-testidattribute - Example:
data-testid="email-input" - Most reliable - always add testId props when possible
- Also checks
data-testas fallback - Supports prefix matching for dynamic IDs
2. label (Primary Fallback)
- Looks for visible text labels associated with the element
- Works with Zenith's
<Text>component (not just HTML<label>tags) - Example:
<Text>Email</Text>next to<TextInput>→ targets by "Email" - This is the primary successful strategy for Zenith components without testIds
3. formField (Form-Aware)
- Uses form structure +
nameattribute + label text - Example:
form 'Personal Details' → field 'email' - Works with Zenith form-like containers (not just
<form>tags) - Secondary fallback when labels aren't clear
4. contextual (Nearby Text)
- Uses nearby visible text as context
- Example: "near text 'Contact Information'"
- Works well with Zenith (lots of Text components)
5. nthMatch (Position-Based)
- Uses element position: "3rd button on the page"
- Fragile - breaks if layout changes
- Used as last resort only
6. css (Raw CSS)
- Raw CSS selector
- Not recommended with Zenith - Theme-UI generates unstable class hashes
- Absolute last resort
Best Practices for Zenith Components
✅ Recommended: Always Add TestIds
// Buttons
<Button testId="submit-btn">Submit</Button>
// Form inputs
<TextInput testId="email-input" labelText="Email" />
<SelectInput testId="state-select" labelText="State" options={...} />
// Accordions
<Accordion testId="person-1-accordion" title="Person 1">
// Any interactive element
<Link testId="next-page-link" href="/next">✅ Good: Use Clear, Unique Labels
If testIds aren't possible:
- Use clear, unique label text
- Keep form structure semantic
- Avoid duplicate labels in the same context
- The recorder will automatically use the label strategy
// These will work via label detection:
<TextInput labelText="Email Address" /> // ✅ Clear label
<TextInput labelText="Company Name" /> // ✅ Unique
// Avoid:
<TextInput labelText="Name" /> // ❌ Ambiguous
<TextInput labelText="Name" /> // ❌ Duplicate⚠️ Avoid: Position-Based Selectors
Don't rely on position - these break easily:
// This generates fragile "3rd button" selector
<div>
<Button>Cancel</Button>
<Button>Back</Button>
<Button>Submit</Button> {/* Position-based, fragile! */}
</div>
// Better: add testId
<Button testId="submit-btn">Submit</Button>Recording Overlay (Inspector Tool)
During recording, use the Recording Overlay to see generated selectors in real-time:
- Start recording (Ctrl+Shift+D → Start Recording)
- Toggle overlay (Ctrl+Shift+O)
- Click "Inspect" button
- Hover over any element (blue highlight appears)
- Click element to see all generated selectors
- Review color-coded strategies:
- 🟢 Green = testId (most reliable)
- 🟡 Yellow = label/formField (semantic, stable)
- 🔴 Red = nthMatch/css (fragile)
- Copy any selector to clipboard
Troubleshooting Selectors
"Element not found" during playback:
- Check if element has testId → add one
- Check if label text is unique → make it more specific
- Use Recording Overlay to see which strategy was used
- Avoid relying on position-based selectors
"Wrong element selected":
- Duplicate labels or ambiguous text
- Add testIds to disambiguate
- Use more specific label text
"Selector breaks after code changes":
- Position-based selector was used (nthMatch)
- Add testId to the element
- Or ensure label text stays consistent
