Previously the textarea was a separate footer with its own border/bg,
breaking the terminal illusion. Move the prompt+input INTO the
scrollable container so it visually flows as the next line of
scrollback — same font, same color scheme as the user-message blocks,
no border, no background.
- Pending-asset chips now render inline just above the prompt within
the same scroll container (drops the heavy footer chrome).
- Click any blank space in the scrollback focuses the input — like a
real terminal where typing always lands at the prompt regardless of
scroll position. Clicks on actual messages are left alone so text
selection still works.
- The prompt scrolls with content; auto-scroll-to-bottom keeps it in
view when new messages arrive.
Replaces the xterm.js scrollback with a Vue-native terminal that can
fold tool-call output. Tool calls collapse to a one-line summary by
default — "$ <cmd> N lines, exit M" with green/red exit-code
colour — and Ctrl+O (or ⌘+O) toggles fold/unfold across every block at
once. Click the block header to toggle a single tool call.
Why this trade-off:
- xterm.js is a character grid; folding requires a parallel logical
model and a full re-render on every toggle. Vue components track
semantic blocks natively, so fold state is just reactive data.
- Side benefits: per-block text selection/copy, native theming via
Tailwind tokens, no ANSI cursor math, no @xterm/* deps in the agent
bundle, easier to extend (per-tool icons, inline images, links).
Implementation notes:
- store.AgentMessage gains an optional 'tool' meta field carrying
script/stdout/stderr/exitCode so the renderer doesn't have to parse
the synthesized text summary back into structure.
- Global keydown listener with capture:true preempts the browser's
default Open-File dialog before Ctrl+O reaches it.
- Drops src/agent/ui/XtermPanel.vue, useXtermReadline.ts, and
shell/useCompletion.ts. The xterm package is still used elsewhere
in the codebase (logs/CommandTerminal), so no dep removal.
New shell commands so the agent can find custom nodes the user has not
yet installed, instead of giving up when local node-search is empty:
- node-search-registry <q> fuzzy node-class search across all
published packs (Comfy Registry)
- pack-search <q> pack-name/description search
- pack-info <pack_id> list every node-class a pack provides
- comfy-codesearch <q> full-text source search across the WHOLE
public ComfyUI community via cs.comfy.org
(catches unregistered repos)
Discovery escalation chain in the system prompt is now:
node-search → node-search-registry → pack-search/info → comfy-codesearch
The agent must walk down before claiming a node is missing.
Layer 1.5 guardrail: any script touching mutating commands (add-node,
connect, set-widget, clear-workflow, save-as, …) is rejected by the
shell runtime until at least one canvas-observing read (graph, node-
list, active-workflow, …) has run in the same turn. Stops the agent
from blindly overwriting an existing workflow it never inspected.
Auto-continue tweaks:
- PROMISSORY_PATTERN extended to catch stop-early phrases ("here's a
plan", "next, I'll", "we can also add", "to build/finish/complete")
that surfaced as premature exits in real sessions.
- MAX_AUTOCONTINUE 3 → 6 so multi-step builds (load → encode → sample →
decode → save) finish without prematurely punting back to the user.
escapeParameter: true in i18n.ts caused slashes in URLs to render as
/ in the popover tooltip. Concatenate the label and URL directly
instead of passing the URL through t() interpolation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contextually show the API key field only after backend is tested and
cloud API is detected, co-located with the cloud API info and the
"Generate an API key" link.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Test button for Comfy API Key input (validates against /customers endpoint)
- Show ✓/✗ status after test
- Update hint text: "Only needed for cloud-API nodes"
- Hide API key section when backend reports --disable-api-nodes in argv
- Show notice when API nodes are disabled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users can now enter their Comfy API key on /connect before clicking
"Connect & Open ComfyUI". The key is saved to localStorage (comfy_api_key)
so it's picked up automatically after connecting — no need to re-enter it
in Settings → API Key.
Existing key is pre-filled from localStorage if already set.
Empty field on connect leaves any existing stored key unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The tooltip string (with HTML-escaped URLs and dot-separated fields) was
showing inside the popover alongside the cleaner popoverLinks section.
Remove it — the links already convey all the same info without the HTML
entity artifacts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full mode only had v-tooltip (plain text on hover). When popoverLinks is
present, the badge now shows a clickable Popover with links, matching the
behavior of icon-only and compact modes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Badge now shows "PR #11118 · abc12345" so reviewers can see the exact
commit at a glance without opening the popover.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends TopbarBadge with a popoverLinks field that renders as <a> tags
in the popover. The PR preview badge now shows clickable links to the
GitHub PR page, author profile, commit, and a "Configure backend →"
link that re-opens /connect to change the backend URL at any time.
Also fixes prPreviewBadges backendUrl to read from localStorage directly
(shows '—' when no remote backend configured, instead of the CF Pages
domain itself).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the backend exposes `system.comfy_api_base` (added in companion
backend PR), use it directly instead of parsing it out of `argv`.
Falls back to argv parsing for older backends that don't yet include
the field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add CopyCodeBlock component with copy-to-clipboard and 2s "copied" checkmark feedback
- Replace all static <code> blocks in ConnectionPanelView with CopyCodeBlock
- Pre-compute launch command strings in script to avoid HTML entity escaping in template
- Expand localAccess section with --listen command for multi-device LAN testing
- Add corsOriginNote explaining why the exact origin matters (not *)
- Add i18n keys: corsOriginNote, localAccessListenDescription, localAccessListenNote
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a prominent amber aside under the panel title showing:
- This is an in-flight PR build, UI may change rapidly, not for production
- Build provenance: PR number, commit hash, author (all linked to GitHub)
- Trust warning: do not connect a backend you care about unless you trust
the PR author, since a malicious frontend can read or modify any
workflow / model / output on the connected backend.
Wires `CI_PR_AUTHOR` from `github.event.pull_request.user.login` through
the build env into a new `__CI_PR_AUTHOR__` define.
When the backend reports a known Comfy Cloud API base (prod or staging via
`--comfy-api-base`), surface a link to the matching platform's API-keys page
(platform.comfy.org or stagingplatform.comfy.org). Custom/unknown bases hide
the link rather than guess a URL.
Parse `--comfy-api-base` out of `/system_stats` `system.argv` and surface the
backend's Comfy Cloud target in the connection panel. When it disagrees with
the frontend's build-time `getComfyApiBaseUrl()`, show an amber warning so
users can spot the mismatch before sign-in tokens get rejected.
Records the cleaner long-term path (add `comfy_api_base` to the backend's
`/features` endpoint) in docs/backend-cloud-api-base-feature-flag.md.
uv pip install puts comfy on PATH immediately, so both commands
chain with && in the same shell. Collapses 4 quick-start steps to 3
and keeps the shell flow linear for copy-paste.
The @/utils/tailwindUtil shim was removed from main in #11453; CI
merge-commit builds fail for this branch. Switch the import to the
package that replaced it. Both paths resolve locally today, so the
change is a drop-in.
comfy install bundles ComfyUI-Manager by default. Surface that in
the quick start, plus an explanatory aside on why Manager smooths
the UX (one-click missing-model/custom-node install). For the
manual python alt path, add the git clone step explicitly.
Move hardcoded English strings (PR #, Version:, Commit:, etc.)
in buildLabel and buildTooltip to i18n keys for consistency
with the rest of the connection panel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace wildcard * with window.location.origin so the
displayed command only opens the backend to this deployment,
not every origin.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add role="status" and aria-live="polite" to the connection
status section so screen readers announce test results.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vi.restoreAllMocks() does not undo vi.stubGlobal(), so stubs
like fetch and WebSocket leaked between tests. Add afterEach
with vi.unstubAllGlobals() and re-stub localStorage in
beforeEach for isolation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parse base URL with new URL() instead of regex stripping so
paths/query/fragments in user-entered URLs cannot corrupt the
WebSocket endpoint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds three ConnectionPanelView tests for the previously uncovered branches:
- HTTP fetch failure renders an error message
- URLs entered without a protocol get http:// prepended
- Connect & Open ComfyUI button appears after a successful HTTP+WS test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codecov reported 0% coverage on the backend-probe logic embedded in
router.ts beforeEnter. The router module is hard to test in isolation
because importing it constructs the router and pulls in cloud/desktop
side effects. Extract the probe to a pure async function in
platform/connectionPanel/ and unit-test it: success, non-2xx, missing
system field, fetch rejection, trailing-slash normalization, and
unconfigured (same-origin) fallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The hardcoded default workflow used on first load referenced
v1-5-pruned-emaonly-fp16.safetensors but had an empty properties object,
so the missing-model panel had no URL to offer for download. The version
in the workflow_templates package already includes properties.models with
the HuggingFace download URL — mirror it here so the Download button
appears when the checkpoint isn't installed locally.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When user lands directly on a SPA route like /connect, the previous logic
set the router base to /connect, causing the /connect route to map to
/connect/connect (and similarly for any deep-linked SPA route). A deploy
directory pathname always ends with /; SPA route pathnames don't. Use the
trailing slash as the discriminator: keep pathname as base when it ends
with / (reverse-proxy subpath case), otherwise fall back to BASE_URL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Flexbox has a known gotcha where auto margins go negative when content
exceeds the container, pushing content off the top. CSS grid's
place-items-center handles overflow correctly — content centers when it
fits, aligns to top when it doesn't, and scrolls properly. Fixes /connect
still being unscrollable from the previous m-auto attempt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Using items-center on a flex container with overflow-auto cuts off the
top of content that exceeds the viewport height because flex centering
doesn't interact with scrolling. Replace with an m-auto inner wrapper,
which still centers short content but lets tall content flow naturally
and scroll. Fixes the long /connect panel being unscrollable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Users without Python installed were getting stuck on pip install comfy-cli.
Restructure the Quick Start to install uv first (which is cross-platform,
a single-line shell install, and doesn't require Python itself to be
present). Then use uv pip install comfy-cli --system. Keep the plain
pip path in the collapsible Alternative section with a note about the
Python 3.10+ requirement.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vue/test-utils was removed from main as part of the test framework
migration. Migrate this test file to @testing-library/vue with
userEvent and Testing Library queries (getByRole, getByDisplayValue,
getByText) to satisfy the testing-library/no-node-access rule.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, when a remote backend URL was set via the connection panel,
api_base contained the full origin and path, causing the WebSocket URL
to be malformed: wss://127.0.0.1:8188http://127.0.0.1:8188/ws.
Now we keep api_host/api_base as just host/path and use a separate
remoteOrigin field that gets prepended in apiURL/fileURL/internalURL.
The WebSocket protocol is also now derived from the remote backend URL
when set, instead of always copying from the page protocol.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When deployed to static hosting (Cloudflare Pages), the frontend now
detects that no backend is available and redirects to /connect instead
of hanging on "Loading ComfyUI". The connection panel includes comfy-cli
quick start guide, connection tester, and "Connect & Go" button.
API requests are routed to the user-configured remote backend URL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add CI workflow to build and deploy to comfy-ui.pages.dev on every push/PR
- Add deploy script with auto PR comments following storybook pattern
- Add ConnectionPanelView at /connect with backend URL config, HTTP/WS test, CLI guide, build info
- Inject CI metadata (branch, PR#, run ID, job ID) as build-time defines
- Add i18n strings, route, unit tests (8/8 passing)
Amp-Thread-ID: https://ampcode.com/threads/T-019d7738-d170-7409-8699-23a55d8ad5e7
Co-authored-by: Amp <amp@ampcode.com>
> Prerequisite work for improved PLY / 3D Gaussian Splatting support —
the per-format loader logic needs to live behind a stable seam before
splat-specific fixes (orientation, async-decoder waits, GPU dispose,
custom bounds) and capability-driven UX gating can be added without
touching `LoaderManager`'s switch every time.
## Summary
Pure refactor. Extracts the per-extension switch inside `LoaderManager`
into three `ModelAdapter` implementations and wires the manager to
dispatch through them. **No behavior change** — same loader code paths,
same outputs, same fallbacks. Sixth in the series splitting up the
https://github.com/Comfy-Org/ComfyUI_frontend/pull/11495.
## Changes
- **What**:
- `ModelAdapter.ts` (new): defines the `ModelAdapter` interface (`kind`,
`extensions`, `capabilities`, `load`), a `ModelLoadContext` that exposes
only the `SceneModelManager` surface adapters need (`setOriginalModel`,
`registerOriginalMaterial`, `standardMaterial`, `materialMode`), and a
shared `fetchModelData(path, filename)` helper.
- `MeshModelAdapter.ts` (new): owns `stl`, `fbx`, `obj`, `gltf`, `glb`.
Each branch is a 1:1 lift of the corresponding `case` from
`LoaderManager.loadModelInternal` on `main`.
- `PointCloudModelAdapter.ts` (new): owns `ply`. Includes the existing
`FastPLYLoader` / `PLYLoader` fallback and the `pointCloud` vs mesh
branching logic.
- `SplatModelAdapter.ts` (new): owns `spz`, `splat`, `ksplat`. Wraps the
`SplatMesh` in a `Group` exactly like the previous `loadSplat` did.
- `LoaderManager.ts`: now owns just an adapter array (default = the
three above) and a small dispatch path. `pickAdapter` matches by
extension and routes PLY → splat when the `Comfy.Load3D.PLYEngine`
setting is `sparkjs` (preserving the previous routing).
`getCurrentAdapter()` is the new public reader used by `Load3d`.
- `Load3d.isSplatModel` / `isPlyModel` now query
`loaderManager.getCurrentAdapter()?.kind` instead of doing
tree-introspection (`containsSplatMesh`) or `instanceof
THREE.BufferGeometry` checks. Same return values, decoupled from the
model shape.
- `LoaderManagerInterface` no longer exposes the per-format loader
fields (`gltfLoader`, `objLoader`, etc.); those are now
adapter-internal.
- `SceneModelManager` is **unchanged** in this PR. Its existing
`containsSplatMesh()` traversal and PLY material-mode rebuild stay put;
a follow-up PR refactors them once capability gating is in place.
## Review Focus
- **Loader equivalence**: the body of every `case` in `main`'s
`LoaderManager.loadModelInternal` is now in the corresponding adapter's
`load()` method. Easiest way to verify: diff `main`'s
`LoaderManager.loadModelInternal` against the four `load()` bodies and
confirm each branch's behavior (file fetch + parse + material wiring +
group wrapping) is byte-identical.
- **Dispatch parity**: `pickAdapter` produces the same routing as `main`
— extension match first, then the PLYEngine === 'sparkjs' override
hoisted up from inside the old `loadPLY`.
- **Capability fields are dormant**: the `ModelAdapterCapabilities`
record (`fitToViewer`, `materialModes`, `fitTargetSize`, …) is declared
on every adapter but **not consumed anywhere in this PR**.
SceneModelManager / Load3d / Load3DControls still read no capability
data. The follow-up PR turns these on.
- **`setOriginalModel` / `registerOriginalMaterial`**: adapters now go
through the `ModelLoadContext` getter rather than reaching into
`modelManager` directly. The context's `standardMaterial` and
`materialMode` are exposed via getters so a late-bound `materialMode` is
read at the actual call site, not snapshotted at context creation.
## Coverage
| File | Stmts | Branch | Funcs | Lines |
|---|---|---|---|---|
| `ModelAdapter.ts` (new) | **100%** | **100%** | **100%** | **100%** |
| `MeshModelAdapter.ts` (new) | **100%** | **100%** | **100%** |
**100%** |
| `PointCloudModelAdapter.ts` (new) | 97.22% | 61.11% | 75% | 97.22% |
| `SplatModelAdapter.ts` (new) | **100%** | **100%** | **100%** |
**100%** |
| `LoaderManager.ts` (modified) | **100%** | 91.17% | 86.66% | **100%**
|
| `Load3d.ts` (modified) | 6.63% | 0% | 13.68% | 6.7% |
All four new files are at or near 100% via dedicated unit tests for each
adapter (load happy path, error propagation, extension declarations,
capability shape). `LoaderManager.test.ts` exercises the dispatch logic
— extension matching, the `ply → splat` sparkjs override, the stale-load
discard, the load-context proxying — across 34 cases. The two changed
`Load3d.ts` methods (`isSplatModel`, `isPlyModel`) get dedicated tests
verifying they read the current adapter's `kind` and fall back to
`false` when none is loaded.
`Load3d.ts`'s overall 6.7% number is the pre-existing baseline — the
existing `Load3d.test.ts` covers façade methods via prototype injection
rather than instantiating the class (the constructor needs
`THREE.WebGLRenderer`, which happy-dom can't provide). PR-F's surface in
`Load3d.ts` is two method bodies, both covered by the new adapter-driven
kind queries test.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11627-refactor-load3d-introduce-ModelAdapter-abstraction-for-the-loader-switch-34d6d73d3650811b8a1ccc55b45100f2)
by [Unito](https://www.unito.io)
## Summary
Pull the right-click vs right-drag detection out of `Load3d` into a
sibling helper. Mechanical refactor — no behavior change. Third of four
small PRs splitting up the [`remove-ply-3dgs-nodes-squashed`
mega-commit.](https://github.com/Comfy-Org/ComfyUI_frontend/pull/11495.)
## Changes
- **What**: New `load3dContextMenuGuard.ts` exports
`attachContextMenuGuard(target, onMenu, { isDisabled, dragThreshold })`.
It installs `mousedown` / `mousemove` / `contextmenu` listeners against
a single `AbortController` and returns one dispose function.
- `Load3d` now calls `attachContextMenuGuard(this.renderer.domElement,
(event) => this.onContextMenuCallback?.(event), { isDisabled: () =>
this.isViewerMode })` and stores the returned disposer in a single
field. Drops four private fields (`rightMouseStart`, `rightMouseMoved`,
`dragThreshold`, `contextMenuAbortController`) plus the now-redundant
`showNodeContextMenu` private method.
- The 5px drag threshold and `isViewerMode` gating are preserved.
## Review Focus
- The three event handlers (`mousedown`, `mousemove`, `contextmenu`)
inside the new helper match the old inline implementations one-for-one —
same `e.button === 2` / `e.buttons === 2` checks, same call to
`exceedsClickThreshold`, same `preventDefault` + `stopPropagation`
ordering.
- `isDisabled: () => this.isViewerMode` replaces the inline `if
(this.isViewerMode) return` early-out — same gate, just lifted to a
callback.
- A single `AbortController.abort()` (in the returned disposer) replaces
the old four-field teardown in `Load3d.remove()`.
- 9 unit tests cover the helper: click vs drag distinction at the
threshold, drag-then-click reset, `isDisabled` short-circuit, and the
disposer detaching all three listeners.
## Coverage
| File | Stmts | Branch | Funcs | Lines |
|---|---|---|---|---|
| `load3dContextMenuGuard.ts` (new) | **100%** | **93.33%** | **100%** |
**100%** |
| `Load3d.ts` (modified) | 7.12% | 0% | 13.97% | 7.18% |
The single uncovered branch on `load3dContextMenuGuard.ts` (line 22) is
the default-parameter fallback for `dragThreshold` when the caller omits
it — `Load3d` always passes `{ isDisabled, dragThreshold: 5 }` through
`attachContextMenuGuard`'s second-arg destructure, so the default never
fires under the production call path. Adding a test that omits
`dragThreshold` would push it to 100%; left as-is to avoid a
change-detector test for a default value.
The `Load3d.ts` numbers are the pre-existing baseline on `main` —
`Load3d.test.ts` covers façade methods via prototype injection rather
than instantiating the class (the constructor needs
`THREE.WebGLRenderer`, which happy-dom can't provide). The
`initContextMenu` rewrite and the `remove()` teardown change both sit in
those same uninstantiated paths and rely on browser e2e for end-to-end
coverage, the same as before. Net: the click-vs-drag logic that
previously had no unit test is now ≥93% covered through the extracted
helper.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11625-refactor-extract-Load3d-right-click-guard-to-load3dContextMenuGuard-34d6d73d36508162aecef46553a3f50d)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for `MaskEditorContent` (the mask editor's main
orchestration container), raising coverage from 0% to **94.11% / 83.72%
/ 83.33% / 94.11%** (statements / branches / functions / lines).
## Changes
- **What**: Add `src/components/maskeditor/MaskEditorContent.test.ts`
(12 tests) covering:
- **Mount**: keyboard listeners attached, ResizeObserver observes the
container, all 5 canvas refs assigned to the store before init runs.
- **Init flow**: `loader.loadFromNode` → `imageLoader.loadImages` →
`panZoom.initializeCanvasPanZoom` → `canvasHistory.saveInitialState` →
`brushDrawing.initGPUResources` → `initPreviewCanvas` chain runs in
order; child UI (`ToolPanel` / `PointerZone` / `SidePanel` /
`BrushCursor`) only renders after init succeeds; GPU preview canvas
resolution matches the mask canvas.
- **Init errors**: rejection from `loader.loadFromNode` or
`panZoom.initializeCanvasPanZoom` is caught, logged, and triggers
`dialogStore.closeDialog()`.
- **ResizeObserver**: callback invokes `panZoom.invalidatePanZoom()`
(captured the constructor argument to call it manually).
- **Drag**: `Ctrl+drag` is preventDefault'd; plain drag is not.
- **Unmount**: cleanup runs `brushDrawing.saveBrushSettings`,
`keyboard.removeListeners`, `canvasHistory.clearStates`,
`store.resetState`, `dataStore.reset`.
## Review Focus
- Heavy mock surface (10 modules): the 3 stores, 5 composables, plus 4
child Vue components and `LoadingOverlay`. All mocks are `vi.hoisted`
module-level. `mockStore` is `reactive()` because the source mutates
`activeLayer` (visible in template binding), `maskCanvas`, etc.; the
rest are plain function bags.
- Child components are stubbed to bare `<div data-testid>` so init
reveal can be asserted via `screen.findByTestId(...)` without engaging
their real implementations (each has its own test file).
- `MockResizeObserver` captures the constructor callback in module-level
`lastResizeCallback`. The "invalidate on resize" test invokes it
manually with empty args — that's enough to exercise the source's `if
(panZoom) { await panZoom.invalidatePanZoom() }` branch since the
callback only consumes `panZoom` from closure.
- happy-dom doesn't propagate `ctrlKey` through the `DragEvent`
constructor, so the drag tests set it via `Object.defineProperty(event,
'ctrlKey', { value })` (same pattern used in `PointerZone.test.ts` for
wheel `clientX/Y`).
- 94.11% line coverage — the two uncovered blocks (`containerRef`
missing, canvas refs missing) are early-return error paths unreachable
when Vue successfully mounts; not worth constructing a fixture to
trigger.
- Style aligned with sibling tests: `should ...` naming, `describe`
grouped by feature, `vi.hoisted` mocks reset via `beforeEach`,
`screen.findByTestId` for async render assertions.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11651-test-add-unit-tests-for-MaskEditorContent-main-container-34e6d73d365081b38af2e057cb7daf9e)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for `TopBarHeader` mask editor dialog component, raising
coverage from 0% to **100%** across statements, branches, functions, and
lines.
## Changes
- **What**: Add `src/components/maskeditor/dialog/TopBarHeader.test.ts`
(17 tests) covering:
- Localized title rendering.
- Undo / Redo buttons forward to `store.canvasHistory.{undo,redo}`.
- Four transform buttons (rotate left / right, mirror horizontal /
vertical) call the matching `canvasTransform` action — parametrized via
`it.each`.
- All four transform error paths: rejected promise is caught, swallowed,
and logged with the right `[TopBarHeader] ... failed:` prefix.
- Invert calls `canvasTools.invertMask`; Clear calls both
`canvasTools.clearMask` and `store.triggerClear`.
- Save: hides brush, awaits `saver.save()`, closes the dialog on
success; switches button text to "Saving" while in-flight; restores
brush + button label and logs on save failure.
- Cancel: closes the dialog with the `global-mask-editor` key.
## Review Focus
- All five composable / store dependencies are mocked at module level
via `vi.hoisted`: `useMaskEditorStore`, `useDialogStore`,
`useCanvasTools`, `useCanvasTransform`, `useMaskEditorSaver`. Only the
store needs `reactive()` (`brushVisible` flips during save flow); the
rest are plain function bags.
- `Button.vue` is stubbed to a thin `<button :disabled>` so role queries
(`getByRole('button', { name: ... })`) resolve cleanly without dragging
in the real UI button's classes / variants.
- Real i18n via `createI18n`. Icon buttons rely on `:title` for their
accessible name; text buttons rely on slot text — both are reachable
through `getByRole('button', { name: ... })`.
- The "Saving" text test uses an unresolved promise to keep the
in-flight state observable; `waitFor` + `void user.click(...)` lets us
assert without awaiting the click. The dangling promise resolves at the
end so vitest doesn't complain.
- `it.each` parametrizes the four transform success paths and
(separately) the four error paths, keeping the file tight.
- Style aligned with sibling tests: `should ...` naming, `describe`
grouped by feature, `vi.hoisted` for cross-test mocks, real i18n with
`createI18n`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11650-test-add-unit-tests-for-TopBarHeader-34e6d73d365081adab66e460cf56accb)
by [Unito](https://www.unito.io)