You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Status: implemented as a Vitest suite under test/ - npm test runs the pure-logic, runtime, context, proxy, export, demo-integrity, MCP-connector and card-hierarchy areas; deep DOM-render/pointer interaction of the designer in jsdom is intentionally limited to registration + crash-free mounting (see "Not automatically testable").
Note (area 17d):MCPSettingsDialog is an internal Preact component and not exported. Its localStorage contract and connector.configure() side-effect isolation are tested in mcp-settings-dialog.test.ts; the component rendering itself requires manual/visual testing until the component is exported.
This document lists everything in BrowserCard that can be tested automatically, organized by area, plus the tooling to run those tests and a short list of aspects that remain manual/visual only.
Goals & scope
verify the pure logic (geometry, validators, runtime, persistence, undo/redo, clipboard, exports) deterministically and without a browser where possible
verify the rendering and editing UI in a simulated DOM (jsdom)
guard the data contracts: a deck that round-trips through JSON must stay valid; the bundled demo decks must always be valid
catch regressions in the small scripting API that scripts and behaviors depend on
Testability note. Several pure helpers are currently module-private (resolveGeometry, computeOffsets, ScriptInstance, makeWidgetProxy/makeCardProxy/makeDeckProxy, buildContext, resolveBehaviorUrl, escapedForHTML, adjustIdCounterFor, ValueIsDeck/ValueIsCardJSON/ValueIsWidgetJSON). To unit-test them directly, either export them (many ValueIs*/acceptable* helpers already are exported) or add a small internal test entry point (e.g. src/internals.ts) that re-exports them for the test build only.
1. Geometry & anchors (pure)
Test
Expectation
resolveGeometry for each horizontal anchor (left-width, left-right, width-right)
correct left/width for given offsets + container width
resolveGeometry for each vertical anchor (top-height, top-bottom, height-bottom)
correct top/height
computeOffsets is the exact inverse of resolveGeometry
round-trip Offsets → geometry → Offsets is identity for all 9 anchor combinations
anchor switch keeps pixel geometry
changing anchors then computeOffsets yields the same on-screen rect
snap-to-grid math
move/resize values snap to multiples of GridWidth/GridHeight; opposite edge stays fixed on resize; min size respected
changeGeometryTo
partial args (null/undefined) keep current computed values; result offsets re-resolve to the requested pixels
a script registers handlers; running an empty script is a no-op
triggered / trigger
invokes the registered handler with arguments and returns its result; undefined when none exists; bubbles up to a linked parent (linkToParent); a handler's exception propagates to the caller
fireLocal
invokes a local handler only (no bubbling) and swallows its errors - used for lifecycle events (ready)
renderResult
returns the render handler's result synchronously; null if absent
handler replacement
a second on('msg', …) replaces the first
error isolation
a throwing script is caught and logged, does not break the instance (note: triggered deliberately re-throws handler errors)
syntax-error guard
a widget with a syntactically invalid script still runs its intrinsic behavior
runaway-loop circuit breaker
when scripts are (re-)run far too often in a short window (a render/mount loop), run suspends execution and warns once instead of freezing the tab
timer auto-cleanup
after/every timers registered in a script are cancelled on teardown (assert with fake timers)
teardown order
obsolete fires; handlers cleared synchronously so a re-run can re-register
9. Script context - buildContext (pure + fakes)
Test
Expectation
Card(nameOrIndex)
returns a ref for valid name/0-based index, else null
CardCount()
reflects the total number of cards
go(target)
resolves card ref / name / 0-based index / nextCard etc. to the right BC_NavTarget
Widget(nameOrIndex)
finds a widget proxy on the current card by name or 0-based index
my.Card.Index (card proxy)
reads the 0-based deck position; assigning it reorders the card
trigger / triggered bindings
buildScriptParams injects both so scripts can fire events on the current visual (and they bubble up to a linked parent)
await answer(...) / await ask(...)
resolve with the chosen button / entered text / null on cancel (drive the dialog callback)
print/println/clearConsole
mutate the deck console buffer
10. Reactive proxies (jsdom)
Test
Expectation
makeWidgetProxy get/set
property write calls forceUpdate; reads reflect the descriptor
live geometry getters
me.x/y/Width/Height computed from current container size
me.changeGeometryTo(...)
writes offsets + triggers update
me.own
reads/writes a private object without triggering re-render or persistence
me.Configuration
exposes the widget's config object
me.Deck / me.Card / me.Card.WidgetList
resolve to the right proxies / ordered widget list
me.trigger / me.triggered
every proxy exposes both, delegating to its $Script instance; null-safe before a script is attached
render-loop protection
a reactive write inside an on('render')/on('update') handler writes the value but does not schedule another render (no infinite loop); writes outside a render pass re-render normally; suppression does not leak across passes
proxy-assignment guard
assigning a Deck/Card/Widget proxy to a property (e.g. the typo my.Value = my) is rejected with a warning and leaves the descriptor JSON-serializable (no circular reference)
11. Event bubbling
Test
Expectation
widget triggered('click') with no local handler
bubbles widget → card → deck and runs the first matching handler only (not all three)
local handler precedence
a handler on the widget itself wins; the card/deck handler does not also fire
Widget('X').triggered(...)
fires on the target widget and bubbles up from there; resolves with the handler's result (undefined if none)
lifecycle ready (fireLocal)
fires locally on each visual and does not bubble
12. Persistence (fake-indexeddb)
Test
Expectation
save
#saveStack writes a JSON-clean copy under bc-deck:<name> in store browser-card/decks
load precedence
a persisted copy supersedes the src attribute / demo deck
list / open / create / rename / delete
the deck-management ops behave correctly and keep ids consistent
migration
entries from the old BrowserCard/Stacks store (bc-stack: keys) are moved once to the new store/prefix
reset
deleting the persisted copy restores the original (pristineDemoDeckJSON)
13. Undo/redo (pure logic)
Test
Expectation
snapshot capture
an edit pushes the prior state; redo stack clears on a new edit
coalescing
edits with the same key within the window collapse into one step; different keys / gestures are separate
restore
undo/redo writes back into the same deck object (identity preserved for persistence/proxies)
depth limit
history is capped at 100 entries
14. Clipboard (stubbed navigator.clipboard)
Test
Expectation
copy widget(s) / card
writes JSON under the right MIME (web application/x-browsercard-widget / …-card) + text/plain fallback; a widget selection is serialized as a JSON array
paste detection
a card vs. widget(s) payload is recognized via custom MIME, else inferred from plain-text JSON (single widget object or widget array)
paste effects
pasted card inserted after current; pasted widget(s) get fresh ids, unique names, top z-indices (relative arrangement preserved) and become the new selection
Shift/Cmd-click toggles widgets; rubber-band rectangle selects every overlapped widget; group move/resize applies to all; group delete/nudge; group bounding box + per-widget member outlines
properties panel
editing a field updates the descriptor; anchor switch preserves geometry; "Configuration (JSON)" edits me.Configuration; with several widgets selected only the group actions are shown
screenshot PNG correctness - the 📷 export depends on html-to-image + real layout; only that it triggers a download is automatable
real IndexedDB across reloads / multiple tabs - persistence logic is covered via fake-indexeddb, but true browser durability is manual
system clipboard across apps/tabs - logic is covered with a stub; real cross-tab paste is manual
touch/pointer "feel" - touch-action, capture, drag smoothness on real devices
external behavior loading over the network - URL resolution is unit-tested; actual import() from GitHub Pages/jsDelivr is manual or an integration test
embedded-module URL availability - that BC_ModuleURL actually serves the module is a deployment check
live WebSocket roundtrip against a real broker - the protocol is unit-tested with a fake WebSocket; an end-to-end test against a running BrowserCard-AIBroker instance is manual or a separate integration suite
live_screenshot with real html2canvas - the error path (no html2canvas) is unit-testable; the actual PNG result requires a real browser with the library loaded
Run npm test in CI (e.g. a GitHub Actions workflow on push/PR) and gate merges on green plus a coverage threshold for the pure-logic modules (geometry, validators, runtime, persistence, undo, clipboard).