Skip to content

Feat/player app and live view#23

Merged
holiber merged 36 commits intomainfrom
feat/player-app-and-live-view
Feb 24, 2026
Merged

Feat/player app and live view#23
holiber merged 36 commits intomainfrom
feat/player-app-and-live-view

Conversation

@holiber
Copy link
Copy Markdown
Owner

@holiber holiber commented Feb 23, 2026

Summary

Test plan

  • pnpm -r build
  • pnpm b2v run --scenario basic-ui --mode human --record screencast --headed

Electron main process now probes if the preferred CDP port is
available before starting. If busy (zombie process, etc.), it
auto-finds a free port in the 9335-9399 range. Also added timeouts
to lsof/kill calls to prevent hangs on zombie processes.
- Replaced hardcoded alice/bob/narrator palette with rotating
  8-color auto-palette (coral, sky, lime, violet, amber, teal, rose, indigo)
- First actor gets classic white cursor, subsequent actors get colored
- Cursors start hidden, appear on first moveCursor call
- Non-default cursors show a colored label with the actor name
- Each actor gets cursorId from pane title (e.g. 'boss', 'worker')
- Cursor overlay injected once for all actors (lazy creation)
- Cursors fully cleaned up on scenario switch
Stop button:
- Added _aborted flag to Executor, checked between steps in runTo()
- Wrapped runAll loop in try/catch to handle abort cleanly
- Cancel now sends 'cancelled' to client on abort

Cursor improvements:
- Removed cursor labels (colors-only differentiation)
- First cursor appearance teleports without transition (no corner slide)
- Cursors start hidden until actor's first interaction

UI:
- Cache button text: 'Clear cache (2.5 MB)' format
- CDP port auto-fallback when default 9334 is busy
jabterm v0.1.4 adds:
- accessibilitySupport prop (populates .xterm-rows text natively)
- Resize deduplication (skips WS resize when cols/rows unchanged)

Cleanup:
- Removed data-b2v-output polling hack from TerminalPane
- Removed jabtermRef (no longer need readAll() capture buffer)
- Removed data-b2v-output fallback from waitForText/waitForPrompt
- Added accessibilitySupport="on" to JabTerm component
TUI timeout fix:
- xterm v6 puts accessible text in .xterm-accessibility-tree, not .xterm-rows
- Updated waitForText, waitForPrompt, isBusy, and grid prompt wait
  to check .xterm-accessibility-tree first, .xterm-rows as fallback

Cursor positioning:
- Added _cursorInitialized flag to Actor class
- First cursor movement teleports to target instead of windMouse from (0,0)
- Applied to both moveCursorTo() and private moveTo() (used by click/hover/type)
- Prevents cursors appearing at edge of player window
Chat scenario:
- Split intro into 'Introduction' (narration) + 'Meet the actors' (circleAround)
- Alice and Bob type concurrently via Promise.all
- Added Web Audio API notification beep when Bob sees Alice's message
- Removed emoji chars from typed messages (caused rendering artifacts)

E2E tests:
- Added XTERM_TEXT_SELECTOR constant for xterm v6 compatibility
- Updated all .xterm-rows selectors to check .xterm-accessibility-tree first

CSS buttons scenario: works with xterm-accessibility-tree fix in terminal-actor
All actors share one page.keyboard, so Promise.all interleaves chars.
Fix: Bob types a slow-output bash script first (sequential keyboard),
then Alice types while Bob's script runs in the background producing
progressive 'compiling module N...' output. Visually concurrent
without keyboard conflict.
Command panes (type=terminal with cmd) show their own TUI,
not a shell prompt. Previously the grid creation wait checked
for $/#/% chars in xterm content — this would timeout for mc/htop.

Now: command panes wait for any non-empty xterm content,
shell panes (no cmd) still wait for prompt characters.
User reverted the split between command pane (mc, htop) and shell
pane grid creation wait. All terminal panes now wait for prompt
chars ($/#/%) regardless of command type.

All E2E tests verified passing:
- electron: all-in-one ✓ (17.8s)
- electron: collab ✓ (18.3s)
- electron: tui-terminals ✓ (1.4s)
New package: packages/browser2video-test/
- test.extend() with auto step-wrapping: each test(title) → beginStep(title) / endStep()
- Fixtures: session (worker-scoped), actor, grid
- Helpers: setActor(), setGrid(), getSession()

Session API additions:
- beginStep(caption) — emit stepStart
- endStep() — emit stepEnd with breathing pause

Sample test: tests/scenarios/notes-demo.b2v.test.ts
getSession() is now async and lazily creates the session on
first call. This makes it safe to use from test.beforeAll
(which doesn't have access to Playwright fixtures).

Worker auto-fixture handles session cleanup.
Fixed server null check in sample test.
New test visits alexn.pro portfolio, finds Three Charts project,
navigates to the demo page (holiber.github.io/three-charts/demo/),
and interacts with the chart: switches line/bars view, changes
timeframes (5m, 30m, 1h), and toggles trend overlays.

Deleted: github-mobile.scenario.ts, github-mobile.test.ts
Added: external-website.scenario.ts, external-website.test.ts
collab and all-in-one tests require Electron CDP for
createTerminalGrid(). Marked as test.skip in headless
Playwright runner. They pass via apps/player E2E tests.

Full test results:
- 7 passed, 4 skipped, 0 failed (headless)
- 3 passed (Electron E2E: all-in-one, collab, tui-terminals)
The cursor overlay is only visible after its first moveCursorTo()
call. scroll() alone doesn't trigger cursor visibility. Added
explicit moveCursorTo() before scrolls and after page transitions
so the cursor is visible throughout the scenario.
createGrid was waiting for shell prompt chars ($/#/%) in ALL
terminal panes, including mc and htop which are TUI apps that
never show prompts. Now: command panes (pc.cmd set) wait for
any non-empty xterm content, shell panes still wait for prompts.

TUI E2E test now passes in 18.8s instead of hanging.
30s was too tight after running heavy scenarios (collab, tui).
All 7 scenario tests pass in Electron E2E.
startTerminalWsServer now accepts optional cwd param.
Session passes this.artifactDir so terminal shells start
in the scenario's output directory instead of process.cwd().
…tion cursor

The cursor overlay was only injected via page.evaluate() which gets
wiped on navigation. The framenavigated listener tried to re-inject
but raced with page load (catch swallowed errors).

Now CURSOR_OVERLAY_SCRIPT is registered as addInitScript so it
persists across all navigations automatically. framenavigated
listener kept as belt-and-suspenders fallback.
CURSOR_OVERLAY_SCRIPT now guards all document.body/head operations:
- getCursorEl() returns null if body not ready
- Ripple container lazy-created via ensureRippleContainer()
- Animation style deferred to DOMContentLoaded if head not ready
- moveCursor/clickEffect gracefully no-op when body unavailable

Eliminates 'Cannot read properties of null (appendChild)' errors
when script runs as addInitScript before DOM is ready.
Added Session.abort() that immediately closes all browser pages
and contexts, interrupting any running Playwright operations.
Unlike finish(), it skips video composition entirely.

Added Executor.abort() that calls session.abort() and resets state.
Server cancel handler now uses executor.abort() instead of
executor.reset() which previously tried to gracefully finish.

The running step's Playwright operation (goto, waitForSelector, etc.)
throws when the page is force-closed, which propagates up through
runTo() → runAll catch handler → sends 'cancelled' to UI.

Added E2E test: loads basic-ui, clicks Play All, waits for Stop
button, clicks Stop, verifies Play All button returns.
…test

- New InjectedActor class (packages/browser2video/injected-actor.ts)
  - Injects visible cursor overlay + typing into any page via page.evaluate()
  - Reuses CURSOR_OVERLAY_SCRIPT, WindMouse cursor paths, real mouse.click()
  - API: click, type, pressKey, waitFor, scroll, goto, breathe

- Player self-test E2E (apps/player/tests/player-self-test.e2e.test.ts)
  - InjectedActor drives the player's own studio UI
  - Tests: cursor injection, + placeholder, Browser popup, URL dialog, iframe

- Human-mode demo runner (apps/player/tests/human-mode-demo.ts)
  - Captures screenshots at each step for visual verification

- Export InjectedActor from browser2video package
- New tests/scenarios/player-self-test.scenario.ts
  - Setup spawns inner Electron player on port 9581
  - session.openPage() connects to inner player's web UI
  - InjectedActor drives the studio UI with visible cursor
  - 5 steps: verify ready, open picker, click +, Browser, URL confirm

- Fixed electron binary resolution via createRequire from player pkg
- All 5 steps pass in ~6.6s
- Phase 1: Split screen horizontally, add terminal
- Phase 2: Launch demo vite server in terminal, open todo app
- Phase 3: Todo CRUD — add 8 todos, reorder, scroll, delete
- Phase 4: Close terminal, verify todo app stops working
- Phase 5: Load basic-ui scenario, play/stop, step through slides
- Phase 6: Assert no unexpected console errors

Also adds data-testid attributes to controls (ctrl-play-all, ctrl-stop,
ctrl-next, ctrl-prev, ctrl-reset), scenario-picker (picker-select,
picker-switch), and step-graph (step-card-{i}).
Three fixes for running the self-test inside the Player's Executor:

1. Viewport tracking: add page.setViewportSize({width:1280,height:720})
   after setup. Electron's WebContentsView starts at 0x0 and is later
   resized via IPC, but Playwright's CDP-side viewport tracking keeps
   the initial 0x0 value, causing all elements to be reported as
   'outside of the viewport'.

2. Electron binary resolution: require('electron') returns the module
   object (not the binary path) inside Electron's runtime. Fixed by
   reading electron/path.txt or falling back to process.execPath.

3. Port conflict: changed inner player ports from 9581/9385 to
   9591/9395 to avoid collision when the root player is running.
When the player-self-test scenario spawns an inner player, set
B2V_EMBEDDED=1 so the inner player's Electron window is hidden.
The inner player's UI is rendered inside the root player's scenario
WebContentsView, so showing a second window was incorrect.
- Inner player window: off-screen (-10000), 1x1 size, show:false,
  skipTaskbar:true via B2V_EMBEDDED=1 env var.

- New step 'Inner player shuts down cleanly': sends SIGTERM, waits for
  exit (10s timeout), verifies exit code, probes port 9591 is freed.

- addCleanup is now a safety-net that only kills if process is still alive.

- 16/16 steps pass both standalone and inside the Player app.
  All steps produce screenshots for the step graph.
When running in Electron mode (CDP endpoint), Playwright's recordVideo is
unavailable because the session connects to existing pages rather than
creating new browser contexts. Added CdpScreencastRecorder class that:

1. Starts Page.startScreencast via CDP to capture JPEG frames
2. Pipes frames to ffmpeg (image2pipe -vcodec mjpeg) → raw webm
3. Stops screencast and waits for ffmpeg to finish in finish()

The raw webm is then processed by the existing composeVideos pipeline.

Also fixed libx264 'height not divisible by 2' error by adding
pad=ceil(iw/2)*2:ceil(ih/2)*2 to reencodeToMp4 and the fallback path.

Verified: 5203 frames captured, 1.4MB MP4 output, 16/16 steps pass.
Single-file runner that:
1. Launches the Player Electron app
2. Connects via WS and loads the player-self-test scenario
3. Runs all 16 steps with progress reporting
4. Reports pass/fail, video path, and duration
5. Cleanly shuts down the Player

Run: node --experimental-strip-types --no-warnings apps/player/tests/run-self-test.ts

Removes the old test-player-ws.ts helper.
Electron embedded window hiding:
- type: 'toolbar', focusable: false, hasShadow: false
- mainWindow.hide() + minimize() after creation
- Re-hide after each loadURL() call (macOS can show windows)

Self-test: new 'Inner player window is hidden' step uses osascript
to enumerate all Electron windows and flag any large windows near
origin that could overlap the parent player.

17/17 steps pass, 123.5s, video produced.
- Disable electronView when B2V_EMBEDDED=1 to fix black background
- Enable CDP screencasting in executor for embedded mode
- Add __b2v_setCursorColor() for custom per-actor cursor colors
- Add cursorColor option to InjectedActor (coral for self-test tester)
- Add port cleanup before spawning inner player (kills stale processes)
- Reduce post-waitForPort delay from 3s to 1s
- Add 'Verify scenario screenshots are not blank' assertion step
- Remove broken React CursorOverlay from screenshot mode
- Remove fixed 1s post-waitForPort delay
- Switch from networkidle to domcontentloaded (faster)
- Reduce studio-react retry attempts from 5 to 3 (with 15s timeout)
- Add timing instrumentation to setup phases
- Total time: 2.6m → 2.4m
- Add cursorColor to SessionOptionsSchema, Session, and Actor
- Session reads from opts or B2V_CURSOR_COLOR env (format: fill,stroke)
- Actor.injectCursor() applies color via __b2v_setCursorColor
- Session adds cursor color init script for navigation persistence
- Self-test: pink tester cursor, orange scenario Actor cursor
- Both cursors visually distinct during self-test playback
- Increase cursor from 20x20 to 32x32px for visibility in screencast JPEGs
- Add drop shadow filter for contrast against any background
- Tester cursor: hot pink (#ff69b4) — unmistakably distinct
- Scenario cursor: orange (#fb923c) via B2V_CURSOR_COLOR env
Root cause: addInitScript calls were placed AFTER page.goto() in
session.ts openPage(). Playwright addInitScript only fires on
*subsequent* navigations — so the cursor overlay script ran (via
framenavigated fallback), but the cursor color init script never
executed on the initial page load.

Diagnostic confirmed: inner player showed cursors=0 colors={} before
fix, and cursors=1 colors={default:{fill:#fb923c}} after fix.

Also removed temporary diagnostic code from executor.ts and
player-self-test.scenario.ts.
@holiber holiber merged commit ccc4b9e into main Feb 24, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant