Bridge LGraphCanvas.read_only to Vue reactivity via onReadOnlyChanged
callback so the existing CSS pointer-events-auto/none toggle on
LGraphNode.vue and NodeWidgets.vue re-evaluates when space key
toggles panning mode. Events then fall through to the LiteGraph
canvas naturally — no per-handler forwarding or force flags needed.
Fixes#7806
Amp-Thread-ID: https://ampcode.com/threads/T-019c796c-e83c-769d-85f4-20a349994bad
## Summary
Use exact BLAKE3 hash lookups first for missing model/media detection,
and add a separate public-inclusive input asset cache so public input
assets are considered missing-detection candidates without changing the
user-only input assets shown in the UI.
## Changes
- **What**:
- Added `assetService.checkAssetHash()` for `HEAD
/api/assets/hash/{hash}` status-only existence checks.
- Added strict BLAKE3 hash helpers so only `blake3:<64 hex>` media
values and raw 64-hex BLAKE3 model metadata are sent to the hash
endpoint.
- Updated missing media detection to group BLAKE3 candidates by hash,
resolve them through the hash endpoint, and fall back to the legacy
asset list path for invalid/unverifiable/non-hash values.
- Updated missing model detection to use hash lookup for BLAKE3-backed
asset-supported candidates before falling back to the existing node-type
asset matching path.
- Added `assetService.getInputAssetsIncludingPublic()` backed by a
dedicated cache that fetches input assets with `include_public=true` for
missing media fallback checks.
- Kept `assetsStore.inputAssets` user-only for widget/UI display, while
invalidating the public-inclusive missing-detection cache when input
assets may change.
- Added abort handling for paginated asset fetches and shared
public-input cache callers so one aborted caller does not cancel the
shared fetch for other callers.
- Added regression coverage for hash lookup, fallback behavior, abort
paths, public input fallback detection, and cache invalidation.
- **Dependencies**: None.
- **Change size**:
- Production code: 4 files, 400 insertions, 24 deletions, net +376.
- Test code: 4 files, 806 insertions, 59 deletions, net +747.
- Total: 8 files, 1206 insertions, 83 deletions, net +1123.
## Review Focus
- The public-inclusive input asset cache is intentionally separate from
`assetsStore.inputAssets`. The existing store data is user-only and
drives the asset widgets/sidebar, so using it for missing input
detection misses public assets. Making that store public-inclusive would
change UI data semantics; this PR instead keeps the UI dataset unchanged
and adds a missing-detection-specific cache in `assetService`.
- Hash lookup is only used when the workflow exposes a valid BLAKE3
hash. Filename-like values and invalid hash values still use the legacy
fallback path.
- Missing model detection keeps the existing fallback behavior for
non-hash candidates and for hash checks that are invalid or fail
transiently.
- Async model download cache refresh behavior is left unchanged; this PR
avoids coupling model download completion to input asset cache
invalidation.
- No browser/e2e test was added because this changes the missing asset
detection data path, not UI interaction or rendering. The behavioral
coverage is in unit tests for the asset service and the missing
media/model scanners.
## Follow-up Items
- Fix `assetsStore.updateAssetTags()` partial-failure recovery. If
`removeAssetTags()` succeeds and `addAssetTags()` fails, the local model
asset cache can roll back to tags that the backend has already removed;
this should be handled in a focused model asset cache PR.
- Consider extracting shared hash-verification flow used by missing
media and missing model scans after this behavior stabilizes.
- Consider adding a concurrency cap or short-lived request cache for
large workflows with many unique hash lookups.
- Consider splitting `assetService.ts` further, e.g. hash helpers, abort
utilities, and the public-inclusive input asset cache.
- Consider tightening the asset hash service API shape so callers do not
directly depend on HTTP-oriented statuses such as `invalid`.
- Consider adding broader mutation-path coverage for public-inclusive
input cache invalidation once the cache has more consumers.
Linear: FE-534
## Screenshots (if applicable)
Before <false positive / missing image / public asset>
https://github.com/user-attachments/assets/db7ce2a9-b169-4fae-bf9f-98bb93d3ee6d
After
https://github.com/user-attachments/assets/29af9f9e-b536-4fcd-a426-3add40bcb165
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11873-Use-hash-lookup-for-missing-asset-detection-3556d73d36508165babafb16614be0d8)
by [Unito](https://www.unito.io)
Automated refresh of `apps/website/src/data/ashby-roles.snapshot.json`
from the Ashby job board API.
**Flow:**
1. `Release: Website` workflow ran (manual trigger).
2. This PR opens with the regenerated snapshot.
3. `CI: Vercel Website Preview` deploys a preview for review.
4. Merging to `main` triggers the production Vercel deploy.
The snapshot fallback in `apps/website/src/utils/ashby.ts` remains
intact: builds without `WEBSITE_ASHBY_API_KEY` continue to use the
committed snapshot.
Triggered by workflow run `25260868155`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11851-chore-website-refresh-Ashby-roles-snapshot-3546d73d365081579f98f13f7b58e611)
by [Unito](https://www.unito.io)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
## Summary
Adds tests for metadata parsers
## Changes
- **What**:
- add test file generation script
- identified & fixed bug in webp exif parsing over-reading
- identified & fix bug in mp3/ogg parser where it would read from a
fixed position instead of relative, causing incorrect reads throwing
RangeError
- added catch in latent + json parsing to resolve errors
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11307-test-add-metadata-parser-coverage-3446d73d36508108ac36dddcec0a54d4)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Add a new customer story for Groove Jones — Crocs x NFL FOOH holiday
campaign for Dick's Sporting Goods, delivered with Comfy.
## Changes
- **What**:
- New entry in
[`apps/website/src/config/customerStories.ts`](https://github.com/Comfy-Org/ComfyUI_frontend/blob/feat/groove-jones-customer-story/apps/website/src/config/customerStories.ts)
registering slug `groove-jones` with cover image hosted on
`media.comfy.org/website/customers/groove-jones/`.
- Added `customers.story.groove-jones.{category,title,body}` and
`customers.detail.groove-jones.topic-1` … `topic-10` translations (en +
zh-CN) in
[`apps/website/src/i18n/translations.ts`](https://github.com/Comfy-Org/ComfyUI_frontend/blob/feat/groove-jones-customer-story/apps/website/src/i18n/translations.ts).
10 sections matching design sidebar: INTRO, THE OUTPUT, THE PROBLEM, HOW
COMFY SOLVED THE PROBLEM, BRAND-TRAINED LORAS, MULTI-MODEL
ORCHESTRATION, THE PIPELINE, VERSION CONTROL, FINISHING IN NUKE, THE
TAKEAWAY.
- Includes 2 pull quotes (Doug Hogan, Dale Carman), 1 final blockquote +
author card, and 3 inline images.
- Routes `/customers/groove-jones` and `/zh-CN/customers/groove-jones`
are auto-generated by `[slug].astro`.
## Review Focus
- Contributors author card uses `TBD` placeholder names/roles — to be
filled in.
- No `readMoreHref` set yet (no public blog post URL).
- All 4 images uploaded to
`gs://comfy-org-videos/website/customers/groove-jones/` and served via
`media.comfy.org`.
<img width="1000" height="535" alt="Kapture 2026-05-02 at 23 17 04"
src="https://github.com/user-attachments/assets/28654d24-0d49-4303-82ac-b6923cd6bc93"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11849-feat-add-Groove-Jones-customer-story-3546d73d36508128a64bd6809ad77447)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
Fixes#11345
## Summary
`clearModel()` in `SceneModelManager` only traversed and disposed
`THREE.Mesh` instances, leaving `THREE.Points` objects (created by
`handlePLYModeSwitch()` for point-cloud mode) leaking GPU geometry and
material memory on repeated point-cloud loads/clears.
## Changes
- `SceneModelManager.ts`: extend the dispose traversal in `clearModel()`
to also handle `THREE.Points`, mirroring the pattern already used by
`removeAllMainModelsFromScene()`.
- `SceneModelManager.test.ts`: add regression test verifying
`geometry.dispose()` and `material.dispose()` are called for
`THREE.Points` children on `clearModel()`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11836-fix-load3d-dispose-THREE-Points-GPU-resources-in-clearModel-3546d73d365081718338e824bc3e737d)
by [Unito](https://www.unito.io)
## Summary
Adds `/llms.txt` at the apex following the [llms.txt
standard](https://llmstxt.org) — a curated, link-based markdown file
that tells LLM-based search agents (ChatGPT search, Perplexity, Claude
search, Google AI Overviews, etc.) what's most important on the site. It
complements `robots.txt` (crawler permissions) and `sitemap-index.xml`
(URL inventory) by giving AI agents a short, prose-friendly index they
can ingest into a context window.
## What's in the file
28 links across 6 sections:
- **Product** (6) — homepage, Local download, Cloud, Cloud pricing, API,
Enterprise
- **Workflows and Gallery** (2) — gallery + community workflows site
- **Customers and Case Studies** (5) — customers index + 4 named studios
(Series Entertainment, Moment Factory, Ubisoft Chord, Open Story
Movement)
- **Developers and Documentation** (4) — docs.comfy.org, ComfyUI repo,
Comfy-Org GitHub org, registry.comfy.org
- **Company** (6) — about, careers, contact, blog, privacy, terms
- **Optional** (5) — `zh-CN` locale variant, long-form enterprise case
studies, blog posts (de-prioritized per spec — agents can skip if
context-limited)
The intro paragraph names the four product surfaces (Local, Cloud, API,
Enterprise), the named customers, and the use-case industries (VFX &
animation, advertising, gaming, eCommerce/fashion) — so an agent that
ingests only the prose still gets the elevator pitch.
## Verification
- All 28 URLs verified live (`HTTP 200`) before commit.
- File is plain markdown — no build step. Astro/Vercel will serve it
from `apps/website/public/llms.txt` exactly as it serves `robots.txt`
(which lives in the same directory and ships at
`https://comfy.org/robots.txt`).
- Will verify on the Vercel preview deploy after this PR opens that
`curl -sI https://<preview>/llms.txt` returns `200` with a sensible
`content-type`. (`robots.txt` currently serves as `text/plain;
charset=utf-8` — `.txt` will likely do the same; that's fine for AI
agents.)
## Decisions
- **No `llms-full.txt` yet.** That variant inlines full prose of key
pages and requires curating substantive content. Deferred to a follow-up
— the marketing-site pages are mostly Vue-rendered hero/feature blocks
rather than long-form prose, so a meaningful `llms-full.txt` would need
either dedicated copy or a build step that flattens i18n strings +
section text. Tracking separately.
- **No comment line in `robots.txt`.** I considered adding a `# AI
agents: see /llms.txt` comment above the `Sitemap:` directive, but
decided against it: (a) the convention is to probe the well-known path
`/llms.txt` directly, not to discover it via robots.txt; (b)
`robots.txt` was just polished in #11823 with a deliberate compact
design and adding a non-standard comment would muddy that; (c) zero
implementations I checked actually parse robots.txt for llms.txt hints.
Easy to add later if needed.
## Context
Third of three follow-ups from the SEO/GEO sweep on 2026-05-02:
1. ~~Comfy-Router: add `X-Content-Type-Options: nosniff` to apex
security headers~~ (separate PR on `Comfy-Org/comfy-router`)
2. ~~Cloudflare: enable "Always Use HTTPS"~~ (dashboard toggle, no PR)
3. **This PR** — add `llms.txt` for GEO discovery
## Testing
- [x] All linked URLs return 200
- [x] File parses as valid markdown
- [ ] Preview deploy serves `/llms.txt` (will verify once preview is up)
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11830-feat-website-add-llms-txt-for-GEO-discovery-by-AI-search-agents-3546d73d365081a98c6bfc5301699f64)
by [Unito](https://www.unito.io)
## P0 SEO fix — entire marketing site is currently `noindex`
A deep-audit sweep on **2026-05-02** confirmed that every page on
`comfy.org` (home, `/about`, `/pricing`, `/customers`, `/careers`, …) is
being served with:
```
x-robots-tag: noindex
```
This hides the entire marketing site from Google and other search
engines.
### Root cause
`comfy.org` reaches the Astro marketing site via a Cloudflare Worker
reverse proxy
([`comfy-router`](https://github.com/Comfy-Org/comfy-router)) that does:
```js
fetch('https://website-frontend-comfyui.vercel.app/...')
```
Per Vercel's KB article [Avoiding duplicate-content with vercel.app
URLs](https://vercel.com/kb/guide/avoiding-duplicate-content-with-vercel-app-urls):
> By default, Vercel adds an `X-Robots-Tag: noindex` HTTP header to all
deployments hosted on `vercel.app` URLs. […] This header tells search
engines like Google not to index these deployment URLs.
Because `website-frontend-comfyui.vercel.app` is **not** registered as a
Production custom domain on the Vercel project, Vercel applies that
header — and our Worker faithfully forwards it back to `comfy.org`
clients (and Googlebot).
### Fix
Vercel's documented workaround for proxied `*.vercel.app` setups is to
override the header via `vercel.json` ([blog post by Dan
Denney](https://www.dandenney.com/posts/front-end-dev/fixing-x-robots-nofollow-with-vercel/)):
```json
"headers": [
{
"source": "/(.*)",
"has": [
{ "type": "host", "value": "website-frontend-comfyui.vercel.app" }
],
"headers": [
{ "key": "X-Robots-Tag", "value": "index, follow" }
]
}
]
```
### Why the `has` host filter
Critical scoping detail: without the `has` filter, the override would
also apply to PR-preview deployments at
`comfy-website-preview-pr-N.vercel.app`. We want previews to **stay**
`noindex` to avoid duplicate-content competition with production.
The filter pins the override to the production deployment hostname only.
The hostname matches `WEBSITE_ORIGIN` in `comfy-router/src/index.js`:
```js
const WEBSITE_ORIGIN = 'https://website-frontend-comfyui.vercel.app';
```
### Defense in depth
A parallel PR on `comfy-router` (TBD) will also strip `X-Robots-Tag:
noindex` at the Worker layer, so the public `comfy.org` response is
correct even if a future Vercel project change re-introduces the
upstream header.
### Verification (after merge + Vercel production deploy)
```bash
# Production should no longer be noindex
curl -sI https://comfy.org/ | grep -i x-robots-tag
curl -sI https://comfy.org/about | grep -i x-robots-tag
curl -sI https://comfy.org/pricing | grep -i x-robots-tag
# Expect: empty output, OR "x-robots-tag: index, follow"
# Direct Vercel production hostname
curl -sI https://website-frontend-comfyui.vercel.app/ | grep -i x-robots-tag
# Expect: "x-robots-tag: index, follow"
# Preview deploys must stay noindex (proves the host filter works)
curl -sI https://comfy-website-preview-pr-<N>.vercel.app/ | grep -i x-robots-tag
# Expect: "x-robots-tag: noindex"
```
### Pre-merge state (for the record)
```
$ curl -sI https://comfy.org/ | grep -iE 'x-robots-tag|x-served-by'
x-served-by: vercel-website
x-robots-tag: noindex
```
### Scope
Minimum delta — only `apps/website/vercel.json`. No other files touched.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11831-fix-website-override-X-Robots-Tag-to-index-follow-on-production-deployment-3546d73d365081489b62e6633d25dfe5)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Christian Byrne <christian@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
*PR Created by the Glary-Bot Agent*
---
## Summary
- Adds `browser_tests/tests/queueNotificationBanners.spec.ts` covering
`useQueueNotificationBanners` composable E2E behavior
- Adds `data-testid="queue-notification-banner"` to
`QueueNotificationBannerHost.vue` for stable test targeting
- Registers the new test ID in `TestIds.queue.notificationBanner`
### Test coverage added (7 tests)
| Group | Tests | Behavior |
|---|---|---|
| Queuing lifecycle | 4 | `promptQueueing` → banner appears,
`promptQueued` upgrades to queued, batch plural text, requestId mismatch
doesn't upgrade |
| Auto-dismiss | 1 | Banner disappears after 4s timeout |
| FIFO queue | 1 | Second notification shows after first auto-dismisses
|
| Direct queued | 1 | `promptQueued` without prior `promptQueueing`
shows banner directly |
### Approach
Tests dispatch `promptQueueing`/`promptQueued` custom events directly
via `window.app.api.dispatchCustomEvent()` inside `page.evaluate()`,
matching how `app.ts` triggers these events during real queue
operations. This avoids needing a running execution pipeline while
exercising the full composable → component → DOM rendering chain.
### Verification
- TypeScript: zero errors
- ESLint: clean
- oxlint: clean
- oxfmt: formatted
- Playwright execution requires running ComfyUI backend (not available
in sandbox)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11366-test-add-queue-notification-banners-lifecycle-browser-tests-3466d73d36508172a7ffd3fe3b4fd925)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Adds a unified manual-trigger release flow for the `apps/website`
package
(careers/marketing site at comfy.org), mirroring how main-app releases
work.
**User-facing flow:**
```
workflow_dispatch ──► fresh Ashby pull ──► auto-PR with snapshot bump
│
▼
existing CI / Vercel preview deploy
│
▼
human merges ──► auto prod deploy
```
The careers data on comfy.org comes from Ashby and is fetched at build
time by
`apps/website/src/utils/ashby.ts`. Without `WEBSITE_ASHBY_API_KEY`, the
build
falls back to a committed snapshot at
`apps/website/src/data/ashby-roles.snapshot.json`. That snapshot has
been
going stale because no CI workflow was passing the API key. This PR
fixes
both: a manual refresh workflow + day-to-day secrets wiring.
## Files
**Added**
- `.github/actions/ashby-pull/action.yaml` — composite action that runs
`pnpm --filter @comfyorg/website ashby:refresh-snapshot` with the Ashby
secrets piped in. Uses the existing `setup-frontend` composite for
pnpm/Node setup.
- `.github/workflows/release-website.yaml` — `workflow_dispatch`-only
release workflow. Checks out `main`, refreshes the snapshot via the
composite action, opens a PR labelled `Release:Website` via
`peter-evans/create-pull-request@c0f553fe…` (the same SHA pin used by
`release-version-bump.yaml`).
**Modified**
- `.github/workflows/ci-website-build.yaml` — adds
`WEBSITE_ASHBY_API_KEY`
and `WEBSITE_ASHBY_JOB_BOARD_NAME` env to the `Build website` step.
- `.github/workflows/ci-vercel-website-preview.yaml` — adds the same env
to both `vercel build` steps (preview + production).
## Snapshot fallback preserved
`apps/website/src/utils/ashby.ts` keeps using the committed snapshot
when
the API key is absent (e.g. fork PRs, secret rotation). Verified
locally:
```
$ pnpm --filter @comfyorg/website ashby:refresh-snapshot
Snapshot refresh aborted. Outcome: stale; reason: missing WEBSITE_ASHBY_API_KEY...
```
The release workflow surfaces this as a job failure, which is the
desired
behavior for a manual release trigger.
## Validation
- `yamllint --config-file .yamllint` on all changed YAML — clean
- `./scripts/cicd/check-yaml.sh` — clean
- `pinact run --check` on new files — clean (matches `.pinact.yaml`
policy)
- `pnpm install --frozen-lockfile` — works with `.nvmrc` Node 24
- Husky pre-commit hooks (eslint + typecheck + lint-staged) passed
## Caveats
- **Cannot fully end-to-end test until merged.** `workflow_dispatch`
workflows only run from branches that exist on `main`. The first
manual run can only happen after this PR lands. The pieces that
*can* be tested pre-merge — yamllint, pinact pin validation, and
the existing `CI: Website Build` / `CI: Vercel Website Preview`
workflows now picking up the secret — will run on this PR.
- **`Release:Website` label needs to be created** in the repo before
the auto-PR step will successfully apply it.
`peter-evans/create-pull-request`
will warn but not fail if the label doesn't exist. Suggested color:
`#4f6ef5` (matches `cloud/*` family in `release-branch-create.yaml`).
- The release workflow uses `secrets.PR_GH_TOKEN` (matching
`release-version-bump.yaml`) so the auto-PR can be authored by a
PAT and trigger downstream CI workflows. Without `PR_GH_TOKEN` it
will fall back behavior is up to GitHub Actions defaults — confirm
the secret exists before the first run.
## Context
Came out of work on `comfy-router#22` + `ComfyUI_frontend#11823`
(comfy.org/countdown subpage / website refresh). Discovered the
8+-day-stale snapshot while auditing the website build path.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11829-feat-ci-add-Release-Website-workflow-to-refresh-Ashby-snapshot-3546d73d3650811eb300d8bcb593c652)
by [Unito](https://www.unito.io)
## Summary
Once
[comfy-router#22](https://github.com/Comfy-Org/comfy-router/pull/22)
ships, `comfy.org/sitemap-index.xml` will return a unified index
aggregating both the website (38 URLs) and workflow-templates sitemaps.
This PR:
1. Reverts `Sitemap:` back to `/sitemap-index.xml` (was `/sitemap-0.xml`
in #11802 as a workaround for the 404).
2. Adds explicit allow records for 21 search and AI/LLM crawlers
(GPTBot, ChatGPT-User, OAI-SearchBot, Google-Extended, ClaudeBot,
Claude-Web, anthropic-ai, PerplexityBot, Perplexity-User,
Applebot-Extended, Bytespider, Amazonbot, CCBot, Meta-ExternalAgent,
Meta-ExternalFetcher, Diffbot, etc.).
3. Adds `Disallow:` for `/_astro/`, `/_website/`, `/_vercel/` — Vercel
build artifacts that aren't useful to crawl.
## Why granular UAs
Stacked `User-agent:` records (per [RFC 9309
§2.2](https://datatracker.ietf.org/doc/html/rfc9309#section-2.2)) share
one rule block. Listing each bot explicitly:
- Signals intent to AI bots that look for their UA in robots.txt before
crawling more aggressively.
- Surfaces our crawl policy clearly to anyone inspecting the file.
- Lets us add per-bot Disallows in future without restructuring.
## Merge order
⚠️ **Do NOT merge until comfy-router#22 is deployed to production.**
Until then, `/sitemap-index.xml` returns 404 and this PR would re-break
the issue PR #11802 patched. Verification:
```bash
curl -sI https://comfy.org/sitemap-index.xml
# expect: HTTP/2 200, x-served-by: worker-sitemap-index
```
Once that returns 200, this is safe to merge.
## Verification (after merge + deploy)
```bash
# robots.txt is served and points at the unified index
curl -s https://comfy.org/robots.txt | grep '^Sitemap:'
# → Sitemap: https://comfy.org/sitemap-index.xml
# Each AI crawler can fetch it
for ua in 'GPTBot/1.0' 'ClaudeBot/1.0' 'PerplexityBot/1.0' 'Google-Extended' 'Applebot-Extended'; do
curl -s -o /dev/null -w "$ua → %{http_code}\n" -A "$ua" https://comfy.org/robots.txt
done
# Sitemap is reachable from robots.txt
SITEMAP=$(curl -s https://comfy.org/robots.txt | awk -F': ' '/^Sitemap:/ {print $2}')
curl -s "$SITEMAP" | xmllint --noout - && echo "valid XML"
```
## Linear / closes
- Closes FE-437 (AI crawler rules)
- Updates FE-432 — the robots.txt change in #11802 was a workaround
that's no longer needed once #22 ships
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11823-feat-website-point-robots-txt-at-sitemap-index-xml-AI-crawler-rules-3546d73d3650811dbceedd06c00db444)
by [Unito](https://www.unito.io)
## Summary
Replaces the 4-step Cloud onboarding survey with a 7-step flow that
captures both ICP attributes and user persona dimensions. The survey
questions are now populated dynamically from remoteConfig.
## Changes
- **What**: New survey questions — Usage, Familiarity, Role, Team size,
Industry, Making, Source. Role / Team size / Industry are gated to
"Work" usage; Education users see a Student / Educator short list for
Role. Most option lists are randomized per visit (familiarity and team
size stay ordered as ordinals). \`SurveyResponses\` extended with
optional \`usage\`, \`role\`, \`teamSize\`, \`source\` fields.
- **Breaking**: None — \`useCase\` and \`workflowRelationship\` remain
optional in the type and existing telemetry normalization keeps working
unchanged.
## Review Focus
- The \`role\` step has a function-form \`options\` so the list can swap
based on \`usage\`. \`steps\` is a computed that filters by
\`showWhen()\` and resolves the option function — verify reactivity when
\`usage\` changes.
- Changing \`usage\` clears the previously-picked \`role\` via a watcher
to prevent a stale value from carrying over between Work / Education
modes.
- Per-visit shuffle is stable: option lists are passed through
\`randomize()\` once at module load, not on every render.
## Screenshots
https://github.com/user-attachments/assets/3602a388-50dc-401e-ada9-ea9016c5052d
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11628-feat-redesign-cloud-onboarding-survey-for-ICP-and-persona-signal-34d6d73d365081f4a792cfe76a987ffb)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Dante <bunggl@naver.com>
## Summary
- Adds `silentOnNotFound` option to `LoadModelOptions` interface,
threaded through `Load3d.loadModel` → `LoaderManager.loadModel`
- 404 errors (detected via message text or `response.status`) are
silently swallowed when `silentOnNotFound: true`; all other errors still
surface a toast
- Sets `silentOnNotFound: true` for output-folder loads in `load3d.ts`
and `saveMesh.ts` — covers shared workflows opened on a machine that
never ran them
## Test plan
- [x] `LoaderManager.test.ts` — 40 unit tests covering 404 suppression,
non-404 still toasts, stale load handling
- [x] `Load3DConfiguration.test.ts` — 4 unit tests verifying
`silentOnNotFound` propagates correctly through `configureForSaveMesh`
and `configure`
- [x] `load3d.spec.ts` — 2 E2E tests: 404 → no toast, 500 → toast
appears
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes error-handling behavior in the 3D model loading pipeline and
extends method signatures/options; risk is mainly missed call sites or
incorrectly classifying non-404 errors as 404 and hiding real failures.
>
> **Overview**
> Prevents noisy user-facing toasts when an *output* 3D model referenced
by `Preview3D`/`SaveGLB` is missing locally by adding a
`silentOnNotFound` flag and suppressing the "Error loading model" toast
specifically for HTTP 404 failures.
>
> Threads the new `LoadModelOptions` through `Load3d.loadModel` →
`LoaderManager.loadModel` and updates `Load3DConfiguration`/callers to
opt in for output-folder loads, with new unit + Playwright coverage (404
stays silent, non-404 still toasts).
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
049f75ef60. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11807-fix-load3d-suppress-error-toast-on-404-when-loading-output-model-file-3536d73d36508129ac0de1d5b081dcf0)
by [Unito](https://www.unito.io)
## Summary
<!-- One sentence describing what changed and why. -->
robots.txt at [comfy.org](https://comfy.org/) references
/sitemap-index.xml which returns 404. The actual working sitemap is at
/sitemap-0.xml (200, 38 URLs). This blocks search engines from
discovering the sitemap.
## Changes
- **What**: <!-- Core functionality added/modified -->
- Update robots.txt to point to /sitemap-0.xml, OR
## Review Focus
<!-- Critical design decisions or edge cases that need attention -->
<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->
## Screenshots (if applicable)
<!-- Add screenshots or video recording to help explain your changes -->
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11802-update-robots-txt-to-point-to-sitemap-0-xml-3536d73d365081bb9545eb96dd1e8025)
by [Unito](https://www.unito.io)
## Summary
Extract a `.github/actions/changes-filter` composite action and adopt it
across path-gated CI workflows, fixing the docs-only PR stall and
removing duplicated `paths:` / `paths-ignore:` filtering across 8
workflows.
## Background
Docs-only PRs stalled on required status checks because workflows using
`paths-ignore: ['**/*.md']` never created a check run, while branch
protection still required it. Observed on #11776 (the `test` check from
`ci-tests-unit.yaml` never appeared).
The fix pattern: keep the workflow triggered, gate downstream jobs on a
`changes` job whose outputs are computed from a path filter. Skipped
jobs count as passing under branch protection.
## What the action emits
| Output | Meaning |
|---|---|
| `should-run` | Any file outside `apps/`, `docs/`, `.storybook/`,
`**/*.md` changed. |
| `app-website-changes` | Shared deps or `apps/website/**` changed. |
| `app-desktop-changes` | Shared deps or `apps/desktop-ui/**` changed. |
| `app-frontend-changes` | Shared deps or `src/**` changed. |
| `packages-changes` | Shared deps or `packages/**` changed. |
| `storybook-changes` | Shared deps or `.storybook/**` changed. |
| `docs-changes` | `docs/**` or any `**/*.md` changed (deps NOT folded
in). |
| `dependency-changes` | Root `package.json`, `pnpm-lock.yaml`, or
`pnpm-workspace.yaml` changed. |
Shared deps are folded into every `app-*`, `packages-changes`, and
`storybook-changes` output so a lockfile bump correctly invalidates each
granular gate. Outputs default to `'true'` for non-`pull_request` events
to avoid the silent-skip footgun on push / merge_group.
## Workflows migrated
| Workflow | Gate | Notes |
|---|---|---|
| `ci-tests-unit.yaml` | `should-run` | Required check (`test`). Fixes
the original stall. |
| `ci-tests-e2e.yaml` | `should-run` | Required check (`e2e-status`).
Replaces inline filter. |
| `ci-perf-report.yaml` | `should-run` | Removes `paths-ignore`. |
| `ci-website-build.yaml` | `app-website-changes \|\| packages-changes`
| Refactor — not a required check, but moves to job-level gating. Filter
scope broadens from `packages/{design-system}` to all `packages/**`
(strictly safer). |
| `ci-website-e2e.yaml` | `app-website-changes \|\| packages-changes` |
Same restructure; `post-starting-comment` also gated to avoid spurious
"tests are running" when E2E is skipped. |
| `ci-dist-telemetry-scan.yaml` | `should-run` | New gate; was
previously running on every PR including docs-only. |
| `ci-oss-assets-validation.yaml` | `should-run` | Same. |
| `ci-size-data.yaml` | `should-run` | Preserves existing repository
guard on the new `changes` job. |
| `ci-tests-storybook.yaml` | `storybook-changes \|\|
app-frontend-changes \|\| packages-changes` | Gates 4 of 6 jobs.
`deploy-production` (push to main) left ungated;
`update-comment-with-chromatic` cascades naturally. |
## Branch protection (verified)
Required status checks on `main` and `core/**`/`cloud/**`: `test`,
`lint-and-format`, `e2e-status`. Only `test` and `e2e-status` use the
composite — `lint-and-format` correctly stays unfiltered (must run on
docs/apps too). The other 6 migrations are refactor wins (less wasted CI
on docs/apps-only PRs), not stall fixes.
## Changes
- **What**: New `.github/actions/changes-filter` composite + 8 workflow
migrations to consume it.
- **Breaking**: None.
- **Dependencies**: New pin on `dorny/paths-filter@de90cc6` — already
covered by `ci-validate-action-pins`.
## Review Focus
- The `should-run` filter excludes `.storybook/**` (granular
`storybook-changes` covers it instead). Storybook's gate combines all
three: `storybook-changes || app-frontend-changes || packages-changes`.
- Two `dorny/paths-filter` steps inside the composite —
`predicate-quantifier=every` is required for the negated globs in
`should-run` but breaks the multi-pattern OR filters.
- The website filter scope intentionally broadens from
`packages/{design-system,tailwind-utils}/**` to all `packages/**` for
consistency and safety.
Fixes#11776
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11785-ci-extract-changes-filter-composite-action-fix-docs-only-PR-stall-3526d73d36508172a1d7fe8c30fa6453)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
ZIP export toast now reports the total number of files instead of the
number of selected jobs when any selected job has multiple outputs.
## Changes
- **What**: In `downloadMultipleAssetsAsZip`
(`src/platform/assets/composables/useMediaAssetActions.ts`), compute
`fileCount` by summing each asset's `outputCount` metadata (fallback 1)
and pass it to `mediaAsset.selection.exportStarted` instead of
`assets.length`. The existing i18n string already handles `file`/`files`
plural.
- **Tests**: 3 new unit tests in `useMediaAssetActions.test.ts` covering
multi-output, single-output fallback, and mixed selections. The
`useToast` and `useI18n` mocks were lifted to hoisted refs so toast call
args are assertable.
## Review Focus
- Reduce uses `count > 1 ? count : 1`, mirroring the
`hasMultiOutputJobs` gate above so a known `outputCount === 1` is still
counted as 1 file (no double-counting).
- Only `downloadMultipleAssetsAsZip` is touched; OSS individual-download
path and direct-download path are unchanged.
Fixes#11736
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11737-fix-report-total-file-count-not-job-count-in-ZIP-export-toast-3516d73d3650811ba78dfdb0a0ae8ea1)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Fixes two visual bugs in the Desktop app at small window sizes: the
search bar getting pushed/clipped in modal headers, and autocomplete
suggestion dropdowns being cut off by `overflow-hidden` ancestors.
## Changes
- **`SearchAutocomplete.vue`**: Wrap `ComboboxContent` in
`ComboboxPortal` so the suggestions dropdown teleports to `<body>`,
escaping `overflow-hidden` ancestors (fixes z-index clipping in Manager
dialog and other modals using `BaseModalLayout`)
- **`BaseModalLayout.vue`**: Replace `shrink-0` with `min-w-0` on the
header content container so the search bar can shrink at narrow window
sizes instead of overflowing and being clipped by the modal root's
`overflow-hidden`
- **`GraphCanvas.vue`**: Fix dead code where the native drag
(`app-drag`) div was nested inside a `v-if="workflowTabsPosition ===
'Topbar'"` block with its own mutually exclusive condition — move it
before the block and add `pointer-events-auto` so Desktop window
dragging works when tabs are in Sidebar position
## Why no E2E tests
- **`SearchAutocomplete` portal**: The fix is structural (teleport to
`<body>`). A meaningful regression test would require opening the
Manager dialog with a real or mocked extension list — that is a
substantial standalone effort tracked in #11714.
- **`BaseModalLayout` header shrink**: A viewport-resize assertion would
test CSS layout behaviour, not application logic; it would be fragile
and low-value.
- **`GraphCanvas` app-drag**: Desktop/Electron-only.
`-webkit-app-region: drag` cannot be exercised in headless Chromium.
Unit tests for `SearchAutocomplete` cover the new code paths (portal
rendering, suggestion display, item selection).
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Low risk UI-only changes: adjusts layout CSS and combobox rendering
via `ComboboxPortal`, plus adds unit tests; no business logic or data
flow changes.
>
> **Overview**
> Fixes small-window Desktop UI issues where modal-header search inputs
could be clipped and autocomplete dropdowns could be cut off by
`overflow-hidden` ancestors.
>
> `SearchAutocomplete` now renders its suggestions list inside a
`ComboboxPortal` (teleporting the popper content outside clipping
containers) and adds a focused unit test suite covering empty/non-empty
suggestions, selection behavior, and `optionLabel` handling.
>
> `BaseModalLayout` tweaks header flexbox constraints (`min-w-0` on the
header content container) to allow the search bar to shrink instead of
overflowing.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
fd32d960f9. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
## Summary
Reverts #10849 (per-instance promoted widget value storage) and its
companion test-pinning PR #11697. The fix in #10849 caused regressions
in promoted-widget serialization (notably the Z-Image-Turbo template,
see #10146 follow-up). A replacement fix is being developed on
`fix/subgraph-promoted-widget-inline-state` and will land separately.
## Changes
- **Revert #11697** — drops the `it.fails`-marked tests that pin the
#10849 corruption symptom. With #10849 reverted, those markers would
falsely flip to passing.
- **Revert #10849** — removes per-instance `_instanceWidgetValues` map,
`_pendingWidgetsValues` configure-time hydration, the `widgets_values`
write path in `SubgraphNode.serialize()`, the `sourceSerialize` field on
`PromotedWidgetView`, the multi-instance Vitest suite, and the
multi-instance E2E test + asset.
- **Conflict resolution** in
`browser_tests/tests/subgraph/subgraphSerialization.spec.ts`: kept the
restored test coverage from #11579 (which is post-#10849 on main) and
removed only the now-unreachable multi-instance test, its helper, and
its workflow constant. Auto-merge with #11698 (`incrementVersion`) and
#11699 (ID type aliases) was clean.
## Review Focus
- Confirm no other on-main code path has come to depend on
`PromotedWidgetView.sourceSerialize` or
`SubgraphNode._instanceWidgetValues` since #10849 (grep is clean
locally).
- Confirm we want to land this revert before the replacement fix on
`fix/subgraph-promoted-widget-inline-state` is ready — this leaves the
original #10146 (multi-instance widget value collision) unfixed in the
meantime.
- The retained #11579 test coverage now exercises pre-#10849 behavior;
some of those assertions were written expecting the #10849 code path. CI
will surface any that need adjustment.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11790-revert-roll-back-10849-11697-per-instance-promoted-widget-values-3536d73d3650814094abd58b6b712d8d)
by [Unito](https://www.unito.io)
## Summary
A follow-up PR of
https://github.com/Comfy-Org/ComfyUI_frontend/issues/11106.
This PR only focus on `layoutMutations.ts`.
`layoutMutations.ts` is the central API for all node layout mutations in
the Vue renderer. It previously had zero test coverage despite
containing non-trivial logic such as guard clauses, ID normalization,
and z-index scanning. This PR addresses issue #11106 to prevent silent
regressions in node positioning and lifecycle.
## What was tested and how
All tests use the real `layoutStore` singleton (no mocks).
`initializeFromLiteGraph` resets node state before each test, and
results are verified via `getNodeLayoutRef().value`.
| Method | Tests | Logic covered |
| :--- | :--- | :--- |
| `moveNode` | 3 | **Guard** (missing node -> no-op); position written
to store; numeric ID coerced to string |
| `resizeNode` | 2 | **Guard**; size written to store |
| `setNodeZIndex` | 2 | **Guard**; zIndex written to store |
| `createNode` | 1 | Node becomes readable with the provided position
and size |
| `deleteNode` | 2 | **Guard**; node removed from store |
| `batchMoveNodes` | 4 | Empty array -> no-op; multiple nodes updated
**atomically**; existing size preserved; missing nodes skipped while
valid ones still update |
| `bringNodeToFront` | 1 | Target node ends up with a **higher zIndex**
than all other nodes |
## What was not tested and why
| Method | Reason skipped |
| :--- | :--- |
| `createLink` / `deleteLink` | `layoutStore` exposes no public API to
query link existence by ID; methods contain no logic beyond a straight
`applyOperation` call. |
| `createReroute` / `deleteReroute` / `moveReroute` | Same reason as
above. |
| `setSource` / `setActor` | Single-line delegation to `layoutStore`; no
logic to test. |
| Default value behavior in `createNode` | Avoids "change-detector"
tests; asserting hardcoded defaults adds no regression value and would
block valid product changes. |
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Test-only changes; no production logic is modified. Main risk is
potential flakiness due to reliance on singleton store state across
tests.
>
> **Overview**
> Adds a new `layoutMutations.test.ts` Vitest suite that exercises
`useLayoutMutations` against the real `layoutStore` singleton
initialized from LiteGraph data.
>
> Tests cover no-op guard clauses for missing nodes, node ID
normalization, position/size/z-index updates, node create/delete
behavior, `batchMoveNodes` semantics (empty input, skipping missing
nodes, preserving size), and `bringNodeToFront` z-index promotion
relative to other nodes.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
bb92de0b5b. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11313-test-add-unit-tests-for-useLayoutMutations-3446d73d36508134a289d6147244e710)
by [Unito](https://www.unito.io)
## Summary
Fix part of the
#https://github.com/Comfy-Org/ComfyUI_frontend/issues/11092
A total of 4 `// @ts-expect-error` directives were removed across 3
files — all caused by PrimeVue's legacy `$el` access pattern (`const
inputElement = inputRef.value.$el`) — by replacing PrimeVue with
Reka-based UI components. 1 corresponding unit test file was added.
## Changes
### `src/components/ui/input/Input.vue`
- **Exposed APIs**: Extended `defineExpose` to include `blur()` and
`setSelectionRange()`. This allows parent components to programmatically
control input behavior without direct DOM manipulation.
### `src/components/ui/textarea/Textarea.vue`
- **Exposed APIs**: Added `focus()` via `defineExpose`.
- **Cleanup**: Removed redundant attribute spreading (`...restAttrs`) to
lean on Vue’s default `$attrs` inheritance, making the component more
predictable.
---
## Refactored Feature Components
### `WidgetMarkdown.vue` (Note/Markdown Widgets)
- **Dependency Swap**: Replaced `primevue/textarea` with local
`Textarea.vue`.
- **Logic Simplification**: Simplified focus logic from
`textareaRef.value?.$el?.focus()` to a typed
`textareaRef.value?.focus()`.
- **Code Style**: Converted arrow functions to function declarations and
removed redundant section comments.
### `PromptDialogContent.vue` (Generic Prompt Dialogs)
- **Component Update**: Replaced PrimeVue `FloatLabel` and `InputText`
with a native `<label>` and local `Input.vue`.
- **Vue 3.5 Adoption**: Implemented **Reactive Destructuring** for
props.
- **Conflict Resolution**: Renamed internal `onConfirm` handler to
`handleConfirm` to prevent collision with destructured props.
### `EditableText.vue` (Node Titles & Sidebar Items)
- **Style Modernization**: Removed `<style scoped>` block in favor of
**Tailwind CSS** utility classes (e.g., `inline`, `w-full`).
- **Clean Implementation**: Replaced PrimeVue PassThrough (`:pt`) logic
with standard `@blur` and `v-bind` attributes.
---
## Testing & Quality Assurance
### Updated Tests
- **Redundancy Removal**: Cleaned up `EditableText.test.ts` and
`WidgetMarkdown.test.ts` by removing unused PrimeVue global
registrations. All 34 existing behavioral tests remain passing.
### New Coverage
- **`PromptDialogContent.test.ts`**: Added 3 new tests to verify:
1. Correct initialization with `defaultValue`.
2. Value persistence when clicking the Confirm button.
3. Form submission via the `Enter` key.
---
## Manual Test Screenshot
All functions have passed testing.
<img width="594" height="530" alt="test5"
src="https://github.com/user-attachments/assets/46a6b3b2-1855-414e-ac78-65668052ce50"
/>
<img width="1190" height="1074" alt="test4"
src="https://github.com/user-attachments/assets/89aa61ab-9401-44c2-9eae-9ca8761df675"
/>
<img width="1154" height="1028" alt="test3"
src="https://github.com/user-attachments/assets/3f63cfdf-8fbd-4dd3-9e42-dbebe4d8d421"
/>
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Moderate risk because it swaps underlying input/textarea components
and ref handling (focus/blur/selection) in interactive UI paths
(editable labels, prompt dialogs, markdown editor), which could subtly
change keyboard/blur behavior.
>
> **Overview**
> Refactors several Vue components to stop using PrimeVue
`InputText`/`Textarea` (and `$el` access) in favor of the project’s
`Input`/`Textarea` components, updating bindings/events and Tailwind
classes accordingly.
>
> Extends the shared `Input` to expose `blur`, `setSelectionRange`, and
`selectAll`, and updates `Textarea` to expose `focus`, enabling callers
to manage focus/selection without DOM internals.
>
> Adds a new unit test suite for `PromptDialogContent` and simplifies
existing tests by removing PrimeVue plugin/component setup; the groups
e2e test replaces a screenshot assertion with a functional visibility
check for the new title input.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9c97314d59. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11324-refactor-replace-PrimeVue-InputText-Textarea-with-project-UI-components-3456d73d36508109a18bc97a7d0487a7)
by [Unito](https://www.unito.io)
## Summary
Replace `vi.mock('vue-i18n')` stub with a real `createI18n` plugin
instance in `useReconnectingNotification` tests.
## Changes
- **What**: Add `setupComposable()` helper that renders a wrapper
component via `@testing-library/vue` with a real `createI18n` plugin.
Assertions now check translated values
(`'Reconnecting'`/`'Reconnected'`) instead of raw i18n keys. Removes the
brittle `vi.mock('vue-i18n')` stub.
## Review Focus
Straightforward test-only change — the composable requires a component
setup context for `useI18n()`, so we render a thin wrapper via
`@testing-library/vue` with the i18n plugin installed.
Fixes#11153
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11386-test-use-real-vue-i18n-plugin-in-useReconnectingNotification-tests-3476d73d3650814ba70eea6df91c8bbe)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Extracts the missing-model pipeline orchestration out of `ComfyApp` and
into an app-independent platform module, while tightening the
workflow-flattening type boundary that refresh needs when rescanning the
live LiteGraph graph.
This PR is intentionally refactor-heavy. It is the follow-up to the
earlier missing-model refresh work: instead of keeping refresh-specific
candidate recheck logic beside the UI, this change makes the refresh
path reuse the existing missing-model pipeline and removes the direct
dependency on private `ComfyApp` pipeline methods.
Linear: FE-499
Issues covered by this PR:
- Fixes#11678
- Fixes#11680
- Partially addresses #11679 by removing the missing-model refresh
path's unsafe `graph.serialize() as unknown as ComfyWorkflowJSON` cast
and replacing it with the narrower flattenable workflow contract.
Broader workflow serialization/type-boundary cleanup outside this
missing-model refresh path remains deferred.
## Changes
- **What**:
- Added `src/platform/missingModel/missingModelPipeline.ts` as the
orchestration module for missing-model detection/verification.
- `runMissingModelPipeline(...)` now owns the pipeline previously
embedded in `ComfyApp`:
- candidate scan and enrichment
- active ancestor filtering for muted/bypassed subgraph containers
- pending warning cache updates
- OSS folder path and file-size follow-up work
- cloud asset verification follow-up work
- surfaced missing-model errors via the existing execution error store
- `refreshMissingModelPipeline(...)` handles the refresh-specific flow:
- calls the injected `reloadNodeDefs()` first
- serializes the current live graph
- preserves model metadata by preferring active workflow `models`, then
falling back to current missing-model candidate metadata
- delegates back into the same pipeline used during workflow load
- Kept `ComfyApp` as the compatibility caller instead of the owner of
the pipeline.
- `loadGraphData(...)` now calls `runMissingModelPipeline(...)` with
`graph`, `graphData`, `missingNodeTypes`, and `silent` options.
- `refreshMissingModels(...)` is now a thin wrapper around
`refreshMissingModelPipeline(...)` and keeps the existing default
`silent: true` refresh behavior.
- The new pipeline module does not import `@/scripts/app`; app-owned
data/actions are passed in as inputs.
- Moved the workflow node-flattening helpers out of `workflowSchema.ts`
and into `src/platform/workflow/core/utils/workflowFlattening.ts`.
- This includes `flattenWorkflowNodes`, `buildSubgraphExecutionPaths`,
and `isSubgraphDefinition`.
- The move is intentional: these helpers are not zod schema definitions
or workflow validation logic. They are core workflow traversal utilities
used to flatten root workflow nodes plus nested subgraph definition
nodes into the execution-shaped node list needed by missing-model
scanning.
- The refresh path receives data from `LGraph.serialize()`, whose return
type is serialized LiteGraph data rather than validated
`ComfyWorkflowJSON`. Previously this forced unsafe typing like
`graph.serialize() as unknown as ComfyWorkflowJSON`.
- The new `FlattenableWorkflowGraph` / `FlattenableWorkflowNode`
structural contract describes only what flattening actually needs:
`nodes`, `definitions.subgraphs`, node `id`, `type`, `mode`,
`widgets_values`, and `properties`.
- This lets both normal workflow-load data (`ComfyWorkflowJSON`) and
refresh-time live graph serialization (`LGraph.serialize()`) flow into
the same scan/enrichment path without pretending serialized LiteGraph
output is a fully validated workflow schema document.
- Updated `missingModelScan.ts` to consume that minimal flattenable
workflow shape via `MissingModelWorkflowData`.
- `MissingModelWorkflowData` extends the flattenable workflow contract
with optional workflow-level `models` metadata.
- Removed now-unnecessary casts around execution IDs, flattened nodes,
and `widgets_values` object access.
- Updated `getSelectedModelsMetadata(...)` to accept readonly widget
value arrays so flattened workflow data can stay read-only.
- Reduced the exported surface of the new pipeline module after `knip`
flagged unused exported internal option/store interfaces.
- Kept `workflowSchema.ts` focused on validation schemas. The flattening
helpers are not re-exported from the schema module because they are
internal workflow core utilities, not public schema API.
- **Breaking**: None intended.
- Internal imports were updated to the new core utility path.
- This repo is not exposing these flattening helpers as a public package
API, so the old schema-local helper location is treated as an internal
implementation detail.
- **Dependencies**: None.
## Review Focus
- **Pipeline extraction / dependency direction**:
- Please verify that `missingModelPipeline.ts` stays independent from
`@/scripts/app`.
- `ComfyApp` should remain the caller/adapter, not the owner of
missing-model pipeline orchestration.
- **Workflow flattening type boundary**:
- The main type-cleanup goal is removing the refresh-time
`graph.serialize() as unknown as ComfyWorkflowJSON` lie.
- `LGraph.serialize()` and validated workflow JSON are not the same
contract. The new flattenable workflow contract is deliberately smaller
and structural because the missing-model enrichment path only needs
enough data to flatten nodes and read embedded model metadata.
- This is why the flattening helpers moved from `workflowSchema.ts` to
`workflow/core/utils`: the logic is reusable workflow traversal, not
validation schema.
- **Behavior preservation**:
- The PR is intended to preserve existing user-facing missing-model
behavior while moving ownership out of `app.ts`.
- Existing async follow-up behavior remains intentionally
fire-and-forget:
- cloud asset verification still surfaces after verification completes
- OSS folder paths still update asynchronously before surfacing
confirmed missing models
- file-size metadata fetching remains asynchronous
- More invasive behavior changes, such as adding non-cloud post-fetch
`isMissingCandidateActive(...)` re-verification or redesigning the
fire-and-forget result contract, are intentionally left for follow-up
work because they are not pure extraction.
- **Downloadable model metadata**:
- `missingModels` returned for download metadata now requires both `url`
and `directory`.
- Candidates without a directory still remain in `confirmedCandidates`,
but they are not exposed as downloadable model metadata. This keeps the
returned downloadable list aligned with what the download flow can
actually use.
- **Test ownership**:
- Complex missing-model pipeline behavior tests moved out of
`src/scripts/app.test.ts` and into
`src/platform/missingModel/missingModelPipeline.test.ts`.
- `app.test.ts` now only covers thin delegation for
`app.refreshMissingModels(...)`.
- Workflow flattening tests moved with the helper from schema tests into
`src/platform/workflow/core/utils/workflowFlattening.test.ts`.
- **Deferred follow-ups**:
- Broader function decomposition for cognitive complexity.
- Wider dependency-injection/port cleanup for stores and services beyond
the app boundary.
- Cloud-specific pipeline unit tests, which need a separate `isCloud`
mocking strategy.
- Additional E2E coverage expansion beyond the existing OSS refresh
path.
- More general workflow serialization/type-boundary cleanup outside the
missing-model refresh path.
## Validation
- `pnpm format`
- `pnpm lint`
- Passed. Existing lint output included a pre-existing
`no-misused-spread` warning and icon-name logs, but the command exited
successfully.
- `pnpm typecheck`
- `pnpm test:unit`
- `714 passed`, `9514 passed | 8 skipped`
- Pre-push `pnpm knip`
- Passed after reducing the exported surface of the new pipeline module.
## Screenshots (if applicable)
Not applicable. This PR is a pipeline/type-boundary refactor with no UI
changes.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11751-refactor-extract-missing-model-refresh-pipeline-3516d73d3650816d9245d4b1324b71c9)
by [Unito](https://www.unito.io)
---------
Co-authored-by: DrJKL <DrJKL0424@gmail.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
*PR Created by the Glary-Bot Agent*
---
## Summary
Adds new `plum` and `ink` color scales for Comfy Hub branding and
standardizes existing tokens to align with current Figma design system.
### Changes
**Phase 1 — New primitives** (`_palette.css`)
- Added `plum-300/400/500/600` and `ink-100` through `ink-900`
**Phase 2 — Token cleanup** (`style.css`)
- Removed deprecated `slate-100/200/300` primitives (cool blue-grey,
removed from Figma)
- Removed duplicate `graphite-400` (identical hex to slate-100)
- Dark mode: migrated 6 slate/graphite references to muted-foreground,
smoke-700, smoke-800, charcoal-200
- Light mode: replaced 3 `ash-500` references with `smoke-800` per
designer alignment
### Token migration detail
| Dark mode token | Old value | New value | Rationale |
|---|---|---|---|
| `--node-component-header-icon` | slate-300 (#5b5e7d) |
muted-foreground (smoke-800) | Figma `node/foreground-secondary` |
| `--node-component-slot-text` | slate-200 (#9fa2bd) | smoke-700
(#a0a0a0) | Lighter neutral for text contrast |
| `--node-component-surface-highlight` | slate-100 (#9c9eab) | smoke-800
(#8a8a8a) | Neutral grey highlight |
| `--node-component-tooltip-border` | slate-300 (#5b5e7d) | charcoal-200
(#494a50) | Consistent with dark border tokens |
| `--text-secondary` | slate-100 (#9c9eab) | smoke-700 (#a0a0a0) |
Adequate contrast on dark surfaces |
| `--widget-background-highlighted` | graphite-400 (#9c9eab) | smoke-800
(#8a8a8a) | Removed duplicate, neutral replacement |
### Visual note
These changes shift some dark mode colors from cool blue-grey to neutral
grey. This is intentional per the design team. The
`--node-component-surface-highlight` and
`--node-component-tooltip-border` tokens should be QA'd as the designer
noted.
### Not included (Phase 3)
Hub Dark overlay theme will ship separately once the Hub UI work is
ready to validate against.
## Screenshots


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11139-feat-add-plum-ink-color-primitives-and-standardize-design-tokens-33e6d73d365081418e13e0efe6161fb5)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Adds `.claude/skills/reviewing-unit-tests/SKILL.md`, a project-scoped
agent skill that gives reviewing agents a concrete rubric for evaluating
Vitest unit-test diffs.
## Changes
- **What**: New skill at `.claude/skills/reviewing-unit-tests/SKILL.md`.
Mirrors the structural template of the peer skill
`.claude/skills/writing-playwright-tests/SKILL.md` (frontmatter shape,
table-driven sections, terse prescriptive prose). Description triggers
on PR review of `*.test.ts` files. Codifies: alias-by-renaming
detection, `vi.mocked()` scope rule, unnecessary-cast detection,
divergence checks against `docs/testing/unit-testing.md` /
`store-testing.md` / `component-testing.md` / `vitest-patterns.md`,
redundant `mockClear()` flag, `vue-i18n`-must-not-be-mocked rule (with
`testI18n` reference), bugfix regression-validity check, leading-close
framing flag, testing-library requirement for new component tests.
## Review Focus
- **Retrospective validation against PR #11737** (head SHA
`4573d62450fd12ac6f06e5e491f8af84ccbd27de`, file
`src/platform/assets/composables/useMediaAssetActions.test.ts`).
Mentally running the skill's checklist against that diff flags:
1. ✅ `getToastAddMock` / `getI18nTMock` module-level helpers — caught by
the "Renaming ≠ Restructuring" section + Mocking Smells row
"Module-level helper functions wrapping mocked composable returns".
2. ✅ `as ReturnType<typeof vi.fn>` casts in those helpers — caught by
the "`vi.mocked()` Scope" section + Mocking Smells row on stray casts.
3. ✅ `vi.mock('vue-i18n', ...)` — caught by Mocking Smells row directing
to mount real `createI18n` per `docs/testing/vitest-patterns.md` and the
shared `testI18n` in
`src/components/searchbox/v2/__test__/testUtils.ts`.
4. ✅ `vi.mock('primevue/usetoast', ...)` — explicitly **not** flagged.
The "Distinguish" paragraph after the Mocking Smells table marks
trivially-shaped third-party hooks as acceptable to mock; "Don't Mock
What You Don't Own" applies to behavior-rich APIs, not single-method
composables.
- **Reference style**: links to `docs/testing/*.md` and
`docs/guidance/typescript.md` instead of restating their content, per
the FE-511 acceptance criterion.
- **Open question**: scope. The repo-local skill is the FE-511
deliverable; equivalent prescriptive rules already live in the user's
global `~/.config/amp/AGENTS.md` "Code Review Rigor" section as a
separate artifact. No duplication intended — the skill is the
project-scoped surface a reviewing agent loads when entering this repo.
Closes FE-511
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11777-docs-add-reviewing-unit-tests-skill-3526d73d365081848759e6a8fab942f0)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Document that `vue-i18n` should not be mocked in tests — mount with a
real `createI18n` plugin instance instead.
## Changes
- **What**: Expanded `docs/testing/vitest-patterns.md` "Don't Mock
`vue-i18n`" section with a concrete code example (covering both
component and composable tests), guidance for asserting on translation
keys with empty messages, and a real-example link to
`src/components/searchbox/v2/__test__/testUtils.ts`. Added a callout at
the top of `docs/testing/unit-testing.md` "Mocking Composables with
Reactive State" cross-linking the new section, since that section
applies to *owned* composables.
## Review Focus
- The previous `vitest-patterns.md` paragraph pointed at a non-existent
`SearchBox.test.ts`; the new link points to the actual shared `testI18n`
helper.
- The "Mocking Composables with Reactive State" pattern should not be
applied to third-party composables like `useI18n` — the callout makes
that explicit.
Surfaced during review of #11737, where the test file mocked `vue-i18n`
and then accumulated structural workarounds (hoisted aliases, helper
functions, type casts) to interact with the mocked `t`. A real
`createI18n` instance avoids the entire ceremony.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11776-docs-prefer-real-createI18n-over-mocking-vue-i18n-in-tests-3526d73d365081d4bc39fbf3c2502e49)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Adds PostHog page analytics to the marketing website (`apps/website/`).
## Changes
- **What**: New `posthog.ts` script with
`initPostHog`/`capturePageview`. Wired into `BaseLayout.astro` behind
`import.meta.env.PROD` (mirroring the GTM gate). Pageviews are captured
on every `astro:page-load` so ClientRouter view-transition navigations
are tracked, not just hard reloads.
- **Dependencies**: `posthog-js` (already in the workspace catalog at
`^1.358.1`; previously used by the workbench telemetry provider).
## Review Focus
- API host is set to `https://t.comfy.org` to match
`src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts` —
confirm that proxy is OK to share with the website surface.
- Project token is hard-coded inline. It is a public `phc_…` frontend
token (designed to ship to clients); this matches the pattern used for
`gtmId` in the same file. Happy to switch to a `PUBLIC_POSTHOG_KEY` env
var if preferred.
- `person_profiles: 'identified_only'` to avoid creating profiles for
every anonymous visitor — flag if you want full anonymous tracking
instead.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11735-feat-website-add-PostHog-analytics-3516d73d3650811189c6d64c3af4ded9)
by [Unito](https://www.unito.io)
## Summary
Embed the HubSpot-hosted contact sales form on `/contact` and
`/zh-CN/contact` so HubSpot owns submission handling, validation,
anti-spam/security updates, tracking context, and form configuration.
## Changes
- **What**: Replaces the custom local contact form stub with a HubSpot
embed component using the HubSpot-hosted developer/raw HTML script and
`hs-form-html` container.
- **Localization**: Uses the existing English form ID by default and
switches to the zh-CN form ID for `/zh-CN/contact`.
- **Styling**: Applies HubSpot form CSS custom properties to match the
Comfy contact page colors and `PP Formula` font more closely.
- **Docs**: Updates the website README with the developer embed snippet
and the zh-CN form ID.
- **Dependencies**: None.
## Why Embedded Form
- HubSpot docs say forms should be loaded with the HubSpot-hosted
JavaScript file, and that security, anti-spam, accessibility, and
performance improvements will not propagate if the embed runtime is
copied or self-hosted:
https://developers.hubspot.com/docs/cms/start-building/building-blocks/modules/forms
- The direct form submission endpoint is documented under `v3 legacy`:
https://developers.hubspot.com/docs/api-reference/legacy/marketing/forms/v3-legacy/submit-data-unauthenticated
- HubSpot’s legacy API overview says numeric-versioned APIs are legacy
and will be deprecated in a future release:
https://developers.hubspot.com/docs/api-reference/legacy/overview
## Review Focus
- Confirm the portal ID and form IDs are correct:
- `en`: `94e05eab-1373-47f7-ab5e-d84f9e6aa262`
- `zh-CN`: `6885750c-02ef-4aa2-ba0d-213be9cccf93`
- Check visual fit on `/contact` and `/zh-CN/contact`, especially font,
background, inputs, radio controls, and submit button.
- Confirm the developer/raw HTML embed remains the preferred integration
over a custom Forms API POST.
## Local Checks
- `pnpm exec oxfmt --check
apps/website/src/components/contact/HubspotFormEmbed.vue
apps/website/README.md`
- `pnpm exec eslint
apps/website/src/components/contact/HubspotFormEmbed.vue`
- `pnpm --filter @comfyorg/website typecheck`
- `pnpm --filter @comfyorg/website test:unit`
- `pnpm --filter @comfyorg/website build`
- Commit hooks: stylelint, oxfmt, oxlint, eslint, `pnpm typecheck`,
`pnpm typecheck:website`
- Push hook: `pnpm knip --cache`
Build completed with existing non-fatal environment warnings: Node
`v25.8.1` vs requested `24.x`, unresolved website font paths deferred to
runtime, `transformWithEsbuild` deprecation, and missing Ashby env
falling back to the committed snapshot.
Incredibly, during the taking of these screenshots, I discovered a bug
in macos, where despite the snapshot/record bar not existing, from me
esc'ing out, some of the tooltips persist. Closing and reopening the lid
did not fix this. I didn't see the process in activity monitor.
<img width="1512" height="862" alt="Screenshot 2026-04-29 at 7 09 55 PM"
src="https://github.com/user-attachments/assets/92518795-6845-4b34-8da3-54048b440eb1"
/>
<img width="1512" height="862" alt="Screenshot 2026-04-29 at 7 13 18 PM"
src="https://github.com/user-attachments/assets/f7609e58-898d-413c-975c-b02b70b84e73"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11723-fix-embed-HubSpot-contact-form-3506d73d365081528bfbe4b024c2a21f)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
*PR Created by the Glary-Bot Agent*
---
## Summary
Fixes a false-positive knip failure in the `i18n: Update Core` workflow
that's been blocking version-bump PRs (e.g. #11769, run
[25141091787](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25141091787/job/73690747401)).
## Root cause
1. The `update-locales` job uses `setup-comfyui-server`, which copies
`tools/devtools/*` → `ComfyUI/custom_nodes/ComfyUI_devtools/*` at the
workspace root.
2. The job's final `git push` triggers the husky `.husky/pre-push` hook,
which runs `pnpm knip`.
3. Knip's `project` glob (`**/*.{js,ts,vue}` in `knip.config.ts`) sweeps
the runtime `ComfyUI/` directory; the existing `tools/devtools/web/**`
ignore doesn't cover the copied path, so
`ComfyUI/custom_nodes/ComfyUI_devtools/web/legacyWidget.js` (added in
#11574) is reported as unused → exit 1 → push aborted.
## Why fix the hook (not knip config or workflows)
- **`knip.config.ts` ignore** (`'ComfyUI/**'`): tried first;
`treatConfigHintsAsErrors: true` makes the unused-pattern hint fatal in
normal CI runs where `ComfyUI/` doesn't exist, breaking `ci-lint-format`
everywhere.
- **`HUSKY=0` per workflow step**: works, but bots can't push workflow
file changes without `workflows` scope, and it would need to be repeated
on every CI workflow that pushes.
- **Skip the hook in CI**: the canonical `pnpm knip` check runs in
`.github/actions/lint-format-verify/action.yml` on every PR, so the
pre-push duplicate is pure dev-side guardrail. Gating it on `$CI` (set
to `true` by all GitHub Actions runners) cleanly removes it from every
CI push without affecting local developer workflow.
## Verification
Reproduced the failure locally by mirroring the CI sequence (`cp -r
tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/` then `pnpm
knip:no-cache`):
- Without fix: `[log] Unused files (1)
ComfyUI/custom_nodes/ComfyUI_devtools/web/legacyWidget.js` → exit 1
(matches CI log)
- With `CI=true`, the hook short-circuits at `exit 0` before invoking
knip
- `pnpm knip` on a clean checkout (no `ComfyUI/`) still passes
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11772-chore-husky-skip-pre-push-knip-hook-in-CI-3526d73d3650819f9a01d549d122b69c)
by [Unito](https://www.unito.io)
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
Allows the website screenshot update workflow to remove its own trigger
label when a label-triggered run completes.
## Changes
- **What**: Grants the screenshot update job `issues: write`, which is
required for `issues.removeLabel`, and keeps the cleanup scoped to
`Update Website Screenshots`.
- **Dependencies**: None.
## Review Focus
Confirm the workflow permission scope is appropriate and that unexpected
label cleanup failures should fail the workflow instead of being
silently swallowed.
## Validation
- `/Users/ben/go/bin/actionlint
.github/workflows/pr-update-website-screenshots.yaml`
- `/Users/ben/Library/Python/3.9/bin/yamllint --config-file .yamllint
.github/workflows/pr-update-website-screenshots.yaml`
- `git diff --check HEAD~1 HEAD`
No browser e2e regression was added because this change only adjusts
GitHub Actions token permissions and label cleanup behavior; it does not
change shipped app/runtime behavior.
Fixes FE-487
## Summary
Stabilize the website nav GitHub star count in visual-test builds so
snapshot comparisons do not drift as the live GitHub count changes.
## Changes
- **What**: Added `WEBSITE_GITHUB_STARS_OVERRIDE` for build-time
star-count overrides and set it to `111000` in the website E2E and
screenshot-update workflows.
- **Dependencies**: None.
## Review Focus
Confirm the deterministic build-time override is preferable to
screenshot masking, since Playwright masks draw a colored rectangle
whose geometry can also drift when masked content changes size.
## Screenshots (if applicable)
Not included; this keeps visual-test input data stable rather than
changing the UI.
## Summary
Part of the `useBrushDrawing` decomposition plan (PR C). Extracts brush
size/hardness adjustment logic (Alt+drag interaction) into a dedicated
`useBrushAdjustment` composable. No runtime behavior is changed — pure
structural refactor.
## Changes
- **New** `src/composables/maskeditor/useBrushAdjustment.ts` —
encapsulates `startBrushAdjustment` and `handleBrushAdjustment`,
including dead zone filtering, dominant axis suppression, and
size/hardness clamping
- **New** `src/composables/maskeditor/useBrushAdjustment.test.ts` — unit
tests covering: no-op before start, dead zone suppression, size increase
on drag, size/hardness clamping, dominant axis lock
- **Updated** `src/composables/maskeditor/useBrushDrawing.ts` — removes
inlined adjustment state and functions, delegates to
`useBrushAdjustment(initialSettings)`
## Test Functionality
Open ComfyUI and enter the MaskEditor of any image node. On the canvas,
Alt + Right-click Drag:
- Drag Right → Increase brush size - pass
- Drag Left → Decrease brush size - pass
- Drag Up → Increase hardness - pass
- Drag Down → Decrease hardness - pass
https://github.com/user-attachments/assets/273e8383-dab5-4c82-ac7b-0a1534dfd770
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Touches core pointer-interaction logic for brush tuning and changes
adjustment behavior (removes delta saturation and uses initial values),
which could subtly affect UX even though scope is localized to the mask
editor.
>
> **Overview**
> Extracts the Alt-drag brush size/hardness adjustment logic out of
`useBrushDrawing` into a new `useBrushAdjustment` composable, and wires
`useBrushDrawing` to delegate to it.
>
> The extracted logic now bases adjustments off the captured initial
brush size/hardness and removes prior delta capping (no ±100px
saturation), which changes how large/continuous drags affect the final
values. Adds a Vitest suite covering dead-zone behavior, dominant-axis
suppression, clamping, and the no-op-before-start contract.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
b2f0ce22d3. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11544-refactor-extract-useBrushAdjustment-from-useBrushDrawing-34a6d73d365081a48897dd77b04ef56b)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Add `name` attributes to the signup form's email, password, and
confirm-password inputs so Chrome's password manager recognizes the form
and offers autofill/save.
## Changes
- **What**: Pass `name` through to the underlying `<input>` on the email
field (via `pt:root:name`) and on both password fields (via
`pt:pc-input-text:root:name`). Without `name`, Chrome can't pair the
email with the password and won't surface the save-password /
suggest-strong-password prompts.
## Review Focus
- The PrimeVue passthrough syntax (`pt:root:*` for `InputText`,
`pt:pc-input-text:root:*` for `Password`) lands the attribute on the
actual `<input>` element — verified in DevTools.
- `confirm-password` is not a standard `autocomplete` token; we keep
`autocomplete="new-password"` on both password fields and only
differentiate via `name`.
## Screenshots (if applicable)
https://github.com/user-attachments/assets/e1e25ab5-8496-4c84-b5f1-630f31956c80
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11636-fix-enable-Chrome-password-autofill-on-signup-form-34e6d73d36508180882cc9ebafb58417)
by [Unito](https://www.unito.io)
## Summary
- Tighten the new `useCoreCommands` help command tests to assert the
exact external URL opened for GitHub issues and Discord.
## Testing
```bash
pnpm test:unit -- src/composables/useCoreCommands.test.ts
pnpm format:check src/composables/useCoreCommands.test.ts
```
Also passed pre-commit `pnpm typecheck` and push hook `pnpm knip`.
Stacked on #11748.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11768-test-assert-core-command-help-urls-3516d73d365081de99d0c71f707d0fb4)
by [Unito](https://www.unito.io)
---------
Co-authored-by: dante01yoon <bunggl@naver.com>
## Summary
Add 35 unit tests for `SceneManager`, the largest remaining gap in the
load3d core (452 LOC). Targets only the logic-bearing methods
(background mode dispatch, render-mode switching, aspect-ratio scaling,
capture pipeline, dispose). Renderer-passthrough internals are
intentionally left to E2E. Follow-up to Tier 1 (#11733), Tier 2, Tier
3a, and Tier 3b.
## Changes
- **What**: 35 new tests covering construction (main scene + background
scene + grid + tiled mesh + default color mode), `toggleGrid`,
`setBackgroundColor` (color update, scene-bg cleanup, panorama-demote,
prior-texture dispose), `setBackgroundImage` (empty-path fallback,
loading-start emit, temp/output subfolder rewrite, /api prefix,
tiled-mesh material swap, panorama scene-background promotion,
prior-texture dispose, error-path fallback), `removeBackgroundImage`,
`setBackgroundRenderMode` (no-op same-mode, color-only emit, image
panorama-promote, image tiled-demote), `updateBackgroundSize`
(no-texture/no-mesh/no-map guards, wide vs. tall image scaling),
`handleResize` (image-bg active vs. color-only),
`getCurrentBackgroundInfo`, `captureScene` (returns 3 data URLs +
restores renderer state, restores grid visibility, propagates errors),
and `dispose` (resource cleanup + scene-background null).
## Review Focus
- **Coverage**: `SceneManager.ts` 89.5% lines / 74.2% branches / 89.5%
funcs. Uncovered lines are concentrated in `renderBackground` and the
deep mesh-traversal loop inside `captureScene` — exactly the
renderer-passthrough territory deferred per the Tier 3c plan.
- **`THREE.Material.needsUpdate` is a write-only setter** in THREE.js —
reading returns `undefined`. The "demote panorama → tiled" test asserts
`mat.map === texture` instead of `mat.needsUpdate === true`, with a
comment explaining why.
- **happy-dom canvas `clientWidth`/`clientHeight` default to 0** —
`makeRenderer()` overrides them via `Object.defineProperty` so
production code reading `renderer.domElement.clientWidth` gets the test
value.
- **`THREE.TextureLoader` is mocked via `vi.mock('three', ...)` with
`importOriginal`**, matching the pattern in `RecordingManager.test.ts`
and `HDRIManager.test.ts`. `mockTextureLoad` is hoisted so each test can
resolve/reject the load callback independently.
- **`vi.spyOn(manager, 'setBackgroundColor')` in three places** to
assert internal delegation (empty-path fallback, error fallback,
`removeBackgroundImage`). Defensible because the delegation IS the
documented contract for these methods.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11762-test-load3d-add-unit-tests-for-SceneManager-3516d73d365081628ff4c146defebac1)
by [Unito](https://www.unito.io)
## Summary
Add 19 unit tests for `Load3dService.copyLoad3dState` (the one method
intentionally deferred from Tier 1). Brings `load3dService.ts` from
54.5% to 100% line coverage. Follow-up to Tier 1 (#11733), Tier 2, and
Tier 3a.
## Changes
- **What**: 19 new tests covering every branch of `copyLoad3dState`:
no-source-model fast path, splat fast path (with and without
`originalURL`), mesh path (existing-target-model removal, SkeletonUtils
clone, originalModel/material/upDirection/texture copy, initial
transform on clone, gizmo transform application, gizmo enable/disable
across both source and target prior states, animation copy when
present/absent), background-image vs. background-color dispatch,
light-intensity falsy fallback, perspective-vs-orthographic FOV gating,
and the always-detach + setupForModel gizmo contract.
## Review Focus
- **Coverage**: `load3dService.ts` lines 54.5% → **100%**, branches 50%
→ **90.9%**, funcs 88.9% → **100%**. Remaining uncovered lines are minor
(`loadSkeletonUtils` cache-hit path, a couple of null-map early
returns).
- **Test fixtures use real `THREE.Object3D` and `THREE.Scene`** so
production code's `.position.set(...)`, `.rotation.set(...)`,
`scene.add/remove` calls work without further stubbing.
- **`makeTarget` memoizes the gizmo manager** (`getGizmoManager: () =>
gizmoManager` rather than returning a fresh literal each call).
Production code calls `getGizmoManager()` multiple times; without
memoization, the `detach` and `setupForModel` mocks would be
unobservable from tests.
- **`state` return on `makeTarget`** exposes mutable `modelManager`,
captured `gizmoManager`/`animationManager`, and
`sceneAdded`/`sceneRemoved` arrays so tests can assert post-state
directly without casts through the production-typed `Load3d` interface.
- **Background-image test uses `createMockLGraphNode({ id, properties
})` overrides** rather than mid-test property mutation.
- **Destructuring-default gotcha**: `const { lightsIntensity = 0.8 } =
overrides` applies the default even when `undefined` is passed
explicitly. The "fallback to setLightIntensity(1)" test passes `0`
instead — production code's `intensity || 1` short-circuits the same
way.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11761-test-load3d-add-unit-tests-for-copyLoad3dState-in-load3dService-3516d73d36508142bc72d97b27b0a36b)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for the three remaining small/wrapper modules in the
load3d domain (`EventManager` pub/sub, `ViewHelperManager` ViewHelper
wrapper, and `load3dLazy` lazy-extension loader). Follow-up to Tier 1
(#11733) and Tier 2.
## Changes
- **What**: 28 new unit tests across 3 files covering EventManager
add/remove/emit semantics, ViewHelperManager container DOM creation +
pointer-event interception + animation-finished camera-state emission +
perspective/orthographic zoom snapshotting + dispose ordering, and
load3dLazy extension registration + 3D-node-type recognition +
Load3D-specific `mesh_upload` injection + `beforeRegisterNodeDef` hook
replay for newly registered extensions.
## Review Focus
- **Coverage** (lines/branches/funcs): EventManager 100% / 100% / 100%,
ViewHelperManager 100% / 83.3% / 72.7%, load3dLazy 95.8% / 80% / 100%.
Aggregate: **98.6% / 85.3% / 85.7%**.
- **`vi.mock` factory side-effects only fire once per test file** —
`load3dLazy.test.ts` originally tried to count dynamic imports of
`./load3d` and `./saveMesh` via spies inside the mock factory, but
factories aren't re-invoked across `vi.resetModules()`. Switched to
verifying observable side effects (`enabledExtensions` getter call
counts, `beforeRegisterNodeDef` replay invocations).
- **Snapshot-vs-diff `enabledExtensions` queue** in
`load3dLazy.test.ts`: production code does `before = new
Set(enabledExtensions); await imports; diff =
enabledExtensions.filter(!before.has)`. To exercise the replay branch,
the mock returns `[]` first (for `before`) and `[newExtension]` second
(for the post-import snapshot) via `mockReturnValueOnce` queueing.
- **`MockViewHelper` is defined inside the `vi.mock()` factory** rather
than `vi.hoisted()`, matching the `GizmoManager.test.ts:15-41`
convention (hoisted handles only, classes inside the factory).
- **PointerEvent propagation tests** require the production code's
`event.stopPropagation()` to actually keep events from bubbling to the
parent in happy-dom; the parent listener gets attached and
asserted-not-called.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11760-test-load3d-add-unit-tests-for-EventManager-ViewHelperManager-and-load3dLazy-3516d73d3650814bb2dac3360ab0d2a1)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for the four Tier 2 untested logic modules in the load3d
domain (`LightingManager`, `ControlsManager`, `exportMenuHelper`, and
`ModelExporter`). Follow-up to the Tier 1 PR (#11733).
## Changes
- **What**: 43 new unit tests across 4 files covering light
setup/intensity scaling/HDRI mode/disposal, OrbitControls construction
(including DOM-parent fallback) and camera-state event emission, the
export submenu builder (item structure, submenu opening, format
dispatch, success/error toasts), and the static `ModelExporter` (URL
parsing, direct-URL fast paths for matching extensions, GLB/OBJ/STL
serialization branches, error toast paths).
## Review Focus
- **Coverage** (lines/branches/funcs): LightingManager 100% / 50% /
90.9%, ControlsManager 100% / 100% / 87.5%, exportMenuHelper 100% / 100%
/ 100%, ModelExporter 98.4% / 95.7% / 100%. Aggregate: **99.2% / 93.5% /
95.1%**.
- **`vi.mock(import('@/lib/litegraph/src/litegraph'), ...)` in
`exportMenuHelper.test.ts`** uses the dynamic-import form so
`importOriginal()` is auto-typed. Required because `apiSchema.ts`
transitively imports `LinkMarkerShape` from the same module — replacing
the whole module breaks the build. The mock replaces only
`LiteGraph.ContextMenu` in-place on the real singleton.
- **`MockContextMenu` must be a class**, not an arrow function —
production code does `new LiteGraph.ContextMenu(...)`. Initial
arrow-function mock failed with "is not a constructor".
- **Fake-timer rejection pattern in `ModelExporter.test.ts`**: rejection
assertions are attached *before* `vi.runAllTimersAsync()` (`const
assertion = expect(p).rejects.toThrow(...); await drain; await
assertion`) to avoid unhandled-rejection warnings.
- **Surprising `detectFormatFromURL` behavior**:
`detectFormatFromURL('?filename=cube')` returns `'cube'`, not `null`,
because `'cube'.split('.').pop()` returns the whole basename when no dot
is present. Test documents this rather than asserting an incorrect
expectation.
- **Two unreachable lines left uncovered**: `LightingManager:65` (`?? 1`
fallback in the `setLightIntensity` multiplier lookup — every light is
registered in the map at construction, so the fallback is dead) and
`ModelExporter:21` (a `try/catch` around `URLSearchParams` whose
constructor cannot throw on the inputs the production code passes).
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11758-test-load3d-add-unit-tests-for-LightingManager-ControlsManager-exportMenuHelper-and-3516d73d365081cb96fff33672503822)
by [Unito](https://www.unito.io)
## Summary
Adds 8 tests across three new describe blocks for
\`src/composables/useCoreCommands.ts\`:
- **Canvas view**: \`Comfy.Canvas.ResetView\`, \`Comfy.Canvas.ZoomIn\`,
\`Comfy.Canvas.ZoomOut\`.
- **Workflow lifecycle**: \`Comfy.OpenClipspace\`,
\`Comfy.RefreshNodeDefinitions\`.
- **Help**: \`Comfy.Help.OpenComfyUIIssues\`,
\`Comfy.Help.OpenComfyOrgDiscord\`, \`Comfy.Help.AboutComfyUI\`.
Adds \`vi.hoisted\` mocks for \`useTelemetry\`, \`useSettingsDialog\`,
and \`useLitegraphService.resetView\` so they remain isolated from the
existing 15-test suite.
## Why this slice
\`useCoreCommands.ts\` exports 118 distinct command callbacks (1356
LOC). A single coverage-backfill PR for the whole file would be unwieldy
and risk merge conflicts (this file is touched frequently). This PR
covers a coherent slice — view/lifecycle/help commands — and follow-up
PRs can pick off remaining clusters.
## Testing
\`\`\`bash
pnpm vitest run src/composables/useCoreCommands.test.ts
\`\`\`
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11748-test-add-unit-tests-for-useCoreCommands-canvas-help-commands-3516d73d365081c384ffcc72c15dfd47)
by [Unito](https://www.unito.io)
When in app mode, widgets can be drawn with size different from the size
of the parent node. Mouse events on legacy canvas widgets require that
the client code query the current state of the node and widget to
determine if any elements are being interacted with. This PR sets the
`widget.width` property when a legacy canvas widget draw operation
occurs so that custom nodes can properly resolve subsequent mouse
events.
At current, no core nodes exist that utilize legacy widgets. As a result
the setup code to test this bug fix is slightly involved.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11574-Fix-legacy-widget-width-in-app-mode-34b6d73d365081caaa34c6204f8361f6)
by [Unito](https://www.unito.io)
## Summary
Adds 22 new tests for \`src/stores/executionStore.ts\`, raising coverage
from **48.7% → 82.9%** lines (functions 47% → 72.5%). Tests drive each
WebSocket handler by capturing handlers registered through the mocked
\`api.addEventListener\` and dispatching CustomEvents at them.
## Test Coverage
WebSocket handlers (driven through \`bindExecutionEvents\`):
- \`execution_start\` — sets activeJobId, seeds queued job entry, clears
initializing state for the starting job.
- \`execution_cached\` — marks listed nodes; no-op when no active job.
- \`execution_interrupted\` — clears active job state.
- \`executed\` — marks executed node; no-op when no active job.
- \`execution_success\` — clears active job and progress state.
- \`executing\` — clears \`_executingNodeProgress\` and activeJobId on
null detail.
- \`progress\` — sets \`_executingNodeProgress\`.
- \`status\` — reads clientId once and stops listening.
- \`execution_error\` — service-level (no node_id) routes to
\`lastPromptError\`; runtime errors route to \`lastExecutionError\`.
- \`notification\` — marks job as initializing on "Waiting for a
machine"; ignores empty id and unrelated text.
Other:
- \`unbindExecutionEvents\` removes every listener registered.
- \`storeJob\` populates queuedJobs, jobIdToWorkflowId,
jobIdToSessionWorkflowPath.
- \`registerJobWorkflowIdMapping\` ignores empty inputs.
- \`ensureSessionWorkflowPath\` is idempotent and updates on change.
## Testing
\`\`\`bash
pnpm vitest run src/stores/executionStore.test.ts
pnpm vitest run src/stores/executionStore.test.ts --coverage
--coverage.include='src/stores/executionStore.ts'
\`\`\`
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11746-test-add-unit-tests-for-executionStore-WebSocket-handlers-3516d73d3650810aa910f5a022fdc17b)
by [Unito](https://www.unito.io)
## Summary
Extends \`src/scripts/pnginfo.test.ts\` with 5 new tests covering the
format-specific delegating wrappers and the safetensors metadata reader.
Lifts pnginfo.ts coverage from **17.6% → 23.2%** lines (the remaining
gap is \`importA1111\`, which needs a refactor before it can be tested
cleanly — left to a follow-up).
## Test Coverage
- \`getPngMetadata\`, \`getFlacMetadata\`, \`getAvifMetadata\` delegate
to their respective \`metadata/*\` modules (mocked).
- \`getLatentMetadata\` returns the \`__metadata__\` object from a
hand-built safetensors header.
- \`getLatentMetadata\` resolves \`undefined\` when the header has no
\`__metadata__\` entry.
## Out of scope
\`importA1111\` (lines 176-542) is a 270-line A1111-prompt →
ComfyUI-graph builder. Testing it requires either heavy LiteGraph mocks
or a refactor that extracts pure parsing helpers from the graph-mutation
code. Tracking separately.
## Testing
\`\`\`bash
pnpm vitest run src/scripts/pnginfo.test.ts
pnpm vitest run src/scripts/pnginfo.test.ts --coverage
--coverage.include='src/scripts/pnginfo.ts'
\`\`\`
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11745-test-add-unit-tests-for-pnginfo-wrappers-and-getLatentMetadata-3516d73d365081c080a6c8146aa1bee8)
by [Unito](https://www.unito.io)
## Summary
Adds 12 tests for `src/scripts/metadata/avif.ts`, raising line coverage
from **2.3% → 90.4%** (statements 88.5%, functions 93.3%).
## Test Coverage
Happy paths:
- Workflow JSON extracted from EXIF Exif item (LE)
- Prompt JSON extracted
- Big-endian (MM) EXIF parsing
- Both prompt and workflow present in separate EXIF entries
Negative paths (each yields `{}` without throwing):
- AVIF major brand is not "avif"
- Meta box missing
- iinf has no Exif item
- EXIF entry uses an unrecognized key
- EXIF entry has malformed JSON
- infe version is unsupported (1)
- iloc box missing while iinf has Exif
- Buffer too short for valid header
## Testing
\`\`\`bash
pnpm vitest run src/scripts/metadata/avif.test.ts
pnpm vitest run src/scripts/metadata/avif.test.ts --coverage
--coverage.include='src/scripts/metadata/avif.ts'
\`\`\`
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11744-test-add-unit-tests-for-avif-metadata-parser-3516d73d365081c5b29adf7a2b9eff62)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for the four largest untested logic-heavy modules in the
load3d domain (`AnimationManager`, `CameraManager`, `RecordingManager`,
and the `load3dService` façade).
## Changes
- **What**: 78 new unit tests across 4 files covering animation
lifecycle (mixer setup, clip switching, play/pause/seek, dispose),
camera state (perspective↔orthographic toggling, FOV gating, state
round-trip, controls rebinding, resize math, model fitting), recording
lifecycle (MediaRecorder wiring, indicator visibility, chunk handling,
export/clear paths, dispose ordering), and the service singleton
(sync/async map access, viewer cache, `handleViewerClose` apply-changes
flow, `handleViewportRefresh` camera-toggle dance).
## Review Focus
- **Coverage**: AnimationManager 100%, CameraManager 88.8%,
RecordingManager 89.1%, load3dService 54.5% lines / 88.9% functions. The
service gap is concentrated in one method — `copyLoad3dState` (lines
217-333) — which is entangled with 3-4 subsystems and is intentionally
deferred to a follow-up PR.
- **happy-dom shims** in `RecordingManager.test.ts`: `MediaRecorder`,
`HTMLCanvasElement.prototype.captureStream`, and `getContext('2d')` are
all stubbed because happy-dom doesn't provide them.
`THREE.TextureLoader` is also mocked because the constructor eagerly
loads an SVG indicator.
- **Singleton state reset** in `load3dService.test.ts`: the service
holds a module-level `viewerInstances` Map that can't be reached from
outside. Tests track every node they create in a `Set` and drain via
`removeViewer` in `beforeEach`. Cleaner than `vi.resetModules()` (which
would re-import the service and break the singleton identity).
- **One subtle THREE behavior**: `AnimationManager.setAnimationTime`
clamps to `duration`, but `AnimationMixer.setTime(duration)` with
`LoopRepeat` wraps `action.time` back to 0. The clamping is therefore
only observable through the emitted progress event, not via
`action.time` directly — the test asserts via the event.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11733-test-load3d-add-unit-tests-for-AnimationManager-CameraManager-RecordingManager-and--3516d73d3650812485a0c91065e161f0)
by [Unito](https://www.unito.io)
## Summary
Remove the unused legacy queue job row implementation before changing
the live queue popover behavior.
## Changes
- Deleted `JobGroupsList` and its test.
- Deleted `QueueJobItem` and its Storybook story.
- Deleted `QueueAssetPreview`, which was only used by `QueueJobItem`.
## Review Focus
- This PR is deletion-only.
- `rg` found no remaining references to these components.
- `knip` and `typecheck` pass.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11621-Remove-unused-queue-job-components-34d6d73d36508164bf32cb581594cd9f)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Unifies media asset download actions behind a single
`downloadAssets(assets?)` API to avoid single and multi asset download
path drift.
## Changes
- **What**: Replaces `downloadAsset` and `downloadMultipleAssets` with
`downloadAssets`, preserving no-arg media context fallback and explicit
asset arrays.
- **Dependencies**: None.
## Review Focus
Download behavior for single-card, context-menu, and sidebar bulk
actions should continue to use the same ZIP-export path for cloud
multi-output jobs.
Fixes#11715
## Screenshots (if applicable)
N/A
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11717-refactor-unify-media-asset-downloads-3506d73d3650810d8bcec9c0194e743d)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
When an output is dragged from the assets panel onto a node, outputs
were being reuploaded. This logic has been simplified to instead
reference the existing asset by resolving the annotated path.
As part of this change, async drop handlers on nodes are also fixed.
Rather than placing obligation of event handling on client code, not
respecting async handlers, or completely ignoring return types, the vue
drop handler will now simply set `app.dragOverNode` and allow the
`document` drop handler to resolve node drag/drop operations without any
of the difficulty from propagation.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11691-Short-circuit-asset-reuploads-simplify-node-dnd-34f6d73d36508157af86e6cf09229781)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
- Add E2E tests for the `ActionBarButtons` toolbar component (FE-111)
- Add `data-testid="action-bar-buttons"` to the container div for stable
test targeting
- Register `TestIds.topbar.actionBarButtons` in `selectors.ts`
## Changes
- `browser_tests/tests/actionBarButtons.spec.ts` — 6 tests across 5
scenarios: empty state, button rendering, icon rendering, multiple
buttons, click handler, mobile label hiding
- `src/components/topbar/ActionBarButtons.vue` — adds `data-testid` to
container
- `browser_tests/fixtures/selectors.ts` — registers new test ID
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Primarily adds Playwright coverage and a `data-testid` attribute;
runtime behavior is unchanged aside from an extra DOM attribute.
>
> **Overview**
> Adds a new Playwright spec (`actionBarButtons.spec.ts`) that verifies
the ActionBarButtons container empty state, rendering (label/icon),
multiple buttons, click handler execution, and mobile label-hiding
behavior by registering buttons via `window.app!.registerExtension`.
>
> Updates the UI and test selector plumbing by adding
`data-testid="action-bar-buttons"` to `ActionBarButtons.vue` and
exposing it as `TestIds.topbar.actionBarButtons` for stable E2E
targeting.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
80f90d1f1d. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11561-test-add-E2E-tests-for-ActionBarButtons-toolbar-component-34b6d73d36508153874fda856a78817f)
by [Unito](https://www.unito.io)
## Summary
<!-- One sentence describing what changed and why. -->
## Changes
- **What**: <!-- Core functionality added/modified -->
- **Breaking**: <!-- Any breaking changes (if none, remove this line)
-->
- **Dependencies**: <!-- New dependencies (if none, remove this line)
-->
## Review Focus
<!-- Critical design decisions or edge cases that need attention -->
<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->
## Screenshots (if applicable)
<!-- Add screenshots or video recording to help explain your changes -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11612-test-add-tests-for-link-related-settings-34c6d73d36508145885bd017162e6fae)
by [Unito](https://www.unito.io)
## Summary
Extends `colorUtil.test.ts` with boundary tests that the existing suite
did not cover: malformed `parseToRgb` inputs, alpha-hex parsing through
`parseToRgb`, negative hue normalization in `hsbToRgb`, the full
`isTransparent` matrix, HSV-vs-HSB equivalence in `toHexFromFormat`, the
bare-hex prefix path, and a non-primary-color round-trip through
`hexToHsva` / `hsvaToHex`.
## Changes
- **What**: Adds 13 Vitest cases across 5 new `describe` blocks
(`parseToRgb edge cases`, `hsbToRgb normalization`, `isTransparent`,
`toHexFromFormat`, plus a non-primary round-trip in the existing
`hexToHsva / hsvaToHex` block). Uses the existing
`vi.mock('es-toolkit/compat')` memoize stub.
## Review Focus
- The non-primary palette round-trip allows ±1 per RGB channel because
`hsbToRgb` floors while `rgbToHex` rounds; the test asserts the bound
rather than exact equality.
- `parseToRgb` is exercised with alpha-bearing hex (`#f008`,
`#ff000080`); the function returns RGB-only, so the alpha is
intentionally discarded.
- `toHexFromFormat({h, s, v}, 'hsb')` covers the HSV-shaped object path
that wraps `hsbToRgb`.
## Testing
\`\`\`bash
pnpm exec vitest run src/utils/colorUtil.test.ts
pnpm format -- src/utils/colorUtil.test.ts
pnpm lint
pnpm typecheck
pnpm knip
\`\`\`
73 tests pass (60 prior + 13 new).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11671-test-add-unit-tests-for-colorUtil-edge-cases-34f6d73d36508136bac9edfae32815ec)
by [Unito](https://www.unito.io)
> Final piece of the PLY / 3D Gaussian Splatting series. Previous PR
made `ModelAdapterCapabilities` load-bearing on the engine side; the UI
was still gating off `isSplatModel` / `isPlyModel` proxies. This PR
routes the viewer and the viewer-mode dialog through the capability
fields directly, so the same source of truth that drives `Load3d`
behavior also drives what the user sees. Eighth and last in the series
splitting up.
## Summary
Snapshot `Load3d.getCurrentModelCapabilities()` into 5 Vue refs on each
model load and pipe them through the existing `Load3D` /
`Load3DControls` / `Load3dViewerContent` / `ModelControls` /
`ViewerModelControls` / `Preview3d` props. Replaces the format-specific
`:is-splat-model` / `:is-ply-model` props and the hardcoded "splat →
drop light/gizmo/export" subtraction with additive capability gates. No
engine behavior changes — capability values are what previous PR already
produces; UI now consumes them.
## Changes
- **`useLoad3d.ts` / `useLoad3dViewer.ts`**: 5 new refs
(`canFitToViewer` / `canUseGizmo` / `canUseLighting` / `canExport` /
`materialModes`) refreshed on every load via
`load3d.getCurrentModelCapabilities()`. `useLoad3dViewer` extracts the
snapshot into a single `captureAdapterFlags(source)` helper because it
runs in three places (initializeViewer / initializeStandaloneViewer /
loadStandaloneModel).
- **`Load3D.vue`**: gate the fit-to-viewer button on `canFitToViewer`;
pass capability refs to `Load3DControls` instead of `isSplatModel` /
`isPlyModel`.
- **`Load3DControls.vue`**: build `availableCategories` additively
(`['scene','model','camera']` plus `light` / `gizmo` / `export` if their
capability is true) rather than subtracting from a fixed list when
`isSplatModel` is true. Forwards `materialModes` to `ModelControls`.
- **`Load3dViewerContent.vue`**: gate the light / gizmo / export sidebar
sections on the capability refs; pass `materialModes` to
`ViewerModelControls`.
- **`ModelControls.vue` / `ViewerModelControls.vue`**: drop the local
`materialModes` computed (which derived its options from `isPlyModel`
and a hardcoded mesh list) and accept `materialModes` as a `readonly
MaterialMode[]` prop. An empty array hides the dropdown entirely.
- **`Preview3d.vue`** (renderer linearMode): mirror the prop swap on the
standalone preview path.
## Review Focus
- **Capability prop wiring is the only public-API change for child
components**. `ModelControls` and `ViewerModelControls` lost
`hideMaterialMode` / `isPlyModel` props. Any extension that imported
these components directly will need to migrate, but they're internal
`src/components/load3d/controls/**` files and not part of the documented
extension surface.
- **Empty-`materialModes` semantics**: previously hidden via
`:hide-material-mode`; now hidden via `materialModes.length === 0`.
`SplatModelAdapter` declares `materialModes: []`, so the splat case
keeps the same behavior — the dropdown disappears. PLY adds
`'pointCloud'` to the array, so the dropdown picks up that mode
automatically without the controls needing an `isPlyModel` branch.
- **`captureAdapterFlags` runs after every load completes**, so
switching between mesh and splat in the same viewer instance updates the
chrome correctly. Verified via the new `Load3D.test.ts` /
`Load3dViewerContent.test.ts` cases.
- **Capability gating is inclusive of `canFitToViewer`** in this PR even
though `Load3DControls` has no fit category — the fit-to-viewer floating
button on `Load3D.vue` is what reads it. PLY's `fitToViewer: true` means
the button stays visible for PLY users.
## Coverage
| File | Stmts | Branch | Funcs |
|---|---|---|---|
| `Load3D.vue` (modified) | 53.3% | **95.5%** | 83.3% |
| `Load3DControls.vue` (modified) | 77.5% | **94.8%** | 86.4% |
| `Load3dViewerContent.vue` (modified) | 60.6% | 72.1% | 54.5% |
| `controls/ModelControls.vue` (modified) | 16.3% | 0% | 0% |
| `controls/viewer/ViewerModelControls.vue` (modified) | **100%** |
**100%** | **100%** |
| `composables/useLoad3d.ts` (modified) | 78.7% | 64.5% | 71.4% |
| `composables/useLoad3dViewer.ts` (modified) | 76.0% | 52.1% | 66.7% |
Four new test files (`Load3D.test.ts` / `Load3DControls.test.ts` /
`Load3dViewerContent.test.ts` /
`controls/viewer/ViewerModelControls.test.ts`) cover the new capability
gating directly: each component is rendered with capability flags
toggled on/off and the appropriate sidebar / dropdown / button
visibility is asserted. Capability prop forwarding from `Load3D.vue` →
`Load3DControls.vue` and from `Load3dViewerContent.vue` →
`ViewerModelControls.vue` is exercised end-to-end.
`controls/ModelControls.vue` is the legacy node-side ModelControls — its
existing tests live elsewhere and were not in this PR's scope; the diff
line covered (the `v-if="materialModes.length > 0"` swap) is exercised
by the new `Load3DControls.test.ts` cases that drive a non-empty / empty
`materialModes` through. `Preview3d.vue` (renderer linearMode) has no
test file in the project; the prop swap there is the same shape as the
`Load3D.vue` swap which is covered.
`useLoad3d.ts` / `useLoad3dViewer.ts` percentages are roughly the
pre-existing baseline. The diff lines (the 5 new refs and the
`captureAdapterFlags` helper) are exercised by the existing composable
tests via the mock that now stubs `getCurrentModelCapabilities()`.
73 new component unit tests; 393 total load3d-related tests pass on this
branch.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11711-feat-load3d-bind-UI-capability-gating-to-ModelAdapterCapabilities-3506d73d365081b3af68f30e3f728e24)
by [Unito](https://www.unito.io)
## Summary
PR B of this https://github.com/Comfy-Org/ComfyUI_frontend/pull/11388
Part of the `useBrushDrawing` decomposition plan (PR B).
Extracts brush settings persistence logic into a dedicated
`useBrushPersistence` composable, reducing the responsibility surface of
`useBrushDrawing`.
No runtime behavior is changed — this is a pure structural refactor.
## Changes
- **New** `src/composables/maskeditor/useBrushPersistence.ts` —
encapsulates `loadAndApply` (reads brush settings from localStorage and
applies them to the store on init) and `save` (debounced write of
current brush settings to localStorage)
- **New** `src/composables/maskeditor/useBrushPersistence.test.ts` —
unit tests covering load from empty storage, full restore round-trip,
missing `stepSize` fallback, corrupted data resilience, and
save-to-localStorage behavior
- **Updated** `src/composables/maskeditor/useBrushDrawing.ts` — removes
the inlined persistence functions and delegates to `useBrushPersistence`
## Test Locally
1. Adjust brush size and hardness, close MaskEditor, then reopen it —
brush parameters should restore to the previous settings (verifies
`save` + `loadAndApply`) - pass
2. Draw a few strokes and confirm the marks appear correctly (verifies
the `saveBrushSettings` public interface is not broken) - pass
https://github.com/user-attachments/assets/961155d5-6742-4668-a419-51c29b850edf
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Low risk refactor that moves localStorage read/write logic behind a
new composable and adds unit tests; main risk is accidental behavior
drift in when/what brush settings are persisted/restored.
>
> **Overview**
> Refactors mask editor brush settings persistence by extracting the
localStorage load/save (including debounced writes and `stepSize`
fallback) out of `useBrushDrawing` into a new `useBrushPersistence`
composable, while keeping the `saveBrushSettings` public API wired
through.
>
> Adds `useBrushPersistence` unit tests covering empty storage,
round-trip restore, missing field defaults, corrupted JSON handling, and
save semantics.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
d87d7c8bcf. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11543-refactor-extract-useBrushPersistence-from-useBrushDrawing-34a6d73d36508144b7edff759a1e3485)
by [Unito](https://www.unito.io)
## Summary
- Add E2E tests for the `LoginButton` toolbar component (FE-109)
- Add `data-testid="login-button"` to `LoginButton.vue` for stable
targeting
- Register `TestIds.topbar.loginButton` in `selectors.ts`
## Changes
- `browser_tests/tests/loginButton.spec.ts` — 7 tests covering:
visibility toggled by `show_signin_button` server feature flag, ARIA
label, click → sign-in dialog, hover popover content, popover dismissal
- `src/components/topbar/LoginButton.vue` — adds `data-testid`
- `browser_tests/fixtures/selectors.ts` — registers new test ID
## Notes
`LoginButton` is rendered in `WorkflowTabs` when `flags.showSignInButton
?? isDesktop` is true. Since `isDesktop = false` in the OSS test build
(`DISTRIBUTION=localhost`), tests enable the button by setting
`window.app.api.serverFeatureFlags.value.show_signin_button = true` —
the established pattern used throughout the test suite (e.g.
`nodeLibraryEssentials.spec.ts`, `shareWorkflowDialog.spec.ts`).
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Low risk: changes are limited to adding stable `data-testid` hooks
plus new Playwright E2E coverage, with only a minor adjustment to a
unit-test performance threshold.
>
> **Overview**
> Adds Playwright E2E coverage for the topbar `LoginButton`, including
feature-flag-driven visibility, ARIA labeling, click-to-open sign-in
dialog, and hover popover behavior (including the *Learn more* link and
dismissal).
>
> To make the tests stable, `LoginButton.vue` now exposes `data-testid`
attributes for the button and popover elements, and `selectors.ts`
registers new `TestIds.topbar.*` entries.
>
> Relaxes the `useModelToNodeStore.getCategoryForNodeType` performance
test threshold (from 10ms to 100ms) while clarifying the intent of the
check.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1d4dd0bdca. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11562-test-add-E2E-tests-for-LoginButton-toolbar-component-34b6d73d365081f1bf2edc747d69ee52)
by [Unito](https://www.unito.io)
## Summary
Extends `useNodePricing.test.ts` with three behavioral gaps the existing
suite did not cover: `getNodePricingConfig` does not leak the compiled
JSONata expression, `pricingRevision` ticks after async evaluation
resolves (with a cache-hit path that does not), and
`formatPricingResult` returns `''` for non-finite numeric inputs across
all four result types.
## Changes
- **What**: Adds 9 Vitest cases across two existing `describe` blocks
(`getNodePricingConfig`, `formatPricingResult`) and one new block
(`reactive revision`). Reuses the existing `priceBadge` and
`createMockNodeWithPriceBadge` helpers.
## Review Focus
- The cache-hit assertion checks that `pricingRevision.value` does not
advance after a second `getNodeDisplayPrice` call with the same
signature, exercising the WeakMap cache hit at `useNodePricing.ts:573`.
- Non-finite coverage spans `type:'usd'`, `type:'range_usd'`,
`type:'list_usd'` (empty + all-non-finite + mixed), and the legacy `{
usd }` shape, matching the four `asFiniteNumber` call sites in
`formatPricingResult`.
- The strip-`_compiled` assertion uses `toHaveProperty` so the test
fails loudly if a future refactor accidentally re-exposes the runtime
JSONata instance to debug consumers.
## Testing
\`\`\`bash
pnpm exec vitest run src/composables/node/useNodePricing.test.ts
pnpm format -- src/composables/node/useNodePricing.test.ts
pnpm lint
pnpm typecheck
pnpm knip
\`\`\`
91 tests pass (82 prior + 9 new).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11673-test-add-unit-tests-for-useNodePricing-edge-cases-34f6d73d365081dab525cdaa71b348a5)
by [Unito](https://www.unito.io)
## Summary
Extends `useWorkspaceAuth.test.ts` with the retry, race,
storage-resilience, and Zod-validation paths the existing suite did not
exercise: exponential backoff on `TOKEN_EXCHANGE_FAILED`, immediate
context clearing on permanent errors, stale-refresh abort when the user
switches workspaces mid-flight, and resilience to `sessionStorage` quota
errors.
## Changes
- **What**: Adds 6 Vitest cases across three new `describe` blocks
(`refreshToken retry/race paths`, `persistToSession resilience`, `Zod
validation on token response`). Reuses the existing `mockGetIdToken`,
`mockTeamWorkspacesEnabled`, `vi.useFakeTimers()`, and
`mockTokenResponse` fixtures.
## Review Focus
- The retry test uses `vi.runAllTimersAsync()` so the three backoff
sleeps (1s + 2s + 4s) drain in a single tick instead of slowing the
suite. Both `console.warn` (per-attempt) and `console.error`
(final-failure) are silenced.
- The race test resolves the in-flight refresh fetch with a token tied
to the OLD workspace AFTER `switchWorkspace('workspace-other')` has run,
so the assertion fails loudly if the stale-request guard regresses.
- The sessionStorage spy targets the instance method
(`vi.spyOn(sessionStorage, 'setItem')`); spying
`Storage.prototype.setItem` does not intercept happy-dom's per-instance
method.
## Testing
\`\`\`bash
pnpm exec vitest run
src/platform/workspace/stores/useWorkspaceAuth.test.ts
pnpm format -- src/platform/workspace/stores/useWorkspaceAuth.test.ts
pnpm lint
pnpm typecheck
pnpm knip
\`\`\`
33 tests pass (27 prior + 6 new).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11674-test-add-unit-tests-for-workspaceAuthStore-retry-race-paths-34f6d73d365081e6a8ecce59a156585e)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
## Summary
The asset context menu's Download action called `downloadAsset`
directly, so multi-output jobs only downloaded the preview file instead
of all outputs. Route through `downloadMultipleAssets`, which detects
multi-output jobs and creates a ZIP export in cloud mode and falls back
to the single-download path otherwise.
## Changes
- **What**: Swap `actions.downloadAsset(asset)` for
`actions.downloadMultipleAssets([asset])` in the per-asset context menu
Download command, and extend the existing unit test to assert the
routing.
- **Breaking**: none
- **Dependencies**: none
## Review Focus
For single-output assets the behavior is unchanged:
`downloadMultipleAssets([asset])` falls through to the
individual-download path when `hasMultiOutputJobs` is false and
`assets.length === 1` (see `useMediaAssetActions.ts:106`). Verified
manually — right-clicking a single-output asset and clicking Download
still produces one file download to the correct `/api/view` URL.
## Notes
This is a focused replacement for the stale #10948. Compared to that
branch:
- Drops the unrelated `bootstrapStore` API-key auth changes (scope
creep).
- Drops the new `assets.cloud.spec.ts` Playwright spec — cloud
asset-export E2E coverage was added in #11610
(`browser_tests/tests/sidebar/assets.spec.ts`), so a separate cloud spec
for this routing change would mostly duplicate it.
- Keeps the unit-test change minimal: extends the existing `ContextMenu`
stub with a `model` prop watcher and adds one new test, rather than
rewriting the whole file from `@testing-library/vue` to
`@vue/test-utils`.
## Verification
- `pnpm test:unit` (MediaAssetContextMenu.test.ts and
useMediaAssetActions.test.ts)
- `pnpm typecheck`
- `pnpm lint`
- `pnpm format` / `oxfmt --check`
- `pnpm knip`
- Manual: started the OSS dev server, generated a single-output asset
via the queue API, opened the assets sidebar, right-clicked the asset,
and confirmed the Download menu item triggers a single-file download
(screenshot attached).
## Screenshots


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11700-fix-route-context-menu-Download-through-downloadMultipleAssets-34f6d73d365081eb8135e8b699640d97)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
> Prerequisite work for improved PLY / 3D Gaussian Splatting support —
splat workflows (and PLY pipelines that go through SHARP / COLMAP) emit
camera pose as extrinsics + intrinsics matrices, and the viewer needs a
way to consume them before that end-to-end story can ship.
## Summary
Adds the ability to drive the Preview3D camera from a pair of
OpenCV-convention extrinsics + intrinsics matrices (as produced by SHARP
/ COLMAP / other SfM pipelines), so backend nodes that emit such
matrices can position the viewer's camera deterministically. Fifth in
the series splitting up
https://github.com/Comfy-Org/ComfyUI_frontend/pull/11495. Fully
self-contained — adds new API surface only, no existing call paths are
modified.
## Changes
- **What**:
- `cameraFromMatrices.ts` (new): pure function
`computeCameraFromMatrices(extrinsics, intrinsics)` returning `{
position, target, fovYDegrees }`. No THREE.js dependency.
Shape-validates the inputs (4×4 and 3×3) with a clear error.
- `Load3d.setCameraFromMatrices(extrinsics, intrinsics)`: wires the
utility into Load3d via `setCameraState` + `setFOV`. Preserves the
caller's existing `zoom` and `cameraType`.
- `load3d.ts` (extension): Preview3D's `onExecuted` extracts
`extrinsics`/`intrinsics` from `result[3]`/`result[4]` (when present)
and calls `setCameraFromMatrices`. The output tuple type is widened to
include the two optional `Matrix` fields.
## Review Focus
- **Convention conversion**: OpenCV is Y-down, Z-forward; three.js is
Y-up, Z-back. The function flips Y and Z on both `position` and `target`
(equivalent to a 180° X-axis rotation of the whole world). The same
rotation is applied elsewhere to splats at load time, so a future splat
+ camera-pose pair lines up.
- **FOV math**: vertical FOV (radians) = `2 * atan(cy / fy)`. We expose
it in degrees, matching `cameraManager.setFOV`'s contract.
- **Camera-state preservation**: `setCameraFromMatrices` reads the
current state to keep `zoom` and `cameraType` intact — only `position`,
`target`, and `fov` come from the matrices.
- **Backwards compatibility**: backend nodes that don't return matrices
simply skip the new branch (the `if (extrinsics && intrinsics)` guard).
Existing Preview3D workflows are unaffected.
- 7 unit tests for the matrix math (identity, rotation, translation, FOV
derivation, shape errors, etc.); 1 unit test for the Load3d wiring
(verifies the destructured `position`/`target`/`fovYDegrees` reach
`setCameraState` + `setFOV` with `zoom`/`cameraType` preserved).
## Coverage
| File | Stmts | Branch | Funcs | Lines |
|---|---|---|---|---|
| `cameraFromMatrices.ts` (new) | **100%** | **100%** | **100%** |
**100%** |
| `Load3d.ts` (modified) | ~7.6% | 0% | ~14.6% | ~7.7% |
| `load3d.ts` (extension, modified) | 0% | 0% | 0% | 0% |
The `Load3d.ts` numbers are the pre-existing baseline — `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 new `setCameraFromMatrices` method body is exercised
by a new unit test that asserts `setCameraState` receives the
destructured matrix output and `setFOV` receives the computed
`fovYDegrees`, with the caller's `zoom`/`cameraType` preserved.
`load3d.ts` (the extension registration file, 0% on `main` and after
this PR) has no unit-test scaffolding in the project — its `onExecuted`
handler runs only after a workflow execution and is exercised end-to-end
via browser tests. The new `if (extrinsics && intrinsics)
load3d.setCameraFromMatrices(...)` branch sits in that path.
Net: the matrix math itself, which previously didn't exist, is now 100%
covered. The wiring layer relies on the same e2e safety net the
surrounding code has always used.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11626-feat-derive-Preview3D-camera-pose-from-EXTRINSICS-INTRINSICS-matrices-34d6d73d3650817297bdf25a83a4f7a2)
by [Unito](https://www.unito.io)
## Summary
Implemented E2E test coverage for Levels 6-12 of the Painter Widget
## Changes
Adds the following coverage to complete the test plan:
Level 6 - Input image connection (3 tests, @slow):
- Width/height/bg-color controls hide when input is linked
- Canvas resizes to match input image dimensions after execution
- Drawing over input image produces canvas content
Level 7 - Clear on empty canvas is harmless
Level 8 - Unchanged canvas does not re-upload on second serialization
Level 9 - Settings persistence:
- Tool selection saved to node.properties.painterTool
- Brush size change saved to node.properties.painterBrushSize
Level 10 - Compact layout collapses to grid-cols-1 when node width <
350px
Level 12 - Rapid drawing accumulates all strokes (checks 3 y-positions)
Supporting changes:
- Add data-testid="painter-controls" to controls grid in
WidgetPainter.vue (needed for compact mode class assertion)
- Add browser_tests/assets/widgets/painter_with_input.json workflow
fixture (LoadImage connected to Painter input slot 0)
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Mostly adds/adjusts Playwright and unit test coverage; the only
runtime change is wrapping pointer-capture calls in `try/catch`, which
is low-risk but touches input-handling paths.
>
> **Overview**
> Completes and expands Painter widget browser test coverage, including
new scenarios for clearing an empty canvas, preventing redundant uploads
when serializing an unchanged canvas, persisting tool/brush-size
settings to node properties, compact layout behavior, multi-stroke
accumulation checks, and an input-image-connected workflow (new
`painter_with_input.json`) with execution/resizing/draw-over-image
assertions.
>
> Hardens `usePainter` pointer handling by tolerating
`setPointerCapture`/`releasePointerCapture` failures (e.g., synthetic
events), with corresponding unit tests updated/added to validate the
behavior and serialization expectations.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
056d4a9f0c. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11613-test-complete-remaining-Painter-widget-E2E-tests-34c6d73d36508158b620f55aa1981cf5)
by [Unito](https://www.unito.io)
> Final architectural step in the PLY / 3D Gaussian Splatting series.
Previous PR introduced `ModelAdapter` with a dormant `capabilities`
field; this PR makes those capabilities load-bearing and replaces the
remaining `instanceof SplatMesh` / `instanceof BufferGeometry` switches
with adapter-driven dispatch. Together with previous one it removes the
last of the format-specific branching from `SceneModelManager` /
`Load3d`. Seventh in the series splitting up the
https://github.com/Comfy-Org/ComfyUI_frontend/pull/11495.
## Summary
Drive viewer behavior (fit-to-viewer, default camera pose, world bounds,
GPU dispose, material rebuild) from `ModelAdapterCapabilities` + 3 new
optional adapter methods, instead of `SceneModelManager` reflecting on
model shape. `Load3d` is rewritten to take its 13 managers as injected
`Load3dDeps`; a new `createLoad3d` factory assembles them and threads a
single `AdapterRef` between `LoaderManager` (writer) and
`SceneModelManager` + `Load3d` (readers). Splat orientation +
decoder-race + sizing bugs are fixed as a side effect — splats now
render upright, fill the grid, and don't lock the OrbitControls target
on first frame.
## Changes
- **`ModelAdapter.ts`**: add `AdapterRef = { current: ModelAdapter |
null }` shared handle and 3 optional adapter methods —
`computeBounds(model)`, `disposeModel(model)`, `defaultCameraPose()`.
- **`SplatModelAdapter.ts`**: implement all 3 optional methods; `await
splatMesh.initialized` so first-frame bounds are populated (fixes a
decoder race that collapsed the OrbitControls target onto the camera);
`quaternion.set(1, 0, 0, 0)` to convert sparkjs's OpenCV (Y-down,
Z-forward) to three.js (Y-up, Z-back); flip `fitToViewer` back to `true`
and bump `fitTargetSize` to `20` so splats fill the 20-unit grid instead
of shrinking to 1/4 of it.
- **`PointCloudModelAdapter.ts`**: extract
`buildPointCloudForMaterialMode` so `SceneModelManager` rebuilds via the
same code path the initial load uses; `setPath` so PLYs that reference
relative assets resolve correctly.
- **`LoaderManager.ts`**: accept optional `AdapterRef`; write through it
instead of the internal `_currentAdapter` field. `clearModel()` now runs
while the old adapter is still current so its `disposeModel()` can
release renderer-owned resources.
- **`SceneModelManager.ts`**: accept 4 capability lambdas
(`getCurrentCapabilities` / `getBoundsFromAdapter` /
`disposeModelViaAdapter` / `getDefaultCameraPose`) with
`DEFAULT_MODEL_CAPABILITIES` / null fallbacks. `setupModel`,
`fitToViewer`, and material-mode rebuild are now capability-driven;
`containsSplatMesh` (30 lines of `instanceof` traversal) and
`handlePLYModeSwitch` (90 lines of duplicated PLY rebuild) are gone.
- **`Load3d.ts`**: ctor switches from manager-creation to deps-injected
(`Load3dDeps`); add `getCurrentModelCapabilities()` reader; gate
`setGizmoEnabled` / `setGizmoMode` / `resetGizmoTransform` /
`applyGizmoTransform` on `capabilities.gizmoTransform`; `isSplatModel` /
`isPlyModel` now read `adapterRef.current?.kind` directly.
- **`createLoad3d.ts`** (new): single factory that builds the renderer
(`createRenderer`), assembles all 13 managers in dependency order, and
threads one shared `AdapterRef` through `LoaderManager` and
`SceneModelManager`'s 4 capability lambdas.
- **`useLoad3d.ts` / `useLoad3dViewer.ts`**: switch from `new
Load3d(container, options)` to `createLoad3d(container, options)`. No
other call-site changes.
## Review Focus
- **Capability dispatch parity**: walk each former hardcoded branch in
`SceneModelManager` and confirm it now falls out of the right
capability:
- `containsSplatMesh()` → `!capabilities.fitToViewer` +
`getDefaultCameraPose()`
- `handlePLYModeSwitch()` → `capabilities.requiresMaterialRebuild` +
`buildPointCloudForMaterialMode()`
- `Box3.setFromObject(model)` for sizing → `getBoundsFromAdapter(model)
?? Box3.setFromObject(model)`
- Mesh/Points geometry+material disposal in `clearModel` → still
happens, plus `disposeModelViaAdapter(obj)` for adapter-owned resources
(sparkjs SplatMesh internal GPU state)
- **`AdapterRef` lifecycle**: one ref is created in `createLoad3d`,
passed to `LoaderManager` (writer) and `SceneModelManager` (read via 4
closures). `LoaderManager.loadModel` clears via the *old* adapter first
(so `disposeModel` runs), then null-resets the ref before picking the
new one. Test `keeps the old adapter current while clearModel runs` pins
this ordering.
- **Splat fixes are user-visible, not pure refactor**:
- Orientation: `quaternion.set(1, 0, 0, 0)` matches the sparkjs README
convention. Without it splats render upside-down and mirrored on Z. Same
rotation is applied to the camera-from-matrices output in PR-E so a
future splat + camera-pose pair lines up.
- Decoder race: `await splatMesh.initialized` ensures `getBoundingBox`
returns a non-zero box on the first call. Without it `setupModel`'s
bounds → camera pipeline placed the OrbitControls target on the camera
origin, locking the view.
- Sizing: `fitTargetSize: 20` (vs. the mesh default of 5) means splat
geometry spans the full 20-unit grid footprint instead of ~1/4 of it.
Mesh assets are unaffected.
- **Gizmo gating**: `setGizmoEnabled(true)` early-returns when
`capabilities.gizmoTransform` is false. Internal
`setGizmoEnabled(false)` still runs (so we can always disable).
`setGizmoMode` / `resetGizmoTransform` / `applyGizmoTransform` no-op
when the capability is off.
- **`createLoad3d` is the single ctor entry**: `new Load3d(...)` is no
longer callable from app code (ctor signature changed to `(container,
deps, options)`). All call sites use `createLoad3d`. Test scaffolding
still uses `Object.create(Load3d.prototype)` + property injection where
it needs to bypass renderer creation.
- **Backwards compatibility**: `LoaderManager`'s `adapterRef` and
`SceneModelManager`'s 4 capability lambdas all have defaults
(`createAdapterRef()` and `() => DEFAULT_MODEL_CAPABILITIES` etc.), so
the existing test suites that construct these classes with the old
signatures still compile and pass without modification beyond what's in
this PR.
## Coverage
| File | Stmts | Branch | Funcs | Lines |
|---|---|---|---|---|
| `ModelAdapter.ts` (modified) | **100%** | **100%** | **100%** |
**100%** |
| `LoaderManager.ts` (modified) | **100%** | 91.7% | 86.7% | **100%** |
| `MeshModelAdapter.ts` (unchanged) | **100%** | **100%** | **100%** |
**100%** |
| `PointCloudModelAdapter.ts` (modified) | **97.9%** | 69.2% | 71.4% |
**97.9%** |
| `SplatModelAdapter.ts` (modified) | **100%** | **100%** | **100%** |
**100%** |
| `SceneModelManager.ts` (modified) | 75.4% | 67.2% | 72.2% | 75.4% |
| `Load3d.ts` (modified) | 29.5% | 30.6% | 26.7% | 30.1% |
| `createLoad3d.ts` (new) | 83.8% | **100%** | 58.3% | 83.8% |
| `useLoad3d.ts` (modified) | 78.2% | 65.1% | 71.4% | 82.2% |
| `useLoad3dViewer.ts` (modified) | 75.2% | 52.1% | 65.9% | 79.4% |
`SplatModelAdapter.ts` jumps to 100% via 6 new tests covering the
orientation set, the `await initialized` decoder wait, `computeBounds`
(world-space transform + null fallback), `disposeModel` (per-SplatMesh
dispose + no-op on non-splat trees), and `defaultCameraPose`.
`createLoad3d.ts` hits 100% branch via a new test file with 12 cases —
`WebGLRenderer` config, `Load3DOptions` forwarding, `AdapterRef`
identity between `LoaderManager` and `SceneModelManager`, and the 4
capability lambdas in both adapter-null and adapter-published states
(each delegates correctly to the adapter's optional methods or falls
back to defaults). The remaining func% reflects the inline
`gizmoTransformChange` callback — not a deliberate skip, just out of
scope for the dispatch-wiring tests.
`SceneModelManager.ts` and `Load3d.ts` numbers are the pre-existing
baseline — the existing `*.test.ts` files cover façade methods via
prototype injection rather than instantiating the classes (`Load3d`
constructor needs `THREE.WebGLRenderer`, which happy-dom can't provide;
`SceneModelManager` covers the new capability paths via its existing
`createManager(overrides)` helper). All new branches (capability gating,
capability-driven `setupModel` / `fitToViewer` / rebuild, adapter-driven
`isSplatModel` / `isPlyModel`) have dedicated tests.
Net diff: **+846 / −370** across 16 files (10 production, 6 test).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11660-refactor-load3d-drive-viewer-behavior-from-ModelAdapter-capabilities-34f6d73d36508130b0ece884add182b9)
by [Unito](https://www.unito.io)
## Summary
Adds unit coverage for `linkFixer` serialized graph repair paths and
fixes input-slot repairs being tracked without mutating the serialized
workflow data.
## Changes
- **What**: Adds 10 Vitest cases for valid links, dry-run reporting,
origin/target repair, missing input-slot cleanup, dangling node cleanup,
stale link deletion, and silent mode.
- **What**: Applies serialized input slot mutations in fix mode and
reports `fixed` only after a rerun confirms the graph is clean.
- **What**: Adds the design-system package to knip workspace config so
the pre-push hook recognizes its exported CSS dependency usage.
- **Dependencies**: None.
## Review Focus
The workflow validation path calls `fixBadLinks` with serialized
workflow JSON, so the new tests stay at that boundary and assert the
repaired graph shape directly.
## Test Coverage
- CI snapshot for `src/utils/linkFixer.ts`: unit lines 1.8%.
- Local targeted coverage: unit lines 83.9%, functions 94.11%, branches
75.15%.
## Testing
```bash
pnpm format -- src/utils/linkFixer.ts src/utils/linkFixer.test.ts
pnpm test:unit -- src/utils/linkFixer.test.ts
pnpm test:unit -- src/utils/linkFixer.test.ts --coverage
pnpm typecheck
pnpm lint
pnpm knip
```
## screenshoot
<img width="1691" height="927" alt="Screenshot 2026-04-28 at 10 09
26 AM"
src="https://github.com/user-attachments/assets/ff59888f-bde3-48f5-853a-60df69e84492"
/>
*PR Created by the Glary-Bot Agent*
---
## Summary
#11579 restored *categorical* test coverage for subgraph serialization
but didn't reproduce the specific Z-Image-Turbo regression introduced by
#10849 — pre-#10849 templates whose `widgets_values` is leftover noise
get corrupted on load because the new code applies that array
positionally to promoted widget views.
This PR adds two **vitest** cases that pin the user-visible symptom
directly: after loading a misaligned legacy payload, the promoted widget
value should reflect the source default, not the legacy
`widgets_values[i]`.
Both use `it.fails` so the suite stays green while the bug is present
and flips to failing the moment the fix on
`fix/subgraph-promoted-widget-inline-state` lands.
## Tests
1. `falls back to source widget value when proxyWidgets is in legacy
2-tuple shape` — configure() with `proxyWidgets: [['-1', 'widget']]` +
`widgets_values: [999]` should leave the widget at the source default
(42), not 999.
2. `does not corrupt unbound promoted widgets when widgets_values length
mismatches view count` — same shape with longer/wrong-length array.
## Verification
- All 8 cases in the file pass under `it.fails` (CI green).
- Removing `.fails` locally produces the expected failures: `expected
42, received 999` and `expected 42, received 111` — confirming both
tests catch the regression.
- `pnpm typecheck`, `pnpm exec eslint`, `pnpm exec oxlint` all clean.
## Why `it.fails` and not plain failing tests
The actual fix (`fix/subgraph-promoted-widget-inline-state`) is
unmerged. Landing genuinely-failing tests on main would break CI for
everyone. `it.fails` documents the bug runnably, keeps CI green, and
signals when the fix lands so the marker can be dropped.
## Why these assertions, not "widgets_values must be undefined"
A first draft asserted that `serialize()` should not write
`widgets_values` at all. That conflicts with existing coverage in the
same file (`preserves per-instance widget values after configure`,
`round-trips per-instance widget values`) which deliberately uses
`widgets_values` for round-trip persistence. These rewritten assertions
target the load-time corruption symptom directly without contradicting
the per-instance contract — feedback from Oracle review.
## Coordination
Mirrors the failing tests already on commit `6a982675e` of the unmerged
`fix/subgraph-promoted-widget-inline-state` branch, with the addition of
`.fails` markers and a clarifying comment so they can land on main
first.
Follow-up to #11579.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11697-test-subgraph-pin-10849-promoted-widget-value-corruption-with-it-fails-34f6d73d365081d7a04dcf48ebeceafe)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
- Add Playwright E2E test coverage for the ComfyHub publish workflow
dialog
- Create `PublishDialog` page object fixture with programmatic dialog
opening via Vite dynamic imports
- Create `PublishApiHelper` for mocking all publish flow API endpoints
(`/hub/profiles/me`, `/hub/labels`, `/hub/workflows`,
`/userdata/*/publish`, `/assets/from-workflow`,
`/hub/assets/upload-url`)
- Add `data-testid` attributes to 6 publish flow components for stable
E2E locators
- 17 test scenarios across 7 describe blocks covering wizard navigation,
form interactions, profile gate, save prompt, and publish submission
## Test plan
- [ ] Run `pnpm test:browser:local -- --grep "Publish dialog"` against
local dev server
- [ ] Verify wizard navigation through Describe → Examples → Finish
steps
- [ ] Verify profile gate flow (with/without profile)
- [ ] Verify save prompt for unsaved workflows
- [ ] Verify publish success/failure scenarios
Fixes#9079
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10770-test-add-E2E-tests-for-publish-flow-wizard-3346d73d3650818094d5fc3a84593402)
by [Unito](https://www.unito.io)
---------
Co-authored-by: dante <dante@danteui-MacStudio.local>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Extends `assetService.test.ts` (which previously only covered
`shouldUseAssetBrowser`) with behavioral tests for the network-bound
methods: metadata fetch error mapping, base64 upload validation,
async-vs-sync upload routing, delete error propagation, model-folder
filtering, update validation, and tag-filtered list defaults.
## Changes
- **What**: Adds 10 Vitest cases across `getAssetMetadata`,
`uploadAssetFromBase64`, `uploadAssetAsync`, `deleteAsset`,
`getAssetModelFolders`, `updateAsset`, and `getAssetsByTag`. Reuses the
existing hoisted `mockDistributionState` / `mockSettingStoreGet` setup
and the existing `vi.mock('@/scripts/api')` boundary; adds local
`buildResponse` and `validAsset` helpers scoped to this file.
## Review Focus
- The localized error path is covered through public methods
(`getAssetMetadata`) rather than reaching for the internal
`getLocalizedErrorMessage`.
- `getAssetModelFolders` test asserts that the request URL omits
`include_public` (the internal call site passes no `includePublic`),
matching the conditional in `handleAssetRequest`.
- `uploadAssetAsync` tests pin the discriminated-union shape (`type:
'async' | 'sync'`) for both 202 and 200 responses.
## Testing
\`\`\`bash
pnpm exec vitest run src/platform/assets/services/assetService.test.ts
pnpm format -- src/platform/assets/services/assetService.test.ts
pnpm lint
pnpm typecheck
pnpm knip
\`\`\`
All 16 tests pass (6 prior + 10 new).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11670-test-add-unit-tests-for-assetService-34f6d73d36508117b1aafaf463e9c820)
by [Unito](https://www.unito.io)
## Summary
Centralize all `LGraph._version` increments behind a single
`incrementVersion()` method to create the seam for a future
`VersionSystem` (ECS Migration Phase 0a).
## Changes
- **What**: Added `LGraph.incrementVersion()` and replaced all 19 direct
`graph._version++` writes across 7 files. Existing null guards at call
sites are preserved. Zero behavioral change — the counter is still only
read by `LGraphCanvas.renderInfo()` for debug display.
## Review Focus
- The new method is mechanical: `incrementVersion(): void {
this._version++ }`. Look for any sites I missed or null-guard
regressions.
- Files updated: `LGraph.ts`, `LGraphNode.ts`, `LGraphCanvas.ts`,
`widgets/BaseWidget.ts`, `subgraph/SubgraphInput.ts`,
`subgraph/SubgraphInputNode.ts`, `subgraph/SubgraphOutput.ts`.
Part of the [ECS Migration
Plan](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/docs/architecture/ecs-migration-plan.md).
Linear: FE-165.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11698-refactor-litegraph-centralize-_version-counter-via-incrementVersion-34f6d73d3650810992f8fa3adbae3f38)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
*PR Created by the Glary-Bot Agent*
---
## Summary
#10759 removed ~12 E2E tests from `subgraphSerialization.spec.ts` during
a reorganization that shifted semantic coverage to Vitest. Several of
the removed tests covered the exact `serialize() → JSON → configure()`
round-trip that #10849's positional `_instanceWidgetValues` path later
regressed on Main — promoted widget values binding to the wrong slots
when loading templates whose `widgets_values` ordering doesn't match
current `proxyWidgets`.
This restores the pre-reorg E2E coverage so future regressions in
promoted-widget serialization are caught at the browser level.
## Restored tests
From `subgraphSerialization.spec.ts` (pre-#10759):
- **Deterministic proxyWidgets Hydrate** (3 tests) — round-trip
stability and compressed `target_slot` resolution.
- **Legacy And Round-Trip Coverage** (5 tests) — includes the most
directly-relevant restorations:
- `Promoted widgets survive serialize -> loadGraphData round-trip`
- `Multi-link input representative stays stable through save/reload`
- `Cloning a subgraph node keeps promoted widget entries on original and
clone`
- **Duplicate ID Remapping** (5 tests) — includes `Promoted widget
tuples are stable after full page reload boot path`.
The 4 tests that already existed on Main (added by #10849 and the
Vue-nodes legacy-prefixed block) are kept as-is.
## Adaptations to current APIs
- Imports reworked for the post-`@e2e/*` alias layout.
- Redundant `comfyPage.nextFrame()` calls dropped — `loadWorkflow` /
`loadGraphData` / `serializeAndReload` already wait internally (#11264).
- Alt-drag clone block wrapped in `try/finally` around
`keyboard.up('Alt')` to match the current `subgraphCrud.spec.ts`
pattern.
- `PromotedWidgetEntry` is now exported from
`browser_tests/helpers/promotedWidgets.ts` so the restored
`expectPromotedWidgetsToResolveToInteriorNodes` helper can type its
argument.
## Review follow-ups applied
- Use `expect.poll()` instead of `expect(async () => …).toPass()` for
the single-value snapshot comparison, per `browser_tests/AGENTS.md`.
- Capture and call `dispose()` from
`SubgraphHelper.collectConsoleWarnings()` inside a `try/finally` so the
console listener is unregistered after the test.
## Verification
- `pnpm typecheck:browser` — clean.
- `pnpm exec eslint` + `pnpm exec oxlint` on changed files — 0 warnings,
0 errors.
- `pnpm exec oxfmt` on changed files — applied (no diff).
- Ran 2 key restored tests against local ComfyUI + dev server:
- `Promoted widgets survive serialize -> loadGraphData round-trip` —
PASS (3.9s)
- `Multi-link input representative stays stable through save/reload` —
PASS (2.9s)
- Full-suite runs in this sandbox are blocked by the existing
`comfyPage` fixture's `createUser` path failing on repeat runs against
persistent backend state — unrelated to this PR. CI will exercise the
full suite.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11579-test-restore-deleted-subgraph-serialization-E2E-tests-34b6d73d365081f29b27c1069476ad17)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
Use `group_by_job_time` for exports spanning multiple jobs while keeping
single-job exports on `preserve`, and add regression coverage for the
new naming-strategy behavior.
## Changes
- **What**: updated the asset export payload and request typing for the
new naming-strategy values, added unit coverage for single-job vs
multi-job export requests, added `@cloud` sidebar browser coverage for
export payloads, and adjusted the cloud Playwright setup helpers so
setup API calls can hit the backend directly and Firebase auth is seeded
on the app origin
- **Breaking**: none
- **Dependencies**: none
## Review Focus
Please sanity-check the cloud Playwright harness changes in `ComfyPage`
and `CloudAuthHelper`, plus the single-job vs multi-job export
naming-strategy assertions in the new browser tests.
## Screenshots (if applicable)
N/A
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11610-Fix-naming-strategy-for-multi-job-asset-exports-34c6d73d365081a68a88ea38d897578f)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Extends `assetsStore.test.ts` with behavioral coverage for the
optimistic-update flows (`updateAssetMetadata`, `updateAssetTags`), the
per-asset deletion-state tracker (`setAssetDeleting` /
`isAssetDeleting`), the input-name resolution (`getInputName` /
`inputAssetsByFilename`), and the cloud-routing branch of
`updateInputs`.
## Changes
- **What**: Adds 8 Vitest cases across 4 new `describe` blocks
(`updateAssetMetadata optimistic cache`, `updateAssetTags diff-based
dispatch`, `setAssetDeleting / isAssetDeleting`, `getInputName`,
`updateInputs cloud routing`). Extends the shared `assetService` mock
with `updateAsset`, `addAssetTags`, and `removeAssetTags` (none of which
were previously stubbed).
## Review Focus
- Cloud-routed tests flip `mockIsCloud.value` inside a `try/finally` so
the existing default (`false`) is restored even if an assertion throws —
same pattern the existing Cloud describe block uses.
- The optimistic-cache rollback test silences the `console.error`
invoked by the catch branch so the test output stays clean.
- The `updateAssetTags` tests pin both the no-op short-circuit and the
add-only path. Remove-only and combined add+remove are already exercised
indirectly through the existing tag-cache invalidation tests.
## Testing
\`\`\`bash
pnpm exec vitest run src/stores/assetsStore.test.ts
pnpm format -- src/stores/assetsStore.test.ts
pnpm lint
pnpm typecheck
pnpm knip
\`\`\`
47 tests pass (39 prior + 8 new).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11672-test-add-unit-tests-for-assetsStore-34f6d73d3650819f8e43e7154541baef)
by [Unito](https://www.unito.io)
## Summary
Localizes secret list date labels through Vue I18n date formatting
instead of the browser default numeric date format, with Storybook
coverage for design review.
## Changes
- **What**: Replaces `toLocaleDateString()` with `useI18n().d(..., {
dateStyle: 'medium' })` for `SecretListItem` dates.
- **What**: Updates `SecretListItem` expectations to match the Vue I18n
date formatter.
- **What**: Adds `SecretListItem` stories for default, never-used,
loading, and disabled states.
- **Dependencies**: None.
## Review Focus
Stacked on #11480 to keep the escaping fix scoped. Please confirm
whether the localized medium date style matches design/product
expectations.
## Screenshots (if applicable)
https://f3ba7229.comfy-storybook.pages.dev/?path=/story/platform-secrets-secretlistitem--default
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11524-fix-localize-secret-date-labels-3496d73d3650814bb70bfb3f870a31cd)
by [Unito](https://www.unito.io)
## Summary
Fix duplicate `CI: Website E2E` workflow runs on pull requests.
## Problem
Two runs were triggered for every PR touching website files:
- `website-e2e (pull_request)` — from the PR event
- `website-e2e (push)` — from the push to a `website/*` branch
The concurrency key used `github.ref`, which evaluates differently for
push (`refs/heads/...`) vs pull_request (`refs/pull/N/merge`), so they
couldn't cancel each other.
## Changes
1. Scope `push` trigger to `main` only (removes `website/*`)
2. Use `github.head_ref || github.ref` in the concurrency group so push
and PR events for the same branch share a group
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11607-fix-prevent-duplicate-website-e2e-CI-runs-on-PRs-34c6d73d3650814c9d24c77b1591e94a)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Global i18n config has escapeParameter as true. This explicitly turns it
to false. I opened a Linear ticket to reconsider changing this back to
false as default globally.
## Summary
Fix the Secrets panel so created and last-used dates render as plain
text instead of HTML-escaped slash entities.
## Changes
- **What**: Compute the Secrets date labels with `t(..., {
escapeParameter: false })` after formatting the date, so vue-i18n does
not escape `/` into `/` for plain-text output.
- **What**: Replace the mocked translation setup in
`SecretListItem.test.ts` with a real `vue-i18n` instance and add a
regression test that asserts the rendered dates do not contain escaped
slash entities.
## Review Focus
This intentionally fixes the i18n interpolation issue shown in the bug
screenshot. It does not change the separate RFC3339Nano parsing behavior
discussed in #11358.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11480-fix-avoid-escaped-secret-date-labels-3486d73d365081c890ecd2a6992d7879)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Deduplicates pending subscription checkout attempt construction so
storage and fallback paths share the same payload creation.
## Changes
- **What**: Build the `PendingSubscriptionCheckoutAttempt` once in
`recordPendingSubscriptionCheckoutAttempt()` and reuse it for
unavailable-storage, failed-write, and successful-write paths.
- **Dependencies**: None.
## Review Focus
This is intended as a no-behavior-change cleanup: unavailable storage
still returns an attempt, failed `setItem()` still returns that attempt
without dispatching, and the pending-checkout event only fires after a
successful storage write.
Linear: FE-209
## Screenshots (if applicable)
N/A
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Follow-up to the closed earlier attempt in #11646. This PR keeps the
same user-facing goal, but changes the implementation to reuse the
existing missing model pipeline for refresh instead of maintaining a
separate candidate-only recheck path.
Adds a missing model refresh action in the Errors tab by reusing the
existing missing model pipeline, so users can re-check models after
downloading or manually placing files without reloading the workflow.
## Changes
- **What**:
- Adds `app.refreshMissingModels()` as a reusable refresh entry point
for the current root graph.
- Splits node definition reloading into `app.reloadNodeDefs()` so
missing-model refresh can pull fresh `object_info` without showing the
generic combo refresh success flow.
- Reuses the existing missing model pipeline instead of adding a
separate candidate-only checker. The refresh path serializes the current
graph, reuses active workflow model metadata when available, falls back
to current missing-model metadata, and then reruns the same candidate
discovery/enrichment/surfacing flow used during workflow load.
- Adds missing model refresh state and error handling to
`missingModelStore`.
- Adds a Refresh button next to Download all in the missing model card
action bar.
- Moves Download all from the Errors tab header into the missing model
card, so the Download all and Refresh actions render or hide together.
- Changes Download all visibility from “more than one downloadable
model” to “at least one downloadable model.”
- Keeps the action bar hidden when there are no downloadable missing
models; Cloud still does not render this action area.
- Normalizes active workflow `pendingWarnings` updates so resolved
missing model warnings do not get revived by stale empty warning
objects.
- Adds test IDs and coverage for the new action bar, refresh state,
refresh delegation, pending warning sync, and E2E refresh behavior.
- **Breaking**: None.
- **Dependencies**: None.
## Review Focus
The main design choice is intentionally reusing the missing model
pipeline for refresh instead of implementing a smaller candidate-only
recheck.
The earlier candidate-only approach was cheaper, but it created a
separate source of truth for missing-model resolution and made edge
cases harder to reason about. In particular, it could diverge from the
behavior used when a workflow is loaded, and it did not naturally handle
the case where a model becomes missing after the workflow is already
open. This version pays the cost of refreshing node definitions and
rerunning the missing-model scan for the current graph, but keeps the
refresh behavior aligned with workflow load semantics.
Expected behavior by environment:
- OSS browser:
- The action bar appears when at least one missing model has a
downloadable URL and directory.
- Download all uses the existing browser download path.
- Refresh reloads `object_info`, refreshes node definitions/combo
values, reruns missing-model detection for the current graph, and clears
the error if the selected model is now available.
- OSS desktop:
- The same action bar appears under the same downloadable-model
condition.
- Download all uses the existing Electron DownloadManager path.
- Refresh uses the same missing-model pipeline as browser, so manually
placed files or desktop-downloaded files can be rechecked without
reloading the workflow.
- Cloud:
- The action bar remains hidden because model download/import is not
supported in this section for Cloud.
A few boundaries are intentional:
- This PR does not add automatic filesystem watching. Browser OSS cannot
reliably observe local model folder changes, so the user-triggered
Refresh button remains the cross-environment mechanism.
- This PR does not redesign the public `refreshComboInNodes` API beyond
extracting `reloadNodeDefs()` for reuse. Further cleanup of toast
behavior or a more explicit object-info reload API can be follow-up
work.
- This PR keeps refresh scoped to missing-model validation; missing
media and missing nodes continue to use their existing flows.
Linear: FE-417
## Screenshots (if applicable)
https://github.com/user-attachments/assets/2e02799f-1374-4377-b7b3-172241517772
## Validation
- `pnpm format`
- `pnpm lint` (passes; existing unrelated warning remains in
`src/platform/workspace/composables/useWorkspaceBilling.test.ts`)
- `pnpm typecheck`
- `pnpm test:unit`
- `pnpm test:browser:local -- --project=chromium
browser_tests/tests/propertiesPanel/errorsTabMissingModels.spec.ts`
- `pnpm build`
- `NX_SKIP_NX_CACHE=true DISTRIBUTION=desktop USE_PROD_CONFIG=true
NODE_OPTIONS='--max-old-space-size=8192' pnpm exec nx build`
- Manual desktop verification through `~/Projects/desktop` after copying
the desktop build into `assets/ComfyUI/web_custom_versions/desktop_app`:
- confirmed the FE bundle is built with `DISTRIBUTION = "desktop"`
- confirmed missing model Download uses the desktop download path
instead of browser download
- confirmed Refresh can clear the missing model error after the model is
available
- Push hook: `pnpm knip --cache`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11661-feat-refresh-missing-models-through-pipeline-34f6d73d3650811488defee54a7a6667)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for `GPUBrushRenderer`, increasing coverage from ~3.5% to
cover constructor initialization, stroke rendering, compositing, preview
blitting, readback, and resource cleanup.
## Changes
- **What**: 27 unit tests for `GPUBrushRenderer` covering all public
methods with comprehensive WebGPU API mocks
## Review Focus
Mock factory approach for WebGPU objects — all GPU globals
(`GPUBufferUsage`, `GPUTextureUsage`, `GPUShaderStage`) are polyfilled
since happy-dom lacks WebGPU support.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11388-test-add-unit-tests-for-GPUBrushRenderer-3476d73d3650814ab0e2c0fb8c424faa)
by [Unito](https://www.unito.io)
## Summary
Extends `useWorkspaceBilling.test.ts` with five behavioral gaps in the
existing suite: `initialize` does not double-fetch balance when free
tier already has positive balance, `subscribe(planSlug)` forwards
`undefined` for return/cancel URLs, `previewSubscribe` does not refresh
status or balance after success, `pollCancelStatus` falls back to a
default error message when failed status omits `error_message`, and
`pollCancelStatus` halts further scheduled polls when a later poll's API
call rejects.
## Changes
- **What**: Adds 5 Vitest cases across four new `describe` blocks
(`initialize free-tier balance refresh`, `subscribe argument
forwarding`, `previewSubscribe does not refresh state`,
`pollCancelStatus error paths`). Reuses the existing `setupBilling()`
factory and `effectScope` lifecycle.
## Review Focus
- The mid-poll rejection test silences `unhandledRejection` for its
duration: `pollCancelStatus`'s rescheduled poll runs through `void
poll()` inside `setTimeout`, so the catch block's rethrow has no awaiter
and surfaces as unhandled. The test pins the observable behavior (no
further scheduled polls) without claiming `cancelSubscription`
propagates the late error.
- The `subscribe(planSlug)` test pins the call shape with explicit
`undefined` arguments so a future signature change breaks the test
rather than silently passing through.
- The free-tier branch is targeted: previously only the zero-balance
reload was tested; this adds the negative case (positive balance → no
second fetch).
## Testing
\`\`\`bash
pnpm exec vitest run
src/platform/workspace/composables/useWorkspaceBilling.test.ts
pnpm format --
src/platform/workspace/composables/useWorkspaceBilling.test.ts
pnpm lint
pnpm typecheck
pnpm knip
\`\`\`
38 tests pass (33 prior + 5 new).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11676-test-add-unit-tests-for-useWorkspaceBilling-polling-and-refresh-paths-34f6d73d36508197bc7bc66d54e805e0)
by [Unito](https://www.unito.io)
## Summary
Extends `useNodeReplacement.test.ts` with five connection-transfer and
graph-mutation edge cases that the existing 23-case suite did not cover:
missing-old-input-slot skip, missing-new-output-index resilience,
set_value on a non-existent widget, set_value with dot-notation new_id,
and the Vue-node refresh path via `nodeGraph.onNodeAdded`.
## Changes
- **What**: Adds 5 Vitest cases in a new `transfer edge cases` describe
block. Reuses the existing `createPlaceholderNode`, `createNewNode`,
`createMockGraph`, `createMockLink`, and `makeMissingNodeType` helpers —
no new test infrastructure introduced.
## Review Focus
- The "missing new output index" test verifies that `replaceWithMapping`
does not throw when `newNode.outputs[newOutputIdx]` is absent, and
asserts the original link's `origin_id` is unchanged so the silent-skip
behavior is pinned (not a swallowed exception).
- The dot-notation `set_value` test pins that the existing dot-notation
guard at `useNodeReplacement.ts:203` covers the `set_value` branch (not
just the `old_id` connection branch already covered at line 187).
- The `onNodeAdded` test asserts the Vue-node sync path that runs after
`replaceWithMapping` bypasses `graph.add()` — a future refactor that
drops the explicit call would silently break the Vue node renderer
otherwise.
## Testing
\`\`\`bash
pnpm exec vitest run
src/platform/nodeReplacement/useNodeReplacement.test.ts
pnpm format -- src/platform/nodeReplacement/useNodeReplacement.test.ts
pnpm lint
pnpm typecheck
pnpm knip
\`\`\`
28 tests pass (23 prior + 5 new).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11677-test-add-unit-tests-for-useNodeReplacement-transfer-edge-cases-34f6d73d3650817aa2ffccdb9fb4a947)
by [Unito](https://www.unito.io)
## Why this fix exists
Surfaced as a dead-end during the FE-227 (asset modal scroll breakage in
cloud-prod) root-cause investigation, **not** as a standalone Storybook
complaint.
The natural local repro path for FE-227 is "bump an `AssetBrowserModal`
Storybook story to ~120 assets and watch the layout misbehave." When
that path was attempted, the stories rendered empty modals regardless of
fixture size. A Codex adversarial review confirmed the cause: three
stories bind `:assets="..."` to a prop the component never declared, so
the binding is silently dropped and the modal falls back to
`assetsStore.getAssets(cacheKey)` — which returns an empty array in
Storybook.
The empty-modal failure mode also silently broke design QA / visual
review on this surface: any reviewer opening these stories has been
seeing "No assets found" for as long as the bug has existed.
Filing this as its own PR so:
- FE-227 stays focused on the cloud-prod scroll bug once a DevTools
datapoint confirms the hypothesis.
- The local repro path for FE-227 (and any future asset-modal layout
regression) becomes usable.
- Visual review on `AssetBrowserModal` is restored.
## What changed
Three `AssetBrowserModal` stories bound `:assets="..."` to a
non-existent prop, so the modal silently fell back to
`assetsStore.getAssets(cacheKey)` — which returns an empty array in
Storybook because the model cache only initializes in cloud distribution
builds. Add an optional `assets` prop on `AssetBrowserModal` that, when
provided, bypasses the store fetch. Production callers continue to use
the store; this is a narrowly scoped Storybook/test seam.
- Fixes FE-232
### Why a prop on the component (Option 1) and not a Storybook decorator
(Option 2)
`assetsStore`'s model cache (`getModelState`) is gated by `if
(isCloud)`, returning an empty stub for desktop/localhost distributions.
Storybook's `.storybook/main.ts` does not define `__DISTRIBUTION__`, so
`isCloud === false` and the store has no public API to seed assets.
Public seed methods (`updateModelsForNodeType` / `updateModelsForTag`)
only delegate to `assetService` network calls. Option 2 (decorator-based
seeding) would require either patching Storybook's Vite `define` config
or building a parallel mock store via `resolve.alias` (the `useJobList`
precedent) — significantly more invasive than a +10 / -1 line component
change. The new prop is documented as a Storybook/test seam in JSDoc and
changes nothing for production callers.
### Bonus
`useAssetBrowserDialog.stories.ts:120` had the **same** broken
`:assets="mockAssets"` binding. The new prop transparently repairs it
without a separate change.
## Before
All three stories render an empty modal (`No assets found`) regardless
of the fixture data they pass.
> Drag-drop the screenshots into the slots below from
`/tmp/fe-232-screenshots/`:
> - `before-default.png` → Default story
> - `before-single-asset-type.png` → Single asset type story
> - `before-no-left-panel.png` → No left panel story
| Story | Screenshot |
|---|---|
| Default | |
<img width="961" height="821" alt="before-default"
src="https://github.com/user-attachments/assets/4a0af0f5-b712-41e2-adbc-c2b4b921045d"
/>
| Single asset type | |
<img width="961" height="821" alt="before-single-asset-type"
src="https://github.com/user-attachments/assets/073a8fa8-7bbb-4ec9-a226-156b7141d9b5"
/>
| No left panel | <img width="961" height="821"
alt="before-no-left-panel"
src="https://github.com/user-attachments/assets/0d45ff3b-5866-4de9-b7aa-5bd9cb1f3566"
/> |
## After
All three stories now render their intended fixture data (asset cards
visible with mock model names, badges, sort/filter controls populated).
> Drag-drop the screenshots into the slots below from
`/tmp/fe-232-screenshots/`:
> - `after-default.png` → Default story
> - `after-single-asset-type.png` → Single asset type story
> - `after-no-left-panel.png` → No left panel story
| Story | Screenshot |
|---|---|
<img width="961" height="821" alt="after-default"
src="https://github.com/user-attachments/assets/a11b2475-bd18-4c30-aece-cf1bdbcc6ac5"
<img width="961" height="821" alt="after-single-asset-type"
src="https://github.com/user-attachments/assets/71e11237-006b-43d9-90de-e9d2d8894e34"
/>
/>
| Default | |
| Single asset type | |
| No left panel | <img width="961" height="821"
alt="after-no-left-panel"
src="https://github.com/user-attachments/assets/5123db87-2ab9-4359-8e61-ac0d8da9494c"
/>
|
## Test plan
- [x] Storybook stories now render fixture data (manually verified all
three via Chrome DevTools MCP)
- [x] `pnpm typecheck` passes on touched files
- [x] `pnpm lint` passes on touched files
- [x] Existing `AssetBrowserModal.test.ts` (14 tests) still passes
- [x] `useAssetBrowserDialog.stories.ts` is also functional (same bug
pattern, repaired by the new prop)
- [ ] No new prop surface added to `AssetBrowserModal` other than the
documented Storybook/test seam (`assets?: AssetItem[]`)
## Summary
Adds 42 unit tests across 5 files covering the value-control widget
family — first batch of a broader effort to raise widget-test coverage.
## Changes
- **What**:
- `WidgetInputNumber.test.ts` (9) — variant selection by widget.type
(int/float/slider/gradientslider), controlWidget wrapping in
WidgetWithControl, modelValue forwarding.
- `WidgetInputNumberGradientSlider.test.ts` (11) — initial value,
min/max/disabled pass-through, default vs custom gradient stops,
precision-derived step, WidgetLayoutField wrapping.
- `WidgetWithControl.test.ts` (5) — renders passed component with
widget/modelValue, initializes ValueControlButton mode from
widget.controlWidget.value, calls controlWidget.update on mode change.
- `ValueControlButton.test.ts` (11) — i18n aria-label per mode, text vs
icon rendering, pointer and keyboard activation, `type="button"` safety.
- `ValueControlPopover.test.ts` (6) — BEFORE/AFTER copy from
settingStore, four option render, v-model updates on selection.
## Review Focus
- Stack follows the existing widget-test pattern (`@testing-library/vue`
+ PrimeVue + `createI18n` where needed, no `@vue/test-utils`).
- `createMockWidget` from `widgetTestUtils.ts` reused; no new helper
extracted (YAGNI — per-file `renderComponent` stays ~10 lines).
- `WidgetWithControl` watcher test asserts first-arg of `update` since
the vue watch callback passes `(newVal, oldVal, onCleanup)`.
- No changes to any widget component source — tests-only PR.
This is one of several focused PRs in a widget-test-coverage sequence;
subsequent PRs cover form-dropdown internals, utility widgets,
media/graph/canvas widgets, and e2e value-type specs.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11440-test-add-unit-tests-for-value-control-widget-family-3486d73d3650813891e1fe8d45eaecaf)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: bymyself <cbyrne@comfy.org>
## Summary
Extends `useMissingModelInteractions.test.ts` with behavioral coverage
for the previously untested public surface: `getComboOptions` (both
asset-supported and widget-driven branches), `getDownloadStatus`, and
the four `handleImport` outcomes (async-pending, async-completed,
sync-with-mismatch, error).
## Changes
- **What**: Adds 10 Vitest cases across three new `describe` blocks
(`getComboOptions`, `getDownloadStatus`, `handleImport`). Extends the
existing module-level mocks with `mockUploadAssetAsync`,
`mockTrackDownload`, and `mockInvalidateModelsForCategory` so the import
flow can be verified at its boundaries.
## Review Focus
- The `handleImport` block uses a shared `setupImportableState(key)`
helper to seed `urlInputs` + `urlMetadata` and stub `validateSourceUrl`
once per test. Each case then asserts a single boundary effect (taskId
tracked, cache invalidated, mismatch recorded, error stored).
- The `getDownloadStatus` happy path relies on the existing getter-style
`mockDownloadList` so the test's mutation lands in the composable
without re-stubbing the asset download store.
- The `getComboOptions` "asset-supported" test asserts both the call
shape (`mockGetAssets` invoked with the candidate's `nodeType`) and the
output shape, so a future refactor that swaps the lookup key fails
loudly.
## Testing
\`\`\`bash
pnpm exec vitest run
src/platform/missingModel/composables/useMissingModelInteractions.test.ts
pnpm format --
src/platform/missingModel/composables/useMissingModelInteractions.test.ts
pnpm lint
pnpm typecheck
pnpm knip
\`\`\`
44 tests pass (34 prior + 10 new).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11675-test-add-unit-tests-for-useMissingModelInteractions-34f6d73d36508112909fe8e49cc68010)
by [Unito](https://www.unito.io)
## Summary
Addresses feedback from the initial v2 node search implementation for
improved UI and UX
## Changes
- **What**:
- add root filter buttons
- remove all extra tree categories leaving only "Most relevant"
- replace input/output selection with popover
- replace price badge with one from node header
- add chevrons and additional styling to category tree
- hide empty categories
- fix bug with hovering selecting item under mouse automatically
- fix tailwind merge with custom sizes removing them
- keyboard navigation
- general tidy/refactor/test
## Screenshots (if applicable)
https://github.com/user-attachments/assets/db798dfa-e248-4b48-bb56-2fa7b6c5f65f
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9714-feat-Node-search-UX-updates-31f6d73d365081cebd96c4253ad1ca53)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
- Move `mockCanvasHistory` / `mockStore` into `vi.hoisted()` so the mock
state is hoisted before module imports, matching the pattern in
`useCanvasTransform.test.ts`.
- Wrap the temporary `document.activeElement` override in `try/finally`
so the property is restored even if the assertion throws, preventing
state leak into subsequent tests.
- Fixes#11658
## Test plan
- [x] `pnpm test:unit src/composables/maskeditor/useKeyboard.test.ts` —
17/17 pass
- [x] `pnpm typecheck`
- [x] `pnpm lint` (no new warnings)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11659-test-harden-useKeyboard-test-setup-with-vi-hoisted-and-try-finally-34f6d73d36508139be2ddc3095ea6952)
by [Unito](https://www.unito.io)
## Root cause
Three surfaces rendered `asset.name` directly even though in Cloud
production `asset.name` often equals `asset.asset_hash`:
1. **Asset browser modal** (`AssetCard.vue`) used `getAssetDisplayName`,
which — when `user_metadata.name` also held the hash — fell through to
the raw hash.
2. **Load Image node widget dropdown** (`useWidgetSelectItems.ts`)
rendered output items as `${asset.name} [output]`.
3. Queue-mapped output assets (`mapTaskOutputToAssetItem`) populate the
human-readable filename only on `asset.display_name`, not on
`user_metadata.filename` / `metadata.filename`. So surfaces that rely
only on `getAssetFilename` still fall through to `asset.name` (the
hash).
Filename / title resolution is now split into three helpers with
distinct responsibilities:
- `getAssetFilename` (unchanged) — canonical filename for serialization
/ identifier use (workflow widget values, schema validation,
missing-model matching). Never substitutes a display-only string.
- `getAssetDisplayFilename` (new) — filename-first label for surfaces
that render a filename. Adds `asset.display_name` as a fallback before
`asset.name`. Used by the Load Image output dropdown.
- `getAssetCardTitle` (new) — card title / delete-dialog label. Prefers
`user_metadata.name` / `metadata.name` when distinct from `asset.name`
(preserves user-renamed model titles from `ModelInfoPanel`), and falls
through to `getAssetDisplayFilename` for the Cloud hash case.
The dropdown item's `name` field (workflow payload value) is still
`${asset.name} [output]`, so Cloud can continue to resolve the asset by
hash.
Fixes FE-228
Source:
https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1776716352588229
## Red / green verification
| Step | SHA | Purpose |
| --- | --- | --- |
| Red (card — metadata.filename) | `20b32e4f0` | `AssetCard.test.ts`
asserts the rendered title is the human-readable filename when
`asset.name === asset_hash`. |
| Green (card) | `ea889b34c` | `AssetCard.vue` switches from
`getAssetDisplayName` to `getAssetFilename`. |
| Red (widget — metadata.filename) | `318feddec` | Asserts the output
dropdown `label` uses `metadata.filename` when `asset.name` is a hash. |
| Green (widget) | `7b19bde15` | `useWidgetSelectItems.ts` derives
`label` from `getAssetFilename`; `name` (serialized) stays
`\${asset.name} [output]`. |
| Red (widget — display_name path) | `b19716e60` | Failing test for the
queue-mapped shape (`display_name` populated, `user_metadata.filename`
absent). |
| Green (util, reverted) | `533e60d6a` | Initial attempt broadened
`getAssetFilename` to include `display_name`; altered filename semantics
for model-asset consumers. |
| Refactor (scope narrowing) | `7c1085f30` | Reverts the util change;
applies `display_name` fallback locally in the output dropdown only. |
| Red (card — display_name path) | `38a9d4828` | Failing test: AssetCard
must fall back to `display_name` when filename metadata is absent. |
| Green (helper split) | `4ca0f620f` | Introduces
`getAssetDisplayFilename`; swaps AssetCard + widget dropdown to use it.
Adds helper unit tests. |
| Red (card — preserves curated name) | `dc2e9231d` | Failing test: a
user-curated `user_metadata.name` distinct from `asset.name` must win
over the filename; plus non-regression guard that
curated-name-equal-to-hash still falls back to filename. |
| Green (card title helper) | `5decf3a2b` | Adds `getAssetCardTitle`
(curated name when distinct, else `getAssetDisplayFilename`). AssetCard
title + delete-dialog swap to it; widget dropdown stays on
`getAssetDisplayFilename`. |
Side-effect audit (`getAssetFilename` still canonical):
- `createModelNodeFromAsset.ts`, `createAssetWidget.ts`,
`missingModelScan.ts`, `useComboWidget.ts`, `useWidgetSelectItems.ts`
asset-mode `name` — all still resolve through `getAssetFilename`, so
model-asset widget serialization and missing-model matching are
unaffected.
## Screenshots
### As is — asset browser card
<img width="1269" height="899" alt="Screenshot 2026-04-21 at 10 49 41
AM"
src="https://github.com/user-attachments/assets/7cffb585-4e64-4037-8bb1-5dd40215597e"
/>
### To be
<img width="1145" height="533" alt="Screenshot 2026-04-25 at 7 25 17 PM"
src="https://github.com/user-attachments/assets/8f12388a-16df-4892-83b4-c8d1f033f190"
/>
_Verified locally via `pnpm dev:cloud`: asset browser cards preserve
user-curated display names, Cloud-hash cases render the filename, and
the Load Image output dropdown also renders the human-readable filename.
The selected value still serializes the hash path._
## Test plan
- [ ] \`pnpm test:unit -- AssetCard.test\` passes (4 cases:
metadata.filename, display_name fallback, curated-name preservation,
curated-name==hash fallback)
- [ ] \`pnpm test:unit -- assetMetadataUtils.test\` passes (53 cases
incl. new helper coverage)
- [ ] \`pnpm test:unit -- useWidgetSelectItems.test\` passes (26 cases)
- [ ] \`pnpm test:unit -- useMissingModelInteractions.test\` passes
(guards against model-asset regressions)
- [ ] Asset browser modal: user-renamed model shows curated name; Cloud
hash outputs show filename
- [ ] Load Image node → widget dropdown → Outputs tab shows original
filenames
- [ ] Delete-confirm dialog references the same curated name as the card
title
- [ ] Model-asset widgets (Checkpoint / LoRA / etc.) still serialize the
model filename
- [ ] Submitting a workflow that references a Cloud output still
executes
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11492-fix-use-getAssetFilename-in-asset-browser-to-avoid-showing-hashes-3496d73d36508148b8a3fb0482fa668e)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Adds 6 @cloud-tagged Playwright tests covering the assets sidebar sort
menu (newest/oldest/longest/fastest). Phase 4 of the assets-test plan.
**Stacked on #11632** — base will retarget to \`main\` once the
foundation PR merges. Independent of #11633 (filter E2E) and can be
reviewed in parallel.
## Changes
- **What**: New file `browser_tests/tests/sidebar/assets-sort.spec.ts`.
Uses the `createJobsWithExecutionTimes()` factory and the
`sortLongestFirst` / `sortFastestFirst` locators added in #11632.
- **Coverage**:
- Settings menu exposes all four sort options in cloud mode
- Default order is newest first (descending `create_time`)
- "Oldest first" reverses the order
- "Longest first" puts the slowest execution at the top
- "Fastest first" puts the quickest execution at the top
- Sort persists across search-input edits
- **Breaking**: none
## Review Focus
- **Misaligned (create_time, duration) axes**: fixture data is
deliberately constructed so newest/oldest and longest/fastest produce
distinct orderings — no test can false-pass by satisfying a different
sort. See the table comment at the top of the spec.
- **`@cloud` tag is required**: sort options are gated behind
`:show-sort-options="isCloud"`, which depends on the compile-time
`__DISTRIBUTION__` flag. Tests run only against the `cloud` Playwright
project.
- **Local verification needed**: maintainer should verify with `pnpm
dev:cloud` + `pnpm test:browser:local --project cloud --grep "sort
options"` before merging — I could not run the cloud dev server
end-to-end in my environment.
Fixes#10779
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11634-test-assets-add-E2E-spec-for-sort-options-34e6d73d365081a79facde5bde2e18c6)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for `useCoordinateTransform` mask editor composable,
raising coverage from 2.43% to 100% (statements / branches / functions /
lines).
## Changes
- **What**: Add
`src/composables/maskeditor/useCoordinateTransform.test.ts` (14 tests)
covering both `screenToCanvas` and `canvasToScreen`: identity (display
matches bitmap), uniform downscale (bitmap larger than display),
`pointerZone`-vs-`canvasContainer` offset, non-uniform per-axis scaling,
screen↔canvas round-trip, and the three "element missing" branches
(`pointerZone` / `canvasContainer` / `maskCanvas` null) that should warn
and return `{x:0,y:0}`.
## Review Focus
- Mocked `createSharedComposable` to a pass-through so each test gets a
fresh transform reading the latest `mockStore` refs (otherwise the
shared instance captures stale element references between tests).
- DOM rects are stubbed via `vi.spyOn(el, 'getBoundingClientRect')`
rather than constructing fake DOMRects, so `unref(...)` in the
composable still receives a real `HTMLElement` / `HTMLCanvasElement`.
- Round-trip test (`screenToCanvas` → `canvasToScreen`) verifies the two
functions are mathematical inverses under the offset + scale
combination, which is the actual invariant the rest of the editor relies
on.
- Style aligned with sibling tests: `should ...` naming, `describe`
grouped by public method, explicit `MockStore` type alias, helper
factories `createElementWithRect` / `createCanvasWithRect`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11640-test-add-unit-tests-for-useCoordinateTransform-mask-editor-composable-34e6d73d3650814d95bdef66e36328e8)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for `useKeyboard` mask editor composable, raising
coverage from 0% to 100% (statements/lines/functions, 95.65% branch).
## Changes
- **What**: Add `src/composables/maskeditor/useKeyboard.test.ts` (17
tests) covering key tracking (`isKeyDown`), space-key
blur/preventDefault, undo/redo shortcuts (Ctrl/Meta+Z, Ctrl+Shift+Z,
Ctrl+Y), modifier-key edge cases (Alt suppression, no-modifier no-op,
Ctrl+Shift+Y ignored), `window blur` clearing keys, and listener
teardown via `removeListeners`.
## Review Focus
- Mock surface is intentionally minimal — only `useMaskEditorStore` is
mocked because the composable only reaches
`store.canvasHistory.{undo,redo}`.
- `afterEach(keyboard.removeListeners)` is required: the composable
attaches listeners to `document` / `window`, so without teardown earlier
test instances leak handlers and inflate mock call counts in later
tests.
- Tests dispatch real `KeyboardEvent`s via `document.dispatchEvent`
rather than calling the internal handlers directly, so they exercise the
actual `addEventListener` wiring.
- Test style aligned with existing mask editor tests: `should ...`
naming, `describe` grouped by public method, explicit `MockStore` /
`MockCanvasHistory` type aliases.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11639-test-add-unit-tests-for-useKeyboard-mask-editor-composable-34e6d73d36508129b437d0270d9424d8)
by [Unito](https://www.unito.io)
## Summary
Lays the test foundation for the two open assets-testing issues — Phase
1 (fixtures + page object) and Phase 2 (unit tests). Phase 3/4 (the
actual E2E specs) follow in stacked PRs.
## Changes
- **What**: 27 new unit tests covering `useMediaAssetFiltering`,
`MediaAssetFilterMenu`, and `MediaAssetSettingsMenu`; reusable
Playwright fixture factories for diverse media kinds and execution-time
specs; new locators + helpers on `AssetsSidebarTab` for the filter menu
and longest/fastest sort options. No production code touched.
- **Breaking**: none
### New files
- `src/platform/assets/composables/useMediaAssetFiltering.test.ts` — 11
cases: single/multi-OR media-type filter, `'3D'` → `'3d'` filename
normalization, exclusion of unsupported kinds, all four sort modes,
`created_at` fallback when `user_metadata.create_time` is absent,
filter+sort composition.
- `src/platform/assets/components/MediaAssetFilterMenu.test.ts` — 6
cases: checkbox rendering, prop-driven `aria-checked`, click toggling
(add/remove/append), keyboard activation (Enter/Space).
- `src/platform/assets/components/MediaAssetSettingsMenu.test.ts` — 10
cases: view-mode v-model, `showSortOptions` and `showGenerationTimeSort`
visibility gates, `v-model:sortBy` round-trip for
newest/oldest/longest/fastest.
### Extended files
- `browser_tests/fixtures/helpers/AssetsHelper.ts` — added
`MediaKindFixture` type, optional `mediaKind` shorthand on
`createMockJob` (sets both filename extension and
`preview_output.mediaType`), plus `createMixedMediaJobs(kinds)` and
`createJobsWithExecutionTimes(specs)` factories for unambiguous
filter/sort assertions.
- `browser_tests/fixtures/components/SidebarTab.ts` — added
`filterButton`, per-type checkbox locators
(`filterImage/Video/Audio/3DCheckbox`), `sortLongestFirst`,
`sortFastestFirst`, plus `openFilterMenu()`, `filterCheckbox(kind)`,
`toggleMediaTypeFilter(kind)`, and `getAssetCardOrder()` helpers.
## Review Focus
- **Naming of `MediaKindFixture` values** — `'images'` is plural to
match existing API conventions emitted by the backend /
`useMediaAssetGalleryStore`; `'video' | 'audio' | '3D'` follow the
singular `MediaKind` type. Open to renaming if a unified shape is
preferred.
- **Filter button locator strategy** — `MediaAssetFilterButton` has no
`aria-label`, so the page object targets it via the
`icon-[lucide--list-filter]` class. Happy to add a `data-testid` or
`aria-label` to the source component if reviewers prefer a more durable
hook (would be a one-line source change in a follow-up).
## Follow-up PRs
- Phase 3 (E2E for media-type filter) → closes#10780
- Phase 4 (E2E for asset sort) → closes#10779
Both are stacked on this branch and can be reviewed/merged in either
order once this lands.
References #10779, #10780.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11632-test-assets-add-unit-tests-and-E2E-fixture-groundwork-for-sidebar-filter-sort-34e6d73d3650815c9900e5fd7cc7eab0)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for `maskEditorDataStore` Pinia store, raising coverage
from 0% to 100% (statements / branches / functions / lines).
## Changes
- **What**: Add `src/stores/maskEditorDataStore.test.ts` (13 tests)
covering initial state, the three `computed` predicates
(`hasValidInput`, `hasValidOutput`, `isReady` including the `isLoading`
interaction), the `setLoading(loading, error?)` action across its three
branches (no error arg, truthy error arg, empty-string error arg — empty
string is falsy so it must NOT clobber an existing `loadError`), and
`reset()` clearing every field including derived predicates.
## Review Focus
- Uses `createTestingPinia({ stubActions: false })` so action
implementations actually run, matching the pattern used by other store
tests in `src/stores/` (e.g. `dialogStore.test.ts`).
- The `setLoading('', '')` test guards a real branch in the source — `if
(error)` skips assignment for empty strings, so callers can't
accidentally clear a previous error by passing `''`. Worth keeping if
anyone tightens the guard later.
- `reset()` test asserts both raw refs and the three computed values
flip back to `false` / `null`, so a future regression in either
direction is caught.
- Style aligned with sibling tests: `should ...` naming, `describe`
grouped by exposed property/action, `createImage` / `createCanvas` /
`createOutputData` helpers to keep arrange blocks short.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11641-test-add-unit-tests-for-maskEditorDataStore-34e6d73d36508121b8e5d185178310ab)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Add unit tests for the three mask editor control components
(`DropdownControl`, `SliderControl`, `ToggleControl`), raising each from
partial (20% / 37.5% / 44.4%) to 100% across all coverage dimensions.
## Changes
- **What**: Add three sibling test files under
`src/components/maskeditor/controls/`:
- `DropdownControl.test.ts` (5 tests): label render, normalization of
`string[]` options into `{label,value}`, pass-through of `{label,value}`
options, `modelValue` reflected as the selected option,
`update:modelValue` emitted on change.
- `SliderControl.test.ts` (4 tests): label render,
`min`/`max`/`step`/`modelValue` exposed on `<input type="range">`,
`step` defaults to `1` when omitted, `update:modelValue` emitted as a
`number` (not string) on input.
- `ToggleControl.test.ts` (5 tests): label render, `modelValue`
reflected as `checked`, `update:modelValue` emitted as `true` / `false`
on toggle.
## Review Focus
- Stack matches the project's prevailing Vue test pattern
(`@testing-library/vue` + `@testing-library/user-event`, e.g.
`Badge.test.ts`, `BatchCountEdit.test.ts`).
- `update:modelValue` is asserted via the `'onUpdate:modelValue'`
callback prop rather than `emitted()` — keeps tests focused on
observable behavior and avoids reaching into the wrapper.
- `SliderControl` uses `fireEvent.input` instead of
`userEvent.type/clear`: `<input type="range">` is non-editable for
`userEvent` (`clear() is only supported on editable elements`). The
single eslint-disable for `testing-library/prefer-user-event` is
annotated with the reason.
- `DropdownControl` test guards both branches of the `string` →
`{label,value}` normalization (string array path and pre-normalized
object array path), since that's the only meaningful logic in the file.
- Style aligned with sibling tests: `should ...` naming, per-file
`renderComponent` helper accepting a `props` override and an `onUpdate`
callback parameter.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11642-test-add-unit-tests-for-mask-editor-control-components-34e6d73d3650812aba2ae34a760489e2)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for `useToolManager` mask editor composable, raising
coverage from 0% to 100% (statements / functions / lines, 97.53%
branch).
## Changes
- **What**: Add `src/composables/maskeditor/useToolManager.test.ts` (35
tests) covering:
- `switchTool`: store update, layer auto-switch via
`newActiveLayerOnSet`, custom-cursor branch, no-cursor (default
`'none'`) branch, missing-`pointerZone` no-throw guard.
- `setActiveLayer`: rgb-while-mask-only-tool → swap to `PaintPen`,
mask-while-`PaintPen` → swap to `MaskPen`, no-swap path.
- `updateCursor`: same custom-cursor / default-cursor split plus
`brushPreviewGradientVisible = false` post-condition.
- `currentTool` watcher: clears `lastColorSelectPoint` only when leaving
`MaskColorFill`.
- `handlePointerDown`: touch-ignore, pen pointer registration,
middle-button pan, space+left pan, `MaskPen`/`PaintPen` left-button
drawing, `PaintPen` continue-drawing branch (`button !== 0 && buttons
=== 1`), `MaskBucket` flood fill (with coord transform),
`MaskColorFill`, alt+right brush adjustment, right-click drawing for
drawing tools, no-op for non-drawing tools.
- `handlePointerMove`: touch-ignore, cursor position update,
middle-button pan, space+left pan, non-drawing-tool ignore, alt+right
brush adjustment while `isAdjustingBrush`, left/right drag drawing.
- `handlePointerUp`: state cleanup (`isPanning` / `brushVisible` /
`isAdjustingBrush`), pen pointer removal, touch-pointer early bail
before `drawEnd`.
## Review Focus
- Mock store is wrapped in `reactive()` so the `watch(() =>
store.currentTool, ...)` actually fires when tests mutate `currentTool`.
Plain object mocks would silently no-op the watcher branch.
- Each `setup()` runs `useToolManager` inside its own `effectScope`,
stopped in `afterEach`. Without scoping, watchers from previous tests
stay attached to the shared reactive store and accumulate (a single
mutation in test N would call `clearLastColorSelectPoint` N times).
- Mocked `app.extensionManager.setting.get` because `useBrushDrawing`
factory reads two settings synchronously at construction time. The mock
returns deterministic defaults so we don't need `useSettingStore`
plumbing.
- Pointer-event factory builds the minimal shape (`button` / `buttons` /
`pointerType` / `offset*` / `client*` / `altKey` / `pointerId`) — no
jsdom `PointerEvent` constructor noise. `preventDefault` is a `vi.fn()`
because the source calls it unconditionally.
- Style aligned with sibling tests: `should ...` naming, `describe`
grouped by exposed function/watcher, typed `MockStore`, helper
`pointerEvent({ ... })` and `setup()`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11643-test-add-unit-tests-for-useToolManager-mask-editor-composable-34e6d73d36508184b017ebd04626b29d)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for `useMaskEditor` composable, raising coverage from 0%
to 100% (statements / branches / functions / lines).
## Changes
- **What**: Add `src/composables/maskeditor/useMaskEditor.test.ts` (7
tests) covering `openMaskEditor`:
- Happy path: dialog opened once, `node` forwarded as a prop, header /
content components attached.
- Modal dialog config (`modal` / `maximizable` / `closable` flags)
forwarded to PrimeVue dialog props.
- Acceptance path for nodes with no `imgs` but `previewMediaType ===
'image'`.
- Three guard paths that should log and bail: `node` is null, node with
empty `imgs` and no image preview, node with empty `imgs` and a
non-image preview type (e.g. `'video'`).
## Review Focus
- Mocked `useDialogStore` with a single shared `showDialog` spy — the
only contract under test is "we forwarded these props to the store
action", so instantiating Pinia would just add noise.
- `TopBarHeader.vue` and `MaskEditorContent.vue` are stubbed because
they pull in the full mask-editor render tree; we only assert they're
forwarded as `headerComponent` / `component`, not what they render.
- `console.error` is spied per-test so the bail messages are observable
but don't pollute runner output.
- `nodeWithImage` factory uses a structural `NodeShape` (`{ imgs?,
previewMediaType? }`) rather than `Partial<LGraphNode>` because the real
`LGraphNode` type requires a `LGraphNodeConstructor`-shaped
`constructor` field, which would force every test to construct a full
graph node — irrelevant to the contract being tested.
- Style aligned with sibling tests: `should ...` naming, `describe`
grouped by exposed function (`openMaskEditor`), helper factory.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11644-test-add-unit-tests-for-useMaskEditor-composable-34e6d73d365081e98336db0a92c37ccf)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for `maskEditorStore` Pinia store, raising coverage from
0% to 100% (statements / branches / functions / lines).
## Changes
- **What**: Add `src/stores/maskEditorStore.test.ts` (30 tests)
covering:
- Brush setters: `setBrushSize` (1–250), `setBrushOpacity` (0–1),
`setBrushHardness` (0–1), `setBrushStepSize` (1–100) — each tested at
lower bound, upper bound, and in-range.
- `resetBrushToDefault`: documents the exact default brush shape.
- Other clamped setters: `setPaintBucketTolerance` /
`setColorSelectTolerance` / `setMaskTolerance` (0–255), `setFillOpacity`
/ `setSelectionOpacity` (0–100), `setMaskOpacity` (0–1), `setZoomRatio`
(0.1–10).
- `setPanOffset` / `setCursorPoint`: copy-by-value semantics — mutating
the input after the call must not leak into store state.
- `resetZoom` / `triggerClear`: monotonic counter bumps.
- `maskColor` computed: `Black`, `White`, `Negative` blend modes plus
the `default:` fallback for unknown values.
- `canUndo` / `canRedo` proxy through to mocked `useCanvasHistory`.
- Canvas → ctx watchers: setting `maskCanvas` / `rgbCanvas` /
`imgCanvas` derives the corresponding `*Ctx` via `getContext('2d', {
willReadFrequently: true })`. Clearing the canvas leaves the previous
ctx in place (parametrized via `it.each` for all three).
- `resetState`: restores all non-DOM state to documented defaults;
explicitly verifies DOM refs (`maskCanvas` / `pointerZone` / `image`)
are NOT cleared so the editor can reuse mounted elements after a reset.
## Review Focus
- `useCanvasHistory` is mocked via `vi.hoisted` so each test gets the
same exposed `canUndo` / `canRedo` refs while the store's internal
`canvasHistory` reference is untouched. Without this, the store would
call into the real history with `null` canvas refs.
- `setPanOffset` / `setCursorPoint` tests mutate the input *after* the
call — that's the actual behavioral contract (defensive copy via
spread), not a default-value check.
- `resetState` test sets *every* field to a non-default before calling,
so the test fails if `resetState` ever forgets to reset a field. Final
assertions are positive (matches default), not weak negative checks.
- The "DOM refs preserved on reset" assertion is the
surprising-on-purpose part: a future refactor that adds
`maskCanvas.value = null` to `resetState` would break the editor's
ability to reuse mounted canvases after clearing internal state.
- `it.each` for the three canvas/ctx pairs covers the watcher's null
branch without three near-duplicate tests.
- `makeCanvas` overrides `canvas.getContext` directly rather than using
`vi.spyOn` because `HTMLCanvasElement.getContext` has overloads (2d /
webgl / webgpu / bitmaprenderer) and TypeScript picks the GPU overload
by default for spy return type inference.
- Style aligned with sibling `maskEditorDataStore.test.ts`:
`createTestingPinia({ stubActions: false })`, `should ...` naming,
`describe` grouped by exposed property/action, no default-only
change-detector tests.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11645-test-add-unit-tests-for-maskEditorStore-34e6d73d3650818e9855cd9f9f13e62a)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for the five mask editor settings panel components,
raising each from 0–35% to ~98–100% across coverage dimensions.
## Changes
- **What**: Add 5 sibling test files under `src/components/maskeditor/`
(47 tests total):
- `PaintBucketSettingsPanel.test.ts` (4): both sliders bind to / write
through to `setPaintBucketTolerance` / `setFillOpacity`. **100%**.
- `ColorSelectSettingsPanel.test.ts` (8): all 7 controls (4 sliders, 2
toggles, 1 dropdown) bind to and write through to the right store
fields/setters; method dropdown casts to `ColorComparisonMethod`.
**100%**.
- `BrushSettingsPanel.test.ts` (13): shape buttons (Arc/Rect), reset to
default, four numeric inputs (size/opacity/hardness/step), logarithmic
size slider including the cached-raw-value path
(`Math.round(Math.pow(250, x))`), color input v-model, color input ref
forwarded to / cleared from store on mount/unmount.
- `ImageLayerSettingsPanel.test.ts` (17): mask opacity slider + canvas
style sync, blend-mode select with all 3 enum values + default fallback,
three layer-visibility checkboxes (with and without canvas refs),
activate-layer button forwarding to `toolManager.setActiveLayer`,
disabled-when-active and "show paint button only when Eraser" branches,
base image preview src binding.
- `SettingsPanelContainer.test.ts` (5): tool → component routing
(`MaskBucket` → bucket panel, `MaskColorFill` → color panel, anything
else → brush panel).
## Review Focus
- Stack matches sibling control tests (`@testing-library/vue` +
`userEvent` + stub child controls). Each panel's child `SliderControl` /
`ToggleControl` / `DropdownControl` is replaced by a minimal `<button>`
stub that emits `update:modelValue` on click, so panel logic is
decoupled from control internals (already covered by their own tests).
- Two panels (`Brush`, `ImageLayer`) need real reactivity
(`brushSettings.size` changes through a setter must propagate to the
slider's modelValue computed; `currentTool` / `activeLayer` mutations
between tests). They use `reactive()` from Vue at top level + a `let
mockStore` reset in `beforeEach`. Plain object mocks would either
silently no-op or leak state between tests.
- The two reactive panels enable file-level `eslint-disable
testing-library/no-container, testing-library/no-node-access` because
the unlabeled DOM (shape divs, unlabeled `<input type="number">`/`<input
type="color">`/`<input type="checkbox">`/`<select>`) genuinely can't be
queried via `screen.getByRole/Label`. Each disable has a comment
explaining why.
- `BrushSettingsPanel` has a non-trivial cached-value branch in
`brushSizeSliderValue` computed: when `rawSliderValue` and `brushSize`
are in sync, the getter returns the raw float instead of recomputing
`log(size)/log(250)`. Test stubs `setBrushSize` to actually write back
so the next read takes the cached path.
- `ImageLayerSettingsPanel` has a `display: 'block' | 'none'` style
binding. happy-dom doesn't populate `el.style.display` from inline style
strings reliably, so the test asserts via `el.getAttribute('style')`
instead.
- `SettingsPanelContainer` uses inline component stubs that render
unique text — assertion is just `textContent.toContain('brush-panel')`
etc. No need for component-instance probing.
- Style aligned with sibling tests: `should ...` naming, `describe`
grouped by feature (button group, slider, etc.), `vi.hoisted` for mock
state where reactivity isn't required.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11647-test-add-unit-tests-for-mask-editor-settings-panels-34e6d73d36508117911bc0850ce085e1)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for three small mask editor Vue components (`SidePanel`,
`MaskEditorButton`, `BrushCursor`), all reaching **100%** across
coverage dimensions.
## Changes
- **What**: Add 3 sibling test files (17 tests total):
- `SidePanel.test.ts` (3): renders both `SettingsPanelContainer` and
`ImageLayerSettingsPanel`; forwards `toolManager` prop through to
`ImageLayerSettingsPanel`; works with the prop omitted.
- `MaskEditorButton.test.ts` (3): renders with localized `aria-label`
when `isSingleImageNode` is true; hidden via `v-show` (style `display:
none`) when false; click executes `Comfy.MaskEditor.OpenMaskEditor`
command.
- `BrushCursor.test.ts` (11): opacity 1 / 0 from `brushVisible`; size =
2 × effective brush size × zoom (with hardness scaling); border-radius
50% / 0% for Arc / Rect; position from `cursorPoint + panOffset -
radius`, with optional `containerRef.getBoundingClientRect` offset;
gradient preview hidden / shown; flat fill at `effectiveHardness === 1`.
## Review Focus
- `SidePanel` test stubs both child panels to bare `<div data-testid>`
so the test verifies wiring (prop forwarding, child rendering) without
being affected by the children's i18n / store dependencies.
- `MaskEditorButton` mocks `useCommandStore.execute` and
`useSelectionState.isSingleImageNode` (as a Vue ref). Real i18n via
`createI18n` + `globalInjection: true` — the template uses `$t(...)`
which requires global injection in composition mode.
- For the v-show hidden case, `getByLabelText('...', { selector:
'button' })` works where `getByRole('button', { hidden: true })` doesn't
reliably resolve through the `<Button>` wrapper component.
- `BrushCursor` uses `reactive()` mock store and queries the brush /
gradient elements by id (`#maskEditor_brush`,
`#maskEditor_brushPreviewGradient`). File-level eslint-disable for
`testing-library/no-node-access` because the component's anchor elements
are styled divs without ARIA roles or labels.
- The `radial-gradient` (hardness < 1) branch of `gradientBackground` is
intentionally **not** asserted via rendered DOM: the computed returns a
multi-line template literal that happy-dom's CSS parser drops entirely.
The math (`getEffectiveBrushSize` / `getEffectiveHardness`) is covered
by `brushUtils.test.ts`. v8 reports 100% branch coverage because Vue
evaluates the computed regardless of whether the resulting style string
is parsed by the DOM.
- Style aligned with sibling tests: `should ...` naming, `describe`
grouped by feature, `vi.hoisted` for command-store / selection-state
mocks, real i18n via `createI18n`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11648-test-add-unit-tests-for-SidePanel-MaskEditorButton-and-BrushCursor-34e6d73d365081e38c4afee32ddf2b0b)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for `PointerZone` and `ToolPanel` mask editor components,
raising each from 0% to **91.89% / 100%** respectively.
## Changes
- **What**: Add 2 sibling test files (24 tests total):
- `PointerZone.test.ts` (13): mount exposes root element to
`store.pointerZone`; pointer events (down/move/up) forward to
`toolManager.handlePointer*`; `pointerleave` hides brush + clears
cursor; `pointerenter` calls `toolManager.updateCursor`; touch events
(start/move/end) forward to `panZoom.handleTouch*`; wheel zooms then
updates cursor with wheel coords; `isPanning` watcher sets cursor to
"grabbing" then back via `updateCursor`; contextmenu's default is
prevented.
- `ToolPanel.test.ts` (11): one container rendered per `allTools`; icon
HTML for each tool; current tool highlighted with
`maskEditor_toolPanelContainerSelected` class while others are not;
clicking a container calls `toolManager.switchTool` with the matching
enum value; zoom indicator rounds `displayZoomRatio * 100`; dimensions
text reflects `image.width × image.height` (or empty when no image);
clicking the zoom indicator calls `store.resetZoom`.
## Review Focus
- `PointerZone` mocks `useToolManager` / `usePanAndZoom` to plain
function bags via factory helpers. The component is mostly forwarding,
so the test's value is in *event mapping* (which event triggers which
handler), not handler internals.
- happy-dom doesn't propagate `clientX` / `clientY` through the
`WheelEvent` constructor; the wheel test sets them via
`Object.defineProperty` after construction. Without this,
`updateCursorPosition` reads `undefined`.
- `PointerZone` ends at 91.89% statements / 70% branch — uncovered lines
are the `onMounted` early-return when `pointerZoneRef.value` is
`undefined` (always set in tests) and the watcher's same guard. Both
unreachable in normal use, intentionally not covered.
- `ToolPanel` mocks `iconsHtml` to `<svg data-testid="icon-${tool}" />`
markers, letting tests assert per-tool rendering via `getByTestId`. Real
i18n via `createI18n` for the reset-zoom tooltip text.
- `getToolContainers` queries by class because tool buttons are
unlabeled `<div>`s with click handlers (no role / aria); a file-level
`eslint-disable testing-library/no-node-access` covers this and the
dimensions-span placeholder query.
- Style aligned with sibling tests: `should ...` naming, `describe`
grouped by feature, `reactive()` mock store + `let mockStore` reset in
`beforeEach`, real i18n where keys are user-visible.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11649-test-add-unit-tests-for-PointerZone-and-ToolPanel-34e6d73d3650817daf85c0d33d16899d)
by [Unito](https://www.unito.io)
## Summary
Add Playwright E2E tests for media type filter in assets sidebar.
## Changes
- Add `filterButton`, `filterCheckbox(label)`, `openFilterMenu()` to
`AssetsSidebarTab` fixture
- New `Assets sidebar - media type filter` describe block with 3 tests:
- Filter menu shows all 4 media type checkboxes (Image, Video, Audio,
3D)
- Unchecking image filter hides image assets
- Re-enabling filter restores hidden assets
- Mock jobs use distinct file extensions (.png, .mp4, .mp3) since
filtering is extension-based
## Review Focus
Tests tagged `@cloud` — filter button is gated behind `isCloud` in
`MediaAssetFilterBar.vue`.
Fixes#10780
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10784-test-assets-add-E2E-tests-for-media-type-filter-3356d73d3650810a8ecdd102e9f5b47e)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.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
Add 301 redirects for old Framer case study URLs to new `/customers/`
pages.
## Changes
- Add `redirects` config to `apps/website/astro.config.ts` mapping two
old Framer enterprise case study URLs to their new Astro customer pages
## Testing
### Automated
- Website build succeeds with redirect pages generated
- Lint, typecheck, and format checks pass
### E2E Verification Steps
1. Deploy to preview
2. Visit
`/cloud/enterprise-case-studies/comfyui-at-architectural-scale-how-moment-factory-reimagined-3d-projection-mapping`
— should 301 redirect to `/customers/moment-factory/`
3. Visit
`/cloud/enterprise-case-studies/how-series-entertainment-rebuilt-game-and-video-production-with-comfyui`
— should 301 redirect to `/customers/series-entertainment/`
Fixes#11583
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11654-chore-add-301-redirects-for-old-Framer-case-study-URLs-34e6d73d36508187a386eed3e25cf1b2)
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)
2026-04-26 17:51:44 -04:00
563 changed files with 43562 additions and 7023 deletions
description: Use when reviewing Vitest unit-test diffs in ComfyUI_frontend, especially new mocks, store tests, component tests, or bugfix regression tests.
---
# Reviewing Unit Tests for ComfyUI_frontend
## Overview
Review for behavior and current repo rules, not motion. Compare to authoritative rules, not prior diffs or legacy snippets.
## Review Workflow
1. Identify the test type: component, store, composable, util, or bugfix regression.
2. Name the behavior the test proves. If you cannot say it in one sentence, request changes.
3. Open the authoritative doc section before judging structure.
4. Scan the red flags below.
5. State the verdict first. Name the failure mode. Cite the doc or rule.
## Source of Truth / Precedence
When docs and examples conflict, use this order:
1. Explicit repo rules, lint rules, and note blocks.
3. Rule sections in [`docs/testing/unit-testing.md`](../../../docs/testing/unit-testing.md), [`docs/testing/store-testing.md`](../../../docs/testing/store-testing.md), and [`docs/testing/component-testing.md`](../../../docs/testing/component-testing.md)
4. Example snippets
5. Prior diffs
Apply these repo-specific clarifications:
- [`docs/testing/component-testing.md`](../../../docs/testing/component-testing.md) starts with the authoritative rule: new component tests use `@testing-library/vue` with `@testing-library/user-event`. The `@vue/test-utils` snippets below it are legacy examples.
- [`docs/testing/store-testing.md`](../../../docs/testing/store-testing.md) still contains `as any` examples. Treat them as legacy snippets, not approval for new or edited test code.
- If docs conflict, prefer the stricter newer rule and call out the doc ambiguity. Do not approve through it.
| "I restructured the mocks" | If the indirection stayed, nothing improved. Flag `alias-by-renaming`. |
| "The docs do it" | Rule, note, and lint beat legacy snippet. Compare to the current rule, not the nearest example. |
| "TypeScript required the cast" | `vi.mocked()` usually narrows mock methods. Assertion-only references need no cast. |
| "Putting it in `beforeEach` is DRY" | Recreating module mock state in hooks hides singleton behavior and drifts from the documented pattern. |
| "It is only a nit" | Explicit repo-rule violations are never nits. |
| "No behavior changed, just cleanup" | Motion != fix. Ask what behavior got stronger. |
| "Mental revert is enough" | For bugfix tests, establish red on pre-fix code or ask the author to show it. |
## Mocking Rules
- Fail helpers that do not remove repeated setup, encode domain meaning, or simplify assertions. Barely earning the abstraction is not enough.
- For composables with reactive or singleton state, define stable mock state inside the `vi.mock()` factory. Access it per test via the composable itself. See [`docs/testing/unit-testing.md`](../../../docs/testing/unit-testing.md) "Mocking Composables with Reactive State".
- This does not ban local test data builders or per-test `vi.spyOn(...)`.
- Mock seams, not the project-owned module you are trying to exercise. For store tests, prefer real Pinia plus `createTestingPinia({ stubActions: false })` per [`docs/testing/vitest-patterns.md`](../../../docs/testing/vitest-patterns.md) and [`docs/testing/store-testing.md`](../../../docs/testing/store-testing.md).
### Alias-by-Renaming
```ts
// Before
constmockAdd=vi.hoisted(()=>vi.fn())
// After: same indirection, new name
functiongetToastAddMock() {
returnuseToast().add
}
```
If the wrapper only renames or relays a mocked value, fail it. Inline the lookup at the call site or fetch the singleton mock via the documented pattern.
- Flag casts whenever `vi.mocked()` would narrow correctly.
- Do not add `vi.mocked()` around assertion-only references just for style.
### Reset Hygiene
- Flag per-mock `mockClear()` or `mockReset()` when `vi.clearAllMocks()` or `vi.resetAllMocks()` already runs in the relevant hook chain.
- Review for redundancy or broken state management. Do not bikeshed `clearAllMocks` vs `resetAllMocks` unless behavior depends on it.
### Third-Party Seams
- Distinguish trivial hooks from behavior-rich APIs.
- Mocking single-method third-party hooks like `primevue/usetoast` is usually acceptable.
- That exception does not justify mocking behavior-rich third-party modules.
### `vue-i18n`
- Never mock `vue-i18n` in component tests.
- Use real `createI18n` per [`docs/testing/vitest-patterns.md`](../../../docs/testing/vitest-patterns.md) and the shared [`testI18n`](../../../src/components/searchbox/v2/__test__/testUtils.ts) setup.
| Mock-only assertion | Accept collaborator-call assertions only when the call is the meaningful external effect and the test also exercises the triggering behavior. |
| Non-behavioral assertion | Reject tests that only check classes, utility hooks, or styling internals. |
| New component test using `@vue/test-utils` | Request changes. Use `@testing-library/vue` plus `@testing-library/user-event`. |
| `any`, `as any`, or `@ts-expect-error` in new or edited test code | Request changes unless the author proves no safer type exists. Legacy doc snippets do not authorize it. |
## Bugfix Regression Proof
For `fix:` PRs or bugfix diffs:
1. Identify the production change that fixes the bug.
2. Verify the new test fails on pre-fix code, or ask the author to show it.
3. If the test passes on broken code, request changes.
A regression test that never proves red does not pin the bug.
## Review Output Rules
- State verdict before procedural questions.
- Do not lead with approval language like `LGTM, just one nit` or `approve and move on?`.
- Name the failure mode directly: `alias-by-renaming`, `unnecessary cast`, `mocked i18n`, `mock-only assertion`, `unproven regression`.
- Link the authoritative doc section in the review comment.
- If an explicit repo rule, lint rule, or authoritative doc note is violated, do not downgrade it to "minor deviation" or "nit".
| New `vi.mock(...)` for a composable | [`docs/testing/unit-testing.md`](../../../docs/testing/unit-testing.md) -> "Mocking Composables with Reactive State" |
| New store test or store mock | [`docs/testing/vitest-patterns.md`](../../../docs/testing/vitest-patterns.md) setup + [`docs/testing/store-testing.md`](../../../docs/testing/store-testing.md) |
| New component test | Top note in [`docs/testing/component-testing.md`](../../../docs/testing/component-testing.md) |
| `vue-i18n` in a component test | [`docs/testing/vitest-patterns.md`](../../../docs/testing/vitest-patterns.md) + [`src/components/searchbox/v2/__test__/testUtils.ts`](../../../src/components/searchbox/v2/__test__/testUtils.ts) |
| Cast around a mock | [`docs/guidance/typescript.md`](../../../docs/guidance/typescript.md) -> "Type Assertion Hierarchy" |
> Comfy is the AI creation engine for visual professionals who demand control over every model, every parameter, and every output. Built around ComfyUI — the open-source node-graph runtime with 60,000+ community nodes and thousands of shared workflows — Comfy ships as a free local app, a managed cloud, an API, and an enterprise platform.
The Comfy ecosystem spans four surfaces:
- **ComfyUI (local)** — the open-source node-graph runtime that runs models on your own hardware.
- **Comfy Cloud** — managed ComfyUI in the browser, with hosted models and storage.
- **Comfy API** — a REST API for triggering workflows from your own apps and pipelines.
- **Comfy Enterprise** — single-tenant deployments, BYO keys, data ownership, and orchestration for teams.
Studios building with Comfy include Series Entertainment, Moment Factory, Open Story Movement, and Ubisoft (La Forge). Use cases concentrate in VFX & animation, advertising & creative studios, gaming, and eCommerce/fashion.
## Product
- [Homepage](https://comfy.org/): Overview of Comfy and the four product surfaces (Local, Cloud, API, Enterprise).
- [Download Comfy (Local)](https://comfy.org/download/): Free desktop app for macOS, Windows, and Linux — runs ComfyUI on your own GPU.
- [Comfy Cloud](https://comfy.org/cloud/): Managed ComfyUI in the browser with hosted models and storage; no local install required.
- [Comfy Cloud Pricing](https://comfy.org/cloud/pricing/): Plans and per-credit pricing for individuals and teams using Comfy Cloud.
- [Comfy API](https://comfy.org/api/): REST API for triggering ComfyUI workflows programmatically from external apps.
- [Comfy Enterprise](https://comfy.org/cloud/enterprise/): Single-tenant ComfyUI deployments with BYO keys, orchestration, and data-ownership guarantees.
## Workflows and Gallery
- [Workflow Gallery](https://comfy.org/gallery/): Curated showcase of ComfyUI outputs — images, video, and 3D — produced by the community.
- [Community Workflows](https://www.comfy.org/workflows/): Browseable library of community-shared ComfyUI workflows you can load and remix.
## Customers and Case Studies
- [Customer Stories](https://comfy.org/customers/): Index of named customers and how they use ComfyUI in production.
- [Series Entertainment](https://comfy.org/customers/series-entertainment/): How Series Entertainment rebuilt game and video production around ComfyUI.
- [Moment Factory](https://comfy.org/customers/moment-factory/): Architectural-scale 3D projection mapping reimagined with ComfyUI at Moment Factory.
- [Ubisoft — Chord](https://comfy.org/customers/ubisoft-chord/): Ubisoft La Forge open-sourcing the Chord model and its ComfyUI integration.
- [Open Story Movement](https://comfy.org/customers/open-story-movement/): How an open-source movement around AI storytelling builds on ComfyUI.
## Developers and Documentation
- [ComfyUI Docs](https://docs.comfy.org/): Official documentation for installing, configuring, and extending ComfyUI.
- [ComfyUI on GitHub](https://github.com/comfyanonymous/ComfyUI): Source repository for the open-source ComfyUI runtime.
- [Comfy-Org on GitHub](https://github.com/Comfy-Org): Organization-wide repositories — frontend, registry, manager, docs, and tooling.
- [Comfy Registry](https://registry.comfy.org/): Public registry of ComfyUI custom nodes and extensions, with versioning and search.
## Company
- [About Comfy](https://comfy.org/about/): Company background, mission, and the team behind ComfyUI.
- [Careers](https://comfy.org/careers/): Open roles across engineering, design, product, and go-to-market.
- [Contact](https://comfy.org/contact/): Sales, partnership, and general contact form.
- [Blog](https://blog.comfy.org/): Product announcements, technical deep-dives, and customer stories.
- [Privacy Policy](https://comfy.org/privacy-policy/): How Comfy collects, uses, and protects personal information.
- [Terms of Service](https://comfy.org/terms-of-service/): Terms governing use of ComfyUI and related Comfy services.
## Optional
- [简体中文 / Chinese homepage](https://comfy.org/zh-CN/): Simplified Chinese localization of the main site.
- [Series Entertainment — long-form case study](https://comfy.org/cloud/enterprise-case-studies/how-series-entertainment-rebuilt-game-and-video-production-with-comfyui): Extended write-up of the Series Entertainment deployment.
- [Moment Factory — long-form case study](https://comfy.org/cloud/enterprise-case-studies/comfyui-at-architectural-scale-how-moment-factory-reimagined-3d-projection-mapping): Extended write-up of Moment Factory's projection-mapping pipeline.
- [Ubisoft Chord announcement (blog)](https://blog.comfy.org/p/ubisoft-open-sources-the-chord-model): Original blog post announcing Ubisoft's open-source Chord model.
- [Open-source storytelling (blog)](https://blog.comfy.org/p/how-open-source-is-fueling-the-open): Blog post on how open source is fueling the Open Story Movement.
en:'Powered by 60,000+ nodes, thousands of workflows,\nand a community that builds faster than any one company could.',
'zh-CN':
'由 60,000+ 节点、数千个工作流\n和一个比任何公司都更快构建的社区驱动。'
'zh-CN':'60,000+ 节点,数千条工作流,\n一个比任何公司速度都更快的社区。'
},
'useCase.cta':{
en:'EXPLORE WORKFLOWS',
@@ -164,7 +167,7 @@ const translations = {
},
'products.local.cta':{
en:'SEE LOCAL FEATURES',
'zh-CN':'查看本地版特性'
'zh-CN':'查看本地版属性'
},
'products.cloud.title':{
en:'Comfy\nCloud',
@@ -176,7 +179,7 @@ const translations = {
},
'products.cloud.cta':{
en:'SEE CLOUD FEATURES',
'zh-CN':'查看云端特性'
'zh-CN':'查看云端属性'
},
'products.api.title':{
en:'Comfy\nAPI',
@@ -188,7 +191,7 @@ const translations = {
},
'products.api.cta':{
en:'SEE API FEATURES',
'zh-CN':'查看 API 特性'
'zh-CN':'查看 API 属性'
},
'products.enterprise.title':{
en:'Comfy\nEnterprise',
@@ -200,7 +203,7 @@ const translations = {
},
'products.enterprise.cta':{
en:'SEE ENTERPRISE FEATURES',
'zh-CN':'查看企业版特性'
'zh-CN':'查看企业版属性'
},
// CaseStudySpotlightSection
@@ -1215,7 +1218,7 @@ const translations = {
'pricing.included.feature4.description':{
en:'All plans will include a monthly pool of credits that are spent on active workflow runtime and <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">Partner Nodes</a> like Nano Banana Pro.',
en:'Run <strong>proprietary models</strong> through Comfy\'s <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">Partner Nodes</a>, such as Nano Banana. The amount of credits each node uses depends on the model and parameters you set in the node, but these credits are the same ones that your monthly subscription comes with. These credits can also be used across Comfy Cloud and local ComfyUI. Read more about Partner nodes <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">here</a>.',
en:'Groove Jones, a Dallas-based creative studio, used Comfy to deliver a hyper-realistic FOOH holiday campaign for the Crocs x NFL collection on a fast-approaching deadline.',
'zh-CN':
'达拉斯创意工作室 Groove Jones 借助 Comfy,在紧迫的节日档期内为 Crocs x NFL 联名系列交付了超写实的 FOOH 营销内容。'
},
'customers.story.readMore':{
en:'READ MORE ON THIS TOPIC',
'zh-CN':'阅读更多相关内容'
@@ -3277,6 +3294,227 @@ const translations = {
'zh-CN':'ComfyUI 博客'
},
// Customer Detail: Groove Jones
// Topic 1: Intro
'customers.detail.groove-jones.topic-1.label':{
en:'INTRO',
'zh-CN':'简介'
},
'customers.detail.groove-jones.topic-1.block.0':{
en:'Groove Jones, a Dallas-based creative studio, builds AI-driven campaigns and immersive experiences for major brands where photoreal polish, creative ambition, and social-ready speed all have to land together. As their work expanded across AI Video, AR, VR, and WebGL for clients like Crocs, the NFL, and Dick\u2019s Sporting Goods, they faced a recurring challenge: delivering feature-film-quality VFX on commercial timelines and budgets.',
'zh-CN':
'位于达拉斯的创意工作室 Groove Jones,为众多大牌客户打造由 AI 驱动的营销活动和沉浸式体验,需要同时兼顾照片级的精细度、创意野心,以及适配社交媒体的交付速度。随着他们为 Crocs、NFL 和 Dick\u2019s Sporting Goods 等客户的工作扩展到 AI 视频、AR、VR 和 WebGL,他们反复遇到同一个挑战:用商业项目的工期和预算,交付电影级的 VFX 质量。'
},
'customers.detail.groove-jones.topic-1.block.1':{
en:'For the Crocs x NFL collection holiday launch, that challenge came to a head. The brief called for hyper-realistic video of giant NFL-licensed Crocs parachuting into real Dick\u2019s Sporting Goods parking lots, across multiple locations, delivered on a fast-approaching holiday deadline. A live-action shoot plus a traditional CG pipeline was off the table.',
en:'The Output Groove Jones Achieved Using Comfy',
'zh-CN':'Groove Jones 借助 Comfy 实现的交付成果'
},
'customers.detail.groove-jones.topic-2.block.0':{
en:'A full FOOH (faux out-of-home) social campaign delivered on a tight holiday deadline\nHyper-realistic videos of giant NFL-licensed Crocs parachuting onto Dick\u2019s Sporting Goods parking lots\nVertical 9:16 deliverables at 2K for Instagram Reels, TikTok, and YouTube Shorts\nSame-day iteration on client notes instead of week-long asset updates\nWinner, Aaron Awards 2024: Best AI Workflow for Production',
en:'The Problem Groove Jones Was Trying to Solve',
'zh-CN':'Groove Jones 试图解决的问题'
},
'customers.detail.groove-jones.topic-3.block.0':{
en:'A traditional pipeline for this creative meant a live-action shoot at multiple store locations plus a full CG build: high-res modeling of every team\u2019s clog, look development, lighting, rendering, compositing, and a new render every time the client wanted a variation. It also meant a large crew (modelers, texture artists, lighting artists, compositors) and a schedule measured in months. Neither the budget nor the holiday window supported that path.',
en:'How Groove Jones Used Comfy to Solve the Problem',
'zh-CN':'Groove Jones 如何用 Comfy 解决问题'
},
'customers.detail.groove-jones.topic-4.block.0':{
en:'Groove Jones\u2019s Senior Creative Technologist, Doug Hogan, rebuilt the production process around Comfy\u2019s node-based workflow system, using their proprietary GrooveTech GenVFX pipeline. Custom LoRAs handled brand accuracy, a single Comfy graph orchestrated multiple generative models, and Nuke handled final polish. For a team with feature-film and commercial roots, the environment was immediately familiar.',
en:'Comfy felt very similar to working inside a traditional CG and compositing pipeline. Node-based logic, clear data flow, modular builds. It felt natural to our artists already.',
en:'Groove Jones trained custom LoRAs on the Crocs NFL Team Clogs and on Dick\u2019s Sporting Goods storefronts, so every generation came out anchored in brand-accurate references. Real team colorways, real product silhouettes, and real store exteriors stayed consistent across shots without per-frame correction, replacing what would normally take weeks of manual look development.',
'zh-CN':
'Groove Jones 基于 Crocs NFL 球队联名鞋款和 Dick\u2019s Sporting Goods 门店外景训练了定制 LoRA,让每一次生成都能锚定品牌精准的参考素材。真实的球队配色、产品轮廓和门店外观在不同镜头之间保持一致,不需要逐帧修正——而这通常意味着数周的 look development 工作量。'
en:'Brand-accurate NFL team colorways generated through custom LoRAs.',
'zh-CN':'通过定制 LoRA 生成的、与品牌精准一致的 NFL 球队配色。'
},
// Topic 6: Multi-Model Orchestration
'customers.detail.groove-jones.topic-6.label':{
en:'MULTI-MODEL ORCHESTRATION',
'zh-CN':'多模型编排'
},
'customers.detail.groove-jones.topic-6.title':{
en:'Multi-Model Orchestration in a Single Graph',
'zh-CN':'单张图内的多模型编排'
},
'customers.detail.groove-jones.topic-6.block.0':{
en:'The creative required different generative models at different stages: Flux for key-frame still development, Gemini Flash 2.5 (Nano Banana) for fast ideation and variants, and Veo 3.1 plus Moonvalley\u2019s Marey for final video generation. Comfy routed between all four inside one graph, so outputs from one model fed directly into the next without ever leaving the environment.',
en:'The Comfy community develops at an almost exponential curve, and we were able to leverage their existing nodes and tools to solve very specific production challenges instead of reinventing the wheel ourselves.',
en:'Storyboards to Previz to Final Shot in One Pipeline',
'zh-CN':'从故事板到 Previz 再到成片,全部在一条流水线内'
},
'customers.detail.groove-jones.topic-7.block.0':{
en:'The workflow opened with traditional storyboards for narrative approval, then moved into CGI blocking to lock composition, camera framing, and story beats. Comfy drove generation from there: the shoe drop, the parking lot reactions, the crowd coverage, and the environmental conversions that turned static summer storefronts into snow-covered holiday scenes, all inside the same graph.',
en:'Composition progression: wireframe blocking, mid-render, and final shot.',
'zh-CN':'构图演进:线框 blocking、中间渲染、最终成片。'
},
// Topic 8: Version Control
'customers.detail.groove-jones.topic-8.label':{
en:'VERSION CONTROL',
'zh-CN':'版本管理'
},
'customers.detail.groove-jones.topic-8.title':{
en:'Workflow Files as Version Control',
'zh-CN':'把工作流文件当作版本管理'
},
'customers.detail.groove-jones.topic-8.block.0':{
en:'Every variant of every shot lived as a Comfy workflow file, which doubled as version control. When notes came in requesting a different team colorway, store exterior, or time of day, the team duplicated a branch instead of rebuilding, which made same-day iteration possible. GPU usage and API credit burn were trackable inside the same environment as the work itself, giving Production real-time visibility into compute cost per iteration.',
'zh-CN':
'每个镜头的每个变体都以 Comfy 工作流文件的形式存在,文件本身就是版本管理。当客户反馈要求换一支球队配色、换一个门店外景或者换一个时间段时,团队只需复制一个分支,而不是重建——这才让"当天迭代"成为可能。GPU 使用量和 API 额度消耗也都能在同一个环境里追踪到,让制作部门实时看到每次迭代的算力成本。'
},
// Topic 9: Finishing in Nuke
'customers.detail.groove-jones.topic-9.label':{
en:'FINISHING IN NUKE',
'zh-CN':'Nuke 终修'
},
'customers.detail.groove-jones.topic-9.title':{
en:'Finishing in Nuke',
'zh-CN':'在 Nuke 中完成终修'
},
'customers.detail.groove-jones.topic-9.block.0':{
en:'Generated shots moved into Nuke for final compositing: falling snow, camera shake, crowd ambience, holiday audio, and 2K mastering in 9:16 for Instagram Reels, TikTok, and YouTube Shorts. Because Comfy handled generation cleanly, Nuke focused on polish and motion enhancement rather than patching generative artifacts.',
en:'By building the FOOH pipeline inside Comfy, Groove Jones turned a brief that would have required an expensive live-action shoot plus months of CG into a fast, iterative, single-environment workflow the client could direct in real time. The project recently won the Aaron Award for Best AI Workflow for Production.',
'zh-CN':
'通过在 Comfy 中搭建整套 FOOH 流水线,Groove Jones 把一个原本需要昂贵实地拍摄加数月 CG 制作的项目,变成了一套高速迭代、单一环境、客户可以实时指挥的工作流。该项目近期还荣获 Aaron Award 的"最佳 AI 制作工作流"奖。'
en:'At Groove Jones, we care deeply about delivering work that makes people say WOW! But we also care about delivering on time and on budget. VFX projects used to operate at razor thin margins. Comfy solved that for us.',
- **`fixtures/utils/`** — Pure utility functions. No `Page` dependency; stateless helpers that can be used anywhere.
- **`fixtures/components/`** — Page object components. Classes that own locators for a specific UI region (e.g. `Actionbar`, `ContextMenu`, `ManageGroupNode`).
- **`fixtures/helpers/`** — Helper classes that coordinate actions across multiple regions without owning a locator surface of their own (e.g. `CanvasHelper`, `WorkflowHelper`, `NodeOperationsHelper`).
- **`fixtures/utils/`** — Standalone utility functions. Exported functions (not classes) used by tests or fixtures (e.g. `fitToView`, `clipboardSpy`, `builderTestUtils`).
### Placement Rule
When adding a new file, use this decision tree:
```mermaid
flowchart TD
A[New file in browser_tests/fixtures/] --> B{Has any code?}
B -- No, JSON/data only --> D[fixtures/data/]
B -- Yes --> C{Is it a class?}
C -- No, exported functions --> U[fixtures/utils/]
C -- Yes --> E{Owns locators for a<br/>specific UI region?}
E -- Yes --> P[fixtures/components/]
E -- No, coordinates actions<br/>across the app --> H[fixtures/helpers/]
message:'canvas should resize to match input image width',
timeout: 15_000
})
.toBe(nw)
// Use dispatchEvent to bypass the LiteGraph canvas z-index overlay that
// intercepts coordinate-based hit testing from page.mouse
constbox=awaitcanvas.boundingBox()
if(!box)thrownewError('Canvas bounding box not found')
conststartX=box.x+box.width*0.3
constendX=box.x+box.width*0.7
constmidY=box.y+box.height*0.5
constpointerOpts={
bubbles: true,
cancelable: true,
pointerId: 1,
button: 0,
isPrimary: true
}
awaitcanvas.dispatchEvent('pointerdown',{
...pointerOpts,
clientX: startX,
clientY: midY
})
for(leti=1;i<=10;i++){
awaitcanvas.dispatchEvent('pointermove',{
...pointerOpts,
clientX: startX+(endX-startX)*(i/10),
clientY: midY
})
}
awaitcanvas.dispatchEvent('pointerup',{
...pointerOpts,
clientX: endX,
clientY: midY
})
awaitexpect
.poll(()=>hasCanvasContent(canvas),{
message:'drawing over input image should produce canvas content'
})
.toBe(true)
})
}
)
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.