Compare commits

...

104 Commits

Author SHA1 Message Date
Yourz
ade1dd00c0 feat(website): implement cloud page and extract shared ReasonSection/FAQSection 2026-04-15 18:51:05 +08:00
GitHub Action
8aa408a9df [automated] Apply ESLint and Oxfmt fixes 2026-04-15 08:32:48 +00:00
Yourz
d319337e3e fix: update for coderabbitai, and make FAQ component props as translate key 2026-04-15 16:29:34 +08:00
Yourz
affced0614 feat(website): implement download/local page
- Add HeroSection, CloudBannerSection, ReasonSection, EcoSystemSection,
  ProductCardsSection, FAQSection components
- Add ProductHeroBadge with two-size node badge variant
- Extend NodeBadge with per-segment class and configurable union icon
- Add useDownloadUrl composable for OS-aware download links
- Add all en/zh-CN translations for the download page
- Replace zh-CN hardcoded download page with shared components
- Add icon-mask utility for CSS mask-based icons
- Add Playwright e2e tests (smoke, interaction, mobile)
- FAQ uses independent expand/collapse (all open by default)
- Sticky headings in ReasonSection and FAQSection on all viewports
2026-04-15 15:28:19 +08:00
Kelly Yang
66e8d570e7 test: expand Image Compare E2E and stabilize widget selectors (#11196)
## Summary

This PR extends **Image Compare** browser tests, adds **stable
`data-testid` hooks** on `WidgetImageCompare.vue`, and aligns slider
interactions with the **same viewport element** used by
`useMouseInElement`, with **layout polling** to reduce flakes.

## What this PR does

- [x] Adds **`data-testid="image-compare-viewport"`** on the compare
area root (the `containerRef` div) so E2E targets the real slider hit
region instead of a long Tailwind class chain or the `<img>` box alone.
- [x] Adds **`data-testid="image-compare-empty"`** on the no-images
branch so the empty state can be asserted **without hard-coded English**
UI text.
- [x] Adds a **smoke** test that the widget **shows both images and the
drag handle** after value injection, with **`waitForImagesLoaded`** (no
extra full-node screenshot to avoid duplicating the default-50 golden).
- [x] Extends slider coverage to **clamp at both edges** (**0%** and
**~100%**) using the viewport locator and **`expect.poll`** until
**`boundingBox()`** is valid before reading coordinates.
- [x] Updates **`moveToPercentage`** to **`expect.poll`** until the
target locator has a **non-empty layout box** before moving the mouse.
- [x] Routes **hover**, **preserve position**, and **25% / 75%
screenshot** mouse moves through **`image-compare-viewport`**
(consistent with `containerRef`).
- [x] Adds an E2E assertion that the **workflow ImageCompare node** size
is **at least 400×350** (matches the widget workflow asset).
- [x] Hardens the **broken image** case: **`page.on('pageerror')`** /
**`page.off`** in **`finally`**, **`http://127.0.0.1:1/...`** URLs for
fast failure, **`expect.soft`** on key UI invariants, and a hard
assertion that **no page errors** were recorded.
- [x] Extends the **large batch (20×20)** test to **page to the last
index** and assert **counters `20 / 20`**, **previous enabled**, and
**next disabled** on both sides.
- [x] Renames the clamp test title to use an **ASCII hyphen** (`0-100%`)
for easier grepping.

## Out of scope (unchanged in this PR)

- [ ] Replacing **`setImageCompareValue`**’s **`page.evaluate`** setup
with a full UI-driven path (would be a larger follow-up).

## Suggested title

`test: expand Image Compare E2E and stabilize widget selectors`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Primarily test and selector-hook changes; low production risk, with
only minor DOM attribute additions that could affect external test
tooling if relied upon.
> 
> **Overview**
> Improves Image Compare Playwright coverage and reduces flakiness by
driving slider interactions through a new stable
`data-testid="image-compare-viewport"` hook and polling for a valid
layout box before mouse moves.
> 
> Updates assertions to avoid localized text (new
`data-testid="image-compare-empty"`), adds smoke coverage that
images/handle render after value injection, validates slider clamping at
both 0% and ~100%, and extends screenshot tests to use the same viewport
target.
> 
> Hardens edge-case tests by ensuring broken image loads don’t raise
uncaught `pageerror`s, adds a minimum node size assertion, and extends
large-batch navigation checks through the final index and
disabled/enabled nav states.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ce3f7fbf8c. 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-11196-test-expand-Image-Compare-E2E-and-stabilize-widget-selectors-3416d73d3650814d8d2bf207943d6205)
by [Unito](https://www.unito.io)
2026-04-14 21:53:13 -04:00
Yourz
121ceda66d feat: implement website layout and homepage (#11168)
## Summary

Implement the website layout system and homepage with all sections,
reusable components, scroll-driven animations, and routing.

## Changes

- **What**:
- Reorganize components into `common/`, `home/`, `company/`, `product/`
directories
  - Add `BaseLayout` with shared `SiteNav` and `SiteFooter`
- Implement homepage sections: Hero, SocialProofBar, ProductShowcase,
UseCase, GetStarted, ProductCards, CaseStudySpotlight, BuildWhat
- Add reusable components: BrandButton, NodeBadge, ProductCard,
FooterLinkColumn, NavDesktopLink, MobileMenu
  - Add PPFormula font family, client logos, and icon assets
  - Add hero/footer logo frame sequences for scroll-driven animations
- Add `useFrameScrub` composable and `smoothScroll` (Lenis + GSAP
ScrollTrigger)
  - Add route config, nav config, and placeholder pages for all routes
  - Add Playwright e2e tests for homepage and navigation
- **Dependencies**: gsap, lenis, @astrojs/check

desktop
![Kapture 2026-04-11 at 19 36
10](https://github.com/user-attachments/assets/e4de2b1d-dea7-4c0b-9f76-134e9437998c)

mobile
![Kapture 2026-04-11 at 19 45
02](https://github.com/user-attachments/assets/4f83deba-229d-40e7-9476-2b3aa74894d1)



## Review Focus

- Component structure and naming conventions under `apps/website/`
- Scroll-driven animation approach (GSAP ScrollTrigger + Lenis smooth
scroll)
- Mobile responsive behavior (MobileMenu, ScrollTrigger matchMedia)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: DrJKL <DrJKL0424@gmail.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-04-15 09:25:41 +08:00
Dante
634d57809b test: add E2E tests for bottom panel core behaviors (#10814)
## Summary
- Add `browser_tests/tests/bottomPanel.spec.ts` with tests for behaviors
not covered by existing `bottomPanelLogs` and `bottomPanelShortcuts`
specs
- Tests cover: close button (X), tab persistence on re-open, resize
gutter visibility and drag, canvas interaction when panel is closed,
cross-panel switching (terminal <-> shortcuts), and registered tab
enumeration
- Extend `BottomPanel` fixture with `closeButton` and `resizeGutter`
locators

## Test plan
- [ ] `pnpm test:browser:local` passes all new tests in
`bottomPanel.spec.ts`
- [ ] Existing `bottomPanelLogs.spec.ts` and
`bottomPanelShortcuts.spec.ts` are unaffected
- [ ] `pnpm typecheck:browser` passes
- [ ] `pnpm lint` passes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10814-test-add-E2E-tests-for-bottom-panel-core-behaviors-3366d73d365081ea9b90c643897845fa)
by [Unito](https://www.unito.io)

---------

Co-authored-by: dante <dante@danteui-MacStudio.local>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-04-14 23:33:11 +00:00
Dante
5807e03c74 test: expand E2E coverage for toolbox actions (#10968)
## Summary
- Adds `selectionToolboxMoreActions.spec.ts` with E2E coverage for
previously untested toolbox actions
- Covers: pin/unpin, minimize/expand, adjust size, copy, duplicate,
refresh button, align (top/left), distribute (horizontal/vertical),
alignment options hidden for single selection, multi-node bypass toggle
- Part of the FixIt Burndown test coverage initiative (toolbox actions)

## Test plan
- [ ] All new tests pass in CI
- [ ] No regressions in existing selectionToolbox tests

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10968-test-expand-E2E-coverage-for-toolbox-actions-33c6d73d3650811286cefdd0eb4f5242)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-04-14 23:20:45 +00:00
Christian Byrne
5c6be4ed66 chore: add .editorconfig for cross-editor consistency (#11128)
## Summary

Add `.editorconfig` for cross-editor formatting consistency, matching
existing oxfmt settings.

## Changes

- **What**: New `.editorconfig` with 2-space indent, LF line endings,
UTF-8, trailing whitespace trimming (except `.md`)

Fixes #11073

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11128-chore-add-editorconfig-for-cross-editor-consistency-33e6d73d365081faabf6fb996144940d)
by [Unito](https://www.unito.io)
2026-04-14 21:36:11 +00:00
Dante
6847c7ba2d fix: store promoted widget values per SubgraphNode instance (#10849)
## Summary

- Multiple SubgraphNode instances of the same blueprint share inner
nodes, causing promoted widget values to collide — the last configured
instance overwrites all previous values
- Add per-instance value storage (`_instanceWidgetValues`) on
SubgraphNode so each instance preserves its own promoted widget values
independently
- Restore `widgets_values` from serialized data into this per-instance
map after promoted views are created during configure

- Fixes #10146

## Root Cause

When loading a workflow with multiple SubgraphNode instances of the same
blueprint:

1. `LGraph.configure()` creates ONE shared Subgraph per blueprint (line
2625)
2. Each SubgraphNode instance calls `configure(instanceData)`
sequentially
3. `PromotedWidgetView.value` setter writes to the **shared inner node's
widget** (`promotedWidgetView.ts:199`)
4. The last instance's `configure()` overwrites all previous instances'
values

**Regression**: Introduced by PR #8594 (WidgetValueStore, v1.41.3) which
centralized widget state without per-instance scoping for shared
blueprints.

## Fix

- **SubgraphNode**: Add `_instanceWidgetValues` Map and
`_pendingWidgetsValues` for configure-time restoration
- **PromotedWidgetView getter**: Check instance map first before falling
back to widget store / inner node
- **PromotedWidgetView setter**: Write to instance map to avoid shared
inner node mutation
- **_internalConfigureAfterSlots**: Apply serialized `widgets_values` to
per-instance map after promoted views are created

## Red-Green Verification

| Commit | CI Status | Purpose |
|--------|-----------|---------|
| `test: add failing tests for multi-instance subgraph widget value
collision` | 🔴 Red | Proves widget values collide across
instances |
| `fix: store promoted widget values per SubgraphNode instance` |
🟢 Green | Per-instance storage prevents collision |

## Test Plan

- [x] CI red on test-only commit
- [x] CI green on fix commit
- [x] Unit test: `preserves promoted widget values after configure with
different widgets_values`
- [x] All 253 existing subgraph tests pass
- [ ] Manual: load workflow from issue image → verify 3 subgraph
instances produce different results

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10849-fix-store-promoted-widget-values-per-SubgraphNode-instance-3386d73d3650815a8544f54adcc0d504)
by [Unito](https://www.unito.io)

---------

Co-authored-by: dante <dante@danteui-MacStudio.local>
2026-04-14 21:22:52 +00:00
AustinMroz
988a546721 Add missing dialog tests (#11133)
Leveraging the fancy coverage functionality of #10930, this PR aims to
add coverage to missing dialogue models.

This has proven quite constructive as many of the dialogues have since
been shown to be bugged.
- The APINodes sign in dialog that displays when attempting to run a
workflow containing Partner nodes while not logged in was intended to
display a list of nodes required to execute the workflow. The import for
this component was forgotten in the original commit (#3532) and the
backing component was later knipped
- Error dialogs resulting are intended to display the file responsible
for the error, but the prop was accidentally left out during the
refactoring of #3265
- ~~The node library migration (#8548) failed to include the 'Edit
Blueprint' button, and had incorrect sizing and color on the 'Delete
Blueprint' button.~~
- On request, the library button changes were spun out to a separate PR

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11133-Add-missing-dialog-tests-33e6d73d3650812cb142d610461adcd4)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-14 14:31:31 -07:00
AustinMroz
25ac047b58 Fix node library action buttons (#11232)
The node library migration (#8548) failed to include the 'Edit
Blueprint' button, and had incorrect sizing and color on the 'Delete
Blueprint' button.

- Re-add edit blueprint which was missed in the migration
- Fix incorrect sizing on delete blueprint
- Fix color (lucide uses background, not text)
- Migrate all action buttons use our capital 'B' Button component and
use standardized variants where possible

Spun out of #11133

┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11232-Fix-node-library-action-buttons-3426d73d365081339cafc7457c0b5421)
by [Unito](https://www.unito.io)
2026-04-14 14:25:09 -07:00
pythongosssss
165984fe4c test: Improve speed of app mode input corruption test (#11236)
## Summary

Speeds up test that was timing out
https://9b579efd.comfyui-playwright-chromium.pages.dev/#?testId=b97e313f05078cede9be-5e6b75a76880fb6a5d96

## Changes

- **What**:
- load prebuilt workflows to reduce test time (17s -> 11s)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11236-test-Improve-speed-of-app-mode-input-corruption-test-3426d73d3650815b9475ec96dfbd7ad5)
by [Unito](https://www.unito.io)
2026-04-14 18:36:04 +00:00
pythongosssss
34a02a29c9 test: Remove unnecessary setup, UseNewMenu and waitForNodes calls (#11237)
## Summary

More simplification

## Changes

- **What**: 
- Remove more UseNewMenu settings calls
- Remove `await comfyPage.setup()`
- Remove `waitForNodes` in vue node tagged tests

┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11237-test-Remove-unnecessary-setup-UseNewMenu-and-waitForNodes-calls-3426d73d36508198a100c218420d479c)
by [Unito](https://www.unito.io)
2026-04-14 17:40:23 +00:00
Alexander Brown
4bd655f625 feat: add PreToolUse hooks to enforce pnpm scripts (#11201)
## Summary

Add Claude Code PreToolUse hooks to block agents from bypassing
package.json scripts with raw tool invocations.

## Changes

- **What**: 15 PreToolUse hooks in `.claude/settings.json` that
intercept `npx`/`pnpx`/bare invocations of tsc, vitest, eslint,
prettier, oxlint, stylelint, and knip — redirecting agents to the
correct `pnpm` script (`pnpm typecheck`, `pnpm test:unit`, `pnpm lint`,
etc.)
- Also removes stale `permissions.allow` entries left over from a
debugging session

## Review Focus

- Pattern coverage: are there common agent invocations we're missing?
- The `if` field only supports simple `*` globs (no alternation), so
each pattern needs its own hook entry

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11201-feat-add-PreToolUse-hooks-to-enforce-pnpm-scripts-3416d73d365081a59a38c86ee4669aee)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-04-14 17:38:15 +00:00
pythongosssss
a09bb81b98 test: Auto wait for nodes after loadWorkflow in vue-node tests (#11238)
## Summary

Updates tests to auto wait for vue-nodes when loading a workflow in a
test with the vue-nodes tag

## Changes

- **What**: 
- If tag includes vue-nodes, wait 
- Remove all load->wait calls

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11238-test-Auto-wait-for-nodes-after-loadWorkflow-in-vue-node-tests-3426d73d3650810e8760c5601186fde8)
by [Unito](https://www.unito.io)
2026-04-14 17:30:49 +00:00
Christian Byrne
aeedb60628 fix(ci): resolve pnpm version in release workflow for frontend/ checkout path (#11224)
The release workflow checks out to `frontend/` subdirectory, but
`pnpm/action-setup` looks for `package.json` at the repo root by
default. This causes `No pnpm version is specified` failures.

Adds `package_json_file: frontend/package.json` so the action can read
the `packageManager` field. Same pattern used in #10972 for the
version-bump workflow.

┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11224-fix-ci-resolve-pnpm-version-in-release-workflow-for-frontend-checkout-path-3426d73d365081c28d16cb01bf8218ef)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-13 21:41:22 -07:00
Christian Byrne
43fb5a8b19 ops: add patch release support to ComfyUI release workflow (#11223)
## Summary

Add `release_type` input (`minor`/`patch`) to the release workflow so
patch releases can target the current production branch instead of
always preferring the next minor.

## Problem

When a patch release is needed for `core/1.42` but `core/1.43` already
exists, the resolver always prefers `1.43`. There was no way to do a
patch release with PyPI publish + ComfyUI PR for the current production
version.

## Changes

- Rename workflow from "Release: Bi-weekly ComfyUI" → "Release: ComfyUI"
(serves both cadences)
- Add `release_type` choice input: `minor` (default, bi-weekly) vs
`patch` (hotfix for current production version)
- Update `resolve-comfyui-release.ts` to read `RELEASE_TYPE` env var for
branch targeting
- Scheduled runs continue to work as before (default to `minor`)

## Usage

```bash
# Bi-weekly minor release (or just let the schedule run)
gh workflow run release-biweekly-comfyui.yaml --ref main

# Patch release for current production version
gh workflow run release-biweekly-comfyui.yaml --ref main --field release_type=patch
```

┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11223-ops-add-patch-release-support-to-ComfyUI-release-workflow-3426d73d365081498c15ff978a7f1116)
by [Unito](https://www.unito.io)
2026-04-13 21:10:17 -07:00
Christian Byrne
c484c3984f fix: remove appendTo override from FormDropdown to restore correct positioning (#11147)
## Problem

PR #10338 added `useTransformCompatOverlayProps()` to FormDropdown and
FormDropdownMenuActions, which sets `appendTo: 'self'` in graph mode.
This breaks PrimeVue Popover positioning inside CSS-transformed
containers — the dropdown appears at incorrect Y positions.

## Root Cause

PrimeVue Popover with `appendTo: 'self'` renders the overlay inside the
component's DOM, inheriting parent CSS transforms. This causes the
popover to miscalculate its position when the parent has `transform:
scale()` or `translate()`.

## Fix

Remove the `appendTo` override from both FormDropdown and
FormDropdownMenuActions. PrimeVue defaults to `appendTo: 'body'`, which
teleports the popover to `<body>` — correctly positioning it outside any
CSS transform context.

- **Graph mode**: restores pre-#10338 behavior (`appendTo: 'body'`
default)
- **App mode**: unaffected — `'body'` is exactly what app mode needs
(prevents sidebar overflow clipping)

## Testing

- Existing unit tests pass (5/5)
- Typecheck clean
- Lint clean
- **E2E test rationale**: No E2E test added — this is a pure removal of
a prop override (reverting to PrimeVue defaults). The positioning bug
requires CSS transforms at specific viewport scales which are
impractical to assert reliably in Playwright. The existing
`subgraph-dom-widget-clipping` perf test exercises dropdown rendering in
transformed contexts and shows no regression.

Fixes #10499
Supersedes #11001 (temporary hotfix for backport)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-13 20:02:39 -07:00
Dante
2524846f5c fix: guard progress_text before canvas init (#11174)
## Summary
Prevent early `progress_text` websocket events from throwing before the
graph canvas is initialized.

## Changes
- **What**: Guard `handleProgressText()` until `canvasStore.canvas`
exists, and add a regression test for a startup-time `progress_text`
event arriving before `GraphCanvas` finishes initialization.

## Review Focus
Confirm this is the right guard point for the startup race between
`GraphView` websocket binding and `GraphCanvas` async setup, and that
progress text behavior is unchanged once the canvas is ready.

## Validation
- `pnpm exec eslint src/stores/executionStore.ts
src/stores/executionStore.test.ts`
- `pnpm exec vitest run src/stores/executionStore.test.ts -t "should
ignore progress_text before the canvas is initialized"`
- `pnpm test:unit -- --run src/stores/executionStore.test.ts` still
reports one unrelated isolated-file failure in
`nodeLocatorIdToExecutionId` on current `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11174-fix-guard-progress_text-before-canvas-init-3406d73d3650813dad23d511fb51add5)
by [Unito](https://www.unito.io)
2026-04-13 23:47:14 +00:00
Comfy Org PR Bot
12f578870e 1.44.4 (#11177)
Patch version increment to 1.44.4

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11177-1-44-4-3416d73d365081c0a2e0def7071c1441)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-04-13 23:19:20 +00:00
Alexander Brown
72eed86cea test: remove redundant setup/settings now handled by @vue-nodes fixture (#11195)
## Summary

Follow-up cleanup for #11184 — removes redundant test setup calls that
the `@vue-nodes` fixture now handles.

## Changes

- **What**: Remove 40 lines of redundant `setSetting`, `setup()`, and
`waitForNodes()` calls across 11 test files
  - `UseNewMenu: 'Top'` calls (already fixture default)
- `setup()` + `waitForNodes()` on default workflow (fixture already does
this for `@vue-nodes`)
- Page reload in `subgraphZeroUuid` (fixture applies VueNodes.Enabled
server-side before navigation)

## Review Focus

Each removal was verified against the fixture's `setupSettings()`
defaults (ComfyPage.ts:420-442) and the `@vue-nodes` auto-setup (lines
454-456). Tests that call `setup()`/`waitForNodes()` after
`loadWorkflow()` or `page.evaluate()` were intentionally kept.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11195-test-remove-redundant-setup-settings-now-handled-by-vue-nodes-fixture-3416d73d36508154827df116a97e9130)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-04-13 22:15:45 +00:00
Benjamin Lu
719ed16d32 fix: track workspace subscription success on immediate subscribe (#11130)
## Summary

Track GTM `subscription_success` when a workspace subscription completes
synchronously in the dialog. The async billing-operation path already
emitted telemetry; the missing gap was the immediate `subscribed`
response.

## Changes

- **What**: Add the missing GTM success emission to both synchronous
workspace subscribe success branches while preserving the existing
toast, billing refresh, and dialog close behavior.

## Review Focus

Verify the synchronous `response.status === "subscribed"` workspace
dialog paths are the only missing frontend success emissions, while the
async billing-operation telemetry path remains unchanged.

This PR intentionally stays minimal. It does not add new browser
coverage yet; the previous component-level unit test was more
implementation-coupled than this fix justified, and a better long-term
test would be a higher-level workspace billing flow test once we have a
cleaner harness.
2026-04-13 14:38:03 -07:00
pythongosssss
5899a9392e test: Simplify vue node/menu test setup (#11184)
## Summary
Simplifies test setup for common settings

## Changes

- **What**: 
- add vue-nodes tag to auto enable nodes 2.0
- remove UseNewMenu Top as this is default

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11184-test-Simplify-vue-node-menu-test-setup-3416d73d3650815487e0c357d28761fe)
by [Unito](https://www.unito.io)
2026-04-13 20:43:25 +00:00
Christian Byrne
e39468567a fix: check server feature flags for progress_text binary format (#10996)
## Problem

API node generation status text (sent via `progress_text` WebSocket
binary messages) was not showing on local ComfyUI, but worked on cloud.

## Root Cause

The binary decoder for `progress_text` messages (eventType 3) checked
`getClientFeatureFlags()?.supports_progress_text_metadata` — the
**client's own flags** — to decide whether to parse the new format with
`prompt_id`. Since the client always advertises
`supports_progress_text_metadata: true`, it always tried to parse the
new wire format:

```
[4B event_type][4B prompt_id_len][prompt_id][4B node_id_len][node_id][text]
```

But the backend PR that adds `prompt_id` to the binary message
([ComfyUI#12540](https://github.com/Comfy-Org/ComfyUI/pull/12540)) was
**closed without merging**, so local ComfyUI still sends the legacy
format:

```
[4B event_type][4B node_id_len][node_id][text]
```

The decoder misinterpreted the `node_id_len` as `prompt_id_len`,
consuming the actual node_id bytes as a prompt_id, then producing
garbled `nodeId` and `text` — silently dropping all progress text
updates via the catch handler.

Cloud worked because the cloud backend supports and echoes the feature
flag.

## Fix

One-line change: check `serverFeatureFlags.value` (what the server
echoed back) instead of `getClientFeatureFlags()` (what the client
advertises).

## Tests

Added 3 tests covering:
- Legacy format parsing when server doesn't support the flag
- New format parsing when server does support the flag  
- Corruption regression test: client advertises support but server
doesn't

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10996-fix-check-server-feature-flags-for-progress_text-binary-format-33d6d73d365081449a0dc918358799de)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-13 18:22:30 +00:00
pythongosssss
a373633ab2 refactor: fix lint errors in tests (#11182)
## Summary

Fix tests failing lint

## Changes

- **What**:
- Fix relative imports
- Fix test not using comfyPage

## 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-11182-refactor-fix-lint-errors-in-tests-3416d73d3650812fbf1bc88554c57de2)
by [Unito](https://www.unito.io)
2026-04-13 15:50:11 +00:00
jaeone94
521019d173 fix: exclude muted/bypassed nodes from missing asset detection (#10856)
## Summary

Muted and bypassed nodes are excluded from execution but were still
triggering missing model/media/node warnings. This PR makes the error
system mode-aware: muted/bypassed nodes no longer produce missing asset
errors, and all error lifecycle events (mode toggle, deletion, paste,
undo, tab switch) are handled consistently.

- Fixes Comfy-Org/ComfyUI#13256

## Behavioral notes

- **Tab switch overlay suppression (intentional)**: Switching back to a
workflow with missing assets no longer re-shows the error overlay. This
reverses the behavior introduced in #10190. The error state is still
restored silently in the errors tab — users can access it via the
properties panel without being interrupted by the overlay on every tab
switch.

## Changes

### 1. Scan filtering

- `scanAllModelCandidates`, `scanAllMediaCandidates`,
`scanMissingNodes`: skip nodes with `mode === NEVER || BYPASS`
- `collectMissingNodes` (serialized data): skip error reporting for
muted/bypassed nodes while still calling `sanitizeNodeName` for safe
`configure()`
- `collectEmbeddedModelsWithSource`: skip muted/bypassed nodes;
workflow-level `graphData.models` only create candidates when active
nodes exist
- `enrichWithEmbeddedMetadata`: filter unmatched workflow-level models
when all referencing nodes are inactive

### 2. Realtime mode change handling

- `useErrorClearingHooks.ts` chains `graph.onTrigger` to detect
`node:property:changed` (mode)
- Deactivation (active → muted/bypassed): remove missing
model/media/node errors for the node
- Activation (muted/bypassed → active): scan the node and add confirmed
errors, show overlay
- Subgraph container deactivation: remove all interior node errors
(execution ID prefix match)
- Subgraph container activation: scan all active interior nodes
recursively
- Subgraph interior mode change: resolve node via
`localGraph.getNodeById()` then compute execution ID from root graph

### 3. Node deletion

- `graph.onNodeRemoved`: remove missing model/media/node errors for the
deleted node
- Handle `node.graph === null` at callback time by using
`String(node.id)` for root-level nodes

### 4. Node paste/duplicate

- `graph.onNodeAdded`: scan via `queueMicrotask` (deferred until after
`node.configure()` restores widget values)
- Guard: skip during `ChangeTracker.isLoadingGraph` (undo/redo/tab
switch handled by pipeline)
- Guard: skip muted/bypassed nodes

### 5. Workflow tab switch optimization

- `skipAssetScans` option in `loadGraphData`: skip full pipeline on tab
switch
- Cache missing model/media/node state per workflow via
`PendingWarnings`
- `beforeLoadNewGraph`: save current store state to outgoing workflow's
`pendingWarnings`
- `showPendingWarnings`: restore cached errors silently (no overlay),
always sync missing nodes store (even when null)
- Preserve UI state (`fileSizes`, `urlInputs`) on tab switch by using
`setMissingModels([])` instead of `clearMissingModels()`
- `MissingModelRow.vue`: fetch file size on mount via
`fetchModelMetadata` memory cache

### 6. Undo/redo overlay suppression

- `silentAssetErrors` option propagated through pipeline →
`surfaceMissingModels`/`surfaceMissingMedia` `{ silent }` option
- `showPendingWarnings` `{ silent }` option for missing nodes overlay
- `changeTracker.ts`: pass `silentAssetErrors: true` on undo/redo

### 7. Error tab node filtering

- Selected node filters missing model/media card contents (not just
group visibility)
- `isAssetErrorInSelection`: resolve execution ID → graph node for
selection matching
- Missing nodes intentionally unfiltered (pack-level scope)
- `hasMissingMediaSelected` added to `RightSidePanel.vue` error tab
visibility
- Download All button: show only when 2+ downloadable models exist

### 8. New store functions

- `missingModelStore`: `addMissingModels`, `removeMissingModelsByNodeId`
- `missingMediaStore`: `addMissingMedia`, `removeMissingMediaByNodeId`
- `missingNodesErrorStore`: `removeMissingNodesByNodeId`
- `missingModelScan`: `scanNodeModelCandidates` (extracted single-node
scan)
- `missingMediaScan`: `scanNodeMediaCandidates` (extracted single-node
scan)

### 9. Test infrastructure improvements

- `data-testid` on `RightSidePanel.vue` tabs (`panel-tab-{value}`)
- Error-related TestIds moved from `dialogs` to `errorsTab` namespace in
`selectors.ts`
- Removed unused `TestIdValue` type
- Extracted `cleanupFakeModel` to shared `ErrorsTabHelper.ts`
- Renamed `openErrorsTabViaSeeErrors` → `loadWorkflowAndOpenErrorsTab`
- Added `aria-label` to pencil edit button and subgraph toggle button

## Test plan

### Unit tests (41 new)

- Store functions: `addMissing*`, `removeMissing*ByNodeId`
- `executionErrorStore`: `surfaceMissing*` silent option
- Scan functions: muted/bypassed filtering, `scanNodeModelCandidates`,
`scanNodeMediaCandidates`
- `workflowService`: `showPendingWarnings` silent, `beforeLoadNewGraph`
caching

### E2E tests (17 new in `errorsTabModeAware.spec.ts`)

**Missing nodes**
- [x] Deleting a missing node removes its error from the errors tab
- [x] Undo after bypass restores error without showing overlay

**Missing models**
- [x] Loading a workflow with all nodes bypassed shows no errors
- [x] Bypassing a node hides its error, un-bypassing restores it
- [x] Deleting a node with missing model removes its error
- [x] Undo after bypass restores error without showing overlay
- [x] Pasting a node with missing model increases referencing node count
- [x] Pasting a bypassed node does not add a new error
- [x] Selecting a node filters errors tab to only that node

**Missing media**
- [x] Loading a workflow with all nodes bypassed shows no errors
- [x] Bypassing a node hides its error, un-bypassing restores it
- [x] Pasting a bypassed node does not add a new error
- [x] Selecting a node filters errors tab to only that node

**Subgraph**
- [x] Bypassing a subgraph hides interior errors, un-bypassing restores
them
- [x] Bypassing a node inside a subgraph hides its error, un-bypassing
restores it

**Workflow switching**
- [x] Does not resurface error overlay when switching back to workflow
with missing nodes
- [x] Restores missing nodes in errors tab when switching back to
workflow

# Screenshots


https://github.com/user-attachments/assets/e0a5bcb8-69ba-4120-ab7f-5c83e4cfc3c5



## Follow-up work

- Extract error-detection computed properties from `RightSidePanel.vue`
into a composable (e.g. `useErrorsTabVisibility`)

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-04-13 12:51:19 +00:00
Kelly Yang
bd82c855e0 test: add minimap E2E tests for graph content and click-to-navigate (#10738)
## Summary

Adds Playwright E2E tests verifying that 
1. the minimap canvas renders node content
2. clears when the graph is empty
3. correctly navigates the main canvas on click

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10738-test-add-minimap-E2E-tests-for-graph-content-and-click-to-navigate-3336d73d365081eb955ce711b3efc57f)
by [Unito](https://www.unito.io)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: changes are limited to adding `data-testid` attributes to
the minimap UI and expanding Playwright E2E assertions, with no
production behavior changes expected.
> 
> **Overview**
> Strengthens minimap E2E coverage by switching existing assertions from
CSS selectors to new `data-testid`-based selectors and adding helper
utilities for canvas/overlay interactions.
> 
> Adds new Playwright tests that verify the minimap canvas renders
content when nodes exist, clears when the graph is emptied, and that
clicking the minimap pans the main canvas (including a
post-`fitViewToSelectionAnimated` tolerance check).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
06e7542af1. 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: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Alexander Brown <DrJKL0424@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
2026-04-13 04:28:04 +00:00
Kelly Yang
5b7ef3fe21 test: Painter Widget E2E Test Plan (#10846)
### Summary of Improvements

* **Custom Test Coverage Extension**: Enhanced the Painter widget E2E
test suite by refactoring logic for better maintainability and
robustness.
* **Stable Component Targeting**: Introduced
`data-testid="painter-dimension-text"` to `WidgetPainter.vue`, providing
a reliable, non-CSS-dependent locator for canvas size verification.
* **Improved Test Organization**: Reorganized existing test scenarios
into logical categories using `test.describe` blocks (Drawing, Brush
Settings, Canvas Size Controls, etc.).
* **Asynchronous Helper Integration**: Converted `hasCanvasContent` to
an asynchronous helper and unified its usage across multiple test cases
to eliminate redundant pixel-checking logic.
* **Locator Resilience**: Updated Reka UI slider interaction logic to
use more precise targeting (`:not([data-slot])`), preventing ambiguity
and improving test stability.
* **Scenario Refinement**: Updated the `pointerup` test logic to
accurately reflect pointer capture behavior when interactions occur
outside the canvas boundaries.
* **Enhanced Verification Feedback**: Added descriptive error messages
to `expect.poll` assertions to provide clearer context on potential
failure points.
* **Standardized Tagging**: Restored the original tagging strategy
(including `@smoke` and `@screenshot` tags) to ensure tests are
categorized correctly for CI environments.

### Red-Green Verification

| Commit | CI Status | Purpose |
| :--- | :--- | :--- |
| `test: refactor painter widget e2e tests and address review findings`
| 🟢 Green | Addresses all E2E test quality and stability issues from
review findings. |

### Test Plan

- [x] **Quality Checks**: `pnpm format`, `pnpm lint`, and `pnpm
typecheck` verified as passing.
- [x] **Component Integration**: `WidgetPainter.vue` `data-testid`
correctly applied and used in tests.
- [x] **Helper Reliability**: `hasCanvasContent` correctly identifies
colored pixels and returns a promise for `expect.poll`.
- [x] **Locator Robustness**: Verified Reka slider locators correctly
exclude internal thumb spans.
- [x] **Boundary Interaction**: Verified `pointerup` correctly ends
strokes when triggered outside the viewport.
- [x] **Tagging Consistency**: Verified `@smoke` and `@screenshot` tags
are present in the final test suite.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10846-test-Painter-Widget-E2E-Test-Plan-3386d73d365081deb70fe4afbd417efb)
by [Unito](https://www.unito.io)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Primarily adds/refactors Playwright E2E tests and stable `data-testid`
hooks, with no changes to Painter drawing logic. Risk is limited to
potential test brittleness or minor UI attribute changes.
> 
> **Overview**
> Expands the Painter widget Playwright suite with new grouped scenarios
covering drawing/erasing behavior, tool switching, brush inputs, canvas
resizing (including preserving drawings), clear behavior, and
serialization/upload flows (including failure toast).
> 
> Refactors the tests to use a shared `@e2e/helpers/painter` module
(`drawStroke`, `hasCanvasContent`, `triggerSerialization`), improves
stability via role/testid-based locators and clearer `expect.poll`
messaging, and adds `data-testid` attributes (e.g.,
`painter-clear-button`, `painter-*-row`, `painter-dimension-text`) to
`WidgetPainter.vue` to avoid CSS-dependent selectors.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
053a8e9ed2. 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: GitHub Action <action@github.com>
2026-04-13 00:13:04 -04:00
Kelly Yang
85de833776 test: add E2E tests for ImageCompare widget (#10767)
## Summary
Add E2E tests for ImageCompare widget
Covers slider interaction, batch navigation, single-image modes, visual
regression screenshots, and edge cases for the ImageCompare Vue node
widget.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10767-test-add-E2E-tests-for-ImageCompare-widget-3346d73d365081c6bfc6fbd97fa04e4d)
by [Unito](https://www.unito.io)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Adds Playwright E2E coverage and screenshot assertions only; main risk
is increased CI runtime/flakiness due to additional image-loading and
hover/position polling.
> 
> **Overview**
> Adds a new Playwright E2E suite for the ImageCompare Vue widget
(tagged `@widget`) that programmatically sets widget values and asserts
rendering for empty, single-image, and dual-image states.
> 
> Expands coverage to **slider behavior** (default 50%, hover movement,
clamping, persistence) using polling on inline `clip-path`/handle
position, and adds **batch navigation** tests for multi-image
before/after sets.
> 
> Introduces **visual regression screenshots** at default and specific
slider positions, plus edge-case tests for broken URLs, rapid updates
resetting batch index, legacy string values, and custom alt text.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
2c65440384. 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: GitHub Action <action@github.com>
2026-04-13 00:09:50 -04:00
Kelly Yang
cab46567c0 test: add E2E tests for ImageCropV2 widget (#10737)
## Summary
Adds Playwright E2E tests for the ImageCropV2 widget covering 
1. the empty state (no source image)
2. default control rendering
3. source image display with crop overlay
4. drag-to-reposition behavior.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10737-test-add-E2E-tests-for-ImageCropV2-widget-3336d73d365081b28ed9db63e5df383e)
by [Unito](https://www.unito.io)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: primarily adds Playwright E2E coverage and introduces
`data-testid` attributes for more stable selectors, with no changes to
core crop behavior.
> 
> **Overview**
> Adds new Playwright E2E coverage for the `ImageCropV2` Vue-node
widget, including workflows/fixtures for a disconnected input and a
`LoadImage -> ImageCropV2 -> PreviewImage` pipeline.
> 
> Tests validate the empty state and default controls, verify the crop
overlay renders after execution with screenshot assertions, and exercise
drag-to-reposition by dispatching pointer events and asserting the
widget’s crop value updates.
> 
> Updates `WidgetImageCrop.vue` to add `data-testid` hooks (empty
state/icon and crop overlay) to make the E2E selectors stable.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9f29272742. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-04-12 23:58:01 -04:00
Comfy Org PR Bot
63435bdb34 1.44.3 (#11170)
Patch version increment to 1.44.3

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11170-1-44-3-3406d73d365081799aa4e189009d123b)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2026-04-12 23:11:20 +00:00
Kelly Yang
20255da61f feat(load3d): add optional HDRI environment lighting to 3D preview nodes (#10818)
## Summary

Adds `HDRIManager` to load `.hdr/.exr` files as equirectangular
environment maps via **three.js** `RGBELoader/EXRLoader`
- Uploads HDRI files to the server via `/upload/image` API so they
persist across page reloads
- Restores HDRI state (enabled, **intensity**, **background**) from node
properties on reload
- Auto-enables "**Show as Background**" on successful upload for
immediate visual feedback
- Hides standard directional lights when HDRI is active; restores them
when disabled
- Hides the Light Intensity control while HDRI is active (lights have no
effect when HDRI overrides scene lighting)
- Limits HDRI availability to PBR-capable formats (.gltf, .glb, .fbx,
.obj); automatically disables when switching to an incompatible model
- Adds intensity slider and "**Show as Background**" toggle to the HDRI
panel

## How to Use HDRI Environment Lighting
1. Load a 3D model using a Load3D or Load3DViewer node (supported
formats: .gltf, .glb, .fbx, .obj)
2. Open the control panel → go to the Light tab
3. Click the globe icon to open the **HDRI panel**
4. Click Upload HDRI and select a` .hdr` or `.exr` file
5. The environment lighting applies automatically — the scene background
also updates to preview the panorama
6. Use the intensity slider to adjust the strength of the environment
lighting
7. Toggle Show as Background to show or hide the HDRI panorama behind
the model

## Screenshots



https://github.com/user-attachments/assets/1ec56ef0-853e-452f-ae2b-2474c9d0d781



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10818-feat-load3d-add-optional-HDRI-environment-lighting-to-3D-preview-nodes-3366d73d365081ea8c7ad9226b8b1e2f)
by [Unito](https://www.unito.io)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds new HDRI loading/rendering path and persists new
`LightConfig.hdri` state, touching Three.js rendering, file uploads, and
node property restoration. Risk is moderate due to new async flows and
potential compatibility/performance issues with model switching and
renderer settings.
> 
> **Overview**
> Adds optional **HDRI environment lighting** to Load3D previews,
including a new `HDRIManager` that loads `.hdr`/`.exr` files into
Three.js environment/background and exposes controls for enable/disable,
background display, and intensity.
> 
> Extends `LightConfig` with an `hdri` block that is persisted on nodes
and restored on reload; `useLoad3d` now uploads HDRI files, loads them
into `Load3d`, maps scene light intensity to HDRI intensity, and
auto-disables HDRI when the current model format doesn’t support it.
> 
> Updates the UI to include embedded HDRI controls under the Light panel
(with dismissable overlays and icon updates), adjusts light intensity
behavior when HDRI is active, and adds tests/strings/utilities
(`getFilenameExtension`, `mapSceneLightIntensityToHdri`, new constants)
to support the feature.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
b12c9722dc. 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: Terry Jia <terryjia88@gmail.com>
2026-04-12 05:55:48 -04:00
Christian Byrne
c2dba8f4ee chore(#11080): consolidate duplicate rgbToHSL — use shared colorUtil (#11134)
## Summary

Consolidate duplicate `rgbToHSL` implementation — mask editor now uses
the shared `colorUtil.ts` version instead of its own copy.

## Changes

- Export `rgbToHsl` from `src/utils/colorUtil.ts` (was private)
- Replace 30-line local `rgbToHSL` in `useCanvasTools.ts` with a 2-line
wrapper that imports from `colorUtil.ts` and scales the return values
from 0-1 to degree/percentage

## Testing

### Automated

- All 176 existing tests pass (`colorUtil.test.ts` + `maskeditor/`
suite)
- No new tests needed — behavior is identical

### E2E Verification Steps

1. Open any image in the mask editor
2. Select the magic wand / color picker tool
3. Use HSL-based color matching — results should be identical to before

## Review Focus

The canonical `rgbToHsl` returns normalized 0-1 values while the mask
editor needs degree/percentage scale (h: 0-360, s: 0-100, l: 0-100). The
local wrapper handles this conversion.

Fixes #11080

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11134-chore-11080-consolidate-duplicate-rgbToHSL-use-shared-colorUtil-33e6d73d36508120bbd8f444f5cc94b6)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-04-12 01:40:55 +00:00
Alexander Brown
6f579c5992 fix: enable playwright/no-force-option lint rule (#11164)
## Summary

Enable the previously disabled `playwright/no-force-option` lint rule at
error level and resolve all 29 violations across 10 files.

## Changes

### Lint rule
- `.oxlintrc.json`: `playwright/no-force-option` changed from `off` to
`error`

### Shared utility
- `CanvasHelper.ts`: Add `mouseClickAt()` and `mouseDblclickAt()`
methods that convert canvas-element-relative positions to absolute page
coordinates and use `page.mouse` APIs, avoiding Playwright's locator
actionability checks that fail when Vue DOM overlays sit above the
`<canvas>` element

### Force removal (20 violations)
- `selectionToolboxActions.spec.ts`: Remove `force: true` from 8 toolbox
button clicks (the `pointer-events: none` splitter overlay does not
intercept `elementFromPoint()`)
- `selectionToolboxSubmenus.spec.ts`: Remove `force: true` from 2
popover menu item clicks
- `BuilderSelectHelper.ts`: Remove `force: true` from 2 widget/node
clicks (builder mode does not disable pointer events)
- `linkInteraction.spec.ts`: Remove `force: true` from 3 slot `dragTo()`
calls (`::after` pseudo-elements do not intercept `elementFromPoint()`)
- `SidebarTab.ts`: Remove `force: true` from toast dismissal (`.catch()`
already handles failures)
- `nodeHelp.spec.ts`: Remove `force: true` from info button click
(preceding `toBeVisible()` assertion is sufficient)

### Rewrites (3 violations)
- `integerWidget.spec.ts`: Replace force-clicking disabled buttons with
`toBeDisabled()` assertions
- `Topbar.ts`: Replace force-click with `waitFor({ state: 'visible' })`
after hover

### Canvas coordinate clicks (9 violations)
- `litegraphUtils.ts`: Convert `NodeReference.click()` and
`navigateIntoSubgraph()` to use
`canvasOps.mouseClickAt()`/`mouseDblclickAt()`
- `subgraphPromotion.spec.ts`: Convert 3 right-click canvas calls to
`canvasOps.mouseClickAt()`
- `selectionToolboxSubmenus.spec.ts`: Convert 1 canvas dismiss-click to
`canvasOps.mouseClickAt()`

## Rationale

The original `force: true` usages were added defensively based on
incorrect assumptions about the `z-999 pointer-events: none` splitter
overlay intercepting Playwright's actionability checks. In reality,
`elementFromPoint()` skips elements with `pointer-events: none`, so the
overlay is transparent to Playwright's hit-test.

For canvas coordinate clicks, `force: true` on a locator does not tunnel
through DOM overlays — it only skips Playwright's preflight checks.
`page.mouse.click()` is the correct API for coordinate-based canvas
interactions.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11164-fix-enable-playwright-no-force-option-lint-rule-33f6d73d365081e78601c6114121d272)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-04-11 19:59:34 +00:00
Dante
e729e5edb8 fix: place cloned node above original in Vue renderer (#10361)
## Summary

Cloned/pasted nodes in Node 2.0 (Vue renderer) mode now appear above the
original node instead of behind it.

## Root Cause

The legacy LiteGraph canvas renderer uses array ordering for z-ordering:
nodes are stored in `graph._nodes` and drawn sequentially, so newly
added nodes (appended to the end) are automatically drawn on top. There
is no explicit z-index.

The Vue renderer (Node 2.0) uses explicit CSS `z-index` for node
ordering. New nodes default to `zIndex: 0` in `layoutMutations.ts`. When
a node has been interacted with, `bringNodeToFront` raises its z-index.
A cloned node at z-index 0 therefore appears behind any previously
interacted node.

The alt-click clone path in `LGraphNode.vue` already handles this
correctly by calling `bringNodeToFront()` after cloning. However, the
menu clone and keyboard paste paths go through `_deserializeItems` in
`LGraphCanvas.ts`, which does not set z-index for new nodes.

| Clone method | Legacy renderer | Vue renderer (before fix) | Vue
renderer (after fix) |
|---|---|---|---|
| Alt-click drag | On top (array order) | On top (`bringNodeToFront`
called) | On top |
| Right-click menu Clone | On top (array order) | Behind original
(z-index 0) | On top |
| Ctrl+C / Ctrl+V | On top (array order) | Behind original (z-index 0) |
On top |

## Steps to Reproduce

1. Enable Node 2.0 mode (Vue renderer) in settings
2. Add any node to the canvas
3. Click or drag the node (raises its z-index via `bringNodeToFront`)
4. Right-click the node and select "Clone"
5. **Expected**: Cloned node appears above the original, immediately
draggable
6. **Actual**: Cloned node appears behind the original; user must move
the original to access the clone

## Changes

After `batchUpdateNodeBounds` in `_deserializeItems`, calls
`bringNodeToFront` for each newly created node so they receive a z-index
above all existing nodes.

## Side Effect Analysis

Checked all call sites of `_deserializeItems`:

1. **Initial graph load / workflow open**: `loadGraphData` in `app.ts`
does NOT call `_deserializeItems`. Workflow loading goes through
`LGraph.configure()` which directly adds nodes and links. The layout
store is initialized separately via `initializeFromLiteGraph`. No side
effect.

2. **Paste from clipboard (Ctrl+V)**: Both `usePaste.ts` (line 52) and
`pasteFromClipboard` (line 4080) call `_deserializeItems`. Pasted nodes
appearing on top is the correct and desired behavior. No issue.

3. **Undo/Redo**: `ChangeTracker.updateState()` calls
`app.loadGraphData()`, which does a full graph reconfigure -- it does
NOT go through `_deserializeItems`. No side effect.

4. **Subgraph blueprint addition**: `litegraphService.ts` (line 906)
calls `_deserializeItems` when adding subgraph blueprints from the node
library. These are freshly placed nodes that should appear on top.
Desired behavior.

5. **Alt-click clone in LGraphNode.vue**: This path calls
`LGraphCanvas.cloneNodes()` -> `_deserializeItems()`, then separately
calls `bringNodeToFront()` again on line 433 of `LGraphNode.vue`. The
second call is now redundant (the node is already at max z-index), but
harmless -- `bringNodeToFront` finds the current max, adds 1, and sets.
The z-index will increment from N to N+1 on the second call. This is a
minor redundancy, not a bug.

6. **Performance**: `bringNodeToFront` iterates all nodes in the layout
store once per call (O(m)) to find max z-index. For n new nodes, the
total cost is O(n*m). In practice, clone/paste operations involve a
small number of nodes (typically 1-10), so this is negligible. For
extremely large pastes (100+ nodes), each call also increments the max
by 1, so z-indices will be sequential (which is actually a reasonable
stacking order).

7. **layoutStore availability**: `layoutStore` is a module-level
singleton (`new LayoutStoreImpl()`) -- not a Pinia store -- so it is
always available. The `useLayoutMutations()` composable is a plain
function returning an object of closures over `layoutStore`. It does not
require Vue component context. No risk of runtime errors.

8. **Legacy renderer (non-Vue mode)**: When Node 2.0 mode is disabled,
the layout store still exists but is not used for rendering. Calling
`bringNodeToFront` will update z-index values in the Yjs document that
are never read. This is harmless.

## Red-Green Verification

| Commit | Result | Description |
|---|---|---|
| `6894b99` `test:` | RED | Test asserts cloned node z-index > original.
Fails with `expected 0 to be greater than 5`. |
| `3567469` `fix:` | GREEN | Calls `bringNodeToFront` for each new node
in `_deserializeItems`. Test passes. |

Fixes #10307

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-04-11 12:12:37 +00:00
Alexander Brown
3043b181d7 refactor: extract composables from VTU holdout components, complete VTL migration (#10966)
## Summary

Extract internal logic from the 2 remaining VTU holdout components into
composables, enabling full VTL migration.

## Changes

- **What**: Extract `useProcessedWidgets` from `NodeWidgets.vue`
(486→135 LOC) and `useWidgetSelectItems`/`useWidgetSelectActions` from
`WidgetSelectDropdown.vue` (563→170 LOC). Rewrite both component test
files as composable unit tests + slim behavioral VTL tests. Remove
`@vue/test-utils` devDependency.
- **Dependencies**: Removes `@vue/test-utils`

## Review Focus

- Composable extraction is mechanical — no logic changes, just moving
code into testable units
- `useProcessedWidgets` handles widget deduplication, promotion border
styling, error detection, and identity resolution (~290 LOC)
- `useWidgetSelectItems` handles the full computed chain from widget
values → dropdown items including cloud asset mode and multi-output job
resolution (~350 LOC)
- `useWidgetSelectActions` handles selection resolution and file upload
(~120 LOC)
- 40 new composable-level unit tests replace 13 `wrapper.vm.*` accesses
across the 2 holdout files

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10966-refactor-extract-composables-from-VTU-holdout-components-complete-VTL-migration-33c6d73d36508148a3a4ccf346722d6d)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-04-10 19:04:16 -07:00
Alexander Brown
8c9328c1b2 feat: add eslint-plugin-playwright via oxlint JS plugins (#11136)
## Summary

Add eslint-plugin-playwright as an oxlint JS plugin scoped to
browser_tests/, enforcing Playwright best practices at lint time.

## Changes

- **What**: Configure eslint-plugin-playwright@2.10.1 via oxlint's alpha
`jsPlugins` field (`.oxlintrc.json` override scoped to
`browser_tests/**/*.ts`). 18 recommended rules +
`prefer-native-locators` + `require-to-pass-timeout` at error severity.
All 173 initial violations resolved (config, auto-fix, manual fixes).
`no-force-option` set to off — 28 violations need triage (canvas overlay
workarounds vs unnecessary force) in a dedicated PR.
- **Dependencies**: `eslint-plugin-playwright@^2.10.1` (devDependency,
required by oxlint jsPlugins at runtime)

## Review Focus

- `.oxlintrc.json` override structure — this is the first use of
oxlint's JS plugins alpha feature in this repo
- Manual fixes in spec files: `waitForSelector` → `locator.waitFor`,
deprecated page methods → locator equivalents, `toPass()` timeout
additions
- Compound CSS selectors replaced with `.and()` (Playwright native
locator composition) to avoid `prefer-native-locators` suppressions
- Lint script changes in `package.json` to include `browser_tests/` in
oxlint targets

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
2026-04-11 01:25:14 +00:00
Christian Byrne
577f373cde fix: auto fit-to-view on first subgraph entry (#10995)
## Summary

Auto-fit viewport to subgraph content on first entry so interior nodes
are immediately visible.

## Changes

- **What**: On cache miss in `restoreViewport()`, call `fitView()` via
`requestAnimationFrame` instead of silently returning. Existing
cache-hit path (revisiting a subgraph) is unchanged.

## Review Focus

The `anyItemOverlapsRect` guard in `app.ts` (workflow load path) is
intentionally **not** touched — it serves a different purpose
(respecting `extra.ds` on workflow load). This fix only affects subgraph
navigation transitions where there is no saved viewport to respect.

Fixes #8173

## Screenshots (if applicable)

N/A — viewport positioning fix; before: empty canvas on subgraph entry,
after: nodes visible.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10995-fix-auto-fit-to-view-on-first-subgraph-entry-33d6d73d365081f3a9b3cc2124979624)
by [Unito](https://www.unito.io)
2026-04-11 00:45:38 +00:00
Christian Byrne
44f88027b6 fix: debounce reconnecting toast to prevent false-positive banner (#10997)
## Summary

Debounce the reconnecting toast with a 1.5s grace period so brief
WebSocket blips don't flash a persistent error banner.

## Problem

After #9543 made all error toasts sticky (no auto-dismiss), brief
WebSocket disconnections (<1s) would show a persistent "Reconnecting..."
error banner that stays until the `reconnected` event fires. On staging,
users see this banner even though jobs are actively executing.

## Changes

- Extract reconnection toast logic from `GraphView.vue` into
`useReconnectingNotification` composable
- Add 1.5s delay (via `useTimeoutFn` from VueUse) before showing the
reconnecting toast
- If `reconnected` fires within the delay window, suppress both the
error and success toasts entirely
- Clean up unused `useToast`/`useI18n` imports from `GraphView.vue`

## Testing

- Sub-1.5s disconnections: no toast shown
- Longer disconnections (>1.5s): error toast shown, cleared with success
toast on reconnect
- Setting `Comfy.Toast.DisableReconnectingToast`: no toasts shown at all
- Multiple rapid `reconnecting` events: only one toast shown

6 unit tests covering all scenarios.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10997-fix-debounce-reconnecting-toast-to-prevent-false-positive-banner-33d6d73d3650810289e8f57c046972f1)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-04-11 00:43:42 +00:00
Comfy Org PR Bot
5d07de1913 1.44.2 (#11151)
Patch version increment to 1.44.2

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11151-1-44-2-33f6d73d3650815c9767fa5668e67a47)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2026-04-11 00:41:25 +00:00
Johnpaul Chiwetelu
f0ae91de43 test: add e2e coverage for alt+drag duplicate and meta multi-select drag (#10994)
## Summary

Add Playwright coverage for two previously-untested canvas gestures:
Alt+drag to duplicate a regular node, and holding Meta across
click-click-click-drag to move multiple selected Vue nodes together.

## Changes

- **What**:
- `browser_tests/tests/interaction.spec.ts` — new `Node Duplication`
sub-describe with `Can duplicate a regular node via Alt+drag`. Asserts
CLIPTextEncode count goes 2 → 3 via poll, original node still exists.
Exercises the legacy canvas path at
`src/lib/litegraph/src/LGraphCanvas.ts:2434-2458`, which was only tested
for subgraph nodes before.
- `browser_tests/tests/vueNodes/interactions/node/move.spec.ts` — new
`should move all selected nodes together when dragging one with Meta
held`. Holds Meta for the entire sequence (3× `click({ modifiers:
['Meta'] })` + drag), asserts selection stays at 3 and all three nodes
move by the same delta. Exercises
`src/renderer/extensions/vueNodes/layout/useNodeDrag.ts:54-191`.
- Small refactor: `getLoadCheckpointHeaderPos` now delegates to a
reusable `getHeaderPos(comfyPage, title)` helper. Added `deltaBetween`
and `expectSameDelta` utilities (stricter than `expectPosChanged`, which
only checks `> 0`).


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10994-test-add-e2e-coverage-for-alt-drag-duplicate-and-meta-multi-select-drag-33d6d73d3650812dbf15c7053f44f0fd)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-04-11 00:18:16 +00:00
Alexander Brown
fb8025c49f chore: disable vitest/require-mock-type-parameters oxlint rule (#11146)
## Summary

Disables the `vitest/require-mock-type-parameters` oxlint rule,
eliminating all 2,813 lint warnings in the codebase.

## Details

Every warning came from this single rule requiring explicit type
parameters on `vi.fn()` calls. Investigation showed:

- **85% are bare `vi.fn()`** — no type info available without manual
cross-referencing
- The rule's auto-fixer is **declared but not implemented** — `lint:fix`
doesn't help
- No existing codemods exist for this
- A full manual sweep would take 3–5 days across ~210 test files

## Test Plan

- `pnpm lint` passes with 0 warnings, 0 errors

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11146-fix-disable-vitest-require-mock-type-parameters-oxlint-rule-33e6d73d36508186bf1cdc2ce6d2ba57)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-04-11 00:18:09 +00:00
Alexander Brown
5c14badc42 fix(vite): hide git rev-parse window on Windows (#11144)
## Summary

Add `windowsHide: true` to the `execSync('git rev-parse HEAD')` call in
`vite.config.mts` to prevent a console window from flashing on Windows
during builds.

## Changes

- **What**: Pass `windowsHide: true` option to `execSync` when fetching
the git commit hash at build time. This suppresses the transient cmd.exe
popup that appears on Windows.

## Review Focus

Minimal, single-option change. `windowsHide` is a Node.js built-in
option for `child_process` methods — no-op on non-Windows platforms.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11144-fix-vite-hide-git-rev-parse-window-on-Windows-33e6d73d365081ed9a14da5f47ccac4d)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-04-10 23:42:26 +00:00
pythongosssss
82fb8ce658 test: App mode - Execution tests (#10801)
## Summary

Adds tests that simulate the execution flow and output feed

## Changes

- **What**: 
- Add ExecutionHelper for mocking network activity
- Refactor ws fixture to use Playwright websocket helper instead of
patching window
- 

## 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-10801-test-App-mode-Execution-tests-3356d73d365081e4acf0c34378600031)
by [Unito](https://www.unito.io)
2026-04-10 23:31:56 +00:00
Christian Byrne
c3e823e55b fix: use standard size-4 for blueprint action icons (#10992)
## Summary

Fix undersized delete and edit icons on user blueprint items in the node
library sidebar.

## Changes

- **What**: Changed blueprint action icons (trash, edit) from `size-3.5`
(14px) to `size-4` (16px), matching the standard icon size used across
the codebase.

## Review Focus

Trivial sizing fix — `size-4` is the codebase-wide convention for
iconify icons in buttons, and what the button base styles default SVGs
to.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10992-fix-use-standard-size-4-for-blueprint-action-icons-33d6d73d365081be8c65f9e2a7b1d6ec)
by [Unito](https://www.unito.io)
2026-04-10 23:04:17 +00:00
pythongosssss
ebc9025de5 fix/feat: App mode - Persist user resized widget heights (#10993)
## Summary

Saves the user sized textarea/image dropzone elements to the linearData
in the workflow.

## Changes

- **What**: 
- Adds a 3rd element to the linearData input tuple for configuration
data
- Add appmode widget resize composable for persisting resizes
- Tests

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10993-fix-feat-App-mode-Persist-user-resized-widget-heights-33d6d73d36508144b700c6bfcbfa5b3c)
by [Unito](https://www.unito.io)
2026-04-10 22:24:46 +00:00
Alexander Brown
0353524e6f refactor: standardize Page Object locators as public readonly instead of getters (#11135)
*PR Created by the Glary-Bot Agent*

---

## Summary

- Convert ~120 getter-based locators across 18 browser test fixture
files to `public readonly` constructor-assigned properties
- Removes unnecessary indirection, makes object shape explicit, and
improves IDE auto-complete / type inference
- Keeps lazy-init getters (`??=`), computed properties, and `private get
page()` convenience accessors as getters

## Changes

**`browser_tests/fixtures/components/`** (6 files):
`ComfyNodeSearchBox`, `ContextMenu`, `SettingDialog`, `SignInDialog`,
`SidebarTab` (all 6 classes), `Topbar`

**`browser_tests/fixtures/`** (4 files): `ComfyPage` (ComfyMenu.buttons,
ComfyPage.visibleToasts), `UserSelectPage`, `ComfyMouse`,
`VueNodeHelpers`

**`browser_tests/fixtures/helpers/`** (7 files): `AppModeHelper`,
`BuilderFooterHelper`, `BuilderSaveAsHelper`, `BuilderSelectHelper`,
`BuilderStepsHelper`, `ToastHelper`, `NodeOperationsHelper`

**`browser_tests/fixtures/utils/`** (1 file): `vueNodeFixtures`

## Validation

- `pnpm typecheck` 
- `pnpm typecheck:browser` 
- `pnpm exec eslint browser_tests/fixtures/` 
- All pre-commit hooks pass (oxfmt, oxlint, eslint, typecheck,
typecheck:browser) 
- No visual/manual verification needed — changes are test fixture
locator declarations only (no UI or runtime behavior change)

Fixes #11131

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11135-refactor-standardize-Page-Object-locators-as-public-readonly-instead-of-getters-33e6d73d3650819690cbc639f3d30daf)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
2026-04-10 21:10:17 +00:00
Alexander Brown
f1bb756929 fix: remove redundant and counterproductive e2e timeout overrides (#11110)
## Summary

Alright, alright, alright. These e2e tests have been runnin' around like
they're late for somethin', settin' tight little timeouts like the
world's gonna end in 250 milliseconds. Man, you gotta *breathe*. Let the
framework do its thing. Go slow to go fast, that's what I always say.

## Changes

- **What**: Removed ~120 redundant timeout overrides from auto-retrying
Playwright assertions (`toBeVisible`, `toBeHidden`, `toHaveCount`,
`toBeEnabled`, `toHaveAttribute`, `toContainText`, `expect.poll`) where
5000ms is already the default. Also removed sub-5s timeouts (1s, 2s, 3s)
that were just *begging* for flaky failures — like wearin' a belt and
suspenders and also holdin' your pants up with both hands. Raised the
absurdly short timeouts in `customMatchers.ts` (250ms `toPass` → 5000ms,
256ms poll → default). Kept `timeout: 5000` on `.toPass()` calls
(defaults to 0), `.waitFor()`, `waitForRequest`, `waitForFunction`,
intentionally-short timeouts inside retry loops, and conditional
`.isVisible()/.catch()` checks — those fellas actually need the help.

## Review Focus

Every remaining timeout in the diff is there for a *reason*. The ones on
`.toPass()` stay because that API defaults to zero — it won't retry at
all without one. The ones on `.waitFor()` and `waitForRequest` stay
because those are locator actions, not auto-retrying assertions. The
intentionally-short ones inside `toPass` retry loops
(`interaction.spec.ts`) and the negative assertions (`actionbar.spec.ts`
confirming no response arrives) — those are *supposed* to be tight.

The short timeouts on regular assertions were actively *encouragin'*
flaky failures. That's like settin' your alarm for 4 AM and then gettin'
mad you're tired. Just... don't do that, man. Let things take the time
they need.

38 files, net -115 lines. Less code, more chill. That's livin'.

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-04-10 19:47:20 +00:00
Christian Byrne
d70c3cbfc2 fix: remove ticket-intake skill from repo (#11127)
Removes the `.claude/skills/ticket-intake/` directory — personal
pipeline automation that doesn't belong in the shared repository.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11127-fix-remove-ticket-intake-skill-from-repo-33e6d73d365081849f00d7f988ac17e5)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-04-10 11:42:47 -07:00
pythongosssss
e38dd1efae test: App mode - Add test for cross-workflow input corruption (#10944)
## Summary

Adds test for the issue where inputs from one workflow would overwrite
those on another after sorting

## Changes

- **What**: 
- add test

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10944-test-App-mode-Add-test-for-cross-workflow-input-corruption-33b6d73d365081deab26d8f23d2318ae)
by [Unito](https://www.unito.io)
2026-04-10 17:55:33 +00:00
Comfy Org PR Bot
62779d3c51 1.44.1 (#11007)
Patch version increment to 1.44.1

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11007-1-44-1-33e6d73d365081e3bbd3c02a3a7ca5d0)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-04-10 17:35:22 +00:00
Christian Byrne
4cf160d66e fix: disable pointer events on non-visible DOM widget overlays (#11063)
## Problem

When a node with DOM widget overlays (e.g. CLIPTextEncode) is collapsed,
the overlay elements can intercept pointer events intended for the
canvas collapse toggler, making click-to-expand unreliable.

## Root Cause

`updateWidgets()` runs during `onDrawForeground` (canvas render cycle)
and sets `widgetState.visible = false` for collapsed nodes. `v-show`
then hides the element with `display: none`. However, there is a timing
gap between the canvas state change and Vue's DOM update — during this
gap the widget overlay still intercepts pointer events.

## Fix

Add `!widgetState.visible` to the `pointerEvents` condition in
`composeStyle()`. This immediately sets `pointer-events: none` when the
widget becomes invisible, preventing event interception before `v-show`
applies `display: none`.

Also restores click-to-expand in the E2E test, removing the programmatic
`node.collapse()` workaround from PR #10967.

- Fixes #11006

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11063-fix-disable-pointer-events-on-non-visible-DOM-widget-overlays-33e6d73d36508179a83cd47121cf933f)
by [Unito](https://www.unito.io)
2026-04-10 14:28:25 +00:00
pythongosssss
c0871ba219 ci: Update actions for queue merge validation (#11114)
## Summary

PR merge queue was enabled but actions were not updated to trigger on
`merge_group`:

https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue#triggering-merge-group-checks-with-github-actions

## Changes

- **What**: 
- `.github/actions/lint-format-verify/action.yml` extrated shared Verify
lint and format steps
- `.github/workflows/ci-lint-format.yaml` updated to use shared
composite action
- `.github/workflows/ci-lint-format-queue.yaml` new action that triggers
on merge_group to validate format
- `.github/workflows/ci-tests-e2e.yaml` triggers on merge_group
- `.github/workflows/ci-tests-unit.yaml` triggers on merge_group

## 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-11114-ci-Update-actions-for-queue-merge-validation-33e6d73d3650815f8c88f40736b513ec)
by [Unito](https://www.unito.io)
2026-04-10 17:09:48 +00:00
Terry Jia
6d4fc1bb10 fix: resolve incorrect GLSL live preview for non-primitive widget types (#11010)
## Summary

Three issues caused GLSL preview to diverge from backend results:

1. Uniform source resolution always read widgets[0] instead of using
link.origin_slot to select the correct widget. Added directValue
fallback for widgets not registered in widgetValueStore.

2. Hex color strings (e.g. "#45edf5") were coerced to 0 by Number().
Added hexToInt to colorUtil and used it in toNumber coercion.

3. Custom size_mode was ignored — preview always used upstream image
dimensions. Now checks size_mode widget first and respects "custom".

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11010-fix-resolve-incorrect-GLSL-live-preview-for-non-primitive-widget-types-33e6d73d36508101a76bfe8383c0c6ab)
by [Unito](https://www.unito.io)
2026-04-10 10:21:27 -04:00
Alexander Brown
0132c77c7d test: harden 82 Playwright specs for deterministic CI runs (#10967)
## Summary

Harden 98 E2E spec files and 8 fixtures/helpers for deterministic CI
runs by replacing race-prone patterns with retry-safe alternatives.

No source code changes -- only `browser_tests/` is touched.

## Changes

- **E2E spec hardening** (98 spec files, 6 fixtures, 2 helpers):

  | Fix class | Sites | Examples |
  |-----------|-------|---------:|
| `expect(await ...)` -> `expect.poll()` | ~153 | interaction,
defaultKeybindings, workflows, featureFlags |
| `const x = await loc.count(); expect(x)` -> `toHaveCount()` | ~19 |
menu, linkInteraction, assets, bottomPanelShortcuts |
| `nextFrame()` -> `waitForHidden()` after menu clicks | ~22 |
contextMenu, rightClickMenu, subgraphHelper |
| Redundant `nextFrame()` removed | many | defaultKeybindings, minimap,
builderSaveFlow |
| `expect(async () => { ... }).toPass()` retry blocks | 5 | interaction
(graphdialog dismiss guard) |
| `force:true` removed from `BaseDialog.close()` | 1 | BaseDialog
fixture |
| ContextMenu `waitForHidden` simplified (check-then-act race removed) |
1 | ContextMenu fixture |
| Non-deterministic node order -> proximity-based selection | 1 |
interaction (toggle dom widget) |
  | Tight poll timeout (250ms) -> >=2000ms | 2 | templates |

- **Helper improvements**: Exposed locator getters on
`ComfyPage.domWidgets`, `ToastHelper.toastErrors`, and
`WorkflowsSidebarTab.activeWorkflowLabel` so callers can use retrying
assertions (`toHaveCount()`, `toHaveText()`) directly.

- **Flake pattern catalog**: Added section 7 table to
`browser_tests/FLAKE_PREVENTION_RULES.md` documenting 8 pattern classes
for reviewers and future authors.

- **Docs**: Fixed bad examples in `browser_tests/README.md` to use
`expect.poll()`.

- **Breaking**: None
- **Dependencies**: None

## Review Focus

- All fixes follow the rules in
`browser_tests/FLAKE_PREVENTION_RULES.md`
- No behavioral changes to tests -- only timing/retry strategy is
updated
- The `ContextMenu.waitForHidden` simplification removes a
swallowed-error anti-pattern; both locators now use direct `waitFor({
state: 'hidden' })`

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-04-09 20:50:56 -07:00
Terry Jia
63eab15c4f Range editor (#10936)
BE change https://github.com/Comfy-Org/ComfyUI/pull/13322

## Summary
Add RANGE widget for image levels adjustment       
- Add RangeEditor widget with three display modes: plain, gradient, and
histogram
- Support optional midpoint (gamma) control for non-linear midtone
adjustment
- Integrate histogram display from upstream node outputs

## Screenshots (if applicable)
<img width="1450" height="715" alt="image"
src="https://github.com/user-attachments/assets/864976af-9eb7-4dd0-9ce1-2f5d7f003117"
/>
<img width="1431" height="701" alt="image"
src="https://github.com/user-attachments/assets/7ee2af65-f87a-407b-8bf2-6ec59a1dff59"
/>
<img width="705" height="822" alt="image"
src="https://github.com/user-attachments/assets/7bcb8f17-795f-498a-9f8a-076ed6c05a98"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10936-Range-editor-33b6d73d365081089e8be040b40f6c8a)
by [Unito](https://www.unito.io)
2026-04-09 18:37:40 -07:00
Terry Jia
277ee5c32e test: add E2E tests for Load3D model upload and drag-drop and basic e2e for 3d viewer (#10957)
## Summary
Add tests verifying real model loading:
- Upload cube.obj via file chooser button
- Drag-and-drop cube.obj onto the 3D canvas
- Add data-testid to LoadingOverlay for stable test selectors.
Add tests verifying 3d viewer openning:
- Open viewer from Load3D node via expand button, verify canvas and
controls sidebar
- Cancel button closes the viewer dialog

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10957-test-add-E2E-tests-for-Load3D-model-upload-and-drag-drop-and-basic-e2e-for-3d-viewer-33c6d73d3650810c8ff8ed656a5164a6)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-04-09 21:36:07 -04:00
Dante
e8787dee9d fix: prevent node context menu from overflowing viewport on desktop (#10854)
## Summary

The node "More Options" (⋮) context menu had `md:max-h-none
md:overflow-y-visible` responsive overrides that removed the height
constraint and scrollability on desktop (768px+). When the menu had many
items (e.g., KSampler), items below the viewport fold were inaccessible
with no scrollbar.

Removed the desktop overrides so `max-h-[80vh] overflow-y-auto` applies
at all screen sizes.

- Fixes #10824

## Red-Green Verification

| Commit | CI Status | Purpose |
|--------|-----------|---------|
| `test: add failing test for node context menu viewport overflow` |
🔴 Red | Proves the test catches the bug |
| `fix: prevent node context menu from overflowing viewport on desktop`
| 🟢 Green | Proves the fix resolves the bug |

## Test Plan

- [ ] CI red on test-only commit
- [ ] CI green on fix commit
- [ ] Manual verification: zoom out to 50%, open node More Options menu,
verify last item ("Remove") is scrollable

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10854-fix-prevent-node-context-menu-from-overflowing-viewport-on-desktop-3396d73d365081989403c981847aeda6)
by [Unito](https://www.unito.io)
2026-04-10 10:31:04 +09:00
Dante
ba0bab3e50 test: add E2E tests for ManagerDialog (#10970)
## Summary
- Add Playwright E2E tests for the ManagerDialog component which had
zero test coverage
- Covers dialog opening, pack browsing, search filtering, tab
navigation, sort controls, search mode switching, error states, install
action, info panel, and close behavior
- All API calls (Algolia search, Manager installed packs, queue) are
mocked with typed responses

Part of the FixIt Burndown test coverage initiative.

## Test plan
- [ ] CI browser tests pass
- [ ] Tests validate core ManagerDialog user flows with mocked APIs
- [ ] No regressions in existing tests

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10970-test-add-E2E-tests-for-ManagerDialog-33c6d73d365081468a4ad8fc1894f05b)
by [Unito](https://www.unito.io)
2026-04-10 10:26:35 +09:00
Kelly Yang
bbb07053c4 test: add E2E tests for CanvasModeSelector toolbar component (#10934)
Adds `browser_tests/tests/canvasModeSelector.spec.ts`, covering the
canvas toolbar mode-selector component that was introduced with no E2E
coverage.
  Covers:
- Trigger button: toolbar visibility, `aria-expanded` state, icon
reflects active mode
- Popover lifecycle: open on click, close on re-click / item selection /
Escape
- Mode switching: clicking Hand/Select drives `canvas.state.readOnly`;
clicking the active item is a no-op
- ARIA state: `aria-checked` and roving `tabindex` track active mode,
including state driven by external commands
- Keyboard navigation: ArrowDown/Up with wraparound, Escape restores
focus to trigger — all using `toBeFocused()` retrying assertions
- Focus management: popover auto-focuses the checked item on open
- Keybinding integration: `H` / `V` keys update both
`canvas.state.readOnly` and the trigger icon
- Shortcut hint display: both menu items render non-empty key-sequence
hints
  22 tests across 7 `describe` groups. All selectors are ARIA-driven.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10934-test-add-E2E-tests-for-CanvasModeSelector-toolbar-component-33b6d73d3650819cb2cfdca22bf0b9a5)
by [Unito](https://www.unito.io)
2026-04-09 20:44:30 -04:00
Christian Byrne
97fca566fb fix: use || instead of ?? and server type in WebcamCapture upload path (#11000)
## Description

Fixes the WebcamCapture image upload path construction that was still
broken on cloud environments after #10220.

### Root cause

The cloud `/upload/image` endpoint returns:
```json
{ "name": "hash.png", "subfolder": "", "type": "input" }
```

The previous fix used `??` (nullish coalescing), which doesn't catch
empty strings:
- `subfolder: ""` → `"" ?? "webcam"` = `""` → path becomes `/hash.png`
(wrong)
- `type` was hardcoded as `[temp]` but cloud stores as `input` → file
not found

### Fix

- `??` → `||` so empty strings fall back to defaults
- Use `data.type` from server response instead of hardcoding `[temp]`

### QA evidence

Prod (cloud/1.42): `ImageDownloadError: the input file
'webcam/1775685296883.png [temp]' doesn't exist`
Staging (cloud/1.42): `ImageDownloadError: Failed to validate images`

### Related

- Fixes the remaining issue from #10220

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11000-fix-use-instead-of-and-server-type-in-WebcamCapture-upload-path-33d6d73d36508156b93cfce0aae8e017)
by [Unito](https://www.unito.io)
2026-04-09 16:08:45 -07:00
Christian Byrne
c6b8883e61 [chore] Update Ingest API types from cloud@48d94b7 (#10925)
## Automated Ingest API Type Update

This PR updates the Ingest API TypeScript types and Zod schemas from the
latest cloud OpenAPI specification.

- Cloud commit: 48d94b7
- Generated using @hey-api/openapi-ts with Zod plugin

These types cover cloud-only endpoints (workspaces, billing, secrets,
assets, tasks, etc.).
Overlapping endpoints shared with the local ComfyUI Python backend are
excluded.

---------

Co-authored-by: MillerMedia <7741082+MillerMedia@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-04-09 14:41:53 -07:00
Christian Byrne
8487c13f14 feat: integrate Typeform survey into feedback button (#10890)
## Summary

Replace Zendesk feedback URLs with Typeform survey (`q7azbWPi`) in the
action bar feedback button and Help Center menu for Cloud/Nightly
distributions.

## Changes

- **What**: 
- `cloudFeedbackTopbarButton.ts`: Replace `buildFeedbackUrl()` (Zendesk)
with direct Typeform survey URL. Remove unused Zendesk import.
- `HelpCenterMenuContent.vue`: Feedback menu item now opens Typeform URL
for Cloud/Nightly builds; falls back to `Comfy.ContactSupport` (Zendesk)
for other distributions. Added external link icon for Cloud/Nightly.
- Help menu item and `Comfy.ContactSupport` command unchanged — support
flows still route to Zendesk.

## Review Focus

- Gating logic: `isCloud || isNightly` correctly limits Typeform
redirect to intended distributions
- Help item intentionally unchanged (support ≠ feedback)

Ticket: COM-17992

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10890-feat-integrate-Typeform-survey-into-feedback-button-33a6d73d36508185abbfe57e7a36b5f7)
by [Unito](https://www.unito.io)
2026-04-09 14:40:52 -07:00
jaeone94
809da9c11c fix: use cloud assets for asset widget default value (#10983)
## Summary

In cloud mode, asset-supported nodes (e.g. CheckpointLoaderSimple) used
the server's `object_info` combo options as their default widget value.
These options list local files on the backend which may not exist in the
user's cloud asset library. When the missing-model pipeline runs (on
undo, reload, or tab switch), it checks widget values against cloud
assets and correctly flags these local-only files as missing — producing
errors that appear to be false positives but are actually valid
detections of unusable defaults.

This PR changes the default value source from server combo options to
the cloud assets store.

## Default Value Behavior (Before → After)

### Cloud + asset-supported widgets (changed)

| Condition | Before | After |
|-----------|--------|-------|
| Assets cached, `inputSpec.default` in assets | `inputSpec.default` |
`inputSpec.default` |
| Assets cached, `inputSpec.default` not in assets | `inputSpec.default`
| `assets[0]` |
| Assets cached, no `inputSpec.default`, `options` exist | `options[0]`
| `assets[0]` |
| Assets not cached, `inputSpec.default` exists | `inputSpec.default` |
`undefined` → "Select model" |
| Assets not cached, no `inputSpec.default`, `options` exist |
`options[0]` | `undefined` → "Select model" |
| Assets not cached, no `inputSpec.default`, no `options` | `undefined`
→ "Select model" | `undefined` → "Select model" |

### Cloud + non-asset widgets (unchanged)

| Condition | Behavior |
|-----------|----------|
| `inputSpec.default` exists | `inputSpec.default` |
| `options` exist | `options[0]` |
| `remote` input | `"Loading..."` |
| None | `undefined` |

### OSS (unchanged)

| Condition | Behavior |
|-----------|----------|
| `inputSpec.default` exists | `inputSpec.default` |
| `options` exist | `options[0]` |
| `remote` input | `"Loading..."` |
| None | `undefined` |

## Root Cause

1. `addComboWidget` called `getDefaultValue(inputSpec)` which returns
`inputSpec.options[0]` — a local file from `object_info`
2. In cloud mode, `shouldUseAssetBrowser()` creates an asset widget with
this local filename as default
3. The model (e.g.
`dynamicrafter/controlnet/dc-sketch_encoder_fp16.safetensors`) exists on
the server but not in the user's cloud asset library
4. On undo/reload, `verifyAssetSupportedCandidates()` checks the widget
value against cloud assets → not found → marked as missing

## Changes

### Production (`useComboWidget.ts`)
- New `resolveCloudDefault(nodeType, specDefault)` function encapsulates
cloud default resolution
- Default priority: `inputSpec.default` (if found in cloud assets) →
first cloud asset → `undefined` (shows "Select model" placeholder)
- Edge case guards: `!= null` check for falsy defaults, `|| undefined`
for empty `getAssetFilename` return
- Server combo options (`object_info`) are no longer used as defaults
for asset widgets

### Unit Tests (`useComboWidget.test.ts`)
- 6 scenarios covering all default value paths:
  - Cloud assets loaded, no `inputSpec.default` → `assets[0]`
  - Cloud assets loaded, `inputSpec.default` in assets → uses default
  - Cloud assets loaded, `inputSpec.default` not in assets → `assets[0]`
  - No cloud assets, with `inputSpec.default` → placeholder
  - No cloud assets, with server options → placeholder
  - Asset widget creation verification
- Test helper refactored: assertions moved from helper to each test for
clarity

### E2E Test (`cloud-asset-default.spec.ts`)
- New `@cloud` tagged test verifying CheckpointLoaderSimple uses first
cloud asset, not server default
- Fixture extension stubs `/api/assets` before app loads (local backend
returns 503 for this endpoint)
- Uses typed mock data from existing `assetFixtures.ts`

## Scope

- **Cloud only**: All changes gated behind `isCloud` +
`shouldUseAssetBrowser()`
- **OSS impact**: None — code path is not entered in non-cloud builds
- **Breaking changes**: None — `useComboWidget` export signature
unchanged

## Review Focus
- Should the `/api/assets` stub in the E2E fixture extension be moved
into `ComfyPage` for all `@cloud` tests?

## Record
Before 


https://github.com/user-attachments/assets/994162a0-b56a-4e84-9b1c-d0f0068196d5



After


https://github.com/user-attachments/assets/ba299990-9bd3-4565-bd09-bffac3db60a9
2026-04-09 15:44:04 +09:00
Dante
65d1313443 fix: preserve CustomCombo options through clone and paste (#10853)
## Summary

- Fix `CustomCombo` copy/paste so the combo keeps its option list and
selected value
- Scope the fix to `src/extensions/core/customWidgets.ts` instead of
changing LiteGraph core deserialization
- Replace the previous round-trip test with a regression test that
exercises the actual clone/paste lifecycle

- Fixes #9927

## Root Cause

`CustomCombo` option widgets override `value` to read from
`widgetValueStore`.
During `node.clone()` and clipboard paste, `configure()` restores widget
values before the new node is added to the graph and before those
widgets are registered in the store.
That meant the option widgets read back as empty while `updateCombo()`
was rebuilding the combo state, so `comboWidget.options.values` became
blank on the pasted node.

## Fix

Keep a local fallback value for each generated `option*` widget in
`customWidgets.ts`.
The getter now returns the store-backed value when available and falls
back to the locally restored value before store registration.
This preserves the option list during `clone().serialize()` and paste
without hard-coding `CustomCombo` behavior into
`LGraphNode.configure()`.

## Why No E2E Test

This regression happens in the internal LiteGraph clipboard lifecycle:
`clone() -> serialize() -> createNode() -> configure() -> graph.add()`.
The failing state is the transient pre-add relationship between
`CustomCombo`'s store-backed option widgets and
`comboWidget.options.values`, which is not directly exposed through a
stable DOM assertion in the current Playwright suite.
A focused unit regression test is the most direct way to cover that
lifecycle without depending on brittle canvas interaction timing.

## Test Plan

- [x] Regression test covers `clone().serialize() -> createNode() ->
configure() -> graph.add()` for `CustomCombo`
- [ ] CI on the latest two commits (`81ac6d2ce`, `94147caf1`)
- [ ] Manual: create `CustomCombo` -> add `alpha`, `beta`, `gamma` ->
select `beta` -> copy/paste -> verify the pasted combo still shows all
three options and keeps `beta` selected
2026-04-09 12:35:20 +09:00
Alexander Brown
f90d6cf607 test: migrate 132 test files from @vue/test-utils to @testing-library/vue (#10965)
## Summary

Migrate 132 test files from `@vue/test-utils` (VTU) to
`@testing-library/vue` (VTL) with `@testing-library/user-event`,
adopting user-centric behavioral testing patterns across the codebase.

## Changes

- **What**: Systematic migration of component/unit tests from VTU's
`mount`/`wrapper` API to VTL's `render`/`screen`/`userEvent` API across
132 files in `src/`
- **Breaking**: None — test-only changes, no production code affected

### Migration breakdown

| Batch | Files | Description |
|-------|-------|-------------|
| 1 | 19 | Simple render/assert tests |
| 2A | 16 | Interactive tests with user events |
| 2B-1 | 14 | Interactive tests (continued) |
| 2B-2 | 32 | Interactive tests (continued) |
| 3A–3E | 51 | Complex tests (stores, composables, heavy mocking) |
| Lint fix | 7 | `await` on `fireEvent` calls for `no-floating-promises`
|
| Review fixes | 15 | Address CodeRabbit feedback (3 rounds) |

### Review feedback addressed

- Removed class-based assertions (`text-ellipsis`, `pr-3`, `.pi-save`,
`.skeleton`, `.bg-black\/15`, Tailwind utilities) in favor of
behavioral/accessible queries
- Added null guards before `querySelector` casts
- Added `expect(roots).toHaveLength(N)` guards before indexed NodeList
access
- Wrapped fake timer tests in `try/finally` for guaranteed cleanup
- Split double-render tests into focused single-render tests
- Replaced CSS class selectors with
`screen.getByText`/`screen.getByRole` queries
- Updated stubs to use semantic `role`/`aria-label` instead of CSS
classes
- Consolidated redundant edge-case tests
- Removed manual `document.body.appendChild` in favor of VTL container
management
- Used distinct mock return values to verify command wiring

### VTU holdouts (2 files)

These files intentionally retain `@vue/test-utils` because their
components use `<script setup>` without `defineExpose`, making internal
computed properties and methods inaccessible via VTL:

1. **`NodeWidgets.test.ts`** — partial VTU for `vm.processedWidgets`
2. **`WidgetSelectDropdown.test.ts`** — full VTU for heavy
`wrapper.vm.*` access

## Follow-up

Deferred items (`ComponentProps` typing, camelCase listener props)
tracked in #10966.

## Review Focus

- Test correctness: all migrated tests preserve original behavioral
coverage
- VTL idioms: proper use of `screen` queries, `userEvent`, and
accessibility-based selectors
- The 2 VTU holdout files are intentional, not oversights

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10965-test-migrate-132-test-files-from-vue-test-utils-to-testing-library-vue-33c6d73d36508199a6a7e513cf5d8296)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-04-08 19:21:42 -07:00
Christian Byrne
2c34d955cb feat(website): add zh-CN translations for homepage and secondary pages (#10157)
## 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-10157-feat-website-add-zh-CN-translations-for-homepage-and-secondary-pages-3266d73d3650811f918cc35eca62a4bc)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-08 19:18:19 -07:00
Christian Byrne
8b6c1b3649 refactor: consolidate SubscriptionTier type (#10487)
## Summary

Consolidate the `SubscriptionTier` type from 3 independent definitions
into a single source of truth in `tierPricing.ts`.

## Changes

- **What**: Exported `SubscriptionTier` from `tierPricing.ts`. Removed
hand-written unions from `workspaceApi.ts` (lines 80-88),
`PricingTable.vue`, and `PricingTableWorkspace.vue`. All now import from
the canonical location.
- **Files**: 4 files changed (type-only, ~5 net lines)

## Review Focus

- This is a type-only change — `pnpm typecheck` is the primary
validation
- If the OpenAPI schema ever adds tiers, there is now one place to
update

## Stack

PR 5/5: #10483#10484#10485#10486 → **→ This PR**
2026-04-08 19:17:44 -07:00
Christian Byrne
026aeb71b2 refactor: decompose MembersPanelContent into focused components (#10486)
## Summary

Decompose the 562-line `MembersPanelContent.vue` into focused
single-responsibility components.

## Changes

- **What**: Extracted `RoleBadge.vue`, `MemberListItem.vue`,
`PendingInvitesList.vue`, and `MemberUpsellBanner.vue` from
`MembersPanelContent.vue`. Added `RoleBadge.test.ts`. The parent
component is slimmed from 562 → ~120 lines.
- **Files**: 6 files changed (4 new components + 1 new test + 1
refactored)

## Review Focus

- Component boundaries — each extracted component has a clear single
responsibility
- `MembersPanelContent.vue` still orchestrates all behavior; extracted
components are presentational
- Visual QA needed: workspace settings panel should look and behave
identically

## Stack

PR 4/5: #10483#10484#10485 → **→ This PR** → #10487

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
2026-04-08 18:57:11 -07:00
Alexander Brown
d96a7d2b32 fix: resolve lint/knip warnings and upgrade oxlint, oxfmt, knip (#10973)
## Changes

- Fix unsafe optional chaining warnings in 2 test files
- Promote `no-unsafe-optional-chaining` to error in oxlintrc
- Remove stale knip ignores (useGLSLRenderer, website deps, astro entry)
- Remove `vue/no-dupe-keys` from oxlintrc (removed from oxlint vue
plugin; `eslint/no-dupe-keys` covers it)
- Un-export unused `UniformSource`/`UniformSources` interfaces
- Dedupe pnpm lockfile

## Dependency Upgrades

| Package | Before | After |
|---------|--------|-------|
| knip | 6.0.1 | 6.3.1 |
| oxlint | 1.55.0 | 1.59.0 |
| oxfmt | 0.40.0 | 0.44.0 |
| eslint-plugin-oxlint | 1.55.0 | 1.59.0 |
| oxlint-tsgolint | 0.17.0 | 0.20.0 |

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10973-fix-resolve-lint-knip-warnings-and-upgrade-oxlint-oxfmt-knip-33c6d73d36508135a773f0a174471cf9)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-04-08 18:30:37 -07:00
Comfy Org PR Bot
1720aa0286 1.44.0 (#10974)
Minor version increment to 1.44.0

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10974-1-44-0-33c6d73d365081d98a3bd646d3374b3b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-04-08 18:13:31 -07:00
Christian Byrne
c671a33182 fix(ci): resolve pnpm version conflict in version bump workflow (#10972)
## Summary

Removes hardcoded `version: 10` from `pnpm/action-setup` and instead
injects the `packageManager` field into `package.json` when absent
(legacy `core/*` branches).

## Why

PR #10952 re-added `version: 10` to fix old branches lacking
`packageManager`. But `main` now has **both** `version: 10` (workflow)
and `packageManager: pnpm@10.33.0` (`package.json`), causing
`pnpm/action-setup` to error with:

> Multiple versions of pnpm specified

Failed run:
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/24158869559

This fix handles both cases:
- **`main`**: has `packageManager` → action reads it directly, no
conflict
- **`core/1.42` etc**: missing `packageManager` → step injects it before
the action runs

E2E test not applicable — this is a CI workflow configuration change
with no user-facing behavior.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10972-fix-ci-resolve-pnpm-version-conflict-in-version-bump-workflow-33c6d73d36508112802df75c0dd5ea50)
by [Unito](https://www.unito.io)
2026-04-08 15:13:33 -07:00
Alexander Brown
25d1ac7456 test: reorganize subgraph test suite into composable domain specs (#10759)
## Summary

Reorganize the subgraph test suite so browser tests are thin
representative user journeys while lower-level Vitest suites own
combinatorics, migration edge cases, and data-shape semantics.

## Changes

- **What**: Migrate 17 flat subgraph browser specs into 10
domain-organized specs under `browser_tests/tests/subgraph/`, move
redundant semantic coverage down to 8 Vitest owner suites, delete all
legacy flat files
- **Browser specs** (54 tests): `subgraphSlots`, `subgraphPromotion`,
`subgraphPromotionDom`, `subgraphSerialization`, `subgraphNavigation`,
`subgraphNested`, `subgraphLifecycle`, `subgraphCrud`, `subgraphSearch`,
`subgraphOperations`
- **Vitest owners** (230 tests): `SubgraphNode.test.ts` (rename/label
propagation), `subgraphNodePromotion.test.ts`,
`promotedWidgetView.test.ts`, `SubgraphSerialization.test.ts`
(duplicate-ID remap), `SubgraphWidgetPromotion.test.ts` (legacy
hydration), `subgraphNavigationStore*.test.ts` (viewport cache,
workflow-switch), `subgraphStore.test.ts` (search aliases, description)
- **Net effect**: browser suite shrinks from ~96 scattered tests to 54
focused journeys

## Review Focus

- Coverage ownership split: each browser test has a unique UI-only
failure mode; semantic coverage lives in Vitest
- `subgraphPromotionDom.spec.ts` forces LiteGraph mode and uses
`canvas.openSubgraph()` instead of `navigateIntoSubgraph()` to avoid a
wrapper-specific DOM overlay duplication issue — entry-affordance
coverage lives in `subgraphNavigation.spec.ts`
- No product code changes — test-only migration

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10759-test-reorganize-subgraph-test-suite-into-composable-domain-specs-3336d73d365081b0a56bcbf809b1f584)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-04-08 15:04:33 -07:00
Johnpaul Chiwetelu
2189172f15 fix: Add timeout and abort mechanism for image upload (#9226) (#9491)
Closes #9226

## Summary

Image uploads had no timeout or abort mechanism, meaning a stalled
upload could hang indefinitely with no user feedback. This adds a
2-minute timeout using `AbortController` and shows a user-friendly toast
message when the upload times out.

## Changes

- `src/composables/node/useNodeImageUpload.ts`: Added `AbortController`
with a 120-second timeout to the `uploadFile` function. The abort signal
is passed to `fetchApi`. In the `handleUpload` error handler,
`AbortError` is now caught separately to display a localized timeout
message.
- `src/locales/en/main.json`: Added `uploadTimedOut` i18n translation
key.

---
Automated by coderabbit-fixer

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9491-fix-Add-timeout-and-abort-mechanism-for-image-upload-9226-31b6d73d365081d7a7d7f7016f3a71c6)
by [Unito](https://www.unito.io)

---------

Co-authored-by: CodeRabbit Fixer <coderabbit-fixer@automated.bot>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-04-08 21:27:32 +00:00
Christian Byrne
9b769656ac test: add ShareWorkflow dialog E2E tests (DLG-05) (#10588)
## Summary
Adds Playwright E2E tests for the ShareWorkflow dialog component and its
various states.

## Tests added
- Dialog opens and shows unsaved state for new workflows (save prompt)
- Ready state shows create link button for saved but unpublished
workflows
- Shared state shows copy URL field with share link after publishing
- Stale state shows update link button when workflow modified after
publishing
- Close button dismisses dialog
- Create link transitions dialog from ready to shared state
- Tab switching between share link and publish to hub (when
comfyHubUploadEnabled)
- Tab aria-selected states update correctly on switch

## Approach
- Share dialog is gated behind `isCloud` (compile-time constant), so
tests invoke it directly via `page.evaluate()` importing
`useShareDialog`
- Share service API calls (`/api/userdata/*/publish`,
`/api/assets/from-workflow`) mocked via `page.route()` for deterministic
state testing
- Dialog state (loading → unsaved → ready → shared → stale) controlled
by mock responses
- Feature flags set via `serverFeatureFlags.value` for tab visibility
testing

## Notes
- All pre-existing TS2307 errors are `.vue` module resolution — no new
type errors
- Tests cover the 5 dialog states: loading, unsaved, ready, shared,
stale

## Task
Part of Test Coverage Q2 Overhaul (DLG-05).

## Conventions
- Uses Vue nodes with new menu enabled
- Tests read as user stories
- No full-page screenshots
- Proper waits, no sleeps
- All API calls mocked

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10588-test-add-ShareWorkflow-dialog-E2E-tests-DLG-05-3306d73d365081a0ab15f333707e493b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-08 14:14:47 -07:00
Christian Byrne
934f1487bd feat: add Comfy Design Standards Figma reference for agents (#10696)
## Summary

Add design standards instructions so agents consult the canonical [Comfy
Design
Standards](https://www.figma.com/design/QreIv5htUaSICNuO2VBHw0/Comfy-Design-Standards)
Figma file before implementing user-facing features.

### Changes

- **`docs/guidance/design-standards.md`** — Auto-loaded guidance for
`src/components/**/*.vue` and `src/views/**/*.vue` with Figma MCP fetch
instructions, section node IDs, and component set references
- **`AGENTS.md`** — Added Design Standards section and Figma link in
External Resources

### Design

All content is fetched **live from Figma** via the Figma MCP tool —
designers can update the file and agents will always see the latest
version. No hardcoded design rules that can go stale.

Ref: COM-17639

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10696-feat-add-Comfy-Design-Standards-Figma-reference-for-agents-3326d73d36508181844fdcaa5c17cf00)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-08 14:13:57 -07:00
Christian Byrne
6f98fe5ba7 docs: add staging environment setup instructions to CONTRIBUTING.md (#10775)
## Summary

Add a "Testing with Cloud & Staging Environments" section to
CONTRIBUTING.md documenting how to test partner/API nodes that require
cloud backend authentication.

## Changes

- **What**: New section in CONTRIBUTING.md between "Dev Server" and
"Access dev server on touch devices" explaining two approaches for
staging/cloud development:
1. Frontend approach: `pnpm dev:cloud` or custom
`DEV_SERVER_COMFYUI_URL` in `.env`
2. Backend approach: `--comfy-api-base https://stagingapi.comfy.org`
flag

## Review Focus

- Accuracy of the `--comfy-api-base` backend flag documentation (sourced
from internal Slack discussion)
- Whether the section placement and level of detail is appropriate

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10775-docs-add-staging-environment-setup-instructions-to-CONTRIBUTING-md-3346d73d36508112bcd4df6ecb7f83c6)
by [Unito](https://www.unito.io)
2026-04-08 14:11:01 -07:00
Rizumu Ayaka
44c3d08b56 perf: add preload and content-visibility attribute to media preview for improved performance (#10806)
## Summary

this pull request is to improve the performance of large workflows
containing numerous Media Previews.

Added content-visibility: auto, which enables the browser to lazily
render Media Previews outside the viewport.

Added preload="metadata", which makes <video> and <audio> elements only
preload metadata instead of the full content.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10806-perf-add-preload-and-content-visibility-attribute-to-media-preview-for-improved-perfor-3356d73d365081238ce8f1a82f8694ec)
by [Unito](https://www.unito.io)
2026-04-08 14:08:47 -07:00
Dante
537e4bc4f2 test: add E2E tests for settings dialog (#10797)
## Summary
- Adds Playwright E2E tests for the Settings dialog covering behaviors
not tested elsewhere:
- About panel displays version badges (navigates to About, verifies
content)
- Boolean setting toggle through the UI persists the value (uses search
+ ToggleSwitch click, verifies via API)
- Dialog can be closed via the close button (complements existing
Escape-key test)

## Test plan
- [ ] `pnpm test:browser:local -- --grep "Settings dialog"` passes
- [ ] No overlap with existing tests in `dialog.spec.ts` or
`useSettingSearch.spec.ts`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10797-test-add-E2E-tests-for-settings-dialog-3356d73d365081e4a3adcd3979048444)
by [Unito](https://www.unito.io)
2026-04-09 05:29:53 +09:00
pythongosssss
4b0b8e7240 test: App mode - Welcome screen state (#10747)
## Summary

Adds tests for validating welcome screen state

## Changes

- **What**: 
- add clear graph util function

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10747-test-App-mode-Welcome-screen-state-3336d73d365081f0ba27d567c3c81505)
by [Unito](https://www.unito.io)
2026-04-08 12:33:51 -07:00
Alexander Brown
3b78dfbe1c test: migrate browser_tests/ to @e2e/ path alias and add lint rule (#10958)
## Summary

Complete the @e2e/ path alias migration started in #10735 by converting
all 354 remaining relative imports and adding a lint rule to prevent
backsliding.

## Changes

- **What**: Migrate all relative imports in browser_tests/ to use
`@e2e/` (intra-directory) and `@/` (src/ imports) path aliases. Add
`no-restricted-imports` ESLint rule banning `./` and `../` imports in
`browser_tests/**/*.ts`. Suppress pre-existing oxlint `no-eval` and
`no-console` warnings exposed by touching those files.

## Review Focus

- ESLint flat-config merging: the `@playwright/test` ban and
relative-import ban are in two separate blocks to avoid last-match-wins
collision with the `useI18n`/`useVirtualList` blocks higher in the
config.
- The `['./**', '../**']` glob patterns (not `['./*', '../*']`) are
needed to catch multi-level relative paths like `../../../src/foo`.

Follows up on #10735

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10958-test-migrate-browser_tests-to-e2e-path-alias-and-add-lint-rule-33c6d73d365081649d1be771eac986fd)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-04-08 11:28:59 -07:00
pythongosssss
036be1c7e9 test: App mode - Pruning tests (#10805)
## Summary

Adds tests that deleted nodes automatically remove selections from app
mode

## Changes

- **What**: 
- always prune when entering app builder (fix)
- add tests (delete output node, delete input node, change dynamic
widget value)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10805-test-App-mode-Pruning-tests-3356d73d365081bcb12fc226af31a724)
by [Unito](https://www.unito.io)
2026-04-08 10:42:47 -07:00
Terry Jia
b494392265 fix: record audio node not releasing microphone in Vue mode (#10829)
## Summary

Root cause: setting modelValue on recording completion triggers
NodeWidgets' updateHandler → widget.callback → litegraph recording
handler (getUserMedia), opening a second mic stream never cleaned up.

Fix:
- Stop writing to modelValue (keep defineModel to absorb parent v-model
without triggering the callback)
- On recording complete, set blob URL on litegraph audioUI DOM element
instead of uploading — let the original serializeValue (uploadAudio.ts)
handle upload at serialization time
- Remove registerWidgetSerialization to stop overriding litegraph's
serializeValue
- Move cleanup() before async onRecordingComplete in useAudioRecorder
- Dispose waveform AudioContext on stop

## Screenshots (if applicable)
before


https://github.com/user-attachments/assets/1e464ea1-53ed-44e2-973b-97eebc63fb76


after

https://github.com/user-attachments/assets/badc8a3f-0761-43bd-a899-d8924f413028

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10829-fix-record-audio-node-not-releasing-microphone-in-Vue-mode-3366d73d36508106b4a4dda31501ec4d)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-04-08 04:31:40 -04:00
jaeone94
3f375bea9c test: comprehensive E2E tests for error dialog, overlay, and errors tab (#10848)
## Summary

Comprehensive Playwright E2E tests for the error systems: ErrorDialog,
ErrorOverlay, and the errors tab (missing nodes, models, media,
execution errors).

## Changes

- **What**:
- **ErrorDialog** (`errorDialog.spec.ts`, 7 tests): configure/prompt
error triggers, Show Report, Copy to Clipboard, Find Issues on GitHub,
Contact Support
- **ErrorOverlay** (`errorOverlay.spec.ts`, 12 tests): error count
labels, per-type button labels (missing nodes/models/media/multiple),
See Errors flow (open panel, dismiss, close), undo/redo persistence
- **Errors tab — common** (`errorsTab.spec.ts`, 3 tests): tab
visibility, search/filter execution errors
- **Errors tab — Missing nodes** (`errorsTabMissingNodes.spec.ts`, 5
tests): MissingNodeCard, packs group, expand/collapse, locate button
- **Errors tab — Missing models** (`errorsTabMissingModels.spec.ts`, 6
tests): group display, model name, expand/referencing nodes, clipboard
copy, OSS Copy URL/Download buttons
- **Errors tab — Missing media** (`errorsTabMissingMedia.spec.ts`, 7
tests): migrated from `missingMedia.spec.ts` with detection,
upload/library/cancel flows, locate
- **Errors tab — Execution** (`errorsTabExecution.spec.ts`, 2 tests):
Find on GitHub/Copy buttons, runtime error panel
- **Shared helpers**: `ErrorsTabHelper.ts` (openErrorsTabViaSeeErrors),
`clipboardSpy.ts` (interceptClipboardWrite/getClipboardText)
- **Component changes**: added `data-testid` to
`ErrorDialogContent.vue`, `FindIssueButton.vue`, `MissingModelRow.vue`,
`MissingModelCard.vue`
  - **Selectors**: registered all new test IDs in `selectors.ts`
- **Test assets**: `missing_nodes_and_media.json` (compound errors),
`missing_models_with_nodes.json` (expand/locate)
- **Migrations**: error tests from `dialog.spec.ts` → dedicated files,
`errorOverlaySeeErrors.spec.ts` → `errorOverlay.spec.ts`,
`missingMedia.spec.ts` → `errorsTabMissingMedia.spec.ts`

## Review Focus

- OSS tests (`@oss` tag) verify Download/Copy URL buttons appear for
models with embedded URLs.
- The `missing_models.json` fixture must remain without nodes — adding
`CheckpointLoaderSimple` nodes causes directory mismatch in
`enrichWithEmbeddedMetadata` that prevents URL enrichment. A separate
`missing_models_with_nodes.json` fixture is used for expand/locate
tests.

## Cloud tests deferred

Missing model cloud environment tests (`@cloud` tag — hidden buttons,
import-unsupported notice) are deferred to a follow-up PR. The
`comfyPage` fixture cannot bypass the Firebase auth guard in cloud
builds, causing `window.app` initialization timeout. A separate infra PR
is needed to add cloud auth bypass to the fixture.

## Bug Discovery

During testing, a bug was found where the **Locate button for missing
nodes in subgraphs fails on initial workflow load**.
`collectMissingNodes` in `loadGraphData` captures execution IDs using
pre-`configure()` JSON node IDs, but `configure()` triggers subgraph
node ID deduplication (PR #8762, always-on since PR #9510) which remaps
colliding IDs. This will be addressed in a follow-up PR.

- Fixes #10847 (tracked, fix pending in separate PR)

## Testing

- 42 new/migrated E2E tests across 8 spec files
- All OSS tests pass locally and in CI

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10848-test-comprehensive-E2E-tests-for-error-dialog-overlay-and-errors-tab-3386d73d36508137a5e4cec8b12fa2fa)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:45:17 +09:00
Dante
c084089fc8 fix: update upload dialog status when async download completes (#10838)
## Summary

- Upload Model dialog stays stuck in "Processing" after async download
completes because the wizard never watches for task completion
- `useUploadModelWizard.ts` sets `uploadStatus = 'processing'` but has
no watcher linking it to `assetDownloadStore.lastCompletedDownload`
- Added a `watch` on `lastCompletedDownload` that transitions
`uploadStatus` to `'success'` when the tracked task finishes

- Fixes #10609

## Root Cause

`uploadModel()` (line 249) sets `uploadStatus = 'processing'` when the
async task starts, but control flow ends there. The `assetDownloadStore`
receives WebSocket completion events and updates
`lastCompletedDownload`, but the wizard never watches this reactive
state.

## Fix

Added a `watch` inside the async branch that monitors
`assetDownloadStore.lastCompletedDownload`. When the completed task ID
matches the upload's task ID, it transitions `uploadStatus` from
`'processing'` to `'success'` and refreshes model caches. The watcher
auto-disposes after firing.

## Red-Green Verification

| Commit | CI Status | Purpose |
|--------|-----------|---------|
| `test: add failing test for upload dialog stuck in processing state` |
🔴 Red | Proves the test catches the bug |
| `fix: update upload dialog status when async download completes` |
🟢 Green | Proves the fix resolves the bug |

## Test Plan

- [x] CI red on test-only commit
- [x] CI green on fix commit
- [x] Unit test: `updates uploadStatus to success when async download
completes`
- [ ] Manual: Import model via URL → verify dialog transitions from
Processing to Success (requires `--enable-assets`)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10838-fix-update-upload-dialog-status-when-async-download-completes-3376d73d365081b6a5b1d2be3804ce4b)
by [Unito](https://www.unito.io)
2026-04-08 13:09:02 +09:00
Alexander Brown
4cb83353cb test: stabilize flaky Playwright tests (#10817)
Stabilize flaky Playwright tests by improving test reliability.

This PR aims to identify and fix flaky e2e tests.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10817-test-stabilize-flaky-Playwright-tests-3366d73d365081ada40de73ce11af625)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-04-07 19:47:27 -07:00
Terry Jia
d73c4406ed test: add basic E2E tests for Load3D node (#10731)
## Summary
Add Playwright tests covering widget rendering, controls menu
interaction, background color change, and recording controls visibility.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10731-test-add-basic-E2E-tests-for-Load3D-node-3336d73d36508194bff9eb2a7c9356b9)
by [Unito](https://www.unito.io)
2026-04-07 22:41:12 -04:00
Dante
ccdde8697c test: add E2E regression tests for workflow tab save bug (#10815)
## Summary

- Adds E2E regression tests for the bug fixed in PR #10745 where closing
an inactive workflow tab would save the active workflow's content into
the closing tab's file
- Three test scenarios covering the full range of the bug surface:
  1. Closing an unmodified inactive tab preserves both workflows
2. Closing a modified inactive tab with "Save" preserves its own content
(not the active tab's)
3. Closing an unsaved inactive tab with "Save As" preserves its own
content

## Linked Issues

- Regression coverage for #10745 / Comfy-Org/ComfyUI#13230

## Test plan

- [ ] `pnpm exec playwright test
browser_tests/tests/topbar/workflowTabSave.spec.ts` passes against the
current main (with the fix)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10815-test-add-E2E-regression-tests-for-workflow-tab-save-bug-3366d73d365081eab409ed303620a959)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-08 10:40:12 +09:00
Christian Byrne
194baf7aee fix(ci): restore pnpm version input for core/* branch compatibility (#10952)
## Summary

Restores `version: 10` to the `pnpm/action-setup` step in
`release-version-bump.yaml`.

## Why

PR #10687 removed the explicit `version: 10` input, relying on
`packageManager` in `package.json` instead. However, this workflow
checks out a **target branch** (e.g. `core/1.42`, `core/1.41`) that
predates #10687 and lacks the `packageManager` field — causing the
action to fail with:

> Error: No pnpm version is specified.

Failed run:
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/24110739586

Adding `version: 10` back is safe — the action only errors when
`version` and `packageManager` **conflict**, and `pnpm@10.x` is
compatible.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10952-fix-ci-restore-pnpm-version-input-for-core-branch-compatibility-33c6d73d3650819282bbf6c194d0d2f1)
by [Unito](https://www.unito.io)
2026-04-07 17:45:42 -07:00
Comfy Org PR Bot
5770837e07 1.43.15 (#10951)
Patch version increment to 1.43.15

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10951-1-43-15-33c6d73d36508183b029ccd217a8403d)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2026-04-07 17:24:14 -07:00
Johnpaul Chiwetelu
4078f8be8f test: add E2E test for subgraph duplicate independent widget values (#10949)
## Summary
- Add E2E test verifying duplicated subgraphs maintain independent
widget values (convert CLIP node to subgraph, duplicate, set different
text in each, assert no bleed)
- Extract `clickMenuItemExact` and `openForVueNode` into `ContextMenu`
fixture for reuse across Vue node tests
- Refactor `contextMenu.spec.ts` to delegate to the new fixture methods

## Test plan
- [x] `pnpm typecheck:browser` passes
- [x] `pnpm lint` passes
- [x] New test passes locally (`pnpm test:browser:local --
browser_tests/tests/subgraph/subgraphDuplicateIndependentValues.spec.ts`)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10949-test-add-E2E-test-for-subgraph-duplicate-independent-widget-values-33b6d73d3650818191c1f78ef8db4455)
by [Unito](https://www.unito.io)
2026-04-08 00:20:23 +01:00
Christian Byrne
ff0453416a ci: pass CODECOV_TOKEN and add codecov.yml for PR comments (#10774)
## What

Follow-up to #10575. Pass `CODECOV_TOKEN` secret to codecov upload
action and add `codecov.yml` config so Codecov posts coverage diff
comments on PRs.

## Changes

- `ci-tests-unit.yaml`: add `token: ${{ secrets.CODECOV_TOKEN }}`
- `codecov.yml`: configure PR comment layout (header, diff, flags,
files)

## Manual Step Required

An admin needs to add the `CODECOV_TOKEN` secret to the repo:

1. Go to [codecov.io](https://app.codecov.io) → sign in → find
`Comfy-Org/ComfyUI_frontend` → Settings → General → copy the Repository
Upload Token
2. Go to [repo
secrets](https://github.com/Comfy-Org/ComfyUI_frontend/settings/secrets/actions)
→ New repository secret → name: `CODECOV_TOKEN`, value: the token

## Testing

Config-only change. Once the secret is added, the next PR will get a
Codecov coverage comment.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10774-ci-pass-CODECOV_TOKEN-and-add-codecov-yml-for-PR-comments-3346d73d36508169bac5e61eecc94063)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-07 15:52:34 -07:00
Benjamin Lu
fd9c67ade8 test: type asset helper update payload (#10751)
What changed

- Typed the `PUT /assets/:id` mock body in `AssetHelper` as
`AssetUpdatePayload` instead of treating it as an untyped record.

Why

- Keeps the mock aligned with the frontend update contract used by the
asset service.
- Narrows the helper without changing behavior, so follow-up typing work
can build on a smaller base.

Validation

- `pnpm typecheck`
- `pnpm typecheck:browser`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10751-test-type-asset-helper-update-payload-3336d73d365081fd8f1bc0c7c49c5ddb)
by [Unito](https://www.unito.io)
2026-04-07 14:26:56 -07:00
Christian Byrne
83f4e7060a test(infra): AssetHelper with builder pattern + deterministic fixtures (#10545)
## What

Adds `AssetHelper` — a builder-pattern helper for mocking asset-related
API endpoints in Playwright E2E tests, plus deterministic fixture data.

## Why

12+ asset-related API endpoints need mocking for asset browser tests
(PNL-02), cloud dialog testing (DLG-08), and other asset-dependent E2E
scenarios. Random mock data from existing `createMockAssets()` is
unsuitable for deterministic E2E assertions.

## What's included

### `AssetHelper.ts` (307 LOC)
- Fluent builder API: `assetHelper.withModels(3).withImages(5).mock()`
- Stateful mock store (Map) for upload→verify→delete flows
- Endpoint coverage: GET/POST/PUT/DELETE `/assets`, download progress
- `mockError()` for error state testing
- `clearMocks()` cleanup matching QueueHelper/FeatureFlagHelper pattern

### `assetFixtures.ts` (304 LOC)
- 11 stable named constants (checkpoints, loras, VAE, embedding, inputs,
outputs)
- Factory functions: `generateModels()`, `generateInputFiles()`,
`generateOutputAssets()`
- Fixed IDs/dates/sizes — no randomness, safe for screenshot comparisons

### ComfyPage integration
- Available as `comfyPage.assets` in all tests

## Testing
- TypeScript compiles clean
- Follows existing QueueHelper/FeatureFlagHelper conventions

## Unblocks
- PNL-02: Asset browser tests (@Jaewon Yoon)
- DLG-08: Assets modal / cloud dialog testing

Part of: Test Coverage Q2 Overhaul

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10545-test-infra-AssetHelper-with-builder-pattern-deterministic-fixtures-32f6d73d365081d3985ef079ff3dbede)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-07 14:23:36 -07:00
AustinMroz
31c789c242 Support svg outputs in assets panel (#10470)
A trivial fix to support svg outputs in the assets panel

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/2fca84f7-40f1-4966-b3dd-96facb8a4067"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/cad1a9fc-f511-43bc-8895-80d931baad1c"
/>|

Note: SVG do not display on cloud in node, or the assets panel because
they are being served with the incorrect content-type of `text/plain`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10470-Support-svg-outputs-in-assets-panel-32d6d73d3650815fbba1fe788297e715)
by [Unito](https://www.unito.io)
2026-04-07 13:53:37 -07:00
pythongosssss
1b05927ff4 test: App mode - setting widget value test (#10746)
## Summary

Adds a test for setting various types of widgets in app mode, then
validating the /prompt API is called with the expected values

## Changes

- **What**: 
- extract duplicated enableLinearMode
- add AppModeWidgetHelper for setting values

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10746-test-App-mode-setting-widget-value-test-3336d73d365081739598fb5280d0127e)
by [Unito](https://www.unito.io)
2026-04-07 13:53:01 -07:00
AustinMroz
97853aa8b0 Update autogrow to always show one optional beyond min (#10748)
It can be a little unclear that autogrow inputs will add more slots as
connections are made. To assist with this, the first optional input
beyond the minimum is always displayed. This ensures users always see a
slot with an optional indicator.

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/1dd1241e-c6a4-46a6-a0b9-08b568decd10"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/79650f9a-7cc6-4484-83a3-2b25e2f1af33"
/>|

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10748-Update-autogrow-to-always-show-one-optional-beyond-min-3336d73d36508184bcc6c79381a62436)
by [Unito](https://www.unito.io)
2026-04-07 12:53:32 -07:00
AustinMroz
ac922fe6aa Remove flex styling from svg (#10941)
This cleans up a minor warning message from the dev server.:
` WARN  Removing unexpected style on "svg": flex`

The icon is displayed in the header of some templates and is not
visually impacted by the change.
<img width="268" height="365" alt="image"
src="https://github.com/user-attachments/assets/a0c03e85-5275-4671-b903-8458d7ba3517"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10941-Remove-flex-styling-from-svg-33b6d73d36508113a7dce77c269fe4cb)
by [Unito](https://www.unito.io)
2026-04-07 12:31:21 -07:00
pythongosssss
6f8e58bfa5 test: App Mode - widget reordering (#10685)
## Summary

Adds tests that simulate the user sorting the inputs and ensures they
are persisted and visible in app mode

## Changes

- **What**: 
- rework input/output selection helpers to accept widget/node names
- extract additional shared helpers

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10685-test-App-Mode-widget-reordering-3316d73d3650813b8348da5f29fd01f8)
by [Unito](https://www.unito.io)
2026-04-07 11:56:43 -07:00
Arthur R Longbottom
6cd3b59d5f fix: don't override loadGraphData viewport on cache miss (#10810)
## Summary

Fix regression from #10247 where template workflows (e.g. LTX2.3) loaded
with a broken viewport.

## Problem

`restoreViewport()` called `fitView()` on every cache miss via rAF. This
raced with `loadGraphData`'s own viewport restore (`extra.ds` for saved
workflows, or its own `fitView()` for templates at line 1266 of app.ts).
The second `fitView()` overwrote the correct viewport, causing templates
with subgraphs to display incorrectly.

## Fix

On cache miss, check if any nodes are already visible in the current
viewport before calling `fitView()`. If `loadGraphData` already
positioned things correctly, we don't override it. Only intervene when
the viewport is genuinely empty (first visit to a subgraph with no prior
cached state AND no loadGraphData restore).

## Review Focus

Single-file change in `subgraphNavigationStore.ts`. The visibility check
mirrors the same pattern used in `app.ts:1272-1281` where loadGraphData
itself checks for visible nodes.

## E2E Regression Test

The existing Playwright tests in
`browser_tests/tests/subgraphViewport.spec.ts` (added in #10247) already
cover viewport restoration after subgraph navigation. The specific
regression (template load viewport race) is not practically testable in
E2E because:
1. Template loading requires the backend's template API which returns
different templates per environment
2. The race condition depends on exact timing between `loadGraphData`'s
viewport restore and the rAF-deferred `restoreViewport` — Playwright
cannot reliably reproduce frame-level timing races
3. The fix is a guard condition (skip fitView if nodes visible) that
makes the behavior idempotent regardless of timing

## Alternative to #10790

This can replace the full revert in #10790 — it preserves the viewport
persistence feature while fixing the template regression.

Fixes regression from #10247
2026-04-07 10:01:54 -07:00
pythongosssss
0b83926c3e fix: Ensure zero uuid root graphs get assigned a valid id (#10825)
## Summary

Fixes an issue where handlers would be leaked causing Vue node rendering
to be corrupted (Vue nodes would not render) due to the
00000000-0000-0000-0000-000000000000 ID being used on the root graph.

## Changes

- **What**: 
- LGraph clear() skips store cleanup for the zero uuid, leaking handlers
that cause the node manager/handlers to be overwritten during operations
such as undo due to stale onNodeAdded hooks
- Ensures that graph configuration assigns a valid ID for root graphs

## Screenshots (if applicable)

Before fix, after doing ctrl+z after entering subgraph
<img width="1011" height="574" alt="image"
src="https://github.com/user-attachments/assets/1ff4692b-b961-4777-bf2d-9b981e311f91"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10825-fix-Ensure-zero-uuid-root-graphs-get-assigned-a-valid-id-3366d73d3650817d8603c71ffb5e5742)
by [Unito](https://www.unito.io)

---------

Co-authored-by: jaeone94 <89377375+jaeone94@users.noreply.github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-04-07 08:50:13 -07:00
Dante
858946b0f5 fix: use getAssetFilename in ModelInfoPanel filename field (#10836)
# Summary

* Model Info sidebar panel displays `asset.name` (registry name) instead
of the actual filename from `user_metadata.filename`
* Other UI components (asset cards, widgets, missing model scan)
correctly use `getAssetFilename()` which prefers
`user_metadata.filename` over `asset.name`
* One-line template fix: `{{ asset.name }}` → `{{
getAssetFilename(asset) }}`
* Fixes #10598 

# Bug

`ModelInfoPanel.vue:35` used raw `asset.name` for the "File Name" field.
When `user_metadata.filename` differs from `asset.name` (e.g. registry
name vs actual path like `checkpoints/v1-5-pruned.safetensors`), users
see inconsistent filenames across the UI.

# AS-IS / TO-BE

<img width="800" height="600" alt="before-after-10598"
src="https://github.com/user-attachments/assets/15beb6c8-4bad-4ed2-9c85-6f8c7c0b6d3e"
/>


| | File Name field shows |
| :--- | :--- |
| **AS-IS** (bug) | `sdxl-lightning-4step` — raw `asset.name` (registry
display name) |
| **TO-BE** (fix) | `checkpoints/sdxl_lightning_4step.safetensors` —
`getAssetFilename(asset)` (actual file path) |

# Red-Green Verification

| Commit | CI Status | Purpose |
| :--- | :--- | :--- |
| `test: add failing test for ModelInfoPanel showing wrong filename` | 🔴
Red | Proves the test catches the bug |
| `fix: use getAssetFilename in ModelInfoPanel filename field` | 🟢 Green
| Proves the fix resolves the bug |

# Test Plan

- [x] CI red on test-only commit
- [x] CI green on fix commit
- [x] Unit test: `prefers user_metadata.filename over asset.name for
filename field`
- [ ] Manual: open Asset Browser → click a model → verify File Name in
Model Info panel matches the actual file path (requires
`--enable-assets`)
2026-04-07 21:26:47 +09:00
Christian Byrne
c5b183086d test: add unit tests for commandStore, extensionStore, widgetStore (STORE-04) (#10647)
## Summary

Adds 43 unit tests covering three priority Pinia stores that previously
had zero test coverage.

### commandStore (18 tests)
- `registerCommand` / `registerCommands` — single and batch
registration, duplicate warning
- `getCommand` — retrieval and undefined for missing
- `execute` — successful execution, metadata passing, error handler
delegation, missing command error
- `isRegistered` — presence check
- `loadExtensionCommands` — extension command registration with source,
skip when no commands
- `ComfyCommandImpl` — label/icon/tooltip resolution (string vs
function), menubarLabel defaulting

### extensionStore (16 tests)
- `registerExtension` — name validation, duplicate detection, disabled
extension warning
- `isExtensionEnabled` / `loadDisabledExtensionNames` — enable/disable
lifecycle
- Always-disabled hardcoded extensions (pysssss.Locking,
pysssss.SnapToGrid, pysssss.FaviconStatus, KJNodes.browserstatus)
- `enabledExtensions` — computed filter
- `isExtensionReadOnly` — hardcoded list check
- `inactiveDisabledExtensionNames` — ghost extension tracking
- Core extension capture and `hasThirdPartyExtensions` detection

### widgetStore (9 tests)
- Core widget availability via `ComfyWidgets`
- Custom widget registration and core/custom precedence
- `inputIsWidget` for both v1 array and v2 object InputSpec formats

## Part of
Test Coverage Q2 Overhaul — Phase 5 (Unit & Component Tests)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10647-test-add-unit-tests-for-commandStore-extensionStore-widgetStore-STORE-04-3316d73d365081e0b4f6ce913130e489)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Alexander Brown <448862+DrJKL@users.noreply.github.com>
2026-04-07 08:42:16 +00:00
915 changed files with 41157 additions and 19477 deletions

View File

@@ -1,9 +1,86 @@
{
"permissions": {
"allow": [
"Bash(pnpx vitest run --testPathPattern=\"draftCacheV2.property\")",
"Bash(pnpx vitest run \"draftCacheV2.property\")",
"Bash(node -e \"const fc = require\\(''fast-check''\\); console.log\\(Object.keys\\(fc\\).filter\\(k => k.includes\\(''string''\\)\\).join\\('', ''\\)\\)\")"
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of running tsc directly.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(vue-tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of running vue-tsc directly.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of running tsc via npx.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of running tsc via pnpx.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpm exec tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of `pnpm exec tsc`.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx vitest *)",
"command": "echo 'Use `pnpm test:unit` (or `pnpm test:unit -- <path>`) instead of npx vitest.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx vitest *)",
"command": "echo 'Use `pnpm test:unit` (or `pnpm test:unit -- <path>`) instead of pnpx vitest.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx eslint *)",
"command": "echo 'Use `pnpm lint` or `pnpm lint:fix` instead of npx eslint.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx eslint *)",
"command": "echo 'Use `pnpm lint` or `pnpm lint:fix` instead of pnpx eslint.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx prettier *)",
"command": "echo 'This project uses oxfmt, not prettier. Use `pnpm format` or `pnpm format:check`.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx prettier *)",
"command": "echo 'This project uses oxfmt, not prettier. Use `pnpm format` or `pnpm format:check`.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx oxlint *)",
"command": "echo 'Use `pnpm oxlint` instead of npx oxlint.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx stylelint *)",
"command": "echo 'Use `pnpm stylelint` instead of npx stylelint.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx knip *)",
"command": "echo 'Use `pnpm knip` instead of npx knip.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx knip *)",
"command": "echo 'Use `pnpm knip` instead of pnpx knip.' >&2 && exit 2"
}
]
}
]
}
}

View File

@@ -0,0 +1,246 @@
---
name: hardening-flaky-e2e-tests
description: 'Diagnoses and fixes flaky Playwright e2e tests by replacing race-prone patterns with retry-safe alternatives. Use when triaging CI flakes, hardening spec files, fixing timing races, or asked to stabilize browser tests. Triggers on: flaky, flake, harden, stabilize, race condition in e2e, intermittent failure.'
---
# Hardening Flaky E2E Tests
Fix flaky Playwright specs by identifying race-prone patterns and replacing them with retry-safe alternatives. This skill covers diagnosis, pattern matching, and mechanical transforms — not writing new tests (see `writing-playwright-tests` for that).
## Workflow
### 1. Gather CI Evidence
```bash
gh run list --workflow=ci-test.yaml --limit=5
gh run download <run-id> -n playwright-report
```
- Open `report.json` and search for `"status": "flaky"` entries.
- Collect file paths, test titles, and error messages.
- Do NOT trust green checks alone — flaky tests that passed on retry still need fixing.
- Use `error-context.md`, traces, and page snapshots before editing code.
- Pull the newest run after each push instead of assuming the flaky set is unchanged.
### 2. Classify the Flake
Read the failing assertion and match it against the pattern table. Most flakes fall into one of these categories:
| # | Pattern | Signature in Code | Fix |
| --- | ------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------- |
| 1 | **Snapshot-then-assert** | `expect(await evaluate()).toBe(x)` | `await expect.poll(() => evaluate()).toBe(x)` |
| 2 | **Immediate count** | `const n = await loc.count(); expect(n).toBe(3)` | `await expect(loc).toHaveCount(3)` |
| 3 | **nextFrame after menu click** | `clickMenuItem(x); nextFrame()` | `clickMenuItem(x); contextMenu.waitForHidden()` |
| 4 | **Tight poll timeout** | `expect.poll(..., { timeout: 250 })` | ≥2000 ms; prefer default 5000 ms |
| 5 | **Immediate evaluate after mutation** | `setSetting(k, v); expect(await evaluate()).toBe(x)` | `await expect.poll(() => evaluate()).toBe(x)` |
| 6 | **Screenshot without readiness** | `loadWorkflow(); nextFrame(); toHaveScreenshot()` | `waitForNodes()` or poll state first |
| 7 | **Non-deterministic node order** | `getNodeRefsByType('X')[0]` with >1 match | `getNodeRefById(id)` or guard `toHaveLength(1)` |
| 8 | **Fake readiness helper** | Helper clicks but doesn't assert state | Remove; poll the actual value |
| 9 | **Immediate graph state after drop** | `expect(await getLinkCount()).toBe(1)` | `await expect.poll(() => getLinkCount()).toBe(1)` |
| 10 | **Immediate boundingBox/layout read** | `const box = await loc.boundingBox(); expect(box!.width)` | `await expect.poll(() => loc.boundingBox().then(b => b?.width))` |
### 3. Apply the Transform
#### Rule: Choose the Smallest Correct Assertion
- **Locator state** → use built-in retrying assertions: `toBeVisible()`, `toHaveText()`, `toHaveCount()`, `toHaveClass()`
- **Single async value** → `expect.poll(() => asyncFn()).toBe(expected)`
- **Multiple assertions that must settle together** → `expect(async () => { ... }).toPass()`
- **Never** use `waitForTimeout()` to hide a race.
```typescript
// ✅ Single value — use expect.poll
await expect
.poll(() => comfyPage.page.evaluate(() => window.app!.graph.links.length))
.toBe(3)
// ✅ Locator count — use toHaveCount
await expect(comfyPage.page.locator('.dom-widget')).toHaveCount(2)
// ✅ Multiple conditions — use toPass
await expect(async () => {
expect(await node1.getValue()).toBe('foo')
expect(await node2.getValue()).toBe('bar')
}).toPass({ timeout: 5000 })
```
#### Rule: Wait for the Real Readiness Boundary
Visible is not always ready. Prefer user-facing assertions when possible; poll internal state only when there is no UI surface to assert on.
Common readiness boundaries:
| After this action... | Wait for... |
| -------------------------------------- | ------------------------------------------------------------ |
| Canvas interaction (drag, click node) | `await comfyPage.nextFrame()` |
| Menu item click | `await contextMenu.waitForHidden()` |
| Workflow load | `await comfyPage.workflow.loadWorkflow(...)` (built-in wait) |
| Settings write | Poll the setting value with `expect.poll()` |
| Node pin/bypass/collapse toggle | `await expect.poll(() => nodeRef.isPinned()).toBe(true)` |
| Graph mutation (add/remove node, link) | Poll link/node count |
| Clipboard write | Poll pasted value |
| Screenshot | Ensure nodes are rendered: `waitForNodes()` or poll state |
#### Rule: Expose Locators for Retrying Assertions
When a helper returns a count via `await loc.count()`, callers can't use `toHaveCount()`. Expose the underlying `Locator` as a getter so callers choose between:
```typescript
// Helper exposes locator
get domWidgets(): Locator {
return this.page.locator('.dom-widget')
}
// Caller uses retrying assertion
await expect(comfyPage.domWidgets).toHaveCount(2)
```
Replace count methods with locator getters so callers can use retrying assertions directly.
#### Rule: Fix Check-then-Act Races in Helpers
```typescript
// ❌ Race: count can change between check and waitFor
const count = await locator.count()
if (count > 0) {
await locator.waitFor({ state: 'hidden' })
}
// ✅ Direct: waitFor handles both cases
await locator.waitFor({ state: 'hidden' })
```
#### Rule: Remove force:true from Clicks
`force: true` bypasses actionability checks, hiding real animation/visibility races. Remove it and fix the underlying timing issue.
```typescript
// ❌ Hides the race
await closeButton.click({ force: true })
// ✅ Surfaces the real issue — fix with proper wait
await closeButton.click()
await dialog.waitForHidden()
```
#### Rule: Handle Non-deterministic Element Order
When `getNodeRefsByType` returns multiple nodes, the order is not guaranteed. Don't use index `[0]` blindly.
```typescript
// ❌ Assumes order
const node = (await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode'))[0]
// ✅ Find by ID or proximity
const nodes = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
let target = nodes[0]
for (const n of nodes) {
const pos = await n.getPosition()
if (Math.abs(pos.y - expectedY) < minDist) target = n
}
```
Or guard the assumption:
```typescript
const nodes = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
expect(nodes).toHaveLength(1)
const node = nodes[0]
```
#### Rule: Use toPass for Timing-sensitive Dismiss Guards
Some UI elements (e.g. LiteGraph's graphdialog) have built-in dismiss delays. Retry the entire dismiss action:
```typescript
// ✅ Retry click+assert together
await expect(async () => {
await comfyPage.canvas.click({ position: { x: 10, y: 10 } })
await expect(dialog).toBeHidden({ timeout: 500 })
}).toPass({ timeout: 5000 })
```
### 4. Keep Changes Narrow
- Shared helpers should drive setup to a stable boundary.
- Do not encode one-spec timing assumptions into generic helpers.
- If a race only matters to one spec, prefer a local wait in that spec.
- If a helper fails before the real test begins, remove or relax the brittle precondition and let downstream UI interaction prove readiness.
### 5. Verify Narrowly
```bash
# Targeted rerun with repetition
pnpm test:browser:local -- browser_tests/tests/myFile.spec.ts --repeat-each 10
# Single test by line number (avoids grep quoting issues on Windows)
pnpm test:browser:local -- browser_tests/tests/myFile.spec.ts:42
```
- Use `--repeat-each 10` for targeted flake verification (use 20 for single test cases).
- Verify with the smallest command that exercises the flaky path.
### 6. Watch CI E2E Runs
After pushing, use `gh` to monitor the E2E workflow:
```bash
# Find the run for the current branch
gh run list --workflow="CI: Tests E2E" --branch=$(git branch --show-current) --limit=1
# Watch it live (blocks until complete, streams logs)
gh run watch <run-id>
# One-liner: find and watch the latest E2E run for the current branch
gh run list --workflow="CI: Tests E2E" --branch=$(git branch --show-current) --limit=1 --json databaseId --jq ".[0].databaseId" | xargs gh run watch
```
On Windows (PowerShell):
```powershell
# One-liner equivalent
gh run watch (gh run list --workflow="CI: Tests E2E" --branch=$(git branch --show-current) --limit=1 --json databaseId --jq ".[0].databaseId")
```
After the run completes:
```bash
# Download the Playwright report artifact
gh run download <run-id> -n playwright-report
# View the run summary in browser
gh run view <run-id> --web
```
Also watch the unit test workflow in parallel if you changed helpers:
```bash
gh run list --workflow="CI: Tests Unit" --branch=$(git branch --show-current) --limit=1
```
### 7. Pre-merge Checklist
Before merging a flaky-test fix, confirm:
- [ ] The latest CI artifact was inspected directly
- [ ] The root cause is stated as a race or readiness mismatch
- [ ] The fix waits on the real readiness boundary
- [ ] The assertion primitive matches the job (poll vs toHaveCount vs toPass)
- [ ] The fix stays local unless a shared helper truly owns the race
- [ ] Local verification uses a targeted rerun
- [ ] No behavioral changes to the test — only timing/retry strategy updated
## Local Noise — Do Not Fix
These are local distractions, not CI root causes:
- Missing local input fixture files required by the test path
- Missing local models directory
- Teardown `EPERM` while restoring the local browser-test user data directory
- Local screenshot baseline differences on Windows
Rules:
- First confirm whether it blocks the exact flaky path under investigation.
- Do not commit temporary local assets used only for verification.
- Do not commit local screenshot baselines.

View File

@@ -1,361 +0,0 @@
---
name: ticket-intake
description: 'Parse ticket URL (Notion or GitHub), extract all data, initialize pipeline run. Use when starting work on a new ticket or when asked to pick up a ticket.'
---
# Ticket Intake
Parses a ticket URL from supported sources (Notion or GitHub), extracts all relevant information, and creates a ticket in the pipeline API.
> **🚨 CRITICAL REQUIREMENT**: This skill MUST register the ticket in the Pipeline API and update the source (Notion/GitHub). If these steps are skipped, the entire pipeline breaks. See [Mandatory API Calls](#mandatory-api-calls-execute-all-three) below.
## Supported Sources
| Source | URL Pattern | Provider File |
| ------ | --------------------------------------------------- | --------------------- |
| Notion | `https://notion.so/...` `https://www.notion.so/...` | `providers/notion.md` |
| GitHub | `https://github.com/{owner}/{repo}/issues/{n}` | `providers/github.md` |
## Quick Start
When given a ticket URL:
1. **Detect source type** from URL pattern
2. **Load provider-specific logic** from `providers/` directory
3. Fetch ticket content via appropriate API
4. Extract and normalize properties to common schema
5. **Register ticket in pipeline API** ← MANDATORY
6. **Update source** (Notion status / GitHub comment) ← MANDATORY
7. **Run verification script** to confirm API registration
8. Output summary and handoff to `research-orchestrator`
## Configuration
Uses the **production API** by default. No configuration needed for read operations.
**Defaults (no setup required):**
- API URL: `https://api-gateway-856475788601.us-central1.run.app`
- Read-only endpoints at `/public/*` require no authentication
**For write operations** (transitions, creating tickets), set:
```bash
export PIPELINE_API_KEY="..." # Get from GCP Secret Manager or ask admin
```
**Optional (for local working artifacts):**
```bash
PIPELINE_DIR="${PIPELINE_DIR:-$HOME/repos/ticket-to-pr-pipeline}"
```
## Mandatory API Calls (Execute ALL Three)
**⚠️ These three API calls are the ENTIRE POINT of this skill. Without them, the ticket is invisible to the pipeline, downstream skills will fail, and Notion status won't update.**
**You MUST make these HTTP requests.** Use `curl` from bash — do not just read this as documentation.
### Call 1: Create Ticket
```bash
API_URL="${PIPELINE_API_URL:-https://api-gateway-856475788601.us-central1.run.app}"
API_KEY="${PIPELINE_API_KEY}"
curl -s -X POST "${API_URL}/v1/tickets" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-H "X-Agent-ID: ${AGENT_ID:-amp-agent}" \
-d '{
"notion_page_id": "NOTION_PAGE_UUID_HERE",
"title": "TICKET_TITLE_HERE",
"source": "notion",
"metadata": {
"description": "DESCRIPTION_HERE",
"priority": "High",
"labels": [],
"acceptanceCriteria": []
}
}'
```
Save the returned `id` — you need it for the next two calls.
### Call 2: Transition to RESEARCH
```bash
TICKET_ID="id-from-step-1"
curl -s -X POST "${API_URL}/v1/tickets/${TICKET_ID}/transition" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-H "X-Agent-ID: ${AGENT_ID:-amp-agent}" \
-d '{
"to_state": "RESEARCH",
"reason": "Intake complete, starting research"
}'
```
### Call 3: Queue Source Update
```bash
curl -s -X POST "${API_URL}/v1/sync/queue" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-H "X-Agent-ID: ${AGENT_ID:-amp-agent}" \
-d '{
"ticket_id": "TICKET_ID_HERE",
"action": "update_status",
"payload": { "status": "In Progress" },
"priority": "normal"
}'
```
> **Note:** The action MUST be `"update_status"` (not `"UPDATE_NOTION_STATUS"`). Valid actions: `update_status`, `update_pr_url`, `mark_done`.
### TypeScript Equivalent (if using pipeline client)
```typescript
import { PipelineClient } from '@pipeline/client'
const client = new PipelineClient({
apiUrl:
process.env.PIPELINE_API_URL ||
'https://api-gateway-856475788601.us-central1.run.app',
agentId: process.env.AGENT_ID!
})
const ticket = await client.createTicket({
notion_page_id: pageId,
title: ticketTitle,
source: 'notion',
metadata: { description, priority, labels, acceptanceCriteria }
})
await client.transitionState(
ticket.id,
'RESEARCH',
'Intake complete, starting research'
)
await client.queueSync(ticket.id, 'update_status', { status: 'In Progress' })
```
## Workflow
### Step 1: Detect Source Type
Parse the URL to determine source:
```javascript
if (url.includes('notion.so')) {
source = 'notion'
// Load providers/notion.md
} else if (url.match(/github\.com\/[^\/]+\/[^\/]+\/issues\/\d+/)) {
source = 'github'
// Load providers/github.md
} else {
// Error: Unsupported URL format
}
```
### Step 2: Load Provider and Fetch Data
Read the appropriate provider file for source-specific instructions:
- **Notion**: `providers/notion.md` - Uses Notion MCP, handles Slack links
- **GitHub**: `providers/github.md` - Uses `gh` CLI, handles Dosu comments
Follow the provider's instructions for:
- Fetching content
- Extracting properties
- **Updating the source** (Notion status → "In Progress", Assignee → pipeline owner)
### Step 3: Normalize to Common Schema
All providers must extract normalized ticket data following `schema.md`:
```json
{
"id": "abc12345",
"url": "https://...",
"source": "notion | github",
"title": "Ticket title",
"description": "Full description",
"status": "Not Started",
"assignee": "username",
"priority": "High",
"area": "UI",
"labels": ["bug", "frontend"],
"acceptanceCriteria": ["Criterion 1", "Criterion 2"],
"fetchedAt": "2024-01-15T10:30:00Z"
}
```
### Step 4: Register Ticket in Pipeline API (MANDATORY — DO NOT SKIP)
**Execute all three API calls from [Mandatory API Calls](#mandatory-api-calls-execute-all-three) above.**
This is not optional. This is not documentation. You MUST make these HTTP requests right now.
1. `createTicket()` → save the returned ticket ID
2. `transitionState(id, 'RESEARCH')` → confirm state changed
3. `queueSync(id, 'update_status', { status: 'In Progress' })` → confirm queued
**If any call fails**, retry once. If it still fails, report the error prominently — do NOT silently continue.
### Step 5: Run Verification Script
After making the API calls, run the verification script to confirm everything worked:
```bash
bash scripts/verify-intake.sh TICKET_ID_OR_NOTION_PAGE_ID
```
**If the script is not available locally**, verify manually via the public API:
```bash
curl -s "${API_URL}/public/tickets/${TICKET_ID}" | jq '{id, state, title, notion_page_id}'
```
Expected output:
```json
{
"id": "...",
"state": "RESEARCH",
"title": "...",
"notion_page_id": "..."
}
```
**If `state` is not `RESEARCH`, go back to Step 4 and complete the missing calls.**
### Step 6: Output Summary and Handoff
Print a clear summary:
```markdown
## Ticket Intake Complete
**Source:** Notion | GitHub
**Title:** [Ticket title]
**ID:** abc12345
**Status:** In Progress (queued)
**Priority:** High
**Area:** UI
### Description
[Brief description or first 200 chars]
### Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2
### Links
- **Ticket:** [Original URL]
- **Slack:** [Slack thread content fetched via slackdump] (Notion only)
### Pipeline
- **API Ticket ID:** abc12345
- **State:** RESEARCH
- **Verified:** ✅ (via verify-intake.sh or public API)
```
**After printing the summary, immediately handoff** to continue the pipeline. Use the `handoff` tool with all necessary context (ticket ID, source, title, description, slack context if any):
> **Handoff goal:** "Continue pipeline for ticket {ID} ({title}). Ticket is in RESEARCH state. Load skill: `research-orchestrator` to begin research phase. Ticket data: source={source}, notion_page_id={pageId}, priority={priority}. {slack context summary if available}"
**Do NOT wait for human approval to proceed.** The intake phase is complete — handoff immediately.
## Error Handling
### Unsupported URL
```
❌ Unsupported ticket URL format.
Supported formats:
- Notion: https://notion.so/... or https://www.notion.so/...
- GitHub: https://github.com/{owner}/{repo}/issues/{number}
Received: [provided URL]
```
### Provider-Specific Errors
See individual provider files for source-specific error handling:
- `providers/notion.md` - Authentication, page not found
- `providers/github.md` - Auth, rate limits, issue not found
### Missing Properties
Continue with available data and note what's missing:
```
⚠️ Some properties unavailable:
- Priority: not found (using default: Medium)
- Area: not found
Proceeding with available data...
```
### API Call Failures
```
❌ Pipeline API call failed: {method} {endpoint}
Status: {status}
Error: {message}
Retrying once...
❌ Retry also failed. INTAKE IS INCOMPLETE.
The ticket was NOT registered in the pipeline.
Downstream skills will not work until this is fixed.
```
## Notes
- This skill focuses ONLY on intake — it does not do research
- Slack thread content is fetched automatically via the `slackdump` skill — no manual copy-paste needed
- ALL API calls (createTicket, transitionState, queueSync) are MANDATORY — never skip them
- The `queueSync` action must be `"update_status"`, NOT `"UPDATE_NOTION_STATUS"`
- Pipeline state is tracked via the API, not local files
- Working artifacts (research-report.md, plan.md) can be saved locally to `$PIPELINE_DIR/runs/{ticket-id}/`
- The `source` field in the ticket determines which research strategies to use
## API Client Reference
### Available Methods
| Method | Description |
| ----------------------------------------------------------- | ------------------------------------------------------------------- |
| `createTicket({ notion_page_id, title, source, metadata })` | Create a new ticket in the API |
| `getTicket(id)` | Retrieve a ticket by ID |
| `findByNotionId(notionPageId)` | Look up a ticket by its Notion page ID |
| `listTickets({ state, agent_id, limit, offset })` | List tickets with optional filters |
| `transitionState(id, state, reason)` | Move ticket to a new state (e.g., `'RESEARCH'`) |
| `setPRCreated(id, prUrl)` | Mark ticket as having a PR created |
| `queueSync(id, action, payload)` | Queue a sync action (`update_status`, `update_pr_url`, `mark_done`) |
| `registerBranch(id, branch, repo)` | Register working branch for automatic PR detection |
### Error Handling
```typescript
import { PipelineClient, PipelineAPIError } from '@pipeline/client';
try {
await client.createTicket({ ... });
} catch (error) {
if (error instanceof PipelineAPIError) {
console.error(`API Error ${error.status}: ${error.message}`);
}
throw error;
}
```

View File

@@ -1,194 +0,0 @@
# GitHub Provider - Ticket Intake
Provider-specific logic for ingesting tickets from GitHub Issues.
## URL Pattern
```
https://github.com/{owner}/{repo}/issues/{number}
https://www.github.com/{owner}/{repo}/issues/{number}
```
Extract: `owner`, `repo`, `issue_number` from URL.
## Prerequisites
- `gh` CLI authenticated (`gh auth status`)
- Access to the repository
## Fetch Issue Content
Use `gh` CLI to fetch issue details:
```bash
# Get issue details in JSON
gh issue view {number} --repo {owner}/{repo} --json title,body,state,labels,assignees,milestone,author,createdAt,comments,linkedPRs
# Get comments separately if needed
gh issue view {number} --repo {owner}/{repo} --comments
```
## Extract Ticket Data
Map GitHub issue fields to normalized ticket data (stored via API):
| GitHub Field | ticket.json Field | Notes |
| ------------ | ----------------- | -------------------------- |
| title | title | Direct mapping |
| body | description | Issue body/description |
| state | status | Map: open → "Not Started" |
| labels | labels | Array of label names |
| assignees | assignee | First assignee login |
| author | author | Issue author login |
| milestone | milestone | Milestone title if present |
| comments | comments | Array of comment objects |
| linkedPRs | linkedPRs | PRs linked to this issue |
### Priority Mapping
Infer priority from labels:
- `priority:critical`, `P0` → "Critical"
- `priority:high`, `P1` → "High"
- `priority:medium`, `P2` → "Medium"
- `priority:low`, `P3` → "Low"
- No priority label → "Medium" (default)
### Area Mapping
Infer area from labels:
- `area:ui`, `frontend`, `component:*` → "UI"
- `area:api`, `backend` → "API"
- `area:docs`, `documentation` → "Docs"
- `bug`, `fix` → "Bug"
- `enhancement`, `feature` → "Feature"
## Update Source
**For GitHub issues, update is optional but recommended.**
Add a comment to indicate work has started:
```bash
gh issue comment {number} --repo {owner}/{repo} --body "🤖 Pipeline started processing this issue."
```
Optionally assign to self:
```bash
gh issue edit {number} --repo {owner}/{repo} --add-assignee @me
```
Log any updates via the Pipeline API:
```typescript
await client.updateTicket(ticketId, {
metadata: {
...ticket.metadata,
githubWrites: [
...(ticket.metadata?.githubWrites || []),
{
action: 'comment',
issueNumber: 123,
at: new Date().toISOString(),
skill: 'ticket-intake',
success: true
}
]
}
})
```
## GitHub-Specific Ticket Fields
Store via API using `client.createTicket()`:
```json
{
"source": "github",
"githubOwner": "Comfy-Org",
"githubRepo": "ComfyUI_frontend",
"githubIssueNumber": 123,
"githubIssueUrl": "https://github.com/Comfy-Org/ComfyUI_frontend/issues/123",
"labels": ["bug", "area:ui", "priority:high"],
"linkedPRs": [456, 789],
"dosuComment": "..." // Extracted Dosu bot analysis if present
}
```
## Dosu Bot Detection
Many repositories use Dosu bot for automated issue analysis. Check comments for Dosu:
```bash
gh issue view {number} --repo {owner}/{repo} --comments | grep -A 100 "dosu"
```
Look for comments from:
- `dosu[bot]`
- `dosu-bot`
Extract Dosu analysis which typically includes:
- Root cause analysis
- Suggested files to modify
- Related issues/PRs
- Potential solutions
Store in ticket data via API:
```json
{
"dosuComment": {
"found": true,
"analysis": "...",
"suggestedFiles": ["src/file1.ts", "src/file2.ts"],
"relatedIssues": [100, 101]
}
}
```
## Extract Linked Issues/PRs
Parse issue body and comments for references:
- `#123` → Issue or PR reference
- `fixes #123`, `closes #123` → Linked issue
- `https://github.com/.../issues/123` → Full URL reference
Store in ticket data via API for research phase:
```json
{
"referencedIssues": [100, 101, 102],
"referencedPRs": [200, 201]
}
```
## Error Handling
### Authentication Error
```
⚠️ GitHub CLI not authenticated.
Run: gh auth login
```
### Issue Not Found
```
❌ GitHub issue not found or inaccessible.
- Check the URL is correct
- Ensure you have access to this repository
- Run: gh auth status
```
### Rate Limiting
```
⚠️ GitHub API rate limited.
Wait a few minutes and try again.
Check status: gh api rate_limit
```

View File

@@ -1,202 +0,0 @@
# Notion Provider - Ticket Intake
Provider-specific logic for ingesting tickets from Notion.
## URL Pattern
```
https://www.notion.so/workspace/Page-Title-abc123def456...
https://notion.so/Page-Title-abc123def456...
https://www.notion.so/abc123def456...
```
Page ID is the 32-character hex string (with or without hyphens).
## Prerequisites
- Notion MCP connected and authenticated
- If not setup: `claude mcp add --transport http notion https://mcp.notion.com/mcp`
- Authenticate via `/mcp` command if prompted
## Fetch Ticket Content
Use `Notion:notion-fetch` with the page URL or ID:
```
Fetch the full page content including all properties
```
## Extract Ticket Data
Extract these properties (names may vary):
| Property | Expected Name | Type |
| ------------- | ------------------------- | ------------ |
| Title | Name / Title | Title |
| Status | Status | Select |
| Assignee | Assignee / Assigned To | Person |
| Description | - | Page content |
| Slack Link | Slack Link / Slack Thread | URL |
| GitHub PR | GitHub PR / PR Link | URL |
| Priority | Priority | Select |
| Area | Area / Category | Select |
| Related Tasks | Related Tasks | Relation |
**If properties are missing**: Note what's unavailable and continue with available data.
## Update Source (REQUIRED)
**⚠️ DO NOT SKIP THIS STEP. This is a required action, not optional.**
**⚠️ Notion Write Safety rules apply (see `$PIPELINE_DIR/docs/notion-write-safety.md` for full reference):**
- **Whitelist**: Only `Status`, `GitHub PR`, and `Assignee` fields may be written
- **Valid transitions**: Not Started → In Progress, In Progress → In Review, In Review → Done
- **Logging**: Every write attempt MUST be logged with timestamp, field, value, previous value, skill name, and success status
Use `Notion:notion-update-page` to update the ticket:
1. **Status**: Set to "In Progress" (only valid from "Not Started")
2. **Assignee**: Assign to pipeline owner (Notion ID: `175d872b-594c-81d4-ba5a-0002911c5966`)
```json
{
"page_id": "{page_id_from_ticket}",
"command": "update_properties",
"properties": {
"Status": "In Progress",
"Assignee": "175d872b-594c-81d4-ba5a-0002911c5966"
}
}
```
**After the update succeeds**, log the write via the Pipeline API:
```typescript
await client.updateTicket(ticketId, {
metadata: {
...ticket.metadata,
notionWrites: [
...(ticket.metadata?.notionWrites || []),
{
field: 'Status',
value: 'In Progress',
previousValue: 'Not Started',
at: new Date().toISOString(),
skill: 'ticket-intake',
success: true
}
]
}
})
```
If update fails, log with `success: false` and continue.
## Notion-Specific Ticket Fields
Store via API using `client.createTicket()`:
```json
{
"source": "notion",
"notionPageId": "abc123def456...",
"slackLink": "https://slack.com/...",
"relatedTasks": ["page-id-1", "page-id-2"]
}
```
## Slack Thread Handling
If a Slack link exists, use the `slackdump` skill to fetch the thread content programmatically.
### Slack URL Conversion
Notion stores Slack links in `slackMessage://` format:
```
slackMessage://comfy-organization.slack.com/CHANNEL_ID/THREAD_TS/MESSAGE_TS
```
Convert to browser-clickable format:
```
https://comfy-organization.slack.com/archives/CHANNEL_ID/pMESSAGE_TS_NO_DOT
```
**Example:**
- Input: `slackMessage://comfy-organization.slack.com/C075ANWQ8KS/1766022478.450909/1764772881.854829`
- Output: `https://comfy-organization.slack.com/archives/C075ANWQ8KS/p1764772881854829`
(Remove the dot from the last timestamp and prefix with `p`)
### Fetching Thread Content
Load the `slackdump` skill and use the **export-thread** workflow:
```bash
# Export thread by URL
slackdump dump "https://comfy-organization.slack.com/archives/CHANNEL_ID/pMESSAGE_TS"
# Or by colon notation (channel_id:thread_ts)
slackdump dump CHANNEL_ID:THREAD_TS
```
Save the thread content to `$RUN_DIR/slack-context.md` and include it in the ticket metadata.
> **No manual action required.** The slackdump CLI handles authentication via stored credentials at `~/.cache/slackdump/comfy-organization.bin`.
## Database Reference: Comfy Tasks
The "Comfy Tasks" database has these properties (verify via `notion-search`):
- **Status values**: Not Started, In Progress, In Review, Done
- **Team assignment**: "Frontend Team" for unassigned tickets
- **Filtering note**: Team filtering in Notion may have quirks - handle gracefully
### Pipeline Owner Details
When assigning tickets, use these identifiers:
| Platform | Identifier |
| --------------- | -------------------------------------- |
| Notion User ID | `175d872b-594c-81d4-ba5a-0002911c5966` |
| Notion Name | Christian Byrne |
| Notion Email | cbyrne@comfy.org |
| Slack User ID | U087MJCDHHC |
| GitHub Username | christian-byrne |
**To update Assignee**, use the Notion User ID (not name):
```
properties: {"Assignee": "175d872b-594c-81d4-ba5a-0002911c5966"}
```
### Finding Active Tickets
To list your active tickets:
```
Use Notion:notion-search for "Comfy Tasks"
Filter by Assignee = current user OR Team = "Frontend Team"
```
## Error Handling
### Authentication Error
```
⚠️ Notion authentication required.
Run: claude mcp add --transport http notion https://mcp.notion.com/mcp
Then authenticate via /mcp command.
```
### Page Not Found
```
❌ Notion page not found or inaccessible.
- Check the URL is correct
- Ensure you have access to this page
- Try re-authenticating via /mcp
```

View File

@@ -1,81 +0,0 @@
# Ticket Schema
Common schema for normalized ticket data across all sources. This data is stored and retrieved via the Pipeline API, not local files.
## Ticket Data Schema
```json
{
// Required fields (all sources)
"id": "string", // Unique identifier (short form)
"url": "string", // Original URL
"source": "notion | github", // Source type
"title": "string", // Ticket title
"description": "string", // Full description/body
"fetchedAt": "ISO8601", // When ticket was fetched
// Common optional fields
"status": "string", // Current status
"assignee": "string", // Assigned user
"priority": "string", // Priority level
"area": "string", // Category/area
"labels": ["string"], // Tags/labels
"acceptanceCriteria": ["string"] // List of AC items
// Source-specific fields (see providers)
// Notion: notionPageId, slackLink, relatedTasks, notionWrites
// GitHub: githubOwner, githubRepo, githubIssueNumber, linkedPRs, dosuComment, referencedIssues
}
```
## Ticket State Schema (via API)
State is managed via the Pipeline API using `client.transitionState()`:
```json
{
"ticketId": "string",
"state": "intake | research | planning | implementation | pr_created | done | failed",
"stateChangedAt": "ISO8601",
// Timestamps tracked by API
"createdAt": "ISO8601",
"updatedAt": "ISO8601"
}
```
## Priority Normalization
All sources should normalize to these values:
| Normalized | Description |
| ---------- | ------------------------- |
| Critical | Production down, security |
| High | Blocking work, urgent |
| Medium | Normal priority (default) |
| Low | Nice to have, backlog |
## Status Normalization
Pipeline tracks these statuses internally:
| Status | Description |
| -------------- | ---------------------------- |
| research | Gathering context |
| planning | Creating implementation plan |
| implementation | Writing code |
| review | Code review in progress |
| qa | Quality assurance |
| done | PR merged or completed |
## ID Generation
IDs are generated by the API when creating tickets. For reference:
- **Notion**: First 8 characters of page ID
- **GitHub**: `gh-{owner}-{repo}-{issue_number}` (sanitized)
Examples:
- Notion: `abc12345`
- GitHub: `gh-comfy-org-frontend-123`

12
.editorconfig Normal file
View File

@@ -0,0 +1,12 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

View File

@@ -0,0 +1,31 @@
name: 'Lint and format verify'
description: >
Runs the lint/format/knip verification suite plus a conditional
browser-tests typecheck. Shared by ci-lint-format.yaml (PR) and
ci-lint-format-queue.yaml (merge queue) so both paths run the exact
same checks. The caller is responsible for checkout and frontend setup
before invoking this action.
runs:
using: composite
steps:
- name: Detect browser_tests changes
id: changed-paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
filters: |
browser_tests:
- 'browser_tests/**'
- name: Verify lint and format
shell: bash
run: |
pnpm lint
pnpm stylelint
pnpm format:check
pnpm knip
- name: Typecheck browser tests
if: steps.changed-paths.outputs.browser_tests == 'true'
shell: bash
run: pnpm typecheck:browser

View File

@@ -0,0 +1,29 @@
# Description: Lint and format verification for GitHub merge queue runs.
# Paired with ci-lint-format.yaml — workflow name and job name must match
# so branch protection resolves a single required check in both the
# pull_request and merge_group contexts. This file runs verify-only steps
# with a read-only token; auto-fix and PR comments live in the PR workflow.
name: 'CI: Lint Format'
on:
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint-and-format:
runs-on: ubuntu-latest
steps:
- name: Checkout merge group ref
uses: actions/checkout@v6
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Verify lint and format
uses: ./.github/actions/lint-format-verify

View File

@@ -1,4 +1,7 @@
# Description: Linting and code formatting validation for pull requests
# Description: Linting and code formatting validation for pull requests.
# Paired with ci-lint-format-queue.yaml - workflow name and job name must
# match so branch protection resolves a single required check in both the
# pull_request and merge_group contexts.
name: 'CI: Lint Format'
on:
@@ -26,14 +29,6 @@ jobs:
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Detect browser_tests changes
id: changed-paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
filters: |
browser_tests:
- 'browser_tests/**'
- name: Run ESLint with auto-fix
run: pnpm lint:fix
@@ -77,16 +72,8 @@ jobs:
echo "See CONTRIBUTING.md for more details."
exit 1
- name: Final validation
run: |
pnpm lint
pnpm stylelint
pnpm format:check
pnpm knip
- name: Typecheck browser tests
if: steps.changed-paths.outputs.browser_tests == 'true'
run: pnpm typecheck:browser
- name: Verify lint and format
uses: ./.github/actions/lint-format-verify
- name: Comment on PR about auto-fix
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository

View File

@@ -8,6 +8,7 @@ on:
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
paths-ignore: ['**/*.md']
merge_group:
workflow_dispatch:
concurrency:

View File

@@ -8,6 +8,7 @@ on:
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
paths-ignore: ['**/*.md']
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -31,4 +32,5 @@ jobs:
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
with:
files: coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false

View File

@@ -1,14 +1,23 @@
# Automated bi-weekly workflow to bump ComfyUI frontend RC releases
name: 'Release: Bi-weekly ComfyUI'
# Release workflow for ComfyUI frontend: version bump → PyPI publish → ComfyUI PR.
# Runs on a bi-weekly schedule for minor releases, or manually for patch/hotfix releases.
name: 'Release: ComfyUI'
on:
# Schedule for Monday at 12:00 PM PST (20:00 UTC)
# Bi-weekly schedule: Monday at 20:00 UTC
schedule:
- cron: '0 20 * * 1'
# Allow manual triggering (bypasses bi-weekly check)
# Manual trigger for both on-demand minor and patch/hotfix releases
workflow_dispatch:
inputs:
release_type:
description: 'minor = next minor version (bi-weekly cadence), patch = hotfix for current production version'
required: true
default: 'minor'
type: choice
options:
- minor
- patch
comfyui_fork:
description: 'ComfyUI fork to use for PR (e.g., Comfy-Org/ComfyUI)'
required: false
@@ -41,10 +50,11 @@ jobs:
- name: Summary
run: |
echo "## Bi-weekly Check" >> $GITHUB_STEP_SUMMARY
echo "## Release Check" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Is release week: ${{ steps.check.outputs.is_release_week }}" >> $GITHUB_STEP_SUMMARY
echo "- Manual trigger: ${{ github.event_name == 'workflow_dispatch' }}" >> $GITHUB_STEP_SUMMARY
echo "- Release type: ${{ inputs.release_type || 'minor (scheduled)' }}" >> $GITHUB_STEP_SUMMARY
resolve-version:
needs: check-release-week
@@ -76,6 +86,8 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
with:
package_json_file: frontend/package.json
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -89,6 +101,8 @@ jobs:
- name: Resolve release information
id: resolve
working-directory: frontend
env:
RELEASE_TYPE: ${{ inputs.release_type || 'minor' }}
run: |
set -euo pipefail

View File

@@ -142,6 +142,20 @@ jobs:
fi
echo "✅ Branch '$BRANCH' exists"
- name: Ensure packageManager field exists
run: |
if ! grep -q '"packageManager"' package.json; then
# Old branches (e.g. core/1.42) predate the packageManager field.
# Inject it so pnpm/action-setup can resolve the version.
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json','utf8'));
pkg.packageManager = 'pnpm@10.33.0';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
echo "Injected packageManager into package.json for legacy branch"
fi
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0

View File

@@ -64,6 +64,7 @@
]
}
],
"no-unsafe-optional-chaining": "error",
"no-self-assign": "allow",
"no-unused-expressions": "off",
"no-unused-private-class-members": "off",
@@ -83,6 +84,7 @@
"typescript/no-unsafe-declaration-merging": "off",
"typescript/no-unused-vars": "off",
"unicorn/no-empty-file": "off",
"vitest/require-mock-type-parameters": "off",
"unicorn/no-new-array": "off",
"unicorn/no-single-promise-in-promise-methods": "off",
"unicorn/no-useless-fallback-in-spread": "off",
@@ -104,8 +106,7 @@
"allowInterfaces": "always"
}
],
"vue/no-import-compiler-macros": "error",
"vue/no-dupe-keys": "error"
"vue/no-import-compiler-macros": "error"
},
"overrides": [
{
@@ -116,13 +117,60 @@
},
{
"files": ["browser_tests/**/*.ts"],
"jsPlugins": ["eslint-plugin-playwright"],
"rules": {
"typescript/no-explicit-any": "error",
"no-async-promise-executor": "error",
"no-control-regex": "error",
"no-useless-rename": "error",
"no-unused-private-class-members": "error",
"unicorn/no-empty-file": "error"
"unicorn/no-empty-file": "error",
"playwright/consistent-spacing-between-blocks": "error",
"playwright/expect-expect": [
"error",
{
"assertFunctionNames": [
"recordMeasurement",
"logMeasurement",
"builderSaveAs"
],
"assertFunctionPatterns": [
"^expect",
"^assert",
"^verify",
"^searchAndExpect",
"waitForOpen",
"waitForClosed",
"waitForRequest"
]
}
],
"playwright/max-nested-describe": "error",
"playwright/no-duplicate-hooks": "error",
"playwright/no-element-handle": "error",
"playwright/no-eval": "error",
"playwright/no-focused-test": "error",
"playwright/no-force-option": "error",
"playwright/no-networkidle": "error",
"playwright/no-page-pause": "error",
"playwright/no-skipped-test": "error",
"playwright/no-unsafe-references": "error",
"playwright/no-unused-locators": "error",
"playwright/no-useless-await": "error",
"playwright/no-useless-not": "error",
"playwright/no-wait-for-navigation": "error",
"playwright/no-wait-for-selector": "error",
"playwright/no-wait-for-timeout": "error",
"playwright/prefer-hooks-on-top": "error",
"playwright/prefer-locator": "error",
"playwright/prefer-to-have-count": "error",
"playwright/prefer-to-have-length": "error",
"playwright/prefer-web-first-assertions": "error",
"playwright/prefer-native-locators": "error",
"playwright/require-to-pass-timeout": "error",
"playwright/valid-expect": "error",
"playwright/valid-expect-in-promise": "error",
"playwright/valid-title": "error"
}
}
]

View File

@@ -179,6 +179,12 @@ This project uses **pnpm**. Always prefer scripts defined in `package.json` (e.g
24. Do not use function expressions if it's possible to use function declarations instead
25. Watch out for [Code Smells](https://wiki.c2.com/?CodeSmell) and refactor to avoid them
## Design Standards
Before implementing any user-facing feature, consult the [Comfy Design Standards](https://www.figma.com/design/QreIv5htUaSICNuO2VBHw0/Comfy-Design-Standards) Figma file. Use the Figma MCP to fetch it live — the file is the single source of truth and may be updated by designers at any time.
See `docs/guidance/design-standards.md` for Figma file keys, section node IDs, and component references.
## Testing Guidelines
See @docs/testing/\*.md for detailed patterns.
@@ -226,6 +232,7 @@ See @docs/testing/\*.md for detailed patterns.
- shadcn/vue: <https://www.shadcn-vue.com/>
- Reka UI: <https://reka-ui.com/>
- PrimeVue: <https://primevue.org>
- Comfy Design Standards: <https://www.figma.com/design/QreIv5htUaSICNuO2VBHw0/Comfy-Design-Standards>
- ComfyUI: <https://docs.comfy.org>
- Electron: <https://www.electronjs.org/docs/latest/>
- Wiki: <https://deepwiki.com/Comfy-Org/ComfyUI_frontend/1-overview>
@@ -311,6 +318,9 @@ When referencing Comfy-Org repos:
- Find existing `!important` classes that are interfering with the styling and propose corrections of those instead.
- NEVER use arbitrary percentage values like `w-[80%]` when a Tailwind fraction utility exists
- Use `w-4/5` instead of `w-[80%]`, `w-1/2` instead of `w-[50%]`, etc.
- NEVER use font-size classes (`text-xs`, `text-sm`, etc.) to size `icon-[...]` (iconify) icons
- Iconify icons size via `width`/`height: 1.2em`, so font-size produces unpredictable results
- Use `size-*` classes for explicit sizing, or set font-size on the **parent** container and let `1.2em` scale naturally
## Agent-only rules

View File

@@ -62,6 +62,37 @@ python main.py --port 8188 --cpu
- Run `pnpm dev:electron` to start the dev server with electron API mocked
- Run `pnpm dev:cloud` to start the dev server against the cloud backend (instead of local ComfyUI server)
#### Testing with Cloud & Staging Environments
Some features — particularly **partner/API nodes** (e.g. BFL, OpenAI, Stability AI) — require a cloud backend for authentication and billing. Running these against a local ComfyUI instance will result in permission errors or logged-out states. There are two ways to connect to a cloud/staging backend:
**Option 1: Frontend — `pnpm dev:cloud`**
The simplest approach. This proxies all API requests to the test cloud environment:
```bash
pnpm dev:cloud
```
This sets `DEV_SERVER_COMFYUI_URL` to `https://testcloud.comfy.org/` automatically. You can also set this variable manually in your `.env` file to target a different environment:
```bash
# .env
DEV_SERVER_COMFYUI_URL=https://stagingcloud.comfy.org/
```
Any `*.comfy.org` URL automatically enables cloud mode, which includes the GCS media proxy needed for viewing generated images and videos. See [.env_example](.env_example) for all available cloud URLs.
**Option 2: Backend — `--comfy-api-base`**
Alternatively, launch the ComfyUI backend pointed at the staging API:
```bash
python main.py --comfy-api-base https://stagingapi.comfy.org --verbose
```
Then run `pnpm dev` as usual. This keeps the frontend in local mode but routes backend API calls through staging.
#### Access dev server on touch devices
Enable remote access to the dev server by setting `VITE_REMOTE_DEV` in `.env` to `true`.

View File

@@ -1,2 +1,3 @@
dist/
.astro/
test-results/

View File

@@ -10,11 +10,6 @@ export default defineConfig({
vite: {
plugins: [tailwindcss()]
},
build: {
assetsPrefix: process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: undefined
},
i18n: {
locales: ['en', 'zh-CN'],
defaultLocale: 'en',

View File

@@ -0,0 +1,163 @@
import { expect, test } from '@playwright/test'
test.describe('Download page @smoke', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/download')
})
test('has correct title', async ({ page }) => {
await expect(page).toHaveTitle('Download Comfy — Run AI Locally')
})
test('CloudBannerSection is visible with cloud link', async ({ page }) => {
const link = page.getByRole('link', { name: /TRY COMFY CLOUD/i })
await expect(link).toBeVisible()
await expect(link).toHaveAttribute('href', 'https://app.comfy.org')
})
test('HeroSection heading and subtitle are visible', async ({ page }) => {
await expect(
page.getByRole('heading', { name: /Run on your hardware/i, level: 1 })
).toBeVisible()
await expect(page.getByText(/The full ComfyUI engine/)).toBeVisible()
})
test('HeroSection has download and GitHub buttons', async ({ page }) => {
const downloadBtn = page
.getByRole('link', { name: /DOWNLOAD LOCAL/i })
.first()
await expect(downloadBtn).toBeVisible()
await expect(downloadBtn).toHaveAttribute('target', '_blank')
const githubBtn = page
.getByRole('link', { name: /INSTALL FROM GITHUB/i })
.first()
await expect(githubBtn).toBeVisible()
await expect(githubBtn).toHaveAttribute(
'href',
'https://github.com/Comfy-Org/ComfyUI'
)
})
test('ReasonSection heading and reasons are visible', async ({ page }) => {
await expect(
page.getByRole('heading', { name: /Why.*professionals.*choose/i })
).toBeVisible()
for (const title of [
'Unlimited',
'Any model',
'Your machine',
'Free. Open Source'
]) {
await expect(page.getByText(title).first()).toBeVisible()
}
})
test('EcoSystemSection heading is visible', async ({ page }) => {
await expect(page.getByText(/An ecosystem that moves faster/)).toBeVisible()
})
test('ProductCardsSection has 3 product cards', async ({ page }) => {
const section = page.locator('section', {
has: page.getByRole('heading', { name: /The AI creation/ })
})
const cards = section.locator('a[href]')
await expect(cards).toHaveCount(3)
})
test('ProductCardsSection links to cloud, api, enterprise', async ({
page
}) => {
const section = page.locator('section', {
has: page.getByRole('heading', { name: /The AI creation/ })
})
for (const href of ['/cloud', '/api', '/cloud/enterprise']) {
await expect(section.locator(`a[href="${href}"]`)).toBeVisible()
}
})
test('FAQSection heading is visible with 8 items', async ({ page }) => {
await expect(page.getByRole('heading', { name: /FAQ/i })).toBeVisible()
const faqButtons = page.locator('button[aria-controls^="faq-panel-"]')
await expect(faqButtons).toHaveCount(8)
})
})
test.describe('FAQ accordion @interaction', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/download')
})
test('all FAQs are expanded by default', async ({ page }) => {
await expect(
page.getByText(/A dedicated GPU is strongly recommended/i)
).toBeVisible()
await expect(page.getByText(/ComfyUI is lightweight/i)).toBeVisible()
})
test('clicking an expanded FAQ collapses it', async ({ page }) => {
const firstQuestion = page.getByRole('button', {
name: /Do I need a GPU/i
})
await firstQuestion.scrollIntoViewIfNeeded()
await firstQuestion.click()
await expect(
page.getByText(/A dedicated GPU is strongly recommended/i)
).toBeHidden()
})
test('clicking a collapsed FAQ expands it again', async ({ page }) => {
const firstQuestion = page.getByRole('button', {
name: /Do I need a GPU/i
})
await firstQuestion.scrollIntoViewIfNeeded()
await firstQuestion.click()
await expect(
page.getByText(/A dedicated GPU is strongly recommended/i)
).toBeHidden()
await firstQuestion.click()
await expect(
page.getByText(/A dedicated GPU is strongly recommended/i)
).toBeVisible()
})
})
test.describe('Download page mobile @mobile', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/download')
})
test('CloudBannerSection is visible', async ({ page }) => {
await expect(page.getByText(/Need more power/)).toBeVisible()
})
test('HeroSection heading is visible', async ({ page }) => {
await expect(
page.getByRole('heading', { name: /Run on your hardware/i, level: 1 })
).toBeVisible()
})
test('download buttons are stacked vertically', async ({ page }) => {
const downloadBtn = page
.getByRole('link', { name: /DOWNLOAD LOCAL/i })
.first()
const githubBtn = page
.getByRole('link', { name: /INSTALL FROM GITHUB/i })
.first()
await downloadBtn.scrollIntoViewIfNeeded()
const downloadBox = await downloadBtn.boundingBox()
const githubBox = await githubBtn.boundingBox()
expect(downloadBox, 'download button bounding box').not.toBeNull()
expect(githubBox, 'github button bounding box').not.toBeNull()
expect(githubBox!.y).toBeGreaterThan(downloadBox!.y)
})
})

View File

@@ -0,0 +1,130 @@
import { expect, test } from '@playwright/test'
test.describe('Homepage @smoke', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('has correct title', async ({ page }) => {
await expect(page).toHaveTitle('Comfy — Professional Control of Visual AI')
})
test('HeroSection heading is visible', async ({ page }) => {
await expect(
page.getByRole('heading', { name: /Professional Control/i, level: 1 })
).toBeVisible()
})
test('SocialProofBar logos are visible', async ({ page }) => {
await expect(
page.locator('img[src*="/icons/clients/"]').first()
).toBeVisible()
})
test('ProductShowcase section is visible', async ({ page }) => {
await expect(page.getByText('HOW', { exact: true }).first()).toBeVisible()
await expect(
page.getByText(/Connect models, processing steps, and outputs/)
).toBeVisible()
})
test('UseCaseSection is visible', async ({ page }) => {
await expect(
page.getByText('Industries that create with ComfyUI')
).toBeVisible()
})
test('GetStartedSection with heading is visible', async ({ page }) => {
await expect(
page.getByRole('heading', { name: 'Get started in minutes' })
).toBeVisible()
})
test('ProductCardsSection has 4 product cards', async ({ page }) => {
const section = page.locator('section', {
has: page.getByRole('heading', { name: /The AI creation/ })
})
const cards = section.locator('a[href]')
await expect(cards).toHaveCount(4)
})
test('CaseStudySpotlight section is visible', async ({ page }) => {
const section = page.locator('section', {
has: page.getByText('Customer Stories')
})
await expect(section).toBeVisible()
await expect(
section.getByRole('heading', { name: /See Comfy/i })
).toBeVisible()
})
test('BuildWhatSection is visible', async ({ page }) => {
// "DOESN'T EXIST" is the actual badge text rendered in the Build What section
await expect(page.getByText("DOESN'T EXIST")).toBeVisible()
})
})
test.describe('Product showcase accordion @interaction', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('first feature is active by default', async ({ page }) => {
await expect(
page.getByText(/Build powerful AI pipelines by connecting nodes/).first()
).toBeVisible()
})
test('clicking inactive feature expands it and collapses previous', async ({
page
}) => {
const secondFeature = page
.getByRole('button', { name: /App mode/i })
.first()
await secondFeature.scrollIntoViewIfNeeded()
await expect(async () => {
await secondFeature.click()
await expect(
page.getByText(/If you are new to ComfyUI/).first()
).toBeVisible({ timeout: 1000 })
}).toPass({ timeout: 10000 })
await expect(
page.getByText(/Build powerful AI pipelines by connecting nodes/).first()
).toBeHidden()
})
})
test.describe('Product cards links @smoke', () => {
test('cards have correct hrefs', async ({ page }) => {
await page.goto('/')
const section = page.locator('section', {
has: page.getByRole('heading', { name: /The AI creation/ })
})
for (const href of ['/download', '/cloud', '/api', '/cloud/enterprise']) {
await expect(section.locator(`a[href="${href}"]`)).toBeVisible()
}
})
})
test.describe('Get started section links @smoke', () => {
test('has download and cloud links', async ({ page }) => {
await page.goto('/')
const section = page.locator('section', {
has: page.getByRole('heading', { name: 'Get started in minutes' })
})
const downloadLink = section.getByRole('link', { name: 'Download Local' })
await expect(downloadLink).toBeVisible()
await expect(downloadLink).toHaveAttribute('href', '/download')
const cloudLink = section.getByRole('link', { name: 'Launch Cloud' })
await expect(cloudLink).toBeVisible()
await expect(cloudLink).toHaveAttribute('href', 'https://app.comfy.org')
})
})

View File

@@ -0,0 +1,161 @@
import { expect, test } from '@playwright/test'
test.describe('Desktop navigation @smoke', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('logo links to homepage', async ({ page }) => {
const logo = page.getByRole('link', { name: 'Comfy home' })
await expect(logo).toBeVisible()
await expect(logo).toHaveAttribute('href', '/')
})
test('has all top-level nav items', async ({ page }) => {
const nav = page.getByRole('navigation', { name: 'Main navigation' })
const desktopLinks = nav.getByTestId('desktop-nav-links')
for (const label of [
'PRODUCTS',
'PRICING',
'COMMUNITY',
'RESOURCES',
'COMPANY'
]) {
await expect(desktopLinks.getByText(label).first()).toBeVisible()
}
})
test('CTA buttons are visible', async ({ page }) => {
const nav = page.getByRole('navigation', { name: 'Main navigation' })
const desktopCTA = nav.getByTestId('desktop-nav-cta')
await expect(
desktopCTA.getByRole('link', { name: 'DOWNLOAD LOCAL' })
).toBeVisible()
await expect(
desktopCTA.getByRole('link', { name: 'LAUNCH CLOUD' })
).toBeVisible()
})
})
test.describe('Desktop dropdown @interaction', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('hovering PRODUCTS shows dropdown items', async ({ page }) => {
const nav = page.getByRole('navigation', { name: 'Main navigation' })
const desktopLinks = nav.getByTestId('desktop-nav-links')
const productsButton = desktopLinks.getByRole('button', {
name: /PRODUCTS/i
})
await productsButton.hover()
const dropdown = productsButton.locator('..').getByTestId('nav-dropdown')
for (const item of [
'Comfy Local',
'Comfy Cloud',
'Comfy API',
'Comfy Enterprise'
]) {
await expect(dropdown.getByText(item)).toBeVisible()
}
})
test('moving mouse away closes dropdown', async ({ page }) => {
const nav = page.getByRole('navigation', { name: 'Main navigation' })
const desktopLinks = nav.getByTestId('desktop-nav-links')
await desktopLinks.getByRole('button', { name: /PRODUCTS/i }).hover()
const comfyLocal = nav.getByRole('link', { name: 'Comfy Local' }).first()
await expect(comfyLocal).toBeVisible()
await page.locator('main').hover()
await expect(comfyLocal).toBeHidden()
})
test('Escape key closes dropdown', async ({ page }) => {
const nav = page.getByRole('navigation', { name: 'Main navigation' })
const desktopLinks = nav.getByTestId('desktop-nav-links')
await desktopLinks.getByRole('button', { name: /PRODUCTS/i }).hover()
const comfyLocal = nav.getByRole('link', { name: 'Comfy Local' }).first()
await expect(comfyLocal).toBeVisible()
await page.keyboard.press('Escape')
await expect(comfyLocal).toBeHidden()
})
})
test.describe('Mobile menu @mobile', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('hamburger button is visible', async ({ page }) => {
await expect(
page.getByRole('button', { name: 'Toggle menu' })
).toBeVisible()
})
test('clicking hamburger opens mobile menu with nav items', async ({
page
}) => {
await page.getByRole('button', { name: 'Toggle menu' }).click()
const menu = page.locator('#site-mobile-menu')
await expect(menu).toBeVisible()
for (const label of ['PRODUCTS', 'PRICING', 'COMMUNITY']) {
await expect(menu.getByText(label).first()).toBeVisible()
}
})
test('clicking section with subitems drills down and back works', async ({
page
}) => {
await page.getByRole('button', { name: 'Toggle menu' }).click()
const menu = page.locator('#site-mobile-menu')
await menu.getByText('PRODUCTS').first().click()
await expect(menu.getByText('Comfy Local')).toBeVisible()
await expect(menu.getByText('Comfy Cloud')).toBeVisible()
await menu.getByRole('button', { name: /BACK/i }).click()
await expect(menu.getByText('PRODUCTS').first()).toBeVisible()
})
test('CTA buttons visible in mobile menu', async ({ page }) => {
await page.getByRole('button', { name: 'Toggle menu' }).click()
const menu = page.locator('#site-mobile-menu')
await expect(
menu.getByRole('link', { name: 'DOWNLOAD LOCAL' })
).toBeVisible()
await expect(menu.getByRole('link', { name: 'LAUNCH CLOUD' })).toBeVisible()
})
})
test.describe('Footer @smoke', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('footer is visible with link sections', async ({ page }) => {
const footer = page.locator('footer')
await expect(footer).toBeVisible()
for (const heading of ['Products', 'Resources', 'Company']) {
await expect(
footer.getByRole('heading', { name: heading }).first()
).toBeVisible()
}
})
test('copyright text is visible', async ({ page }) => {
await expect(
page.locator('footer').getByText(/© \d{4} Comfy Org/)
).toBeVisible()
})
})

View File

@@ -0,0 +1,48 @@
import { expect, test } from '@playwright/test'
test.describe('Desktop layout @smoke', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('navigation links visible and hamburger hidden', async ({ page }) => {
const nav = page.getByRole('navigation', { name: 'Main navigation' })
const desktopLinks = nav.getByTestId('desktop-nav-links')
await expect(desktopLinks.getByText('PRODUCTS').first()).toBeVisible()
await expect(desktopLinks.getByText('PRICING').first()).toBeVisible()
await expect(page.getByRole('button', { name: 'Toggle menu' })).toBeHidden()
})
test('product cards in grid layout', async ({ page }) => {
const section = page.locator('section', {
has: page.getByRole('heading', { name: /The AI creation/ })
})
const cards = section.locator('a[href]')
await expect(cards).toHaveCount(4)
const firstBox = await cards.nth(0).boundingBox()
const secondBox = await cards.nth(1).boundingBox()
expect(firstBox, 'first card bounding box').not.toBeNull()
expect(secondBox, 'second card bounding box').not.toBeNull()
expect(firstBox!.y).toBeCloseTo(secondBox!.y, 0)
})
})
test.describe('Mobile layout @mobile', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('hamburger visible and desktop nav hidden', async ({ page }) => {
await expect(
page.getByRole('button', { name: 'Toggle menu' })
).toBeVisible()
})
test('SocialProofBar shows two marquee rows on mobile', async ({ page }) => {
const mobileContainer = page.getByTestId('social-proof-mobile')
await expect(mobileContainer).toBeVisible()
})
})

View File

@@ -6,16 +6,24 @@
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
"preview": "astro preview",
"typecheck": "astro check",
"test:e2e": "playwright test",
"test:e2e:local": "PLAYWRIGHT_LOCAL=1 playwright test"
},
"dependencies": {
"@astrojs/sitemap": "catalog:",
"@comfyorg/design-system": "workspace:*",
"@comfyorg/tailwind-utils": "workspace:*",
"@vercel/analytics": "catalog:",
"gsap": "catalog:",
"lenis": "catalog:",
"vue": "catalog:"
},
"devDependencies": {
"@astrojs/check": "catalog:",
"@astrojs/vue": "catalog:",
"@playwright/test": "catalog:",
"@tailwindcss/vite": "catalog:",
"astro": "catalog:",
"tailwindcss": "catalog:",
@@ -75,6 +83,16 @@
"cwd": "apps/website",
"command": "astro check"
}
},
"test:e2e": {
"executor": "nx:run-commands",
"dependsOn": [
"build"
],
"options": {
"cwd": "apps/website",
"command": "playwright test"
}
}
}
}

View File

@@ -0,0 +1,30 @@
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: 'html',
use: {
baseURL: 'http://localhost:4321',
trace: process.env.CI ? 'on-first-retry' : 'on'
},
webServer: {
command: 'pnpm preview',
port: 4321,
reuseExistingServer: !process.env.CI
},
projects: [
{
name: 'desktop',
use: { ...devices['Desktop Chrome'] },
grepInvert: /@mobile/
},
{
name: 'mobile',
use: { ...devices['Pixel 5'] },
grep: /@mobile/
}
]
})

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31.0589 13.0316H16.014V19.1831H24.6589C24.5199 20.0537 24.2078 20.9101 23.7509 21.691C23.2273 22.5858 22.5801 23.2669 21.9167 23.7857C19.9294 25.3395 17.6126 25.6572 16.0035 25.6572C11.9389 25.6572 8.46594 22.9766 7.12148 19.3341C7.06723 19.2019 7.0312 19.0654 6.98733 18.9304C6.69023 18.0034 6.5279 17.0215 6.5279 16.001C6.5279 14.939 6.70369 13.9223 7.0242 12.9621C8.28844 9.17522 11.8397 6.34675 16.0064 6.34675C16.8445 6.34675 17.6516 6.44854 18.417 6.65159C20.1661 7.11561 21.4034 8.0295 22.1615 8.75237L26.7361 4.18101C23.9534 1.57756 20.3259 3.9369e-09 15.9988 3.9369e-09C12.5396 -7.59723e-05 9.34593 1.09971 6.72881 2.95838C4.60641 4.46571 2.86573 6.48384 1.69099 8.82767C0.598311 11.0009 0 13.4092 0 15.9986C0 18.5881 0.599225 21.0215 1.69191 23.1746V23.1891C2.84605 25.4749 4.5338 27.4431 6.58508 28.9435C8.3771 30.2543 11.5904 32 15.9988 32C18.534 32 20.7809 31.5336 22.7625 30.6595C24.192 30.029 25.4585 29.2066 26.6052 28.1496C28.1203 26.753 29.3069 25.0255 30.1168 23.038C30.9268 21.0505 31.36 18.8029 31.36 16.3662C31.36 15.2314 31.2483 14.0791 31.0589 13.0316Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,4 @@
<svg width="32" height="31" viewBox="0 0 32 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.3587 19.8988L22.9972 11.9646C23.5187 11.5757 24.2642 11.7274 24.5127 12.3316C25.8207 15.5179 25.2363 19.3471 22.634 21.9762C20.0318 24.6052 16.411 25.1818 13.1015 23.8686L9.48617 25.5598C14.6716 29.1406 20.9684 28.255 24.9032 24.277C28.0243 21.1237 28.9909 16.8255 28.0871 12.9496L28.0952 12.9578C26.7845 7.26377 28.4175 4.98781 31.7625 0.333808C31.8416 0.223459 31.9208 0.113108 32 0L27.5982 4.44709V4.4333L12.356 19.9016" fill="white"/>
<path d="M10.1634 21.8272C6.44151 18.2353 7.0832 12.6764 10.2589 9.47079C12.6072 7.09824 16.4546 6.12993 19.8133 7.55344L23.4204 5.87061C22.7706 5.3961 21.9377 4.88574 20.982 4.52709C16.6622 2.73116 11.4904 3.62499 7.97884 7.16997C4.60108 10.5825 3.53887 15.8297 5.36292 20.3071C6.72549 23.6535 4.49185 26.0204 2.24183 28.4096C1.44449 29.2564 0.644421 30.1034 0 31L10.1606 21.8299" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 952 B

View File

@@ -0,0 +1,5 @@
<svg width="38" height="16" viewBox="0 0 38 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.6714 2.88536C26.1493 2.82951 26.7876 2.87631 27.2746 2.87552C29.4061 2.87217 29.7407 4.85107 31.0086 6.17285C31.3057 5.47951 31.7391 4.76441 32.1866 4.149C32.6673 3.48769 33.0911 3.10066 33.953 2.95519C34.7328 2.82353 36.8597 2.74599 37.4893 3.17876C37.6223 3.27022 37.7198 3.40009 37.7342 3.55864C37.7879 4.1504 34.524 8.38331 33.8977 9.14783C34.6224 10.5615 37.9453 14.0003 37.9993 15.2938C38.0058 15.4508 37.9689 15.5748 37.8512 15.6902C37.4841 16.0502 36.5552 15.9679 36.0583 15.9783C35.4029 15.9928 34.8918 16.0117 34.2446 15.9184C32.7906 15.7089 32.4029 14.8649 31.7106 13.8079C31.4542 13.4166 31.1547 13.018 30.868 12.641C29.8939 13.549 29.4433 14.7027 28.431 15.5215C27.7186 16.0978 24.0748 16.1659 23.6489 15.5402C23.3554 14.4588 26.2687 11.0941 26.96 10.1276C27.1912 9.80439 27.3923 9.66031 27.5531 9.23699L27.5618 9.21374C27.0682 8.21459 23.6269 4.06408 23.8836 3.31986C24.2686 2.90918 25.1029 2.92321 25.6714 2.88536Z" fill="white"/>
<path d="M13.5798 0.0217006C14.0245 0.0102399 17.731 -0.0733401 17.8671 0.188355C18.0783 0.594232 18.0745 2.39664 17.9571 2.91885C18.7829 2.90052 20.9815 2.73358 21.576 3.24255C21.9254 3.54165 21.8894 6.31329 21.4914 6.7223C20.9809 6.93681 18.6254 6.86654 17.9992 6.8141C18.2005 8.24188 17.6814 9.92179 18.1662 11.2688C18.2188 11.4149 18.4418 11.6515 18.5582 11.7758L18.5851 11.7807C19.4248 11.9253 20.4783 11.7921 21.4193 11.8541C21.5894 11.9423 21.7529 12.0279 21.8509 12.193C22.2597 12.8822 22.1381 14.3802 21.9594 15.1114C21.9191 15.307 21.8419 15.6435 21.6653 15.748C20.9367 16.1792 19.0596 15.9477 18.2447 15.8484C16.5346 15.6399 15.0424 15.373 13.9488 14.018C12.762 12.5472 12.9954 10.9199 12.9709 9.17573C12.9618 8.52672 12.9588 7.41995 13.0502 6.80867C12.4361 6.84166 10.8569 7.01497 10.4917 6.5846C10.1043 6.12824 10.3072 3.9303 10.3518 3.3296C10.9517 2.70004 12.1816 2.90197 13.0395 2.9117C13.0534 2.19219 12.7947 0.87298 13.1333 0.195009C13.203 0.055524 13.4145 0.0516102 13.5798 0.0217006Z" fill="white"/>
<path d="M1.66628 2.88823C2.58555 2.81214 4.10841 2.81874 4.945 3.20141C5.33248 5.15667 4.99029 7.60389 5.06565 9.60325C5.07579 9.8735 4.9874 11.6351 5.25399 11.7357C6.28618 12.1249 11.1291 11.506 11.8399 12.0455C12.163 12.56 12.0235 15.1134 11.8775 15.7598C11.3062 15.9103 11.0189 15.8876 10.4264 15.8899L5.78094 15.8919C3.59892 15.9082 2.36972 16.0953 0.208639 15.5943C0.116058 15.3247 0.0349184 14.7783 0.0303189 14.4959C-0.0272935 10.99 0.0150458 7.4781 0.0142792 3.97146C0.0140433 2.80644 0.604555 2.93016 1.66628 2.88823Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.1935 32L24.9858 15.5745L27.22 19.5198H31.4952L27.22 12.0998L29.4765 8.3063H12.2615L14.5178 4.36093L12.2615 0.332235L7.41455 8.3063H2.39172L11.6896 23.9422H7.41455L4.85911 27.9711H13.194L15.6878 32L15.6431 31.9985C15.4212 31.9838 15.2231 31.8597 15.1159 31.668L13.1938 28.3031H4.85911C4.6194 28.3031 4.40156 28.1756 4.2872 27.9711L2.03092 24.1776C1.85061 23.8609 1.8506 23.4694 2.03092 23.1527L3.95303 19.7881L0.135229 12.9026C-0.0450889 12.586 -0.0450637 12.1944 0.135229 11.8778L2.39151 8.08424C2.50586 7.87972 2.72371 7.75222 2.96341 7.75222H7.41455L11.6896 0.332235C11.8039 0.127714 12.0218 0 12.2615 0H16.7124C16.9521 0 17.17 0.127714 17.2843 0.332235L19.2064 3.69689H27.5411C27.7808 3.69689 27.9986 3.82439 28.113 4.02891L30.3693 7.82245C30.5496 8.13912 30.5496 8.53061 30.3693 8.84729L28.113 12.6408L31.6316 19.0976C31.8119 19.4143 31.8119 19.8058 31.6316 20.1224L29.3751 23.916C29.2608 24.1204 29.0431 24.248 28.8034 24.248H24.9856L20.7106 31.668C20.5962 31.8725 20.3784 32 20.1387 32H16.1935Z" fill="white"/>
<path d="M22.0609 13.703H10.6869L16.1935 23.001L22.0609 13.703Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.1935 32L24.9858 15.5745L27.22 19.5198H31.4952L27.22 12.0998L29.4765 8.3063H12.2615L14.5178 4.36093L12.2615 0.332235L7.41455 8.3063H2.39172L11.6896 23.9422H7.41455L4.85911 27.9711H13.194L15.6878 32L15.6431 31.9985C15.4212 31.9838 15.2231 31.8597 15.1159 31.668L13.1938 28.3031H4.85911C4.6194 28.3031 4.40156 28.1756 4.2872 27.9711L2.03092 24.1776C1.85061 23.8609 1.8506 23.4694 2.03092 23.1527L3.95303 19.7881L0.135229 12.9026C-0.0450889 12.586 -0.0450637 12.1944 0.135229 11.8778L2.39151 8.08424C2.50586 7.87972 2.72371 7.75222 2.96341 7.75222H7.41455L11.6896 0.332235C11.8039 0.127714 12.0218 0 12.2615 0H16.7124C16.9521 0 17.17 0.127714 17.2843 0.332235L19.2064 3.69689H27.5411C27.7808 3.69689 27.9986 3.82439 28.113 4.02891L30.3693 7.82245C30.5496 8.13912 30.5496 8.53061 30.3693 8.84729L28.113 12.6408L31.6316 19.0976C31.8119 19.4143 31.8119 19.8058 31.6316 20.1224L29.3751 23.916C29.2608 24.1204 29.0431 24.248 28.8034 24.248H24.9856L20.7106 31.668C20.5962 31.8725 20.3784 32 20.1387 32H16.1935Z" fill="white"/>
<path d="M22.0609 13.703H10.6869L16.1935 23.001L22.0609 13.703Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f2ff59" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M7 17L17 7" />
<path d="M7 7h10v10" />
</svg>

After

Width:  |  Height:  |  Size: 236 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="12" viewBox="0 0 20 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 1C20 1.55228 19.5523 2 19 2H17.5C16.6716 2 16 2.67157 16 3.5C16 4.32843 16.6716 5 17.5 5H19C19.5523 5 20 5.44772 20 6C20 6.55228 19.5523 7 19 7H7.5C6.67157 7 6 7.67157 6 8.5C6 9.32843 6.67157 10 7.5 10H19C19.5523 10 20 10.4477 20 11C20 11.5523 19.5523 12 19 12H1C0.447715 12 0 11.5523 0 11C0 10.4477 0.447715 10 1 10H2.5C3.32843 10 4 9.32843 4 8.5C4 7.67157 3.32843 7 2.5 7H1C0.447715 7 0 6.55228 0 6C0 5.44772 0.447715 5 1 5H12.5C13.3284 5 14 4.32843 14 3.5C14 2.67157 13.3284 2 12.5 2H1C0.447716 2 0 1.55228 0 1C0 0.447715 0.447715 0 1 0H19C19.5523 0 20 0.447715 20 1Z" fill="#211927"/>
</svg>

After

Width:  |  Height:  |  Size: 705 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,3 @@
<svg width="52" height="64" viewBox="0 0 52 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M50.3711 21.8353C46.1108 24.4706 43.4795 28.9882 43.4795 34.0078C43.4795 39.6549 46.8627 44.8 52 46.9333C50.9976 50.1961 49.494 53.2078 47.6145 55.9686C44.8578 59.8588 41.9759 63.8745 37.7157 63.8745C33.4554 63.8745 32.2024 61.3647 27.1904 61.3647C22.3036 61.3647 20.5494 64 16.5398 64C12.5301 64 9.77349 60.3608 6.64096 55.8431C2.50602 49.5686 0.125301 42.2902 0 34.6353C0 22.2118 8.01928 15.5608 16.0386 15.5608C20.2988 15.5608 23.8072 18.3216 26.4386 18.3216C28.9446 18.3216 32.9542 15.4353 37.7157 15.4353C42.7277 15.3098 47.4892 17.6941 50.3711 21.8353ZM35.4602 10.1647C37.5904 7.6549 38.7181 4.51765 38.8434 1.2549C38.8434 0.878431 38.8434 0.376471 38.7181 0C35.0843 0.376471 31.7012 2.13333 29.3205 4.89412C27.1904 7.27843 25.9373 10.2902 25.812 13.5529C25.812 13.9294 25.812 14.3059 25.9373 14.6824C26.188 14.6824 26.5639 14.8078 26.8145 14.8078C30.1976 14.5569 33.3301 12.8 35.4602 10.1647Z" fill="#4D3762"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,11 @@
<svg width="162" height="17" viewBox="0 0 162 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.145075 16.3507L14.2657 7.49409H21.6162C21.858 7.49409 22.0514 7.68874 22.0514 7.93206C22.0514 8.12671 21.9547 8.22403 21.858 8.2727L14.8944 12.4577C14.4591 12.7497 14.2657 13.285 14.2657 13.6743V16.3021H23.1153V0.973258C23.1153 0.681281 22.8735 0.437966 22.535 0.437966H14.1206L0 9.24596V16.3507H0.145075Z" fill="#4D3762"/>
<path d="M116.06 16.3507H128.778V12.8957H120.074V10.0246H127.086V6.56949H120.074V3.89303H128.778V0.437966H116.06V16.3507Z" fill="#4D3762"/>
<path d="M36.172 0.437966L30.0789 16.3507H34.2377L35.3016 13.3823H42.0718L43.0873 16.3507H47.3912L41.298 0.437966H36.172ZM36.5106 9.9759L38.735 3.74705L40.9112 9.9759H36.5106Z" fill="#4D3762"/>
<path d="M69.7811 3.89303V16.3507H73.7948V3.89303H78.8725V0.437966H64.6551V3.89303H69.7811Z" fill="#4D3762"/>
<path d="M58.8521 10.2192C58.8521 12.2631 57.5948 13.139 55.4187 13.139C53.3393 13.139 51.9852 12.2631 51.9852 10.2192V0.389303H47.9715V10.7058C47.9715 15.2802 52.0336 16.74 55.467 16.74C58.8521 16.74 62.9142 15.3288 62.9142 10.7058V0.389303H58.9005L58.8521 10.2192Z" fill="#4D3762"/>
<path d="M162 0.437966H157.213L151.458 6.91013V0.437966H147.445V16.3507H151.458V10.3165L156.923 16.3507H162L154.65 8.51601L162 0.437966Z" fill="#4D3762"/>
<path d="M106.582 0.437966H99.183V16.3507H106.582C112.24 16.3507 113.787 14.2096 113.787 8.2727C113.787 2.67646 112.288 0.437966 106.582 0.437966ZM106.388 12.8957H103.197V3.89303H106.388C108.903 3.89303 109.677 4.57431 109.677 8.2727C109.677 11.6304 108.855 12.8957 106.388 12.8957Z" fill="#4D3762"/>
<path d="M140.965 6.9588L137.289 6.52083C135.162 6.27752 134.581 5.74222 134.581 5.01228C134.581 4.18501 135.694 3.45507 137.821 3.45507C139.901 3.45507 140.965 4.23367 141.158 5.40158H145.123C144.93 2.09251 142.125 0 138.063 0C133.227 0 130.761 1.94652 130.761 5.40158C130.761 8.17537 132.405 9.53793 135.742 9.87857L139.465 10.2679C140.868 10.4139 141.545 10.9978 141.545 11.6791C141.545 12.5064 140.578 13.2363 138.305 13.2363C135.645 13.2363 134.581 12.555 134.436 11.1925H130.568C130.761 14.8422 133.131 16.74 138.208 16.74C142.851 16.74 145.414 14.6962 145.414 11.6304C145.365 8.90532 143.818 7.29944 140.965 6.9588Z" fill="#4D3762"/>
<path d="M88.254 0C82.4993 0 79.5495 3.55239 79.5495 8.32136C79.5495 13.139 82.5477 16.6914 88.254 16.6914C94.0086 16.6914 97.0068 13.139 97.0068 8.32136C97.0068 3.55239 94.0086 0 88.254 0ZM88.254 13.139C85.2558 13.139 83.6599 11.2898 83.6599 8.32136C83.6599 5.40158 85.3041 3.55239 88.254 3.55239C91.2522 3.55239 92.848 5.40158 92.848 8.32136C92.8964 11.3385 91.2522 13.139 88.254 13.139Z" fill="#4D3762"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,3 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32.0001 0C14.3391 0 0 14.3369 0 32.0001C0 49.6633 14.318 64.0001 32.0001 64.0001C49.6822 64.0001 64.0001 49.6842 64.0001 32.0001C64.0001 14.3158 49.6822 0 32.0001 0ZM19.3431 19.3685H37.5927L34.8175 23.8105H16.5677L19.3431 19.3685ZM49.8504 41.5369L47.075 37.1159H38.9804L41.7556 32.6737H44.3207L41.2301 27.7264L32.6097 41.5369H9.5874L15.138 32.6737H11.0592L13.8345 28.2317H31.6216L28.8462 32.6737H20.3522L17.5769 37.1159H30.1289L41.2091 19.3685L55.0646 41.558H49.8293L49.8504 41.5369Z" fill="#4D3762"/>
</svg>

After

Width:  |  Height:  |  Size: 615 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32.0892 0.000172855L25.2041 19.3042L29.8845 19.3057C33.256 19.3067 34.6908 21.965 33.6291 24.942L26.783 44.1367H19.894L27.4262 23.0162H23.8791L16.3476 44.1367H9.39908L24.8539 0.800758C10.6215 4.04761 0 16.7825 0 31.9988C0 46.9783 10.2925 59.552 24.1908 63.0382L39.805 19.1966H51.1132C53.452 19.1966 56.3937 20.7737 55.1225 24.3631L48.7735 42.2905C47.9625 44.5804 46.0347 45.0445 44.499 45.0445H37.5473L30.8049 63.9755C31.2019 63.99 31.5996 64 31.9999 64C49.6729 64 64 49.6733 64 31.9994C64 14.3555 49.7209 0.048001 32.0882 0L32.0892 0.000172855ZM45.4109 22.9645L38.8528 41.3738H42.3451L48.9044 22.9645H45.4109Z" fill="#4D3762"/>
</svg>

After

Width:  |  Height:  |  Size: 742 B

View File

@@ -0,0 +1,3 @@
<svg width="152" height="10" viewBox="0 0 152 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M147.888 4.54103C147.888 2.60302 147.032 1.78601 145.04 1.78601H126.894V7.35304H145.04C147.013 7.35304 147.888 6.46004 147.888 4.54103ZM152.001 4.54103C152.001 7.35304 149.544 8.93005 147.236 8.93005H123.004V0.228001H147.255C149.563 0.228001 152.001 1.72901 152.001 4.54103ZM78.9504 1.78601H100.987V0.228001H76.7542C74.4464 0.228001 71.9896 1.74801 71.9896 4.54103C71.9896 7.35304 74.4464 8.93005 76.7542 8.93005H101.005V7.37204H78.9504C76.9776 7.37204 76.1028 6.46004 76.1028 4.54103C76.1028 2.60302 76.9403 1.78601 78.9504 1.78601ZM6.49545 7.33404C4.69012 7.33404 3.88982 6.55504 3.88982 4.80703V0.209001H0V4.80703C0 7.46704 2.32645 8.91105 4.50401 8.91105H28.0849V7.35304L6.49545 7.33404ZM60.1527 0.228001V7.37204H41.7644C39.9591 7.37204 39.1588 6.59304 39.1588 4.84503V0.228001H35.269V4.82603C35.269 7.48604 37.5954 8.93005 39.773 8.93005H64.0425V0.228001H60.1527ZM110.106 9.12005H113.884V0H110.106V9.12005Z" fill="#4D3762"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="134" height="37" viewBox="0 0 134 37" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M126.84 17.2267L134.001 36.1802C131.902 35.8924 129.762 35.4813 127.622 35.1524L123.589 24.6683L119.432 34.289C117.415 33.9601 115.399 33.8367 113.341 33.5901L120.625 17.0212L114.041 0H120.173L123.877 9.49731L127.828 0H134.001L126.84 17.2267ZM109.226 0H103.67V32.85C105.48 32.9734 107.374 33.0556 109.226 33.2612V0ZM98.8956 32.48C93.8335 32.1511 88.7715 31.8222 83.6271 31.7399V0H89.2653V26.3951C92.5166 26.4774 95.7678 26.724 98.8956 26.8885V32.48ZM77.5773 18.5013H69.8401V31.3699H64.2431V0H80.0878V5.6326H69.8401V12.8275H77.5773V18.5013ZM54.7362 5.6326V31.6577C52.8431 31.6577 50.9088 31.6577 49.0568 31.7399V5.6326H43.2128V0H60.6214V5.6326H54.7362ZM37.1219 18.7891C34.6526 18.7891 31.6894 18.7891 29.5905 18.9124V27.2996C32.9241 27.053 36.2165 26.8063 39.5912 26.6829V32.11L23.9522 33.3434V0H39.5912V5.6326H29.5905V13.2387C31.7717 13.2387 35.1464 13.1153 37.1219 13.1153V18.7891ZM5.76171 15.6233V35.358C3.74511 35.6046 1.93429 35.8924 0 36.1802V0H5.39131L12.7581 20.557V0H18.4375V33.8367C16.4209 34.1657 14.4043 34.289 12.2642 34.5768L5.76171 15.6233Z" fill="#4D3762"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="106" height="38" viewBox="0 0 106 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M105.999 0L28.4741 32.9616C22.0197 35.7266 16.5806 37.1091 12.2294 37.1091C7.2979 37.1091 3.74436 35.3628 1.4962 31.8702C-1.33213 27.3589 -0.0992643 20.1554 4.75966 12.5152C7.66051 8.0767 11.3591 4.00197 14.9852 0.072763C14.1149 1.45526 6.64521 13.9705 14.8401 19.8643C16.4356 21.0285 18.7563 21.6106 21.5846 21.6106C23.8328 21.6106 26.4435 21.2468 29.3444 20.5192L105.999 0Z" fill="#4D3762"/>
</svg>

After

Width:  |  Height:  |  Size: 548 B

View File

@@ -0,0 +1,3 @@
<svg width="152" height="16" viewBox="0 0 152 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.43281 3.52349H8.38328C9.61244 3.52349 10.1515 4.08285 10.1515 5.38666C10.1515 6.69149 9.61244 7.25287 8.38328 7.25287H4.43281V3.52349ZM0 15.2522H4.43281V10.3194H10.0426C13.6161 10.3194 14.7353 7.83251 14.7353 5.38666C14.7353 2.58988 13.4499 0.456918 9.83538 0.456918H0V15.2522ZM16.6176 15.2522H21.0519V0.456918H16.6176V15.2522ZM22.5223 15.2522H28.0176L30.8508 10.7764L33.6836 15.2522H39.1758L33.4019 7.56323L38.5633 0.456918H33.2914L30.8508 4.43479L28.4097 0.456918H23.1379L28.2982 7.56323L22.5223 15.2522ZM39.4383 7.8528C39.4383 13.987 43.4065 15.7066 48.1002 15.7066C52.796 15.7066 56.7642 13.987 56.7642 7.8528C56.7637 1.72067 52.7955 0 48.1002 0C43.4065 0 39.4383 1.72067 39.4383 7.8528ZM44.1346 7.8528C44.1346 4.76594 45.7356 3.31557 48.0997 3.31557C50.4856 3.31557 52.0684 4.76594 52.0684 7.8528C52.0684 10.961 50.4856 12.3895 48.0997 12.3895C45.7361 12.3895 44.1346 10.9615 44.1346 7.8528ZM58.7579 15.2522H62.9288V4.18579H62.9683L66.4694 15.2522H70.1199L73.624 4.18579H73.6605V15.2522H77.8339V0.456918H71.3516L68.3136 10.2155H68.2771L65.2402 0.456918H58.7579V15.2522ZM79.8281 7.8528C79.8281 13.987 83.7943 15.7066 88.491 15.7066C93.1863 15.7066 97.154 13.987 97.154 7.8528C97.1535 1.72067 93.1858 0 88.4905 0C83.7943 0 79.8281 1.72067 79.8281 7.8528ZM84.5223 7.8528C84.5223 4.76594 86.1254 3.31557 88.4905 3.31557C90.8759 3.31557 92.4587 4.76594 92.4587 7.8528C92.4587 10.961 90.8759 12.3895 88.4905 12.3895C86.1254 12.3895 84.5223 10.9615 84.5223 7.8528ZM99.1457 15.2522H103.432V5.90646H103.47L109.84 15.2517H114.739V0.456918H110.455V9.718H110.417L104.251 0.456918H99.1457V15.2522ZM117.424 15.2522H126.459C130.277 15.2522 133.368 13.6776 133.368 7.8533C133.368 2.92255 131.153 0.457425 127.035 0.457425H117.424V15.2522ZM121.858 3.77198H125.21C127.578 3.77198 128.787 5.07782 128.787 7.83251C128.787 9.86253 128.098 11.9346 125.563 11.9346H121.858V3.77198ZM134.674 7.8528C134.674 13.987 138.641 15.7066 143.339 15.7066C148.033 15.7066 152 13.987 152 7.8528C151.999 1.72067 148.033 0 143.338 0C138.641 0 134.674 1.72067 134.674 7.8528ZM139.37 7.8528C139.37 4.76594 140.972 3.31557 143.338 3.31557C145.722 3.31557 147.305 4.76594 147.305 7.8528C147.305 10.961 145.722 12.3895 143.338 12.3895C140.972 12.3895 139.37 10.9615 139.37 7.8528Z" fill="#4D3762"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,11 @@
<svg width="140" height="19" viewBox="0 0 140 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M91.1909 5.7923H86.9427L88.6109 8.1785H90.7641L88.9601 18.3776H92.7233L94.5274 8.1785H99.2411L91.8116 2.25149L91.1909 5.7923Z" fill="#4D3762"/>
<path d="M17.5748 0H0.465556L0 2.67485H6.65358L3.86024 18.3776H7.62349L10.4168 2.67485H17.1092L17.5748 0Z" fill="#4D3762"/>
<path d="M22.3079 10.7571H15.7707C16.1587 9.46782 17.439 7.69741 19.7474 7.69741C22.2691 7.67817 22.5407 9.58328 22.3079 10.7571ZM20.3681 5.52289C15.5767 5.52289 12.5118 8.50564 11.9105 11.9887C11.2315 15.8374 13.6175 18.6662 18.4477 18.6662C20.6009 18.6662 22.6183 18.2621 24.2671 17.3577L24.7715 14.4904C23.1614 15.6257 21.1246 16.2608 19.3982 16.2608C16.9928 16.2608 15.0724 15.2601 15.3828 12.9894H25.334C25.4504 12.72 25.6056 12.2389 25.7026 11.6616C26.1875 8.87127 25.1982 5.52289 20.3681 5.52289Z" fill="#4D3762"/>
<path d="M36.9923 5.52289C34.6452 5.52289 32.8799 6.48507 32.0458 7.46649H31.9488L28.6317 5.52289C28.5153 6.61977 28.3407 8.10153 28.0886 9.54479L26.5367 18.3776H30.3L31.5996 10.9688C31.6578 10.6032 31.7936 10.2183 31.9488 9.96815C32.5114 8.98673 33.6946 8.10153 35.1883 8.10153C37.2445 8.10153 37.8071 9.48706 37.4967 11.2767L36.2746 18.3776H40.0378L41.3569 10.9111C42.0553 6.92767 39.5917 5.52289 36.9923 5.52289Z" fill="#4D3762"/>
<path d="M82.1707 5.52289C79.8235 5.52289 78.0583 6.48507 77.2242 7.46649H77.1272L73.8101 5.52289C73.6937 6.61977 73.5191 8.10153 73.2669 9.54479L71.7151 18.3776H75.4783L76.8168 10.9688C76.875 10.6032 77.0108 10.2183 77.166 9.96815C77.7285 8.98673 78.9118 8.10153 80.4055 8.10153C82.4617 8.10153 83.0242 9.48706 82.7139 11.2767L81.453 18.3776H85.2162L86.5353 10.9111C87.2336 6.92767 84.7701 5.52289 82.1707 5.52289Z" fill="#4D3762"/>
<path d="M67.5057 10.7571H60.9491C61.3371 9.46782 62.6173 7.69741 64.9257 7.69741C67.4475 7.67817 67.7191 9.58328 67.5057 10.7571ZM65.5465 5.52289C60.7551 5.52289 57.6902 8.50564 57.0889 11.9887C56.4099 15.8374 58.7959 18.6662 63.626 18.6662C65.7792 18.6662 67.7967 18.2621 69.4455 17.3577L69.9499 14.4904C68.3398 15.6257 66.303 16.2608 64.5766 16.2608C62.1712 16.2608 60.2508 15.2601 60.5611 12.9894H70.5124C70.6288 12.72 70.784 12.2389 70.881 11.6616C71.3659 8.87127 70.3766 5.52289 65.5465 5.52289Z" fill="#4D3762"/>
<path d="M42.9476 11.9117C42.2686 15.8182 45.0232 18.647 49.6787 18.647C51.541 18.647 53.7524 18.1274 54.5865 17.8003L55.1878 14.4326C53.8882 15.8374 52.1035 16.1261 50.8426 16.1261C48.2821 16.1261 46.3423 14.6443 46.7884 12.0657C47.1764 9.75647 49.3878 7.98606 52.2587 7.98606C53.4226 7.98606 55.0132 8.37094 56.0219 9.6795L56.6039 6.25415C55.7892 5.94625 54.4895 5.50365 52.5691 5.50365C47.351 5.52289 43.6071 8.15926 42.9476 11.9117Z" fill="#4D3762"/>
<path d="M116.35 8.46715C116.001 8.06304 115.613 7.428 115.38 6.96616H114.604C114.178 7.48573 113.557 8.06304 113.014 8.46715H116.35ZM115.574 10.0259H110.977L112.354 10.6417L112.102 11.8925H115.148L115.574 10.0259ZM117.766 9.73723L117.281 11.8925H118.31C119.008 11.8925 119.512 12.0465 119.376 12.8932C119.144 14.4326 118.717 16.5687 118.387 18.3776H112.8L112.335 16.7419H116.447C116.641 15.8759 116.874 14.7983 116.99 13.9323C117.029 13.6437 116.913 13.5282 116.622 13.5282H109.658L110.279 10.1798C109.774 10.4108 109.018 10.6609 108.281 10.7956L108.727 8.77505C110.026 8.46715 111.346 7.83212 112.16 6.94691H109.057L109.406 5.36895H113.247C113.46 5.08029 113.615 4.69542 113.712 4.42601H109.561L109.871 2.79031H111.365L110.841 0.384871H112.956L113.46 2.8288H114.197C114.43 2.02057 114.604 0.942933 114.624 0.0384871H116.777C116.738 0.942933 116.602 2.00133 116.408 2.8288H117.262C117.592 2.0783 117.941 1.1931 118.213 0.384871H120.366C120.075 1.13537 119.687 2.05906 119.338 2.8288H120.851L120.443 4.4645H115.943C115.865 4.73391 115.691 5.13802 115.555 5.40743H121.083L120.715 6.9854H117.495C118.193 8.00531 119.842 8.77505 121.122 9.04446L120.405 10.8341C119.493 10.6417 118.542 10.2183 117.766 9.73723ZM115.419 15.9336H108.3L108.009 14.3364H115.768V14.3557L115.419 15.9336ZM107.039 2.27074H105.992C105.817 3.2714 105.662 4.31055 105.449 5.38819H106.865L107.388 2.65561C107.466 2.34771 107.33 2.27074 107.039 2.27074ZM106.593 7.04313H105.138C104.925 8.12077 104.692 9.25614 104.459 10.3723H105.972L106.593 7.04313ZM109.522 1.69343C108.494 7.2933 107.563 12.8162 106.515 18.3968H104.401C104.828 16.2993 105.293 14.067 105.662 12.008H104.149C103.683 14.1632 103.198 16.3378 102.752 18.3968H100.638C102.422 12.7585 103.295 6.94691 104.304 0.538819H108.261C109.289 0.538819 109.697 0.712011 109.522 1.69343Z" fill="#4D3762"/>
<path d="M138.639 0.558062H128.843L129.27 2.40544H136.583C137.068 2.40544 137.3 2.46317 137.184 3.00199C136.912 4.50299 136.621 6.02323 136.33 7.50498H133.149L133.692 4.34904L131.462 3.05972L130.647 7.50498H128.28L127.97 9.35236H130.298C129.813 12.3543 129.231 15.4141 128.61 18.3968H131.151C131.733 15.3756 132.334 12.3543 132.839 9.35236H136.001C135.477 12.3736 134.895 15.4718 134.294 18.3968H138.775L139.551 16.5302H137.165C138.115 11.7578 139.085 6.79297 139.939 1.98208C140.132 0.962177 139.919 0.558062 138.639 0.558062ZM127.873 4.38753C127.562 2.92502 127.155 1.40478 126.825 0H124.459C124.769 1.44326 125.118 2.98275 125.39 4.38753H127.873ZM122.694 5.36895H127.097C126.651 8.27472 126.146 11.2382 125.584 14.1825C126.418 13.5667 127.31 12.8932 128.106 12.2004L127.64 14.8945C126.146 16.1453 124.09 17.6078 122.209 18.6662C123.023 14.8945 123.78 10.9111 124.323 7.21632H122.344L122.694 5.36895Z" fill="#4D3762"/>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,3 @@
<svg width="151" height="48" viewBox="0 0 151 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M54.3983 15.028V25.7062C54.3983 30.042 57.3539 32.8056 61.6585 32.8056C66.1863 32.8056 68.9577 29.8912 68.9577 25.4138V15.028H65.1254V25.2187C65.1254 27.6601 63.8959 29.1741 61.6993 29.1741C59.292 29.1741 58.2308 27.433 58.2308 25.4138V15.028H54.3983ZM70.821 15.1755V32.6314H76.9251C80.3058 32.6314 83.1785 30.9071 83.1785 27.4872C83.1785 23.9175 80.0548 23.1263 80.0548 23.1263C80.0548 23.1263 82.3678 22.4838 82.3678 19.4298C82.3678 16.3778 79.4442 15.1755 76.9251 15.1755H70.821ZM74.6821 18.4911H76.535C77.821 18.4911 78.6438 19.1684 78.6438 20.347C78.6438 21.4013 77.9457 22.1968 76.6325 22.1968H74.6822L74.6821 18.4911ZM74.6821 24.9274H76.73C78.6558 24.9274 79.3235 25.9166 79.3235 27.0971C79.3235 28.4708 78.4597 29.2761 76.9251 29.2761H74.6822V24.9275L74.6821 24.9274ZM84.629 15.1743H88.481V32.6301H84.629V15.1743ZM126.66 32.6301H122.808V15.1743H134.9V18.7825H126.66V22.7808H134.168V25.7062H126.66V32.6301ZM136.314 15.1743V18.7825H141.287V32.6301H145.188V18.7825H150.064V15.1743H136.314ZM92.8785 26.8259L89.8184 29.0632C89.8184 29.0632 90.8758 32.9305 96.1768 32.9305C99.7069 32.9305 102.304 30.8704 102.304 27.2487C102.304 21.4121 94.1433 22.6503 94.1433 19.6487C94.1433 18.8587 94.8457 18.161 96.1422 18.161C98.2224 18.161 99.0129 20.2015 99.0129 20.2015L102.074 18.0533C102.074 18.0533 100.878 14.8664 96.2959 14.8664C92.53 14.8664 90.5296 17.2825 90.5296 19.7755C90.5296 25.7235 98.4532 24.4325 98.4532 27.5541C98.4532 28.6075 97.6231 29.3815 96.3303 29.3726C93.7688 29.3726 92.8785 26.8259 92.8785 26.8259ZM112.251 14.8584C107.715 14.8584 105.693 17.3207 105.693 17.3207L106.278 17.9058C106.278 17.9058 103.343 19.9904 103.343 24.467C103.343 29.6181 107.316 32.9267 112.272 32.9267C118.031 32.9267 121.299 28.6774 121.299 23.9001C121.299 18.8059 117.568 14.8584 112.251 14.8584ZM112.333 18.4818C115.409 18.4818 117.404 20.8326 117.404 23.8849C117.404 26.9056 115.433 29.3216 112.333 29.3216C109.271 29.3216 107.259 27.1833 107.259 24.3451C107.259 21.762 109.009 20.6363 109.009 20.6363L108.375 20.0024C108.375 20.0024 109.724 18.4818 112.333 18.4818ZM21.5432 0C6.78898 0 0.420489 13.1328 0.420489 13.1328L2.29511 14.4748C2.29511 14.4748 -0.0639514 18.961 0.00133032 24.7151C0.00133032 36.4616 9.35181 47.5268 23.1165 47.5268C35.6212 47.5268 45.9508 37.3488 45.9508 24.744C45.9508 8.30461 33.2024 0 21.5432 0ZM21.6712 4.00737C33.1987 4.00737 42.1437 13.2966 42.1437 24.0958C42.1437 35.5436 33.5336 43.5049 23.5005 43.5049C16.1163 43.5049 9.45504 37.8817 9.45504 30.1718C9.45504 25.7188 11.8219 22.6143 14.4741 20.9597L14.8622 21.3809C14.1364 21.9488 12.0668 25.2733 12.0668 28.5409C12.0668 34.6069 16.8237 38.808 22.8171 38.808C30.4516 38.808 35.5432 32.2965 35.5432 24.7358C35.5432 15.9391 27.7784 8.47099 18.4339 8.47099C13.2655 8.47099 8.87885 10.6937 6.3582 12.6559L5.96594 12.3009C9.26362 7.21623 14.9744 4.00737 21.6712 4.00737ZM18.2316 12.4226C24.1947 12.4226 30.0166 16.3676 31.4697 21.9672L30.9412 22.1674C28.332 18.7219 24.0566 15.8664 19.0079 15.8664C9.71774 15.8664 4.83881 24.3083 5.55505 31.359L5.00797 31.5593C5.00797 31.5593 3.98187 29.2952 3.98187 26.6105C3.98187 18.8359 10.3665 12.4226 18.2316 12.4226ZM22.1729 22.1798C24.9961 22.1798 27.2539 24.4681 27.2539 27.1844C27.2539 29.4323 25.8107 30.7644 25.8107 30.7644L27.5202 31.9598C27.5202 31.9598 25.8251 34.621 22.3649 34.621C19.0551 34.621 16.1278 31.9496 16.1278 28.3571C16.1278 24.6554 19.3119 22.1798 22.1729 22.1798Z" fill="#4D3762"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f2ff59" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6L6 18" />
<path d="M6 6l12 12" />
</svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@@ -0,0 +1,5 @@
<svg width="213" height="252" viewBox="0 0 213 252" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M113.891 241.07C114.663 240.74 115.378 240.339 116.018 239.858C119.563 237.196 119.19 233.305 115.185 231.166L82.5103 213.57C80.7481 212.649 80.5683 210.948 82.1141 209.787C82.3951 209.576 82.7135 209.405 83.0339 209.255L118.439 193.433C119.996 192.739 122.105 192.742 123.552 193.515L156.203 210.912C163.084 214.586 172.995 214.602 180.347 211.325L197.65 203.799C203.417 201.279 204.58 194.765 199.211 191.703L161.926 171.792C155.081 168.137 145.219 168.107 137.894 171.318L124.319 177.251C122.76 177.923 120.684 177.918 119.237 177.145L109.763 172.086C102.971 168.459 93.2401 168.399 85.9246 171.535L40.2587 191.485C34.7284 194.099 32.5998 199.828 38.9002 204.268L45.2838 207.677C47.0303 208.61 47.2102 210.311 45.6488 211.484C45.3833 211.683 45.0826 211.864 44.7446 212.004L31.2805 217.876C30.5753 218.202 29.9073 218.567 29.3296 219.001C25.8007 221.651 26.1718 225.521 30.1589 227.65L70.1782 249.187C77.041 252.852 86.9186 252.87 94.2571 249.626L113.858 241.072L113.891 241.07Z" fill="#F2FF59"/>
<path d="M104.261 118.957C104.273 117.985 104.096 117.016 103.841 116.059C102.429 110.759 97.068 108.263 92.0768 110.485L51.5756 128.509C49.405 129.501 47.1266 128.426 46.511 126.114C46.3991 125.694 46.3594 125.268 46.3478 124.855L46.5093 79.6575C46.4985 77.6776 47.6894 75.6852 49.4924 74.8828L90.1351 56.7436C98.7103 52.9274 104.316 43.5678 104.261 34.2174V9.01374C104.247 8.13187 104.245 7.21909 104.015 6.35524C102.609 1.07876 97.4477 -1.40313 92.4785 0.808285L43.2922 22.6974C34.7611 26.494 29.172 35.7971 29.1585 45.0737L29.11 62.2525C29.0926 64.2188 27.9174 66.1781 26.1144 66.9804L14.3071 72.235C5.84186 76.0022 0.104287 85.151 2.96039e-05 94.3635V156.109C-0.00222287 157.024 0.124117 157.961 0.360394 158.848C1.77184 164.148 6.96164 166.643 11.9528 164.422L23.5623 159.255C25.7391 158.287 28.0175 159.362 28.6393 161.697C28.745 162.093 28.8067 162.51 28.7962 162.932L28.7324 179.961C28.7521 180.867 28.844 181.766 29.0741 182.63C30.4793 187.906 35.6409 190.388 40.6101 188.177L89.8059 166.231C98.3591 162.425 104.282 153.098 104.261 143.785V118.99V118.957Z" fill="#F2FF59"/>
<path d="M212.999 155.785C213.007 154.849 212.925 153.795 212.663 152.613C211.209 146.063 205.925 138.562 200.86 135.857L159.759 113.908C157.556 112.757 155.236 109.484 154.603 106.628C154.487 106.108 154.445 105.654 154.432 105.24L154.436 61.2966C154.418 59.3542 155.621 58.5727 157.451 59.5498L198.696 81.525C207.398 86.1723 213.089 82.505 212.999 73.3274V48.857C212.982 47.9834 212.86 46.9755 212.624 45.908C211.177 39.3878 205.921 31.9276 200.879 29.2345L150.963 2.57821C142.306 -2.04523 136.658 1.57626 136.677 10.6146L136.689 27.3292C136.678 29.2308 135.491 29.9954 133.661 29.0182L121.679 22.6193C113.088 18.0316 107.475 21.5446 107.402 30.4318L107.435 90.4925C107.436 91.3831 107.568 92.4198 107.811 93.5161C109.265 100.065 114.548 107.566 119.614 110.271L131.395 116.563C133.604 117.743 135.924 121.016 136.564 123.901C136.673 124.391 136.737 124.858 136.728 125.26L136.724 141.813C136.747 142.716 136.844 143.683 137.08 144.75C138.528 151.271 143.783 158.731 148.825 161.424L198.75 188.034C207.43 192.67 213.053 189.019 212.999 179.911V155.802V155.785Z" fill="#F2FF59"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="45" height="48" viewBox="0 0 45 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M35.6487 36.021C35.733 35.7387 35.7791 35.4411 35.7791 35.1283C35.7791 33.3963 34.3675 31.9924 32.6262 31.9924H18.4956C17.7361 32 17.1147 31.3896 17.1147 30.6342C17.1147 30.4969 17.1377 30.3672 17.1684 30.2451L20.9734 17.0606C21.1345 16.4807 21.6715 16.0534 22.3005 16.0534L36.4848 16.0382C39.4766 16.0382 42.0005 14.0315 42.76 11.2923L44.8926 3.94468C44.9616 3.68526 45 3.40296 45 3.12065C45 1.39628 43.5961 0 41.8624 0L24.7017 0C21.7252 0 19.209 1.99142 18.4342 4.70005L16.992 9.71292C16.8232 10.2852 16.2939 10.7048 15.6648 10.7048H11.5453C8.59189 10.7048 6.0987 12.6581 5.30089 15.3362L0.11507 33.3505C0.0383566 33.6175 0 33.9075 0 34.1974C0 35.9294 1.41152 37.3333 3.15292 37.3333H7.20338C7.96284 37.3333 8.58421 37.9437 8.58421 38.7067C8.58421 38.8364 8.56887 38.9661 8.53051 39.0882L7.09598 44.0553C7.02694 44.3224 6.98091 44.597 6.98091 44.8794C6.98091 46.6037 8.38476 48 10.1185 48L27.2869 47.9847C30.2711 47.9847 32.7873 45.9857 33.5544 43.2618L35.641 36.0286L35.6487 36.021Z" fill="#F2FF59"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="42.66" height="94.14" viewBox="0 0 42.66 94.14" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.5464 94.1386H42.6566V0C30.658 0 18.2687 9.66676 14.9543 21.5781L0.813667 72.5605C-2.48629 84.4719 4.54783 94.1386 16.5464 94.1386Z" fill="#F2FF59"/>
</svg>

After

Width:  |  Height:  |  Size: 304 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="32" viewBox="0 0 20 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 32V0C20 5.39616 15.5172 9.78053 10 9.78053C4.48276 9.78053 0 5.416 0 0V32C0 26.6038 4.48276 22.2195 10 22.2195C15.5172 22.2195 20 26.6038 20 32Z" fill="#F2FF59"/>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@@ -0,0 +1,3 @@
<svg width="42.66" height="94.14" viewBox="0 0 42.66 94.14" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.1101 0H0V94.1387C11.9985 94.1387 24.3878 84.4719 27.7022 72.5606L41.8428 21.5781C45.1428 9.66678 38.1087 0 26.1101 0Z" fill="#F2FF59"/>
</svg>

After

Width:  |  Height:  |  Size: 291 B

View File

@@ -0,0 +1,3 @@
<svg width="62" height="94.14" viewBox="0 0 62 80" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.9346 0C33.456 0.000149153 39.6242 8.20368 36.7305 18.3115L33.9385 28.0635C32.7454 32.2159 35.8674 36.3555 40.1826 36.3555C42.9814 36.3555 45.4493 34.5653 46.3311 31.9268L47.7129 27.002C49.4225 20.9287 55.812 16 62 16V64H48.5342C42.3461 64 38.7182 59.0713 40.4199 52.998L40.8398 51.5L40.8301 51.4922C42.0104 47.3146 38.8756 43.1751 34.5352 43.1748C31.6287 43.1748 29.0515 45.1048 28.252 47.9111L24.3047 61.6885H24.2793C21.3855 71.7964 10.5089 80 0 80V0H22.9346Z" fill="#F2FF59"/>
</svg>

After

Width:  |  Height:  |  Size: 625 B

View File

@@ -0,0 +1,3 @@
<svg width="87.7" height="95.23" viewBox="0 0 87.7 95.23" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M87.6679 0C75.187 0 62.2689 9.77836 58.8171 21.8272L54.0387 38.5685C53.1343 41.7321 50.2553 43.8966 46.9843 43.8966C42.1156 43.8966 38.5883 39.2042 39.9299 34.4967L43.5475 21.8272C47.0145 9.77836 39.6736 0 27.1927 0H0V95.2254C12.4809 95.2254 25.399 85.4471 28.8508 73.3982L33.5839 56.7932C34.4883 53.6296 37.3674 51.465 40.6383 51.465C45.5222 51.465 49.0343 56.1574 47.6928 60.865L44.1053 73.3982C40.6685 85.4471 47.9942 95.2254 60.4902 95.2254H87.6829V0H87.6679Z" fill="#F2FF59"/>
</svg>

After

Width:  |  Height:  |  Size: 631 B

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.71973 11.5878H20.2197" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.9697 3.33777V19.8378" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

View File

@@ -0,0 +1,110 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 900" fill="none">
<!-- Background geometric lines -->
<g stroke="#49378B" stroke-width="1.5" fill="none" opacity="0.4">
<!-- Outer hexagonal frame layers -->
<path d="M400 80 L600 200 L600 440 L400 560 L200 440 L200 200 Z" />
<path d="M400 120 L570 220 L570 420 L400 520 L230 420 L230 220 Z" />
<!-- Connector lines going up -->
<line x1="300" y1="160" x2="300" y2="60" />
<line x1="400" y1="120" x2="400" y2="20" />
<line x1="500" y1="160" x2="500" y2="60" />
<!-- Bottom platform layers -->
<path d="M250 520 L550 520 L600 560 L600 600 L400 700 L200 600 L200 560 Z" opacity="0.3" />
<path d="M280 620 L520 620 L560 650 L560 680 L400 760 L240 680 L240 650 Z" opacity="0.2" />
<path d="M320 700 L480 700 L510 720 L510 740 L400 800 L290 740 L290 720 Z" opacity="0.15" />
</g>
<!-- 3D Isometric cube cluster -->
<g transform="translate(400, 380)">
<!-- Back layer cubes (purple/dark) -->
<!-- Top back -->
<g transform="translate(0, -100)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
</g>
<!-- Middle row - left back -->
<g transform="translate(-70, -55)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
</g>
<!-- Middle row - right back -->
<g transform="translate(70, -55)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
</g>
<!-- Yellow accent cubes - front facing -->
<!-- Top -->
<g transform="translate(0, -65)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
</g>
<!-- Middle left yellow -->
<g transform="translate(-70, -20)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
</g>
<!-- Middle right yellow -->
<g transform="translate(70, -20)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
</g>
<!-- Center purple -->
<g transform="translate(0, -20)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
</g>
<!-- Bottom row -->
<g transform="translate(-70, 25)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
</g>
<g transform="translate(70, 25)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
</g>
<!-- Front bottom yellow -->
<g transform="translate(0, 25)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
</g>
<!-- Outer corner yellow accents -->
<g transform="translate(-105, 5)">
<polygon points="0,-25 20,-12 20,12 0,25 -20,12 -20,-12" fill="#f2ff59" />
<polygon points="0,-25 20,-12 0,-2 -20,-12" fill="#f2ff59" />
<polygon points="0,-2 20,-12 20,12 0,25" fill="#d4e04e" />
</g>
<g transform="translate(105, 5)">
<polygon points="0,-25 20,-12 20,12 0,25 -20,12 -20,-12" fill="#f2ff59" />
<polygon points="0,-25 20,-12 0,-2 -20,-12" fill="#f2ff59" />
<polygon points="0,-2 -20,-12 -20,12 0,25" fill="#e0ec50" />
</g>
<g transform="translate(0, -135)">
<polygon points="0,-25 20,-12 20,12 0,25 -20,12 -20,-12" fill="#f2ff59" />
<polygon points="0,-25 20,-12 0,-2 -20,-12" fill="#f2ff59" />
</g>
</g>
<!-- Bottom arrow/chevron shape -->
<path d="M340 780 L400 820 L460 780 L460 850 L400 890 L340 850 Z" fill="#211927" stroke="#49378B" stroke-width="1" opacity="0.5" />
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Some files were not shown because too many files have changed in this diff Show More