In jsdom, Blob does not implement stream(), but Node.js Response
internally calls blob.stream() when constructing with a Blob body.
Replace the new Response(blob) pattern with a plain object mock that
exposes .blob() directly, matching the download path used in production.
Extract and unit-test the 8 pure fill helpers and 2 derived functions
from ExternalConnectModal so they are independently verifiable.
Exported: fillPythonSnippet, fillCurlSnippet, fillChannelSnippet,
fillUniversalMcpSnippet, fillHermesSnippet, fillCodexSnippet,
fillOpenClawSnippet, buildFilledSnippets, buildTabOrder.
Issue: #709 follow-up (pure-helper extraction)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Issue 1 (fixed): "successful upload" test passed 1 file to uploadChatFiles
but expected result.length===2 from the mock. Now passes 2 files so the
assertion validates the complete response round-trip.
Issue 2 (fixed): fetchMock.mockRestore() called inline at end of each test
without try/finally. Now uses beforeEach/afterEach pattern consistent with
downloadChatFile describe block and consoleErrorSpy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New test cases in uploads.test.ts covering the two untested exports:
- uploadChatFiles empty-file guard (returns [] without calling fetch)
- uploadChatFiles successful upload returns ChatAttachment[]
- uploadChatFiles throws on non-ok response
- downloadChatFile opens external HTTPS URLs via window.open (no fetch)
- downloadChatFile fetches and triggers blob download for platform attachments
- downloadChatFile throws on non-ok download response
Closes gap from canvas test coverage audit (2026-05-13).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Object.keys({ attachments: undefined }) still includes "attachments" as a
key, breaking the "returns a plain object with expected keys" test. Fix by
conditionally spreading attachments only when non-empty, and Object.freeze
the return value to preserve the existing immutability assertion.
Fixes 2 test cases in createMessage.test.ts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two pre-existing canvas test failures (45 total in full suite, 2 visible
at end of truncated output):
1. canvas/src/components/tabs/FilesTab/tree.ts
getIcon() extracted the extension as-is (".JSON") but FILE_ICONS keys
are lowercase (".json"). Fix: lowercase the extension before lookup.
Fixes src/components/__tests__/getIcon.test.ts > is case-insensitive
for extension lookup.
2. canvas/src/store/__tests__/canvas-topology-pure.test.ts
sortParentsBeforeChildren returns nodes in input order. The test
expectation ["root","orphan"] assumed non-existent-parent orphans
always trail roots, but the algorithm preserves input sequence.
Corrected the test expectation to match actual algorithm behavior.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug: `extractAgentText({ parts: [] })` fell through all three source
checks (parts, artifacts, status.message) and returned the error
string `"(Could not extract response text)"` instead of `""`. Empty tasks
should render as blank bubbles, not error indicators.
Fix: check `typeof task === "string"` first, then walk all three
sources. Return `""` when every source is exhausted rather than
falling through to the catch/error string.
Added 11 dedicated tests for `extractAgentText` covering:
- Normal extraction from parts, artifacts, status.message
- Precedence (parts > artifacts > status.message)
- String fallback
- Empty parts/array/undefined fields returning ""
- Null/undefined status.message toleration
Also merged all fixes from fix/test-declarations (37 previously
failing vitest cases resolved).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Key fixes:
- MissingKeysModal: add missing aria-hidden="true" to AllKeysModal
backdrop (ProviderPickerModal had it; AllKeysModal was missing it)
- MissingKeysModal.a11y: use class-based backdrop selector in jsdom
- ContextMenu: fix Tab key test to fire on menu element; offline nodes
use hasAttribute("disabled") instead of queryByRole().toBeNull()
- ConversationTraceModal: correct part-text expectation (joins all parts)
- Legend: fix palette-offset test to use document.querySelector on fixed
panel div, not .closest("div") which found inner text element
- OnboardingWizard: use RTL rerender for auto-advance (second render()
created a new component instance without shared state)
- PurchaseSuccessModal: mock history.replaceState to prevent SecurityError
in jsdom; replace setTimeout-promises with advanceTimersByTime
- Spinner: use getAttribute("class") instead of .className (SVGAnimatedString
in jsdom)
- TestConnectionButton: move Spinner outside <button> to fix accessible
name conflict; use hasAttribute("disabled"); fix error text assertion
- Tooltip: focus first focusable child inside trigger ref, not wrapper div
- TestConnectionButton component: restructure JSX — Spinner as sibling
- createMessage: conditional attachments spread (only include when non-empty)
- BundleDropZone: fix DragEvent in jsdom with createDragOverEvent helper
All 2257 canvas tests pass; npm run build succeeds.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two pre-existing canvas test failures:
1. canvas/src/components/tabs/FilesTab/tree.ts:getIcon()
FILE_ICONS keys are lowercase (".json") but the extension was looked
up as-is (".JSON"). Result: FILE_ICONS[".JSON"] → undefined → fallback
"📄" instead of "{}".
Fix: lowercase the extension before FILE_ICONS lookup. Also added ?.
null-coalescing on split().pop() to handle filenames without extension.
2. canvas/src/store/__tests__/canvas-topology-pure.test.ts
sortParentsBeforeChildren test expectation was wrong: it assumed orphan
would come after root, but when parentId references a missing node
the orphan keeps its input order (orphan, then root). Updated the
expectation and corrected the comment to match the actual behaviour.
Closes#697.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds isolated tests for DropTargetBadge — the floating drag-target affordance.
Render-condition coverage:
- Renders nothing when dragOverNodeId is null
- Renders nothing when dragOverNodeId node has no store match
- Renders nothing when getInternalNode returns undefined
- Renders badge with correct name when all inputs are valid
- Badge text follows 'Drop into: <name>' format
- Badge contains exact target name from store
- Renders nothing when target name is null (empty data.name)
Ghost visibility (slot rect inside parent bounds) is deferred to
integration tests that render the full canvas — flowToScreenPosition
coordinate arithmetic is better covered there.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds isolated tests for the pure tree-traversal core of
useOrgDeployState. The buildDeployMap function handles:
- Root / leaf identification via parent-chain walk
- isDeployingRoot: true when any descendant is "provisioning"
- isActivelyProvisioning: true only for the node itself
- isLockedChild: true for non-root nodes in a deploying tree
- isLockedChild: also true for nodes in deletingIds (cross-cutting)
- descendantProvisioningCount: non-zero only on root nodes
- O(n) single-pass walk verified on 50-node tree
Also exports buildDeployMap for direct unit testing (was internal).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
React error #185 (Maximum update depth exceeded) on mobile chat tab.
Root cause: useCanvasStore((s) => s.agentMessages[agentId] ?? []) used
a `?? []` fallback in the selector. Zustand uses Object.is for selector
equality. When agentMessages[agentId] is undefined (initial state), the
fallback creates a NEW [] reference on every store update. Zustand sees
this as a state change and re-renders the component. The component reads
from the store again, gets another new [] reference, and the cycle
repeats until React hits the depth cap.
Fix: remove `?? []` from the selector (returns undefined when no messages)
and move the fallback to the useState initializer:
storedMessages = useCanvasStore(selector) // returns undefined | T[]
[messages] = useState(() => (storedMessages ?? []).map(...))
The useState initializer only runs once on mount, so the `?? []`
there is safe — it creates the initial state once, then messages are
managed via setMessages.
Fixes issue #651.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the Claude Design handoff (Molecules AI Mobile.html) as a
viewport-gated React tree under canvas/src/components/mobile/. < 640px
renders the new shell instead of the desktop ReactFlow canvas.
Six screens, all bound to live store data:
- Home (agent list + filter chips + spawn FAB)
- Canvas (mini-graph with pinch-to-zoom + pan + reset)
- Detail (status pills, tabs: Overview / Activity / Config / Memory;
Activity hits /workspaces/:id/activity)
- Chat (textarea composer, IME-safe Enter, sendInFlightRef guard;
bootstraps from agentMessages so the prior thread shows on entry)
- Comms (live A2A feed via /workspaces/:id/activity + ACTIVITY_LOGGED)
- Spawn (bottom sheet; fetches /templates so users pick what's actually
installed on their platform)
Plus a Me tab for mobile theme/accent/density.
Design system (palette.ts + primitives.tsx) ports tokens 1:1 from the
handoff: cream + dark palettes, T1-T4 tier chips, status dots with
halo, JetBrains Mono for IDs/timestamps. Inter + JetBrains Mono are
self-hosted via next/font/google so CSP `font-src 'self'` is honoured.
URL routing: routes sync to ?m=<route>&a=<id>; popstate restores route;
deep links seed initial state. /?m=detail without ?a collapses to home.
Accent override flows through React context (MobileAccentProvider) —
not by mutating the static MOL_LIGHT/MOL_DARK singletons.
SSR flash: isMobile is tri-state; loading spinner stays up until
matchMedia resolves so mobile devices never paint the desktop tree.
Desktop responsiveness fixes (separate but ride along):
- Toolbar: full-width with overflow-x-auto on mobile, logo text + count
hidden < sm, divider/border collapse to sm: only.
- SidePanel: full-screen on mobile via matchMedia, resize handle hidden.
- Canvas: MiniMap hidden < sm (was overlapping the New Workspace FAB).
Tests (51 total, 33 new):
- palette.test.ts (12) - normalizeStatus, tierCode, light/dark parity
- components.test.ts (10) - toMobileAgent field mapping + classifyForFilter
- MobileApp.test.tsx (12) - route stack, deep links, popstate, tab bar
hidden on chat, spawn overlay
- SidePanel.tabs.test.tsx (18) - regression-clean
Verified: tsc --noEmit clean across mobile/, page.tsx, layout.tsx.
Not yet verified: live phone browser (needs CP backend hydrated).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tests canvas/src/lib/hydrate.ts: hydrateCanvas() with exponential backoff retry.
Cases:
1. Success on first attempt → { error: null }
2. Viewport fetch failure is non-fatal → store still hydrates
3. Success after 1 retry → onRetrying(1) called once, result { error: null }
4. onRetrying called correctly on each failed attempt
5. All attempts fail → error message after MAX_RETRIES
6. onRetrying called MAX_RETRIES-1 times before final exhausted attempt
7. Total elapsed time ≈ sum of exponential delays (1s + 2s = 3s)
Each attempt makes 2 parallel api.get calls (workspaces + viewport); mocks
set up per parallel-call to avoid Promise.all consuming wrong mock slots.
Issue: #701
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two bugs in the test suite for SearchDialog.tsx:
1. Zustand-compatible mock: the old vi.fn-only mock updated
mockStoreState.searchOpen directly without notifying Zustand's
useSyncExternalStore subscriber, so the Cmd+K test opened the
dialog but the component never re-rendered (body stayed <div />).
Fix: add subscribe() + getState() to the mock so React flushes
the re-render when setSearchOpen fires. Also add act() wrapper
around the keydown event for additional safety.
2. Stale React state: fireEvent.change did not reliably flush the
onChange → query state update before ArrowDown fired, causing the
component to read stale filtered/nodes state. Fix: manually set
input.value, fire onChange inside act(), then call rerender() to
force the component to see the new query before keyboard events.
Affected tests:
- "clears the query when Cmd+K opens the dialog" (was: body=<div />)
- "Enter selects the highlighted workspace" (was: selected n2 not n1)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fix test isolation in ApprovalBanner: replace vi.spyOn per-test with
module-level vi.hoisted + vi.mock so the mock is stable across tests.
Add EmptyState.test.tsx covering:
- Loading/empty/template-fetched states
- Template grid rendering (name, tier badge, model label)
- Deploy-on-click
- Create blank workspace (POST, loading, error, retry, canvas-store wiring)
- Rendering (welcome, tips, OrgTemplatesSection)
Fix vi.hoisted pattern for multiple vi.mock calls: use a single
vi.hoisted() returning all mock fns as m.<field>, then reference m.<field>
inside each vi.mock factory. This avoids "Cannot access before
initialization" errors that arise when vi.hoisted factories are called
before module-level vi.mock hoisting completes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add two test files that supersede the failing version in PR #611:
FilesTab.test.tsx (25 cases):
- NotAvailablePanel: heading, mono runtime, Chat tab hint, SVG aria-hidden,
layout classes
- FilesToolbar: directory selector, all four options, setRoot on change,
file count display, New/Upload/Clear conditional on /configs vs
/workspace/home/plugins, aria-labels on all buttons, click callbacks
BudgetSection.test.tsx (14 cases, new path tabs/__tests__/):
- Loading indicator, fetch errors, 402 as exceeded banner
- Used/limit stats, unlimited display, remaining credits
- Progress bar cap at 100%, bar hidden for unlimited
- Exceeded banner on 402, clears after save
- Save errors, input update after save, null for cleared input
- Saving state while patch in flight
- isApiError402 regression coverage
Fixes#608: removes the overly-prescriptive focus-visible:ring-2 test
(PR #611 added a test for a CSS class FilesToolbar does not implement).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
NotAvailablePanel (12 cases):
- Heading, description text, runtime name display, SVG icon with
aria-hidden, mono font for runtime, Chat tab guidance
- Full-height flex container class names
- h3 heading role, SVG aria-hidden, descriptive paragraph
- Short and complex runtime names
FilesToolbar (17 cases):
- Directory select with aria-label, file count display
- Export and Refresh buttons always visible
- New/Upload/Clear shown only when root="/configs", hidden for
/workspace, /home, /plugins
- setRoot called on directory change
- onNewFile, onDownloadAll, onClearAll, onRefresh called on click
- Hidden file input present with aria-label when on /configs
- All buttons have accessible names
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Scope:
- form-inputs.test.tsx (new): 35 cases covering TextInput, NumberInput,
Toggle, TagList, Section. Section coverage includes aria-expanded,
aria-controls, content id, and aria-hidden indicator span.
- form-inputs.tsx (Section): add aria-expanded + aria-controls to the
toggle button and a matching id on the collapsible content region;
aria-hidden on the ▾/▸ indicator so screen readers skip it.
Test isolation fixes (afterEach(cleanup) missing → DOM element accumulation):
- ApprovalBanner.test.tsx
- StatusDot.test.tsx — also adds { hidden: true } to getByRole("img")
since @testing-library/dom v10+ excludes
aria-hidden elements from accessible queries
- ValidationHint.test.tsx — also fixes checkmark test that assumed
✓ + "Valid format" were one text node
- TopBar.test.tsx
- RevealToggle.test.tsx
- StatusBadge.test.tsx
Tooltip.test.tsx:
- Adds vi.useFakeTimers() beforeEach / vi.useRealTimers() afterEach
(tests called vi.advanceTimersByTime without fake timers)
- Fixes aria-describedby test to check the wrapper div, not the button
KeyValueField.tsx:
- Adds role="textbox" to the <input> element so getByRole("textbox")
finds it in @testing-library/dom v10 (password inputs lack implicit
textbox role in jsdom).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two bugs in yaml-utils.ts toYaml():
1. tools: [] was only emitted when config.tools.length > 0,
but the test asserts it's always present. Add blank-line
separator + unconditional list("tools", ...) so MINIMAL_CONFIG
with tools: [] renders correctly.
2. Nested list values (e.g. runtime_config.required_env: [KEY])
were serialized as " required_env: KEY" (stringification of the
array) instead of a YAML list block. Fix obj() to detect
Array.isArray(sv) and emit a list block with 4-space indent.
Closes#269.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both are data constants exported from design-tokens.ts — TIER_CONFIG
maps tier levels 1-4 to label/color/border CSS classes, and
COMM_TYPE_LABELS maps a2a_send/a2a_receive/task_update to display
labels. No logic to test; structural shape coverage.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Issue: MEDIUM priority from canvas accessibility audit (2026-05-09).
The existing Quick Start help dialog in Toolbar omitted most keyboard shortcuts
from useKeyboardShortcuts.ts — users couldn't discover them visually.
Changes:
- Toolbar.tsx: enhance the help dialog (role="dialog") to include all
documented shortcuts: Esc, Enter, Shift+Enter, Cmd+], Cmd+[, Z, plus
mouse interaction tips for Palette, Right-click, Dbl-click, Shift+click.
Renamed from "Quick start" to "Shortcuts & tips".
- canvas-audit-items.md: update Keyboard Shortcuts section from PARTIAL
to complete; mark help dialog item as done.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>