Compare commits

..

1 Commits

Author SHA1 Message Date
Jin Yi
9e6939290d feature: queue tab 2026-02-06 20:02:16 +09:00
862 changed files with 12866 additions and 63529 deletions

View File

@@ -1,200 +0,0 @@
---
name: writing-playwright-tests
description: 'Writes Playwright e2e tests for ComfyUI_frontend. Use when creating, modifying, or debugging browser tests. Triggers on: playwright, e2e test, browser test, spec file.'
---
# Writing Playwright Tests for ComfyUI_frontend
## Golden Rules
1. **ALWAYS look at existing tests first.** Search `browser_tests/tests/` for similar patterns before writing new tests.
2. **ALWAYS read the fixture code.** The APIs are in `browser_tests/fixtures/` - read them directly instead of guessing.
3. **Use premade JSON workflow assets** instead of building workflows programmatically.
- Assets live in `browser_tests/assets/`
- Load with `await comfyPage.workflow.loadWorkflow('feature/my_workflow')`
- Create new assets by starting with `browser_tests/assets/default.json` and manually editing the JSON to match your desired graph state
## Vue Nodes vs LiteGraph: Decision Guide
Choose based on **what you're testing**, not personal preference:
| Testing... | Use | Why |
| ---------------------------------------------- | -------------------------------- | ---------------------------------------- |
| Vue-rendered node UI, DOM widgets, CSS states | `comfyPage.vueNodes.*` | Nodes are DOM elements, use locators |
| Canvas interactions, connections, legacy nodes | `comfyPage.nodeOps.*` | Canvas-based, use coordinates/references |
| Both in same test | Pick primary, minimize switching | Avoid confusion |
**Vue Nodes requires explicit opt-in:**
```typescript
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
```
**Vue Node state uses CSS classes:**
```typescript
const BYPASS_CLASS = /before:bg-bypass\/60/
await expect(node).toHaveClass(BYPASS_CLASS)
```
## Common Issues
These are frequent causes of flaky tests - check them first, but investigate if they don't apply:
| Symptom | Common Cause | Typical Fix |
| ---------------------------------- | ------------------------- | -------------------------------------------------------------------------------------- |
| Test passes locally, fails in CI | Missing nextFrame() | Add `await comfyPage.nextFrame()` after canvas ops (not needed after `loadWorkflow()`) |
| Keyboard shortcuts don't work | Missing focus | Add `await comfyPage.canvas.click()` first |
| Double-click doesn't trigger | Timing too fast | Add `{ delay: 5 }` option |
| Elements end up in wrong position | Drag animation incomplete | Use `{ steps: 10 }` not `{ steps: 1 }` |
| Widget value wrong after drag-drop | Upload incomplete | Add `{ waitForUpload: true }` |
| Test fails when run with others | Test pollution | Add `afterEach` with `resetView()` |
| Local screenshots don't match CI | Platform differences | Screenshots are Linux-only, use PR label |
## Test Tags
Add appropriate tags to every test:
| Tag | When to Use |
| ------------- | ----------------------------------------- |
| `@smoke` | Quick essential tests |
| `@slow` | Tests > 10 seconds |
| `@screenshot` | Visual regression tests |
| `@canvas` | Canvas interactions |
| `@node` | Node-related |
| `@widget` | Widget-related |
| `@mobile` | Mobile viewport (runs on Pixel 5 project) |
| `@2x` | HiDPI tests (runs on 2x scale project) |
```typescript
test.describe('Feature', { tag: ['@screenshot', '@canvas'] }, () => {
```
## Retry Patterns
**Never use `waitForTimeout`** - it's always wrong.
| Pattern | Use Case |
| ------------------------ | ---------------------------------------------------- |
| Auto-retrying assertions | `toBeVisible()`, `toHaveText()`, etc. (prefer these) |
| `expect.poll()` | Single value polling |
| `expect().toPass()` | Multiple assertions that must all pass |
```typescript
// Prefer auto-retrying assertions when possible
await expect(node).toBeVisible()
// Single value polling
await expect.poll(() => widget.getValue(), { timeout: 2000 }).toBe(100)
// Multiple conditions
await expect(async () => {
expect(await node1.getValue()).toBe('foo')
expect(await node2.getValue()).toBe('bar')
}).toPass({ timeout: 2000 })
```
## Screenshot Baselines
- **Screenshots are Linux-only.** Don't commit local screenshots.
- **To update baselines:** Add PR label `New Browser Test Expectations`
- **Mask dynamic content:**
```typescript
await expect(comfyPage.canvas).toHaveScreenshot('page.png', {
mask: [page.locator('.timestamp')]
})
```
## CI Debugging
1. Download artifacts from failed CI run
2. Extract and view trace: `npx playwright show-trace trace.zip`
3. CI deploys HTML report to Cloudflare Pages (link in PR comment)
4. Reproduce CI: `CI=true pnpm test:browser`
5. Local runs: `pnpm test:browser:local`
## Anti-Patterns
Avoid these common mistakes:
1. **Arbitrary waits** - Use retrying assertions instead
```typescript
// ❌ await page.waitForTimeout(500)
// ✅ await expect(element).toBeVisible()
```
2. **Implementation-tied selectors** - Use test IDs or semantic selectors
```typescript
// ❌ page.locator('div.container > button.btn-primary')
// ✅ page.getByTestId('submit-button')
```
3. **Missing nextFrame after canvas ops** - Canvas needs sync time
```typescript
await node.drag({ x: 50, y: 50 })
await comfyPage.nextFrame() // Required
```
4. **Shared state between tests** - Tests must be independent
```typescript
// ❌ let sharedData // Outside test
// ✅ Define state inside each test
```
## Quick Start Template
```typescript
// Path depends on test file location - adjust '../' segments accordingly
import {
comfyPageFixture as test,
comfyExpect as expect
} from '../fixtures/ComfyPage'
test.describe('FeatureName', { tag: ['@canvas'] }, () => {
test.afterEach(async ({ comfyPage }) => {
await comfyPage.canvasOps.resetView()
})
test('should do something', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('myWorkflow')
const node = (await comfyPage.nodeOps.getNodeRefsByTitle('KSampler'))[0]
// ... test logic
await expect(comfyPage.canvas).toHaveScreenshot('expected.png')
})
})
```
## Finding Patterns
```bash
# Find similar tests
grep -r "KSampler" browser_tests/tests/
# Find usage of a fixture method
grep -r "loadWorkflow" browser_tests/tests/
# Find tests with specific tag
grep -r '@screenshot' browser_tests/tests/
```
## Key Files to Read
| Purpose | Path |
| ----------------- | ------------------------------------------ |
| Main fixture | `browser_tests/fixtures/ComfyPage.ts` |
| Helper classes | `browser_tests/fixtures/helpers/` |
| Component objects | `browser_tests/fixtures/components/` |
| Test selectors | `browser_tests/fixtures/selectors.ts` |
| Vue Node helpers | `browser_tests/fixtures/VueNodeHelpers.ts` |
| Test assets | `browser_tests/assets/` |
| Existing tests | `browser_tests/tests/` |
**Read the fixture code directly** - it's the source of truth for available methods.

View File

@@ -1,81 +0,0 @@
name: 'CI: Dist Telemetry Scan'
on:
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project
run: pnpm build
env:
DISTRIBUTION: localhost
- name: Scan dist for GTM telemetry references
run: |
set -euo pipefail
echo '🔍 Scanning for Google Tag Manager references...'
if rg --no-ignore -n \
-g '*.html' \
-g '*.js' \
-e 'Google Tag Manager' \
-e '(?i)\bgtm\.js\b' \
-e '(?i)googletagmanager\.com/gtm\.js\\?id=' \
-e '(?i)googletagmanager\.com/ns\.html\\?id=' \
dist; then
echo '❌ ERROR: Google Tag Manager references found in dist assets!'
echo 'GTM must be properly tree-shaken from OSS builds.'
exit 1
fi
echo '✅ No GTM references found'
- name: Scan dist for Mixpanel telemetry references
run: |
set -euo pipefail
echo '🔍 Scanning for Mixpanel references...'
if rg --no-ignore -n \
-g '*.html' \
-g '*.js' \
-e '(?i)mixpanel\.init' \
-e '(?i)mixpanel\.identify' \
-e 'MixpanelTelemetryProvider' \
-e 'mp\.comfy\.org' \
-e 'mixpanel-browser' \
-e '(?i)mixpanel\.track\(' \
dist; then
echo '❌ ERROR: Mixpanel references found in dist assets!'
echo 'Mixpanel must be properly tree-shaken from OSS builds.'
echo ''
echo 'To fix this:'
echo '1. Use the TelemetryProvider pattern (see src/platform/telemetry/)'
echo '2. Call telemetry via useTelemetry() hook'
echo '3. Use conditional dynamic imports behind isCloud checks'
exit 1
fi
echo '✅ No Mixpanel references found'

View File

@@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }}
token: ${{ !github.event.pull_request.head.repo.fork && secrets.PR_GH_TOKEN || github.token }}
token: ${{ secrets.PR_GH_TOKEN }}
- name: Setup frontend
uses: ./.github/actions/setup-frontend

2
.gitignore vendored
View File

@@ -96,5 +96,3 @@ vitest.config.*.timestamp*
# Weekly docs check output
/output.txt
.amp

View File

@@ -60,6 +60,11 @@
{
"name": "primevue/sidebar",
"message": "Sidebar is deprecated in PrimeVue 4+. Use Drawer instead: import Drawer from 'primevue/drawer'"
},
{
"name": "@/i18n--to-enable",
"importNames": ["st", "t", "te", "d"],
"message": "Don't import `@/i18n` directly, prefer `useI18n()`"
}
]
}
@@ -96,7 +101,6 @@
"typescript/restrict-template-expressions": "off",
"typescript/unbound-method": "off",
"typescript/no-floating-promises": "error",
"typescript/no-explicit-any": "error",
"vue/no-import-compiler-macros": "error",
"vue/no-dupe-keys": "error"
},

View File

@@ -98,10 +98,12 @@ const config: StorybookConfig = {
},
build: {
rolldownOptions: {
experimental: {
strictExecutionOrder: true
},
treeshake: false,
output: {
keepNames: true,
strictExecutionOrder: true
keepNames: true
},
onwarn: (warning, warn) => {
// Suppress specific warnings

View File

@@ -90,6 +90,7 @@ const preview: Preview = {
{ value: 'light', icon: 'sun', title: 'Light' },
{ value: 'dark', icon: 'moon', title: 'Dark' }
],
showName: true,
dynamicTitle: true
}
}

View File

@@ -52,15 +52,14 @@
"reference",
"plugin",
"custom-variant",
"utility",
"source"
"utility"
]
}
],
"function-no-unknown": [
true,
{
"ignoreFunctions": ["theme", "v-bind", "from-folder", "from-json"]
"ignoreFunctions": ["theme", "v-bind"]
}
]
},

View File

@@ -2,57 +2,57 @@
* @Comfy-org/comfy_frontend_devs
# Desktop/Electron
/apps/desktop-ui/ @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/stores/electronDownloadStore.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/extensions/core/electronAdapter.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
/vite.electron.config.mts @benceruleanlu @Comfy-org/comfy_frontend_devs
/apps/desktop-ui/ @benceruleanlu
/src/stores/electronDownloadStore.ts @benceruleanlu
/src/extensions/core/electronAdapter.ts @benceruleanlu
/vite.electron.config.mts @benceruleanlu
# Common UI Components
/src/components/chip/ @viva-jinyi @Comfy-org/comfy_frontend_devs
/src/components/card/ @viva-jinyi @Comfy-org/comfy_frontend_devs
/src/components/button/ @viva-jinyi @Comfy-org/comfy_frontend_devs
/src/components/input/ @viva-jinyi @Comfy-org/comfy_frontend_devs
/src/components/chip/ @viva-jinyi
/src/components/card/ @viva-jinyi
/src/components/button/ @viva-jinyi
/src/components/input/ @viva-jinyi
# Topbar
/src/components/topbar/ @pythongosssss @Comfy-org/comfy_frontend_devs
/src/components/topbar/ @pythongosssss
# Thumbnail
/src/renderer/core/thumbnail/ @pythongosssss @Comfy-org/comfy_frontend_devs
/src/renderer/core/thumbnail/ @pythongosssss
# Legacy UI
/scripts/ui/ @pythongosssss @Comfy-org/comfy_frontend_devs
/scripts/ui/ @pythongosssss
# Link rendering
/src/renderer/core/canvas/links/ @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/renderer/core/canvas/links/ @benceruleanlu
# Partner Nodes
/src/composables/node/useNodePricing.ts @jojodecayz @bigcat88 @Comfy-org/comfy_frontend_devs
/src/composables/node/useNodePricing.ts @jojodecayz @bigcat88
# Node help system
/src/utils/nodeHelpUtil.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/stores/workspace/nodeHelpStore.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/services/nodeHelpService.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/utils/nodeHelpUtil.ts @benceruleanlu
/src/stores/workspace/nodeHelpStore.ts @benceruleanlu
/src/services/nodeHelpService.ts @benceruleanlu
# Selection toolbox
/src/components/graph/selectionToolbox/ @Myestery @Comfy-org/comfy_frontend_devs
/src/components/graph/selectionToolbox/ @Myestery
# Minimap
/src/renderer/extensions/minimap/ @jtydhr88 @Myestery @Comfy-org/comfy_frontend_devs
/src/renderer/extensions/minimap/ @jtydhr88 @Myestery
# Workflow Templates
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki @Comfy-org/comfy_frontend_devs
/src/components/templates/ @Myestery @christian-byrne @comfyui-wiki @Comfy-org/comfy_frontend_devs
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
/src/components/templates/ @Myestery @christian-byrne @comfyui-wiki
# Mask Editor
/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp @Comfy-org/comfy_frontend_devs
/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp @Comfy-org/comfy_frontend_devs
/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp
/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp
# 3D
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-org/comfy_frontend_devs
/src/components/load3d/ @jtydhr88 @Comfy-org/comfy_frontend_devs
/src/extensions/core/load3d.ts @jtydhr88
/src/components/load3d/ @jtydhr88
# Manager
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata @Comfy-org/comfy_frontend_devs
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
# Translations
/src/locales/ @Comfy-Org/comfy_maintainer @Comfy-org/comfy_frontend_devs

View File

@@ -201,7 +201,7 @@ The project supports three types of icons, all with automatic imports (no manual
2. **Iconify Icons** - 200,000+ icons from various libraries: `<i class="icon-[lucide--settings]" />`, `<i class="icon-[mdi--folder]" />`
3. **Custom Icons** - Your own SVG icons: `<i-comfy:workflow />`
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Tailwind CSS icon classes (`icon-[comfy--template]`) are provided by `@iconify/tailwind4`, configured in `packages/design-system/src/css/style.css`. Custom icons are stored in `packages/design-system/src/icons/` and loaded via `from-folder` at build time.
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `packages/design-system/src/icons/` and processed by `packages/design-system/src/iconCollection.ts` with automatic validation.
For detailed instructions and code examples, see [packages/design-system/src/icons/README.md](packages/design-system/src/icons/README.md).

View File

@@ -4,40 +4,11 @@ See `@docs/guidance/playwright.md` for Playwright best practices (auto-loaded fo
## Directory Structure
```text
browser_tests/
├── assets/ - Test data (JSON workflows, images)
├── fixtures/
│ ├── ComfyPage.ts - Main fixture (delegates to helpers)
│ ├── ComfyMouse.ts - Mouse interaction helper
│ ├── VueNodeHelpers.ts - Vue Nodes 2.0 helpers
│ ├── selectors.ts - Centralized TestIds
│ ├── components/ - Page object components
│ │ ├── ContextMenu.ts
│ │ ├── SettingDialog.ts
│ │ ├── SidebarTab.ts
│ │ └── Topbar.ts
│ ├── helpers/ - Focused helper classes
│ │ ├── CanvasHelper.ts
│ │ ├── CommandHelper.ts
│ │ ├── KeyboardHelper.ts
│ │ ├── NodeOperationsHelper.ts
│ │ ├── SettingsHelper.ts
│ │ ├── WorkflowHelper.ts
│ │ └── ...
│ └── utils/ - Utility functions
├── helpers/ - Test-specific utilities
└── tests/ - Test files (*.spec.ts)
```
- `assets/` - Test data (JSON workflows, fixtures)
- Tests use premade JSON workflows to load desired graph state
## After Making Changes
- Run `pnpm typecheck:browser` after modifying TypeScript files in this directory
- Run `pnpm exec eslint browser_tests/path/to/file.ts` to lint specific files
- Run `pnpm exec oxlint browser_tests/path/to/file.ts` to check with oxlint
## Skill Documentation
A Playwright test-writing skill exists at `.claude/skills/writing-playwright-tests/SKILL.md`.
The skill documents **meta-level guidance only** (gotchas, anti-patterns, decision guides). It does **not** duplicate fixture APIs - agents should read the fixture code directly in `browser_tests/fixtures/`.

View File

@@ -1,205 +0,0 @@
{
"last_node_id": 7,
"last_link_id": 5,
"nodes": [
{
"id": 1,
"type": "T2IAdapterLoader",
"pos": [100, 100],
"size": [300, 80],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "CONTROL_NET",
"type": "CONTROL_NET",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "T2IAdapterLoader"
},
"widgets_values": ["t2iadapter_model.safetensors"]
},
{
"id": 2,
"type": "CheckpointLoaderSimple",
"pos": [100, 300],
"size": [315, 98],
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
},
{
"id": 3,
"type": "ResizeImagesByLongerEdge",
"pos": [500, 100],
"size": [300, 80],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [1],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ResizeImagesByLongerEdge"
},
"widgets_values": [1024]
},
{
"id": 4,
"type": "ImageScaleBy",
"pos": [500, 280],
"size": [300, 80],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 1
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [2, 3],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ImageScaleBy"
},
"widgets_values": ["lanczos", 1.5]
},
{
"id": 5,
"type": "ImageBatch",
"pos": [900, 100],
"size": [300, 80],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "image1",
"type": "IMAGE",
"link": 2
},
{
"name": "image2",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [4],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ImageBatch"
},
"widgets_values": []
},
{
"id": 6,
"type": "SaveImage",
"pos": [900, 300],
"size": [300, 80],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 3
}
],
"properties": {
"Node name for S&R": "SaveImage"
},
"widgets_values": ["ComfyUI"]
},
{
"id": 7,
"type": "PreviewImage",
"pos": [1250, 100],
"size": [300, 250],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 4
}
],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
}
],
"links": [
[1, 3, 0, 4, 0, "IMAGE"],
[2, 4, 0, 5, 0, "IMAGE"],
[3, 4, 0, 6, 0, "IMAGE"],
[4, 5, 0, 7, 0, "IMAGE"]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [0, 0]
}
},
"version": 0.4
}

View File

@@ -1,186 +0,0 @@
{
"last_node_id": 5,
"last_link_id": 2,
"nodes": [
{
"id": 1,
"type": "Load3DAnimation",
"pos": [100, 100],
"size": [300, 100],
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MESH",
"type": "MESH",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "Load3DAnimation"
},
"widgets_values": ["model.glb"]
},
{
"id": 2,
"type": "Preview3DAnimation",
"pos": [450, 100],
"size": [300, 100],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "mesh",
"type": "MESH",
"link": null
}
],
"properties": {
"Node name for S&R": "Preview3DAnimation"
},
"widgets_values": []
},
{
"id": 3,
"type": "ConditioningAverage ",
"pos": [100, 300],
"size": [300, 100],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "conditioning_to",
"type": "CONDITIONING",
"link": null
},
{
"name": "conditioning_from",
"type": "CONDITIONING",
"link": null
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [1],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ConditioningAverage "
},
"widgets_values": [1]
},
{
"id": 4,
"type": "SDV_img2vid_Conditioning",
"pos": [450, 300],
"size": [300, 150],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": null
},
{
"name": "init_image",
"type": "IMAGE",
"link": null
},
{
"name": "vae",
"type": "VAE",
"link": null
}
],
"outputs": [
{
"name": "positive",
"type": "CONDITIONING",
"links": [],
"slot_index": 0
},
{
"name": "negative",
"type": "CONDITIONING",
"links": [],
"slot_index": 1
},
{
"name": "latent",
"type": "LATENT",
"links": [2],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "SDV_img2vid_Conditioning"
},
"widgets_values": [1024, 576, 14, 127, 25, 0.02]
},
{
"id": 5,
"type": "KSampler",
"pos": [800, 300],
"size": [300, 262],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 1
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [42, "fixed", 20, 8, "euler", "normal", 1]
}
],
"links": [
[1, 3, 0, 5, 1, "CONDITIONING"],
[2, 4, 2, 5, 3, "LATENT"]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [0, 0]
}
},
"version": 0.4
}

View File

@@ -1,599 +0,0 @@
{
"id": "9a37f747-e96b-4304-9212-7abcaad7bdac",
"revision": 0,
"last_node_id": 5,
"last_link_id": 5,
"nodes": [
{
"id": 5,
"type": "1e38d8ea-45e1-48a5-aa20-966584201867",
"pos": [788, 433.5],
"size": [210, 108],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "string_a",
"type": "STRING",
"widget": {
"name": "string_a"
},
"link": 4
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [5]
}
],
"properties": {
"proxyWidgets": [["-1", "string_a"]]
},
"widgets_values": [""]
},
{
"id": 2,
"type": "PreviewAny",
"pos": [1135, 429],
"size": [250, 145.5],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 5
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewAny"
},
"widgets_values": [null, null, false]
},
{
"id": 1,
"type": "PrimitiveStringMultiline",
"pos": [456, 450],
"size": [225, 121.5],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [4]
}
],
"properties": {
"Node name for S&R": "PrimitiveStringMultiline"
},
"widgets_values": ["Outer\n"]
}
],
"links": [
[4, 1, 0, 5, 0, "STRING"],
[5, 5, 0, 2, 0, "STRING"]
],
"groups": [],
"definitions": {
"subgraphs": [
{
"id": "1e38d8ea-45e1-48a5-aa20-966584201867",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 6,
"lastLinkId": 9,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "New Subgraph",
"inputNode": {
"id": -10,
"bounding": [351, 432.5, 120, 60]
},
"outputNode": {
"id": -20,
"bounding": [1315, 432.5, 120, 60]
},
"inputs": [
{
"id": "7bf3e1d4-0521-4b5c-92f5-47ca598b7eb4",
"name": "string_a",
"type": "STRING",
"linkIds": [1],
"localized_name": "string_a",
"pos": [451, 452.5]
}
],
"outputs": [
{
"id": "fbe975ba-d7c2-471e-a99a-a1e2c6ab466d",
"name": "STRING",
"type": "STRING",
"linkIds": [9],
"localized_name": "STRING",
"pos": [1335, 452.5]
}
],
"widgets": [],
"nodes": [
{
"id": 3,
"type": "StringConcatenate",
"pos": [815, 373],
"size": [347, 231],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"localized_name": "string_a",
"name": "string_a",
"type": "STRING",
"widget": {
"name": "string_a"
},
"link": 1
},
{
"localized_name": "string_b",
"name": "string_b",
"type": "STRING",
"widget": {
"name": "string_b"
},
"link": 2
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [7]
}
],
"properties": {
"Node name for S&R": "StringConcatenate"
},
"widgets_values": ["", "", ""]
},
{
"id": 6,
"type": "9be42452-056b-4c99-9f9f-7381d11c4454",
"pos": [955, 775],
"size": [210, 88],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "string_a",
"name": "string_a",
"type": "STRING",
"widget": {
"name": "string_a"
},
"link": 7
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [9]
}
],
"properties": {
"proxyWidgets": [["-1", "string_a"]]
},
"widgets_values": [""]
},
{
"id": 4,
"type": "PrimitiveStringMultiline",
"pos": [313, 685],
"size": [325, 109],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [2]
}
],
"properties": {
"Node name for S&R": "PrimitiveStringMultiline"
},
"widgets_values": ["Inner 1\n"]
}
],
"groups": [],
"links": [
{
"id": 2,
"origin_id": 4,
"origin_slot": 0,
"target_id": 3,
"target_slot": 1,
"type": "STRING"
},
{
"id": 1,
"origin_id": -10,
"origin_slot": 0,
"target_id": 3,
"target_slot": 0,
"type": "STRING"
},
{
"id": 7,
"origin_id": 3,
"origin_slot": 0,
"target_id": 6,
"target_slot": 0,
"type": "STRING"
},
{
"id": 6,
"origin_id": 6,
"origin_slot": 0,
"target_id": -20,
"target_slot": 1,
"type": "STRING"
},
{
"id": 9,
"origin_id": 6,
"origin_slot": 0,
"target_id": -20,
"target_slot": 0,
"type": "STRING"
}
],
"extra": {}
},
{
"id": "9be42452-056b-4c99-9f9f-7381d11c4454",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 9,
"lastLinkId": 12,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "New Subgraph",
"inputNode": {
"id": -10,
"bounding": [680, 774, 120, 60]
},
"outputNode": {
"id": -20,
"bounding": [1320, 774, 120, 60]
},
"inputs": [
{
"id": "01c05c51-86b5-4bad-b32f-9c911683a13d",
"name": "string_a",
"type": "STRING",
"linkIds": [4],
"localized_name": "string_a",
"pos": [780, 794]
}
],
"outputs": [
{
"id": "a8bcf3bf-a66a-4c71-8d92-17a2a4d03686",
"name": "STRING",
"type": "STRING",
"linkIds": [12],
"localized_name": "STRING",
"pos": [1340, 794]
}
],
"widgets": [],
"nodes": [
{
"id": 5,
"type": "StringConcatenate",
"pos": [860, 719],
"size": [400, 200],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"localized_name": "string_a",
"name": "string_a",
"type": "STRING",
"widget": {
"name": "string_a"
},
"link": 4
},
{
"localized_name": "string_b",
"name": "string_b",
"type": "STRING",
"widget": {
"name": "string_b"
},
"link": 7
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [11]
}
],
"properties": {
"Node name for S&R": "StringConcatenate"
},
"widgets_values": ["", "", ""]
},
{
"id": 6,
"type": "PrimitiveStringMultiline",
"pos": [401, 973],
"size": [400, 200],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [7]
}
],
"properties": {
"Node name for S&R": "PrimitiveStringMultiline"
},
"widgets_values": ["Inner 2\n"]
},
{
"id": 9,
"type": "7c2915a5-5eb8-4958-a8fd-4beb30f370ce",
"pos": [1046, 985],
"size": [210, 88],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "string_a",
"name": "string_a",
"type": "STRING",
"widget": {
"name": "string_a"
},
"link": 11
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [12]
}
],
"properties": {
"proxyWidgets": [["-1", "string_a"]]
},
"widgets_values": [""]
}
],
"groups": [],
"links": [
{
"id": 4,
"origin_id": -10,
"origin_slot": 0,
"target_id": 5,
"target_slot": 0,
"type": "STRING"
},
{
"id": 7,
"origin_id": 6,
"origin_slot": 0,
"target_id": 5,
"target_slot": 1,
"type": "STRING"
},
{
"id": 11,
"origin_id": 5,
"origin_slot": 0,
"target_id": 9,
"target_slot": 0,
"type": "STRING"
},
{
"id": 10,
"origin_id": 9,
"origin_slot": 0,
"target_id": -20,
"target_slot": 0,
"type": "STRING"
},
{
"id": 12,
"origin_id": 9,
"origin_slot": 0,
"target_id": -20,
"target_slot": 0,
"type": "STRING"
}
],
"extra": {}
},
{
"id": "7c2915a5-5eb8-4958-a8fd-4beb30f370ce",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 8,
"lastLinkId": 10,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "New Subgraph",
"inputNode": {
"id": -10,
"bounding": [262, 1222, 120, 60]
},
"outputNode": {
"id": -20,
"bounding": [1330, 1222, 120, 60]
},
"inputs": [
{
"id": "934a8baa-d79c-428c-8ec9-814ad437d7c7",
"name": "string_a",
"type": "STRING",
"linkIds": [9],
"localized_name": "string_a",
"pos": [362, 1242]
}
],
"outputs": [
{
"id": "4c3d243b-9ff6-4dcd-9dbf-e4ec8e1fc879",
"name": "STRING",
"type": "STRING",
"linkIds": [10],
"localized_name": "STRING",
"pos": [1350, 1242]
}
],
"widgets": [],
"nodes": [
{
"id": 7,
"type": "StringConcatenate",
"pos": [870, 1038],
"size": [400, 200],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "string_a",
"name": "string_a",
"type": "STRING",
"widget": {
"name": "string_a"
},
"link": 9
},
{
"localized_name": "string_b",
"name": "string_b",
"type": "STRING",
"widget": {
"name": "string_b"
},
"link": 8
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [10]
}
],
"properties": {
"Node name for S&R": "StringConcatenate"
},
"widgets_values": ["", "", ""]
},
{
"id": 8,
"type": "PrimitiveStringMultiline",
"pos": [442, 1296],
"size": [400, 200],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [8]
}
],
"properties": {
"Node name for S&R": "PrimitiveStringMultiline"
},
"widgets_values": ["Inner 3\n"]
}
],
"groups": [],
"links": [
{
"id": 8,
"origin_id": 8,
"origin_slot": 0,
"target_id": 7,
"target_slot": 1,
"type": "STRING"
},
{
"id": 9,
"origin_id": -10,
"origin_slot": 0,
"target_id": 7,
"target_slot": 0,
"type": "STRING"
},
{
"id": 10,
"origin_id": 7,
"origin_slot": 0,
"target_id": -20,
"target_slot": 0,
"type": "STRING"
}
],
"extra": {}
}
]
},
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [-7, 144]
},
"frontendVersion": "1.38.13"
},
"version": 0.4
}

View File

@@ -1,86 +0,0 @@
{
"id": "save-image-and-webm-test",
"revision": 0,
"last_node_id": 12,
"last_link_id": 2,
"nodes": [
{
"id": 10,
"type": "LoadImage",
"pos": [50, 100],
"size": [315, 314],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [1, 2]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": ["example.png", "image"]
},
{
"id": 11,
"type": "SaveImage",
"pos": [450, 100],
"size": [210, 270],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 1
}
],
"outputs": [],
"properties": {},
"widgets_values": ["ComfyUI"]
},
{
"id": 12,
"type": "SaveWEBM",
"pos": [450, 450],
"size": [210, 368],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 2
}
],
"outputs": [],
"properties": {},
"widgets_values": ["ComfyUI", "vp9", 6, 32]
}
],
"links": [
[1, 10, 0, 11, 0, "IMAGE"],
[2, 10, 0, 12, 0, "IMAGE"]
],
"groups": [],
"config": {},
"extra": {
"frontendVersion": "1.17.0",
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"version": 0.4
}

View File

@@ -14,7 +14,6 @@ import { ComfyTemplates } from '../helpers/templates'
import { ComfyMouse } from './ComfyMouse'
import { VueNodeHelpers } from './VueNodeHelpers'
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
import { ComfyNodeSearchBoxV2 } from './components/ComfyNodeSearchBoxV2'
import { ContextMenu } from './components/ContextMenu'
import { SettingDialog } from './components/SettingDialog'
import { BottomPanel } from './components/BottomPanel'
@@ -167,7 +166,6 @@ export class ComfyPage {
// Components
public readonly searchBox: ComfyNodeSearchBox
public readonly searchBoxV2: ComfyNodeSearchBoxV2
public readonly menu: ComfyMenu
public readonly actionbar: ComfyActionbar
public readonly templates: ComfyTemplates
@@ -212,7 +210,6 @@ export class ComfyPage {
this.workflowUploadInput = page.locator('#comfy-file-input')
this.searchBox = new ComfyNodeSearchBox(page)
this.searchBoxV2 = new ComfyNodeSearchBoxV2(page)
this.menu = new ComfyMenu(page)
this.actionbar = new ComfyActionbar(page)
this.templates = new ComfyTemplates(page)

View File

@@ -1,29 +0,0 @@
import type { Locator, Page } from '@playwright/test'
import type { ComfyPage } from '../ComfyPage'
export class ComfyNodeSearchBoxV2 {
readonly dialog: Locator
readonly input: Locator
readonly results: Locator
readonly filterOptions: Locator
constructor(readonly page: Page) {
this.dialog = page.getByRole('search')
this.input = this.dialog.locator('input[type="text"]')
this.results = this.dialog.getByTestId('result-item')
this.filterOptions = this.dialog.getByTestId('filter-option')
}
categoryButton(categoryId: string): Locator {
return this.dialog.getByTestId(`category-${categoryId}`)
}
filterBarButton(name: string): Locator {
return this.dialog.getByRole('button', { name })
}
async reload(comfyPage: ComfyPage) {
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
}
}

View File

@@ -23,7 +23,9 @@ export class SettingDialog extends BaseDialog {
* @param value - The value to set
*/
async setStringSetting(id: string, value: string) {
const settingInputDiv = this.root.locator(`div[id="${id}"]`)
const settingInputDiv = this.page.locator(
`div.settings-container div[id="${id}"]`
)
await settingInputDiv.locator('input').fill(value)
}
@@ -32,31 +34,16 @@ export class SettingDialog extends BaseDialog {
* @param id - The id of the setting
*/
async toggleBooleanSetting(id: string) {
const settingInputDiv = this.root.locator(`div[id="${id}"]`)
const settingInputDiv = this.page.locator(
`div.settings-container div[id="${id}"]`
)
await settingInputDiv.locator('input').click()
}
get searchBox() {
return this.root.getByPlaceholder(/Search/)
}
get categories() {
return this.root.locator('nav').getByRole('button')
}
category(name: string) {
return this.root.locator('nav').getByRole('button', { name })
}
get contentArea() {
return this.root.getByRole('main')
}
async goToAboutPanel() {
const aboutButton = this.root.locator('nav').getByRole('button', {
name: 'About'
})
await aboutButton.click()
await this.page.waitForSelector('.about-container')
await this.page.getByTestId(TestIds.dialogs.settingsTabAbout).click()
await this.page
.getByTestId(TestIds.dialogs.about)
.waitFor({ state: 'visible' })
}
}

View File

@@ -226,7 +226,9 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
await expect(bottomPanel.shortcuts.manageButton).toBeVisible()
await bottomPanel.shortcuts.manageButton.click()
await expect(comfyPage.settingDialog.root).toBeVisible()
await expect(comfyPage.settingDialog.category('Keybinding')).toBeVisible()
await expect(comfyPage.page.getByRole('dialog')).toBeVisible()
await expect(
comfyPage.page.getByRole('option', { name: 'Keybinding' })
).toBeVisible()
})
})

View File

@@ -244,9 +244,21 @@ test.describe(
await comfyPage.settings.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
const parsed = await comfyPage.page.evaluate(() => {
return window['app'].graph.serialize()
})
const parsed = await (
await comfyPage.page.waitForFunction(
() => {
const workflow = localStorage.getItem('workflow')
if (!workflow) return null
try {
const data = JSON.parse(workflow)
return Array.isArray(data?.nodes) ? data : null
} catch {
return null
}
},
{ timeout: 3000 }
)
).jsonValue()
expect(parsed.nodes).toBeDefined()
expect(Array.isArray(parsed.nodes)).toBe(true)
for (const node of parsed.nodes) {

View File

@@ -1,32 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Confirm dialog text wrapping', { tag: ['@mobile'] }, () => {
test('@mobile confirm dialog buttons are visible with long unbreakable text', async ({
comfyPage
}) => {
const longFilename = 'workflow_checkpoint_' + 'a'.repeat(200) + '.json'
await comfyPage.page.evaluate((msg) => {
window
.app!.extensionManager.dialog.confirm({
title: 'Confirm',
type: 'default',
message: msg
})
.catch(() => {})
}, longFilename)
const dialog = comfyPage.page.getByRole('dialog')
await expect(dialog).toBeVisible()
const confirmButton = dialog.getByRole('button', { name: 'Confirm' })
await expect(confirmButton).toBeVisible()
await expect(confirmButton).toBeInViewport()
const cancelButton = dialog.getByRole('button', { name: 'Cancel' })
await expect(cancelButton).toBeVisible()
await expect(cancelButton).toBeInViewport()
})
})

View File

@@ -37,7 +37,7 @@ test.describe('Load workflow warning', { tag: '@ui' }, () => {
})
test('Does not report warning on undo/redo', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'v1 (legacy)')
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
await comfyPage.page
@@ -61,21 +61,16 @@ test('Does not report warning on undo/redo', async ({ comfyPage }) => {
})
test.describe('Execution error', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setup()
})
test('Should display an error message when an execution error occurs', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('nodes/execution_error')
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
// Wait for the error overlay to be visible
const errorOverlay = comfyPage.page.locator('[data-testid="error-overlay"]')
await expect(errorOverlay).toBeVisible()
// Wait for the element with the .comfy-execution-error selector to be visible
const executionError = comfyPage.page.locator('.comfy-error-report')
await expect(executionError).toBeVisible()
})
})
@@ -249,13 +244,9 @@ test.describe('Missing models warning', () => {
test.describe('Settings', () => {
test('@mobile Should be visible on mobile', async ({ comfyPage }) => {
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator(
'[data-testid="settings-dialog"]'
)
await expect(settingsDialog).toBeVisible()
const contentArea = settingsDialog.locator('main')
await expect(contentArea).toBeVisible()
const isUsableHeight = await contentArea.evaluate(
const settingsContent = comfyPage.page.locator('.settings-content')
await expect(settingsContent).toBeVisible()
const isUsableHeight = await settingsContent.evaluate(
(el) => el.clientHeight > 30
)
expect(isUsableHeight).toBeTruthy()
@@ -265,9 +256,7 @@ test.describe('Settings', () => {
await comfyPage.page.keyboard.down('ControlOrMeta')
await comfyPage.page.keyboard.press(',')
await comfyPage.page.keyboard.up('ControlOrMeta')
const settingsLocator = comfyPage.page.locator(
'[data-testid="settings-dialog"]'
)
const settingsLocator = comfyPage.page.locator('.settings-container')
await expect(settingsLocator).toBeVisible()
await comfyPage.page.keyboard.press('Escape')
await expect(settingsLocator).not.toBeVisible()
@@ -286,15 +275,10 @@ test.describe('Settings', () => {
test('Should persist keybinding setting', async ({ comfyPage }) => {
// Open the settings dialog
await comfyPage.page.keyboard.press('Control+,')
await comfyPage.page.waitForSelector('[data-testid="settings-dialog"]')
await comfyPage.page.waitForSelector('.settings-container')
// Open the keybinding tab
const settingsDialog = comfyPage.page.locator(
'[data-testid="settings-dialog"]'
)
await settingsDialog
.locator('nav [role="button"]', { hasText: 'Keybinding' })
.click()
await comfyPage.page.getByLabel('Keybinding').click()
await comfyPage.page.waitForSelector(
'[placeholder="Search Keybindings..."]'
)
@@ -314,10 +298,7 @@ test.describe('Settings', () => {
await input.press('Alt+n')
const requestPromise = comfyPage.page.waitForRequest(
(req) =>
req.url().includes('/api/settings') &&
!req.url().includes('/api/settings/') &&
req.method() === 'POST'
'**/api/settings/Comfy.Keybinding.NewBindings'
)
// Save keybinding
@@ -345,23 +326,17 @@ test.describe('Support', () => {
comfyPage
}) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
// Prevent loading the external page
await comfyPage.page
.context()
.route('https://support.comfy.org/**', (route) =>
route.fulfill({ body: '<html></html>', contentType: 'text/html' })
)
const popupPromise = comfyPage.page.waitForEvent('popup')
const pagePromise = comfyPage.page.context().waitForEvent('page')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
const popup = await popupPromise
const newPage = await pagePromise
const url = new URL(popup.url())
expect(url.hostname).toBe('support.comfy.org')
await newPage.waitForLoadState('networkidle')
await expect(newPage).toHaveURL(/.*support\.comfy\.org.*/)
const url = new URL(newPage.url())
expect(url.searchParams.get('tf_42243568391700')).toBe('oss')
await popup.close()
await newPage.close()
})
})

View File

@@ -7,29 +7,22 @@ test.beforeEach(async ({ comfyPage }) => {
})
test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setup()
})
test(
'Report error on unconnected slot',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.canvasOps.disconnectEdge()
await comfyPage.page.keyboard.press('Escape')
await comfyPage.canvasOps.clickEmptySpace()
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
await expect(
comfyPage.page.locator('[data-testid="error-overlay"]')
).toBeVisible()
await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible()
await comfyPage.page
.locator('[data-testid="error-overlay"]')
.getByRole('button', { name: 'Dismiss' })
.locator('.p-dialog')
.getByRole('button', { name: 'Close' })
.click()
await comfyPage.page
.locator('[data-testid="error-overlay"]')
.waitFor({ state: 'hidden' })
await comfyPage.page.locator('.comfy-error-report').waitFor({
state: 'hidden'
})
await expect(comfyPage.canvas).toHaveScreenshot(
'execution-error-unconnected-slot.png'
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -37,9 +37,12 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
// Monitor for server feature flags
const checkInterval = setInterval(() => {
const flags = window.app?.api?.serverFeatureFlags?.value
if (flags && Object.keys(flags).length > 0) {
window.__capturedMessages!.serverFeatureFlags = flags
if (
window.app?.api?.serverFeatureFlags &&
Object.keys(window.app.api.serverFeatureFlags).length > 0
) {
window.__capturedMessages!.serverFeatureFlags =
window.app.api.serverFeatureFlags
clearInterval(checkInterval)
}
}, 100)
@@ -93,7 +96,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
}) => {
// Get the actual server feature flags from the backend
const serverFlags = await comfyPage.page.evaluate(() => {
return window.app!.api.serverFeatureFlags.value
return window.app!.api.serverFeatureFlags
})
// Verify we received real feature flags from the backend
@@ -126,8 +129,8 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
// Test that the method only returns true for boolean true values
const testResults = await comfyPage.page.evaluate(() => {
// Temporarily modify serverFeatureFlags to test behavior
const original = window.app!.api.serverFeatureFlags.value
window.app!.api.serverFeatureFlags.value = {
const original = window.app!.api.serverFeatureFlags
window.app!.api.serverFeatureFlags = {
bool_true: true,
bool_false: false,
string_value: 'yes',
@@ -144,7 +147,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
}
// Restore original
window.app!.api.serverFeatureFlags.value = original
window.app!.api.serverFeatureFlags = original
return results
})
@@ -279,8 +282,8 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
// Monitor when feature flags arrive by checking periodically
const checkFeatureFlags = setInterval(() => {
if (
window.app?.api?.serverFeatureFlags?.value
?.supports_preview_metadata !== undefined
window.app?.api?.serverFeatureFlags?.supports_preview_metadata !==
undefined
) {
window.__appReadiness!.featureFlagsReceived = true
clearInterval(checkFeatureFlags)
@@ -317,8 +320,8 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
// Wait for feature flags to be received
await newPage.waitForFunction(
() =>
window.app?.api?.serverFeatureFlags?.value
?.supports_preview_metadata !== undefined,
window.app?.api?.serverFeatureFlags?.supports_preview_metadata !==
undefined,
{
timeout: 10000
}
@@ -328,7 +331,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
const readiness = await newPage.evaluate(() => {
return {
...window.__appReadiness,
currentFlags: window.app!.api.serverFeatureFlags.value
currentFlags: window.app!.api.serverFeatureFlags
}
})

View File

@@ -10,8 +10,6 @@ import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'v1 (legacy)')
})
test.describe('Group Node', { tag: '@node' }, () => {

View File

@@ -13,9 +13,6 @@ import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
// Wait for the legacy menu to appear and canvas to settle after layout shift.
await comfyPage.page.locator('.comfy-menu').waitFor({ state: 'visible' })
await comfyPage.nextFrame()
})
test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => {
@@ -736,25 +733,6 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png'
)
// Wait for V2 persistence debounce to save the modified workflow
const start = Date.now()
await comfyPage.page.waitForFunction((since) => {
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i)
if (!key?.startsWith('Comfy.Workflow.DraftIndex.v2:')) continue
const json = window.localStorage.getItem(key)
if (!json) continue
try {
const index = JSON.parse(json)
if (typeof index.updatedAt === 'number' && index.updatedAt >= since) {
return true
}
} catch {
// ignore
}
}
return false
}, start)
await comfyPage.setup({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png'
@@ -777,17 +755,10 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.saveWorkflow(workflowB)
// Wait for sessionStorage to persist the workflow paths before reloading
// V2 persistence uses sessionStorage with client-scoped keys
await comfyPage.page.waitForFunction(() => {
for (let i = 0; i < window.sessionStorage.length; i++) {
const key = window.sessionStorage.key(i)
if (key?.startsWith('Comfy.Workflow.OpenPaths:')) {
return true
}
}
return false
})
// Wait for localStorage to persist the workflow paths before reloading
await comfyPage.page.waitForFunction(
() => !!window.localStorage.getItem('Comfy.OpenWorkflowsPaths')
)
await comfyPage.setup({ clearStorage: false })
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,162 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
type ComfyPage = Parameters<Parameters<typeof test>[2]>[0]['comfyPage']
async function setVueMode(comfyPage: ComfyPage, enabled: boolean) {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', enabled)
if (enabled) {
await comfyPage.vueNodes.waitForNodes()
}
}
async function addGhostAtCenter(comfyPage: ComfyPage) {
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.nextFrame()
const viewport = comfyPage.page.viewportSize()!
const centerX = Math.round(viewport.width / 2)
const centerY = Math.round(viewport.height / 2)
await comfyPage.page.mouse.move(centerX, centerY)
await comfyPage.nextFrame()
const nodeId = await comfyPage.page.evaluate(
([clientX, clientY]) => {
const node = window.LiteGraph!.createNode('VAEDecode')!
const event = new MouseEvent('click', { clientX, clientY })
window.app!.graph.add(node, { ghost: true, dragEvent: event })
return node.id
},
[centerX, centerY] as const
)
await comfyPage.nextFrame()
return { nodeId, centerX, centerY }
}
function getNodeById(comfyPage: ComfyPage, nodeId: number | string) {
return comfyPage.page.evaluate((id) => {
const node = window.app!.graph.getNodeById(id)
if (!node) return null
return { ghost: !!node.flags.ghost }
}, nodeId)
}
for (const mode of ['litegraph', 'vue'] as const) {
test.describe(`Ghost node placement (${mode} mode)`, () => {
test.beforeEach(async ({ comfyPage }) => {
await setVueMode(comfyPage, mode === 'vue')
})
test('positions ghost node at cursor', async ({ comfyPage }) => {
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.nextFrame()
const viewport = comfyPage.page.viewportSize()!
const centerX = Math.round(viewport.width / 2)
const centerY = Math.round(viewport.height / 2)
await comfyPage.page.mouse.move(centerX, centerY)
await comfyPage.nextFrame()
const result = await comfyPage.page.evaluate(
([clientX, clientY]) => {
const node = window.LiteGraph!.createNode('VAEDecode')!
const event = new MouseEvent('click', { clientX, clientY })
window.app!.graph.add(node, { ghost: true, dragEvent: event })
const canvas = window.app!.canvas
const rect = canvas.canvas.getBoundingClientRect()
const cursorCanvasX =
(clientX - rect.left) / canvas.ds.scale - canvas.ds.offset[0]
const cursorCanvasY =
(clientY - rect.top) / canvas.ds.scale - canvas.ds.offset[1]
return {
diffX: node.pos[0] + node.size[0] / 2 - cursorCanvasX,
diffY: node.pos[1] - 10 - cursorCanvasY
}
},
[centerX, centerY] as const
)
await comfyPage.nextFrame()
expect(Math.abs(result.diffX)).toBeLessThan(5)
expect(Math.abs(result.diffY)).toBeLessThan(5)
})
test('left-click confirms ghost placement', async ({ comfyPage }) => {
const { nodeId, centerX, centerY } = await addGhostAtCenter(comfyPage)
const before = await getNodeById(comfyPage, nodeId)
expect(before).not.toBeNull()
expect(before!.ghost).toBe(true)
await comfyPage.page.mouse.click(centerX, centerY)
await comfyPage.nextFrame()
const after = await getNodeById(comfyPage, nodeId)
expect(after).not.toBeNull()
expect(after!.ghost).toBe(false)
})
test('Escape cancels ghost placement', async ({ comfyPage }) => {
const { nodeId } = await addGhostAtCenter(comfyPage)
const before = await getNodeById(comfyPage, nodeId)
expect(before).not.toBeNull()
expect(before!.ghost).toBe(true)
await comfyPage.page.keyboard.press('Escape')
await comfyPage.nextFrame()
const after = await getNodeById(comfyPage, nodeId)
expect(after).toBeNull()
})
test('Delete cancels ghost placement', async ({ comfyPage }) => {
const { nodeId } = await addGhostAtCenter(comfyPage)
const before = await getNodeById(comfyPage, nodeId)
expect(before).not.toBeNull()
expect(before!.ghost).toBe(true)
await comfyPage.page.keyboard.press('Delete')
await comfyPage.nextFrame()
const after = await getNodeById(comfyPage, nodeId)
expect(after).toBeNull()
})
test('Backspace cancels ghost placement', async ({ comfyPage }) => {
const { nodeId } = await addGhostAtCenter(comfyPage)
const before = await getNodeById(comfyPage, nodeId)
expect(before).not.toBeNull()
expect(before!.ghost).toBe(true)
await comfyPage.page.keyboard.press('Backspace')
await comfyPage.nextFrame()
const after = await getNodeById(comfyPage, nodeId)
expect(after).toBeNull()
})
test('right-click cancels ghost placement', async ({ comfyPage }) => {
const { nodeId, centerX, centerY } = await addGhostAtCenter(comfyPage)
const before = await getNodeById(comfyPage, nodeId)
expect(before).not.toBeNull()
expect(before!.ghost).toBe(true)
await comfyPage.page.mouse.click(centerX, centerY, { button: 'right' })
await comfyPage.nextFrame()
const after = await getNodeById(comfyPage, nodeId)
expect(after).toBeNull()
})
})
}

View File

@@ -27,7 +27,6 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
})
test.describe('Selection Toolbox', () => {

View File

@@ -18,10 +18,7 @@ test.describe('Node search box', { tag: '@node' }, () => {
'Comfy.LinkRelease.ActionShift',
'search box'
)
await comfyPage.settings.setSetting(
'Comfy.NodeSearchBoxImpl',
'v1 (legacy)'
)
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
})
test(`Can trigger on empty canvas double click`, async ({ comfyPage }) => {
@@ -48,10 +45,7 @@ test.describe('Node search box', { tag: '@node' }, () => {
await comfyPage.setup({ clearStorage: true })
// Simulate new user with 1.24.1+ installed version
await comfyPage.settings.setSetting('Comfy.InstalledVersion', '1.24.1')
await comfyPage.settings.setSetting(
'Comfy.NodeSearchBoxImpl',
'v1 (legacy)'
)
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
// Don't set LinkRelease settings explicitly to test versioned defaults
await comfyPage.canvasOps.disconnectEdge()
@@ -221,14 +215,6 @@ test.describe('Node search box', { tag: '@node' }, () => {
await expectFilterChips(comfyPage, ['MODEL', 'CLIP'])
})
test('Does not add duplicate filter with same type and value', async ({
comfyPage
}) => {
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
await expectFilterChips(comfyPage, ['MODEL'])
})
test('Can remove filter', async ({ comfyPage }) => {
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
await comfyPage.searchBox.removeFilter(0)
@@ -291,10 +277,7 @@ test.describe('Release context menu', { tag: '@node' }, () => {
'Comfy.LinkRelease.ActionShift',
'search box'
)
await comfyPage.settings.setSetting(
'Comfy.NodeSearchBoxImpl',
'v1 (legacy)'
)
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
})
test(
@@ -338,10 +321,7 @@ test.describe('Release context menu', { tag: '@node' }, () => {
await comfyPage.setup({ clearStorage: true })
// Simulate existing user with pre-1.24.1 version
await comfyPage.settings.setSetting('Comfy.InstalledVersion', '1.23.0')
await comfyPage.settings.setSetting(
'Comfy.NodeSearchBoxImpl',
'v1 (legacy)'
)
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
// Don't set LinkRelease settings explicitly to test versioned defaults
await comfyPage.canvasOps.disconnectEdge()
@@ -362,10 +342,7 @@ test.describe('Release context menu', { tag: '@node' }, () => {
'Comfy.LinkRelease.Action',
'context menu'
)
await comfyPage.settings.setSetting(
'Comfy.NodeSearchBoxImpl',
'v1 (legacy)'
)
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.canvasOps.disconnectEdge()
// Context menu should appear due to explicit setting, not search box

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -1,149 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
test.describe('Node search box V2', { tag: '@node' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.Action',
'search box'
)
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.ActionShift',
'search box'
)
await comfyPage.searchBoxV2.reload(comfyPage)
})
test('Can open search and add node', async ({ comfyPage }) => {
const { searchBoxV2 } = comfyPage
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
await comfyPage.canvasOps.doubleClick()
await expect(searchBoxV2.input).toBeVisible()
await searchBoxV2.input.fill('KSampler')
await expect(searchBoxV2.results.first()).toBeVisible()
await comfyPage.page.keyboard.press('Enter')
await expect(searchBoxV2.input).not.toBeVisible()
const newCount = await comfyPage.nodeOps.getGraphNodesCount()
expect(newCount).toBe(initialCount + 1)
})
test('Can add first default result with Enter', async ({ comfyPage }) => {
const { searchBoxV2 } = comfyPage
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
await comfyPage.canvasOps.doubleClick()
await expect(searchBoxV2.input).toBeVisible()
// Default results should be visible without typing
await expect(searchBoxV2.results.first()).toBeVisible()
// Enter should add the first (selected) result
await comfyPage.page.keyboard.press('Enter')
await expect(searchBoxV2.input).not.toBeVisible()
const newCount = await comfyPage.nodeOps.getGraphNodesCount()
expect(newCount).toBe(initialCount + 1)
})
test.describe('Category navigation', () => {
test('Favorites shows only bookmarked nodes', async ({ comfyPage }) => {
const { searchBoxV2 } = comfyPage
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'KSampler'
])
await searchBoxV2.reload(comfyPage)
await comfyPage.canvasOps.doubleClick()
await expect(searchBoxV2.input).toBeVisible()
await searchBoxV2.categoryButton('favorites').click()
await expect(searchBoxV2.results).toHaveCount(1)
await expect(searchBoxV2.results.first()).toContainText('KSampler')
})
test('Category filters results to matching nodes', async ({
comfyPage
}) => {
const { searchBoxV2 } = comfyPage
await comfyPage.canvasOps.doubleClick()
await expect(searchBoxV2.input).toBeVisible()
await searchBoxV2.categoryButton('sampling').click()
await expect(searchBoxV2.results.first()).toBeVisible()
const count = await searchBoxV2.results.count()
expect(count).toBeGreaterThan(0)
})
})
test.describe('Filter workflow', () => {
test('Can filter by input type via filter bar', async ({ comfyPage }) => {
const { searchBoxV2 } = comfyPage
await comfyPage.canvasOps.doubleClick()
await expect(searchBoxV2.input).toBeVisible()
// Click "Input" filter chip in the filter bar
await searchBoxV2.filterBarButton('Input').click()
// Filter options should appear
await expect(searchBoxV2.filterOptions.first()).toBeVisible()
// Type to narrow and select MODEL
await searchBoxV2.input.fill('MODEL')
await searchBoxV2.filterOptions
.filter({ hasText: 'MODEL' })
.first()
.click()
// Filter chip should appear and results should be filtered
await expect(
searchBoxV2.dialog.getByText('Input:', { exact: false }).locator('..')
).toContainText('MODEL')
await expect(searchBoxV2.results.first()).toBeVisible()
})
})
test.describe('Keyboard navigation', () => {
test('Can navigate and select with keyboard', async ({ comfyPage }) => {
const { searchBoxV2 } = comfyPage
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
await comfyPage.canvasOps.doubleClick()
await expect(searchBoxV2.input).toBeVisible()
await searchBoxV2.input.fill('KSampler')
const results = searchBoxV2.results
await expect(results.first()).toBeVisible()
// First result selected by default
await expect(results.first()).toHaveAttribute('aria-selected', 'true')
// ArrowDown moves selection
await comfyPage.page.keyboard.press('ArrowDown')
await expect(results.nth(1)).toHaveAttribute('aria-selected', 'true')
await expect(results.first()).toHaveAttribute('aria-selected', 'false')
// ArrowUp moves back
await comfyPage.page.keyboard.press('ArrowUp')
await expect(results.first()).toHaveAttribute('aria-selected', 'true')
// Enter selects and adds node
await comfyPage.page.keyboard.press('Enter')
await expect(searchBoxV2.input).not.toBeVisible()
const newCount = await comfyPage.nodeOps.getGraphNodesCount()
expect(newCount).toBe(initialCount + 1)
})
})
})

View File

@@ -5,7 +5,6 @@ import { TestIds } from '../../fixtures/selectors'
test.describe('Properties panel position', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
// Open a sidebar tab to ensure sidebar is visible
await comfyPage.menu.nodeLibraryTab.open()
await comfyPage.actionbar.propertiesButton.click()

View File

@@ -4,7 +4,6 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'v1 (legacy)')
})
test.describe('Record Audio Node', { tag: '@screenshot' }, () => {

View File

@@ -53,11 +53,6 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
await comfyPage.settings.setSetting(
'Comfy.NodeSearchBoxImpl',
'v1 (legacy)'
)
})
test.describe('Loading options', () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -1,42 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe(
'Save Image and WEBM preview',
{ tag: ['@screenshot', '@widget'] },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('Can preview both SaveImage and SaveWEBM outputs', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow(
'widgets/save_image_and_animated_webp'
)
await comfyPage.vueNodes.waitForNodes()
await comfyPage.runButton.click()
const saveImageNode = comfyPage.vueNodes.getNodeByTitle('Save Image')
const saveWebmNode = comfyPage.vueNodes.getNodeByTitle('SaveWEBM')
// Wait for SaveImage to render an img inside .image-preview
await expect(saveImageNode.locator('.image-preview img')).toBeVisible({
timeout: 30000
})
// Wait for SaveWEBM to render a video inside .video-preview
await expect(saveWebmNode.locator('.video-preview video')).toBeVisible({
timeout: 30000
})
await expect(comfyPage.page).toHaveScreenshot(
'save-image-and-webm-preview.png'
)
})
}
)

View File

@@ -5,7 +5,6 @@ import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe('Node library sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [])
await comfyPage.settings.setSetting(
'Comfy.NodeLibrary.BookmarksCustomization',

View File

@@ -123,14 +123,17 @@ test.describe('Workflows sidebar', () => {
test('Can save workflow as', async ({ comfyPage }) => {
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json')
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow.json', 'workflow3.json'])
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json',
'workflow3.json'
])
await comfyPage.menu.topbar.saveWorkflowAs('workflow4.json')
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json'])
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json',
'workflow3.json',
'workflow4.json'
])
})
test('Exported workflow does not contain localized slot names', async ({
@@ -217,22 +220,24 @@ test.describe('Workflows sidebar', () => {
await topbar.saveWorkflow('workflow1.json')
await topbar.saveWorkflowAs('workflow2.json')
await comfyPage.nextFrame()
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['workflow1.json', 'workflow2.json'])
await expect
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
.toEqual('workflow2.json')
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'workflow1.json',
'workflow2.json'
])
expect(await comfyPage.menu.workflowsTab.getActiveWorkflowName()).toEqual(
'workflow2.json'
)
await topbar.saveWorkflowAs('workflow1.json')
await comfyPage.confirmDialog.click('overwrite')
// The old workflow1.json should be deleted and the new one should be saved.
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['workflow2.json', 'workflow1.json'])
await expect
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
.toEqual('workflow1.json')
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'workflow2.json',
'workflow1.json'
])
expect(await comfyPage.menu.workflowsTab.getActiveWorkflowName()).toEqual(
'workflow1.json'
)
})
test('Does not report warning when switching between opened workflows', async ({

View File

@@ -1,100 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Subgraph duplicate ID remapping', { tag: ['@subgraph'] }, () => {
const WORKFLOW = 'subgraphs/subgraph-nested-duplicate-ids'
test('All node IDs are globally unique after loading', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow(WORKFLOW)
const result = await comfyPage.page.evaluate(() => {
const graph = window.app!.canvas.graph!
// TODO: Extract allGraphs accessor (root + subgraphs) into LGraph
// TODO: Extract allNodeIds accessor into LGraph
const allGraphs = [graph, ...graph.subgraphs.values()]
const allIds = allGraphs
.flatMap((g) => g._nodes)
.map((n) => n.id)
.filter((id): id is number => typeof id === 'number')
return { allIds, uniqueCount: new Set(allIds).size }
})
expect(result.uniqueCount).toBe(result.allIds.length)
expect(result.allIds.length).toBeGreaterThanOrEqual(10)
})
test('Root graph node IDs are preserved as canonical', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow(WORKFLOW)
const rootIds = await comfyPage.page.evaluate(() => {
const graph = window.app!.canvas.graph!
return graph._nodes
.map((n) => n.id)
.filter((id): id is number => typeof id === 'number')
.sort((a, b) => a - b)
})
expect(rootIds).toEqual([1, 2, 5])
})
test('All links reference valid nodes in their graph', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow(WORKFLOW)
const invalidLinks = await comfyPage.page.evaluate(() => {
const graph = window.app!.canvas.graph!
const labeledGraphs: [string, typeof graph][] = [
['root', graph],
...[...graph.subgraphs.entries()].map(
([id, sg]) => [`subgraph:${id}`, sg] as [string, typeof graph]
)
]
const isNonNegative = (id: number | string) =>
typeof id === 'number' && id >= 0
return labeledGraphs.flatMap(([label, g]) =>
[...g._links.values()].flatMap((link) =>
[
isNonNegative(link.origin_id) &&
!g._nodes_by_id[link.origin_id] &&
`${label}: origin_id ${link.origin_id} not found`,
isNonNegative(link.target_id) &&
!g._nodes_by_id[link.target_id] &&
`${label}: target_id ${link.target_id} not found`
].filter(Boolean)
)
)
})
expect(invalidLinks).toEqual([])
})
test('Subgraph navigation works after ID remapping', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow(WORKFLOW)
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('5')
await subgraphNode.navigateIntoSubgraph()
const isInSubgraph = () =>
comfyPage.page.evaluate(
() => window.app!.canvas.graph?.isRootGraph === false
)
expect(await isInSubgraph()).toBe(true)
await comfyPage.page.keyboard.press('Escape')
await comfyPage.nextFrame()
expect(await isInSubgraph()).toBe(false)
})
})

View File

@@ -19,10 +19,6 @@ const SELECTORS = {
test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting(
'Comfy.NodeSearchBoxImpl',
'v1 (legacy)'
)
})
// Helper to get subgraph slot count
@@ -65,10 +61,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await subgraphNode.navigateIntoSubgraph()
const initialCount = await getSubgraphSlotCount(comfyPage, 'inputs')
const [vaeEncodeNode] = await comfyPage.nodeOps.getNodeRefsByType(
'VAEEncode',
true
)
const vaeEncodeNode = await comfyPage.nodeOps.getNodeRefById('2')
await comfyPage.subgraph.connectFromInput(vaeEncodeNode, 0)
await comfyPage.nextFrame()
@@ -84,10 +77,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await subgraphNode.navigateIntoSubgraph()
const initialCount = await getSubgraphSlotCount(comfyPage, 'outputs')
const [vaeEncodeNode] = await comfyPage.nodeOps.getNodeRefsByType(
'VAEEncode',
true
)
const vaeEncodeNode = await comfyPage.nodeOps.getNodeRefById('2')
await comfyPage.subgraph.connectToOutput(vaeEncodeNode, 0)
await comfyPage.nextFrame()
@@ -830,7 +820,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
// Open settings dialog using hotkey
await comfyPage.page.keyboard.press('Control+,')
await comfyPage.page.waitForSelector('[data-testid="settings-dialog"]', {
await comfyPage.page.waitForSelector('.settings-container', {
state: 'visible'
})
@@ -840,7 +830,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
// Dialog should be closed
await expect(
comfyPage.page.locator('[data-testid="settings-dialog"]')
comfyPage.page.locator('.settings-container')
).not.toBeVisible()
// Should still be in subgraph

View File

@@ -54,10 +54,7 @@ async function searchAndExpectResult(
test.describe('Subgraph Search Aliases', { tag: ['@subgraph'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting(
'Comfy.NodeSearchBoxImpl',
'v1 (legacy)'
)
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
})
test('Can set search aliases on subgraph and find via search', async ({

View File

@@ -1,56 +0,0 @@
import { expect } from '@playwright/test'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
/**
* Tests that templates are automatically fitted to view when loaded.
*
* When openSource === 'template', fitView() is called to ensure
* templates with saved off-screen viewport positions (extra.ds)
* are always displayed correctly.
*/
test.describe('Template Fit View', { tag: ['@canvas', '@workflow'] }, () => {
test('should automatically fit view when loading a template with off-screen saved position', async ({
comfyPage
}) => {
await comfyPage.settings.setSetting('Comfy.EnableWorkflowViewRestore', true)
// Serialize the current default graph, inject an extreme off-screen
// viewport position, then reload it as a template. Without the fix,
// the saved offset [-5000, -5000] would be restored and nodes would
// be invisible.
const viewportState = await comfyPage.page.evaluate(async () => {
const app = window.app!
const workflow = app.graph.serialize()
workflow.extra = {
...workflow.extra,
ds: { scale: 1, offset: [-5000, -5000] }
}
await app.loadGraphData(workflow as ComfyWorkflowJSON, true, true, null, {
openSource: 'template'
})
return {
offsetX: app.canvas.ds.offset[0],
offsetY: app.canvas.ds.offset[1],
nodeCount: app.graph._nodes.length
}
})
expect(viewportState.nodeCount).toBeGreaterThan(0)
// fitView() should have overridden the saved [-5000, -5000] offset
expect(
viewportState.offsetX,
'Viewport X offset should not be the saved off-screen value'
).not.toBe(-5000)
expect(
viewportState.offsetY,
'Viewport Y offset should not be the saved off-screen value'
).not.toBe(-5000)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -22,6 +22,7 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
name: 'TestSettingsExtension',
settings: [
{
// Extensions can register arbitrary setting IDs
id: 'TestHiddenSetting' as TestSettingId,
name: 'Test Hidden Setting',
type: 'hidden',
@@ -29,6 +30,7 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
category: ['Test', 'Hidden']
},
{
// Extensions can register arbitrary setting IDs
id: 'TestDeprecatedSetting' as TestSettingId,
name: 'Test Deprecated Setting',
type: 'text',
@@ -37,6 +39,7 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
category: ['Test', 'Deprecated']
},
{
// Extensions can register arbitrary setting IDs
id: 'TestVisibleSetting' as TestSettingId,
name: 'Test Visible Setting',
type: 'text',
@@ -49,143 +52,238 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
})
test('can open settings dialog and use search box', async ({ comfyPage }) => {
const dialog = comfyPage.settingDialog
await dialog.open()
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible()
await expect(dialog.searchBox).toHaveAttribute(
// Find the search box
const searchBox = comfyPage.page.locator('.settings-search-box input')
await expect(searchBox).toBeVisible()
// Verify search box has the correct placeholder
await expect(searchBox).toHaveAttribute(
'placeholder',
expect.stringContaining('Search')
)
})
test('search box is functional and accepts input', async ({ comfyPage }) => {
const dialog = comfyPage.settingDialog
await dialog.open()
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible()
await dialog.searchBox.fill('Comfy')
await expect(dialog.searchBox).toHaveValue('Comfy')
// Find and interact with the search box
const searchBox = comfyPage.page.locator('.settings-search-box input')
await searchBox.fill('Comfy')
// Verify the input was accepted
await expect(searchBox).toHaveValue('Comfy')
})
test('search box clears properly', async ({ comfyPage }) => {
const dialog = comfyPage.settingDialog
await dialog.open()
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible()
await dialog.searchBox.fill('test')
await expect(dialog.searchBox).toHaveValue('test')
// Find and interact with the search box
const searchBox = comfyPage.page.locator('.settings-search-box input')
await searchBox.fill('test')
await expect(searchBox).toHaveValue('test')
await dialog.searchBox.clear()
await expect(dialog.searchBox).toHaveValue('')
// Clear the search box
await searchBox.clear()
await expect(searchBox).toHaveValue('')
})
test('settings categories are visible in sidebar', async ({ comfyPage }) => {
const dialog = comfyPage.settingDialog
await dialog.open()
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible()
expect(await dialog.categories.count()).toBeGreaterThan(0)
// Check that the sidebar has categories
const categories = comfyPage.page.locator(
'.settings-sidebar .p-listbox-option'
)
expect(await categories.count()).toBeGreaterThan(0)
// Check that at least one category is visible
await expect(categories.first()).toBeVisible()
})
test('can select different categories in sidebar', async ({ comfyPage }) => {
const dialog = comfyPage.settingDialog
await dialog.open()
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible()
const categoryCount = await dialog.categories.count()
// Click on a specific category (Appearance) to verify category switching
const appearanceCategory = comfyPage.page.getByRole('option', {
name: 'Appearance'
})
await appearanceCategory.click()
if (categoryCount > 1) {
await dialog.categories.nth(1).click()
// Verify the category is selected
await expect(appearanceCategory).toHaveClass(/p-listbox-option-selected/)
})
await expect(dialog.categories.nth(1)).toHaveClass(
/bg-interface-menu-component-surface-selected/
)
}
test('settings content area is visible', async ({ comfyPage }) => {
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible()
// Check that the content area is visible
const contentArea = comfyPage.page.locator('.settings-content')
await expect(contentArea).toBeVisible()
// Check that tab panels are visible
const tabPanels = comfyPage.page.locator('.settings-tab-panels')
await expect(tabPanels).toBeVisible()
})
test('search functionality affects UI state', async ({ comfyPage }) => {
const dialog = comfyPage.settingDialog
await dialog.open()
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible()
await dialog.searchBox.fill('graph')
await expect(dialog.searchBox).toHaveValue('graph')
// Find the search box
const searchBox = comfyPage.page.locator('.settings-search-box input')
// Type in search box
await searchBox.fill('graph')
// Verify that the search input is handled
await expect(searchBox).toHaveValue('graph')
})
test('settings dialog can be closed', async ({ comfyPage }) => {
const dialog = comfyPage.settingDialog
await dialog.open()
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible()
// Close with escape key
await comfyPage.page.keyboard.press('Escape')
await expect(dialog.root).not.toBeVisible()
// Verify dialog is closed
await expect(settingsDialog).not.toBeVisible()
})
test('search box has proper debouncing behavior', async ({ comfyPage }) => {
const dialog = comfyPage.settingDialog
await dialog.open()
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible()
await dialog.searchBox.fill('a')
await dialog.searchBox.fill('ab')
await dialog.searchBox.fill('abc')
await dialog.searchBox.fill('abcd')
// Type rapidly in search box
const searchBox = comfyPage.page.locator('.settings-search-box input')
await searchBox.fill('a')
await searchBox.fill('ab')
await searchBox.fill('abc')
await searchBox.fill('abcd')
await expect(dialog.searchBox).toHaveValue('abcd')
// Verify final value
await expect(searchBox).toHaveValue('abcd')
})
test('search excludes hidden settings from results', async ({
comfyPage
}) => {
const dialog = comfyPage.settingDialog
await dialog.open()
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible()
await dialog.searchBox.fill('Test')
// Search for our test settings
const searchBox = comfyPage.page.locator('.settings-search-box input')
await searchBox.fill('Test')
await expect(dialog.contentArea).toContainText('Test Visible Setting')
await expect(dialog.contentArea).not.toContainText('Test Hidden Setting')
// Get all settings content
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
// Should show visible setting but not hidden setting
await expect(settingsContent).toContainText('Test Visible Setting')
await expect(settingsContent).not.toContainText('Test Hidden Setting')
})
test('search excludes deprecated settings from results', async ({
comfyPage
}) => {
const dialog = comfyPage.settingDialog
await dialog.open()
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible()
await dialog.searchBox.fill('Test')
// Search for our test settings
const searchBox = comfyPage.page.locator('.settings-search-box input')
await searchBox.fill('Test')
await expect(dialog.contentArea).toContainText('Test Visible Setting')
await expect(dialog.contentArea).not.toContainText(
'Test Deprecated Setting'
)
// Get all settings content
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
// Should show visible setting but not deprecated setting
await expect(settingsContent).toContainText('Test Visible Setting')
await expect(settingsContent).not.toContainText('Test Deprecated Setting')
})
test('search shows visible settings but excludes hidden and deprecated', async ({
comfyPage
}) => {
const dialog = comfyPage.settingDialog
await dialog.open()
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible()
await dialog.searchBox.fill('Test')
// Search for our test settings
const searchBox = comfyPage.page.locator('.settings-search-box input')
await searchBox.fill('Test')
await expect(dialog.contentArea).toContainText('Test Visible Setting')
await expect(dialog.contentArea).not.toContainText('Test Hidden Setting')
await expect(dialog.contentArea).not.toContainText(
'Test Deprecated Setting'
)
// Get all settings content
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
// Should only show the visible setting
await expect(settingsContent).toContainText('Test Visible Setting')
// Should not show hidden or deprecated settings
await expect(settingsContent).not.toContainText('Test Hidden Setting')
await expect(settingsContent).not.toContainText('Test Deprecated Setting')
})
test('search by setting name excludes hidden and deprecated', async ({
comfyPage
}) => {
const dialog = comfyPage.settingDialog
await dialog.open()
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible()
await dialog.searchBox.clear()
await dialog.searchBox.fill('Hidden')
await expect(dialog.contentArea).not.toContainText('Test Hidden Setting')
const searchBox = comfyPage.page.locator('.settings-search-box input')
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
await dialog.searchBox.clear()
await dialog.searchBox.fill('Deprecated')
await expect(dialog.contentArea).not.toContainText(
'Test Deprecated Setting'
)
// Search specifically for hidden setting by name
await searchBox.clear()
await searchBox.fill('Hidden')
await dialog.searchBox.clear()
await dialog.searchBox.fill('Visible')
await expect(dialog.contentArea).toContainText('Test Visible Setting')
// Should not show the hidden setting even when searching by name
await expect(settingsContent).not.toContainText('Test Hidden Setting')
// Search specifically for deprecated setting by name
await searchBox.clear()
await searchBox.fill('Deprecated')
// Should not show the deprecated setting even when searching by name
await expect(settingsContent).not.toContainText('Test Deprecated Setting')
// Search for visible setting by name - should work
await searchBox.clear()
await searchBox.fill('Visible')
// Should show the visible setting
await expect(settingsContent).toContainText('Test Visible Setting')
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -102,7 +102,6 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
// await comfyPage.setup()
await comfyPage.workflow.loadWorkflow('vueNodes/simple-triple')
await comfyPage.vueNodes.waitForNodes()
@@ -929,10 +928,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
'Comfy.LinkRelease.ActionShift',
'context menu'
)
await comfyPage.settings.setSetting(
'Comfy.NodeSearchBoxImpl',
'v1 (legacy)'
)
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
@@ -998,10 +994,6 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
'Comfy.LinkRelease.ActionShift',
'search box'
)
await comfyPage.settings.setSetting(
'Comfy.NodeSearchBoxImpl',
'v1 (legacy)'
)
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
@@ -1056,11 +1048,6 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyPage,
comfyMouse
}) => {
await comfyPage.settings.setSetting(
'Comfy.NodeSearchBoxImpl',
'v1 (legacy)'
)
// Setup workflow with a KSampler node
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.nodeOps.waitForGraphNodes(0)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -1,58 +0,0 @@
import { expect } from '@playwright/test'
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../../../../fixtures/ComfyPage'
test.describe('Vue Nodes Image Preview', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
await comfyPage.vueNodes.waitForNodes()
})
async function loadImageOnNode(comfyPage: ComfyPage) {
const loadImageNode = (
await comfyPage.nodeOps.getNodeRefsByType('LoadImage')
)[0]
const { x, y } = await loadImageNode.getPosition()
await comfyPage.dragDrop.dragAndDropFile('image64x64.webp', {
dropPosition: { x, y }
})
const imagePreview = comfyPage.page.locator('.image-preview')
await expect(imagePreview).toBeVisible()
await expect(imagePreview.locator('img')).toBeVisible()
await expect(imagePreview).toContainText('x')
return imagePreview
}
// TODO(#8143): Re-enable after image preview sync is working in CI
test.fixme('opens mask editor from image preview button', async ({
comfyPage
}) => {
const imagePreview = await loadImageOnNode(comfyPage)
await imagePreview.locator('[role="img"]').hover()
await comfyPage.page.getByLabel('Edit or mask image').click()
await expect(comfyPage.page.locator('.mask-editor-dialog')).toBeVisible()
})
// TODO(#8143): Re-enable after image preview sync is working in CI
test.fixme('shows image context menu options', async ({ comfyPage }) => {
await loadImageOnNode(comfyPage)
const nodeHeader = comfyPage.vueNodes.getNodeByTitle('Load Image')
await nodeHeader.click()
await nodeHeader.click({ button: 'right' })
const contextMenu = comfyPage.page.locator('.p-contextmenu')
await expect(contextMenu).toBeVisible()
await expect(contextMenu.getByText('Open Image')).toBeVisible()
await expect(contextMenu.getByText('Copy Image')).toBeVisible()
await expect(contextMenu.getByText('Save Image')).toBeVisible()
await expect(contextMenu.getByText('Open in Mask Editor')).toBeVisible()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -31,12 +31,7 @@ test.describe('Vue Integer Widget', () => {
await expect(seedWidget).toBeVisible()
// Delete the node that is linked to the slot (freeing up the widget)
// Click on the header to select the node (clicking center may land on
// the widget area where pointerdown.stop prevents node selection)
await comfyPage.vueNodes
.getNodeByTitle('Int')
.locator('.lg-node-header')
.click()
await comfyPage.vueNodes.getNodeByTitle('Int').click()
await comfyPage.vueNodes.deleteSelected()
// Test widget works when unlinked

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -3,6 +3,7 @@
"style": "new-york",
"typescript": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/assets/css/style.css",
"baseColor": "stone",
"cssVariables": true,

View File

@@ -279,46 +279,5 @@ export default defineConfig([
'import-x/no-duplicates': 'off',
'import-x/consistent-type-specifier-style': 'off'
}
},
// i18n import enforcement
// Vue components must use the useI18n() composable, not the global t/d/st/te
{
files: ['**/*.vue'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: '@/i18n',
importNames: ['t', 'd', 'te'],
message:
"In Vue components, use `const { t } = useI18n()` instead of importing from '@/i18n'."
}
]
}
]
}
},
// Non-composable .ts files must use the global t/d/te, not useI18n()
{
files: ['**/*.ts'],
ignores: ['**/use[A-Z]*.ts', '**/*.test.ts', 'src/i18n.ts'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'vue-i18n',
importNames: ['useI18n'],
message:
"useI18n() requires Vue setup context. Use `import { t } from '@/i18n'` instead."
}
]
}
]
}
}
])

29
global.d.ts vendored
View File

@@ -5,33 +5,8 @@ declare const __ALGOLIA_APP_ID__: string
declare const __ALGOLIA_API_KEY__: string
declare const __USE_PROD_CONFIG__: boolean
interface ImpactQueueFunction {
(...args: unknown[]): void
a?: unknown[][]
}
type GtagGetFieldName = 'client_id' | 'session_id' | 'session_number'
interface GtagGetFieldValueMap {
client_id: string | number | undefined
session_id: string | number | undefined
session_number: string | number | undefined
}
interface GtagFunction {
<TField extends GtagGetFieldName>(
command: 'get',
targetId: string,
fieldName: TField,
callback: (value: GtagGetFieldValueMap[TField]) => void
): void
(...args: unknown[]): void
}
interface Window {
__CONFIG__: {
gtm_container_id?: string
ga_measurement_id?: string
mixpanel_token?: string
require_whitelist?: boolean
subscription_required?: boolean
@@ -55,10 +30,6 @@ interface Window {
badge?: string
}
}
dataLayer?: Array<Record<string, unknown>>
gtag?: GtagFunction
ire_o?: string
ire?: ImpactQueueFunction
}
interface Navigator {

View File

@@ -35,6 +35,18 @@
background-size: cover;
background-repeat: no-repeat;
}
#vue-app:has(#loading-logo) {
display: contents;
color: var(--fg-color);
& #loading-logo {
place-self: center;
font-size: clamp(2px, 1vw, 6px);
line-height: 1;
overflow: hidden;
max-width: 100vw;
border-radius: 20ch;
}
}
.visually-hidden {
position: absolute;
width: 1px;
@@ -53,6 +65,36 @@
<body class="litegraph grid">
<div id="vue-app">
<span class="visually-hidden" role="status">Loading ComfyUI...</span>
<svg
width="520"
height="520"
viewBox="0 0 520 520"
fill="none"
xmlns="http://www.w3.org/2000/svg"
id="loading-logo"
>
<mask
id="mask0_227_285"
style="mask-type: alpha"
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="520"
height="520"
>
<path
d="M0 184.335C0 119.812 0 87.5502 12.5571 62.9055C23.6026 41.2274 41.2274 23.6026 62.9055 12.5571C87.5502 0 119.812 0 184.335 0H335.665C400.188 0 432.45 0 457.094 12.5571C478.773 23.6026 496.397 41.2274 507.443 62.9055C520 87.5502 520 119.812 520 184.335V335.665C520 400.188 520 432.45 507.443 457.094C496.397 478.773 478.773 496.397 457.094 507.443C432.45 520 400.188 520 335.665 520H184.335C119.812 520 87.5502 520 62.9055 507.443C41.2274 496.397 23.6026 478.773 12.5571 457.094C0 432.45 0 400.188 0 335.665V184.335Z"
fill="#EEFF30"
/>
</mask>
<g mask="url(#mask0_227_285)">
<rect y="0.751831" width="520" height="520" fill="#172DD7" />
<path
d="M176.484 428.831C168.649 428.831 162.327 425.919 158.204 420.412C153.966 414.755 152.861 406.857 155.171 398.749L164.447 366.178C165.187 363.585 164.672 360.794 163.059 358.636C161.446 356.483 158.921 355.216 156.241 355.216H129.571C121.731 355.216 115.409 352.308 111.289 346.802C107.051 341.14 105.946 333.242 108.258 325.134L140.124 213.748L143.642 201.51C148.371 184.904 165.62 171.407 182.097 171.407H214.009C217.817 171.407 221.167 168.868 222.215 165.183L232.769 128.135C237.494 111.545 254.742 98.048 271.219 98.048L339.468 97.9264L389.431 97.9221C397.268 97.9221 403.59 100.831 407.711 106.337C411.949 111.994 413.054 119.892 410.744 128L396.457 178.164C391.734 194.75 374.485 208.242 358.009 208.242L289.607 208.372H257.706C253.902 208.372 250.557 210.907 249.502 214.588L222.903 307.495C222.159 310.093 222.673 312.892 224.291 315.049C225.904 317.202 228.428 318.469 231.107 318.469C231.113 318.469 276.307 318.381 276.307 318.381H326.122C333.959 318.381 340.281 321.29 344.402 326.796C348.639 332.457 349.744 340.355 347.433 348.463L333.146 398.619C328.423 415.209 311.174 428.701 294.698 428.701L226.299 428.831H176.484Z"
fill="#F0FF41"
/>
</g>
</svg>
</div>
<script type="module" src="src/main.ts"></script>
</body>

View File

@@ -20,6 +20,10 @@ const config: KnipConfig = {
'packages/tailwind-utils': {
project: ['src/**/*.{js,ts}']
},
'packages/design-system': {
entry: ['src/**/*.ts'],
project: ['src/**/*.{js,ts}', '*.{js,ts,mts}']
},
'packages/registry-types': {
project: ['src/**/*.{js,ts}']
}
@@ -27,7 +31,6 @@ const config: KnipConfig = {
ignoreBinaries: ['python3', 'gh'],
ignoreDependencies: [
// Weird importmap things
'@iconify-json/lucide',
'@iconify/json',
'@primeuix/forms',
'@primeuix/styled',
@@ -64,8 +67,7 @@ const config: KnipConfig = {
},
tags: [
'-knipIgnoreUnusedButUsedByCustomNodes',
'-knipIgnoreUnusedButUsedByVueNodesBranch',
'-knipIgnoreUsedByStackedPR'
'-knipIgnoreUnusedButUsedByVueNodesBranch'
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.41.1",
"version": "1.39.8",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",
@@ -18,7 +18,7 @@
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
"dev:cloud": "cross-env DEV_SERVER_COMFYUI_URL='https://testcloud.comfy.org/' nx serve",
"dev:desktop": "nx dev @comfyorg/desktop-ui",
"dev:electron": "cross-env DISTRIBUTION=desktop nx serve --config vite.electron.config.mts",
"dev:electron": "nx serve --config vite.electron.config.mts",
"dev:no-vue": "cross-env DISABLE_VUE_PLUGINS=true nx serve",
"dev": "nx serve",
"devtools:pycheck": "python3 -m compileall -q tools/devtools",
@@ -70,14 +70,13 @@
"@primevue/themes": "catalog:",
"@sentry/vue": "catalog:",
"@sparkjsdev/spark": "catalog:",
"@tiptap/core": "catalog:",
"@tiptap/extension-link": "catalog:",
"@tiptap/extension-table": "catalog:",
"@tiptap/extension-table-cell": "catalog:",
"@tiptap/extension-table-header": "catalog:",
"@tiptap/extension-table-row": "catalog:",
"@tiptap/pm": "catalog:",
"@tiptap/starter-kit": "catalog:",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
"@tiptap/extension-table": "^2.10.4",
"@tiptap/extension-table-cell": "^2.10.4",
"@tiptap/extension-table-header": "^2.10.4",
"@tiptap/extension-table-row": "^2.10.4",
"@tiptap/starter-kit": "^2.10.4",
"@vueuse/core": "catalog:",
"@vueuse/integrations": "catalog:",
"@xterm/addon-fit": "^0.10.0",
@@ -94,9 +93,9 @@
"extendable-media-recorder-wav-encoder": "^7.0.129",
"firebase": "catalog:",
"fuse.js": "^7.0.0",
"glob": "catalog:",
"glob": "^11.0.3",
"jsonata": "catalog:",
"jsondiffpatch": "catalog:",
"jsondiffpatch": "^0.6.0",
"loglevel": "^1.9.2",
"marked": "^15.0.11",
"pinia": "catalog:",
@@ -194,7 +193,7 @@
},
"pnpm": {
"overrides": {
"vite": "catalog:"
"vite": "^8.0.0-beta.8"
}
}
}

View File

@@ -4,6 +4,7 @@
"description": "Shared design system for ComfyUI Frontend",
"type": "module",
"exports": {
"./tailwind-config": "./tailwind.config.ts",
"./css/*": "./src/css/*"
},
"scripts": {
@@ -11,7 +12,7 @@
},
"dependencies": {
"@iconify-json/lucide": "catalog:",
"@iconify/tailwind4": "catalog:"
"@iconify/tailwind": "catalog:"
},
"devDependencies": {
"tailwindcss": "catalog:",

View File

@@ -7,22 +7,11 @@
@plugin 'tailwindcss-primeui';
@plugin "@iconify/tailwind4" {
scale: 1.2;
icon-sets: from-folder(comfy, './packages/design-system/src/icons');
}
/* Safelist dynamic comfy icons for node library folders */
@source inline("icon-[comfy--{ai-model,bfl,bria,bytedance,credits,extensions-blocks,file-output,gemini,grok,hitpaw,ideogram,image-ai-edit,kling,ltxv,luma,magnific,mask,meshy,minimax,moonvalley-marey,node,openai,pin,pixverse,play,recraft,rodin,runway,sora,stability-ai,template,tencent,topaz,tripo,veo,vidu,wan,wavespeed,workflow}]");
/* Safelist dynamic comfy icons for essential nodes (kebab-case of node names) */
@source inline("icon-[comfy--{load-image,save-image,load-video,save-video,load-3-d,save-glb,image-batch,image-crop,image-scale,image-rotate,image-blur,image-invert,canny,recraft-remove-background-node,kling-lip-sync-audio-to-video-node,load-audio,save-audio,stability-text-to-audio,lora-loader,clip-text-encode,get-video-components,tencent-text-to-model-node,tencent-image-to-model-node,open-ai-chat-node,subgraph-blueprint-canny-to-video-ltx-2-0,subgraph-blueprint-pose-to-video-ltx-2-0}]");
@config '../../tailwind.config.ts';
@custom-variant touch (@media (hover: none));
@theme {
--shadow-interface: var(--interface-panel-box-shadow);
--text-xxs: 0.625rem;
--text-xxs--line-height: calc(1 / 0.625);
@@ -1192,7 +1181,7 @@ button.comfy-queue-btn {
.graphdialog {
min-height: 1em;
background-color: var(--comfy-menu-bg);
z-index: 1500;
z-index: 41; /* z-index is set to 41 here in order to appear over selection-overlay-container which should have a z-index of 40 */
}
.graphdialog .name {

View File

@@ -0,0 +1,100 @@
import { existsSync, readFileSync, readdirSync } from 'fs'
import { join } from 'path'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
const fileName = fileURLToPath(import.meta.url)
const dirName = dirname(fileName)
const customIconsPath = join(dirName, 'icons')
// Iconify collection structure
interface IconifyIcon {
body: string
width?: number
height?: number
}
interface IconifyCollection {
prefix: string
icons: Record<string, IconifyIcon>
width?: number
height?: number
}
// Create an Iconify collection for custom icons
export const iconCollection: IconifyCollection = {
prefix: 'comfy',
icons: {},
width: 16,
height: 16
}
/**
* Validates that an SVG file contains valid SVG content
*/
function validateSvgContent(content: string, filename: string): void {
if (!content.trim()) {
throw new Error(`Empty SVG file: ${filename}`)
}
if (!content.includes('<svg')) {
throw new Error(`Invalid SVG file (missing <svg> tag): ${filename}`)
}
// Basic XML structure validation
const openTags = (content.match(/<svg[^>]*>/g) || []).length
const closeTags = (content.match(/<\/svg>/g) || []).length
if (openTags !== closeTags) {
throw new Error(`Malformed SVG file (mismatched svg tags): ${filename}`)
}
}
/**
* Loads custom SVG icons from the icons directory
*/
function loadCustomIcons(): void {
if (!existsSync(customIconsPath)) {
console.warn(`Custom icons directory not found: ${customIconsPath}`)
return
}
try {
const files = readdirSync(customIconsPath)
const svgFiles = files.filter((file) => file.endsWith('.svg'))
if (svgFiles.length === 0) {
console.warn('No SVG files found in custom icons directory')
return
}
svgFiles.forEach((file) => {
const name = file.replace('.svg', '')
const filePath = join(customIconsPath, file)
try {
const content = readFileSync(filePath, 'utf-8')
validateSvgContent(content, file)
iconCollection.icons[name] = {
body: content
}
} catch (error) {
console.error(
`Failed to load custom icon ${file}:`,
error instanceof Error ? error.message : error
)
// Continue loading other icons instead of failing the entire build
}
})
} catch (error) {
console.error(
'Failed to read custom icons directory:',
error instanceof Error ? error.message : error
)
// Don't throw here - allow build to continue without custom icons
}
}
// Load icons when this module is imported
loadCustomIcons()

View File

@@ -251,25 +251,26 @@ Icons are automatically imported using `unplugin-icons` - no manual imports need
The icon system has two layers:
1. **Tailwind CSS Plugin** (`@iconify/tailwind4`):
- Configured via `@plugin` directive in `packages/design-system/src/css/style.css`
- Uses `from-folder(comfy, ...)` to load SVGs from `packages/design-system/src/icons/`
- Auto-cleans and optimizes SVGs at build time
1. **Build-time Processing** (`packages/design-system/src/iconCollection.ts`):
- Scans `packages/design-system/src/icons/` for SVG files
- Validates SVG content and structure
- Creates Iconify collection for Tailwind CSS
- Provides error handling for malformed files
2. **Vite Runtime** (`vite.config.mts`):
- Enables direct SVG import as Vue components
- Supports dynamic icon loading
```css
/* CSS configuration for Tailwind icon classes */
@plugin "@iconify/tailwind4" {
prefix: 'icon';
scale: 1.2;
icon-sets: from-folder(comfy, './packages/design-system/src/icons');
}
```
```typescript
// Build script creates Iconify collection
export const iconCollection: IconifyCollection = {
prefix: 'comfy',
icons: {
workflow: { body: '<svg>...</svg>' },
node: { body: '<svg>...</svg>' }
}
}
// Vite configuration for component-based usage
Icons({
compiler: 'vue3',

View File

@@ -1,10 +0,0 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1471_12672)">
<path d="M365.047 229.802H310.584L256.118 153.072L86.2157 392.168H140.796L256.115 229.807H310.581L195.261 392.168H249.994L365.047 229.802L512 436.698H470.893V436.7H426.019V392.343L365.047 306.532L304.415 392.178V436.698H163.632L163.629 436.703H109.164L109.167 436.698H-0.105347L256.118 76L365.047 229.802Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_1471_12672">
<rect width="512" height="512" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 579 B

View File

@@ -1,18 +0,0 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1471_12667)">
<g clip-path="url(#clip1_1471_12667)">
<path d="M0.307203 236.902C1.1008 227.43 4.48 211.021 6.5792 201.139C23.5008 121.805 83.0208 46.1824 160.691 20.3776V389.734H411.443C419.84 389.734 446.259 381.158 455.168 377.446C468.685 371.814 480.691 363.648 491.597 354.074C465.357 424.038 401.024 480.896 329.242 501.094C314.01 505.37 290.611 510.694 275.226 512H239.59C223.667 508.16 207.155 507.136 191.206 503.117C103.936 481.152 29.7984 406.63 8.704 318.925C5.0176 303.59 4.0704 287.744 0.307203 272.538C1.024 260.89 -0.665597 248.397 0.307203 236.877V236.902Z" fill="#8E4CFF"/>
<path d="M265.062 211.43H375.808C378.394 211.43 392.55 217.498 395.75 219.494C427.213 238.925 420.122 291.226 384.794 301.952C382.771 302.566 366.669 305.69 365.619 305.69H268.877L265.062 301.875V211.456V211.43Z" fill="#8E4CFF"/>
<path d="M265.062 137.549V48.4096H373.248C374.861 48.4096 384.205 53.8624 386.611 55.424C406.528 68.3776 414.49 96.5376 401.152 117.069C398.106 121.754 380.339 137.574 375.808 137.574H265.062V137.549Z" fill="#8E4CFF"/>
<path d="M489.062 147.738C496.128 167.706 505.523 187.264 506.906 208.845L491.674 192.282C477.773 180.736 460.928 174.003 443.29 170.624L489.062 147.738Z" fill="#8E4CFF"/>
</g>
</g>
<defs>
<clipPath id="clip0_1471_12667">
<rect width="512" height="512" fill="white"/>
</clipPath>
<clipPath id="clip1_1471_12667">
<rect width="512" height="512" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,13 +0,0 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1471_12658)">
<path d="M324.094 389.858L284.667 379.567V191.5L326.871 180.816C350.01 174.941 369.446 170.154 370.371 170.339C371.112 170.339 371.667 222.027 371.667 285.326V400.334L367.594 400.15C365.189 400.15 345.566 395.361 324.094 389.835V389.857V389.858Z" fill="#00C8D2"/>
<path d="M138.667 343.325C138.667 279.602 139.229 227.339 140.166 227.339C140.914 227.154 160.573 231.998 184.164 237.913L226.667 248.65L226.292 342.975L225.73 437.278L187.535 447.107C166.565 452.463 146.906 457.47 144.097 458.029L138.667 459.334V343.325Z" fill="#3C8CFF"/>
<path d="M423.667 248.299C423.667 38.7081 423.853 27.4506 427.037 28.3797C428.722 28.9368 445.386 33.1843 463.921 37.8029C482.458 42.6075 500.807 47.2031 504.739 48.1312L511.667 49.9884L511.293 248.67L510.731 447.539L472.722 457.148C451.939 462.486 432.279 467.291 429.284 468.057L423.667 469.334V248.299Z" fill="#78E6DC"/>
<path d="M-0.333038 248.845C-0.333038 140.208 0.222275 51.334 1.14852 51.334C1.88822 51.334 21.3242 56.1412 44.4631 61.8769L86.667 72.583V248.66C86.667 345.267 86.296 424.55 85.9262 424.55C85.3709 424.55 65.7494 429.544 42.4262 435.466L-0.333038 446.334V248.823V248.844V248.845Z" fill="#325AB4"/>
</g>
<defs>
<clipPath id="clip0_1471_12658">
<rect width="512" height="512" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -1,3 +0,0 @@
<svg width="48" height="24" viewBox="0 0 48 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34 3.11111V6.07407C34 6.68795 33.4163 7.18519 32.6956 7.18519C31.975 7.18519 31.3913 6.68795 31.3913 6.07407V4.22222H25.3042V19.7778H28.3479C29.0685 19.7778 29.6523 20.275 29.6523 20.8889C29.6523 21.5028 29.0685 22 28.3479 22H19.6521C18.9315 22 18.3477 21.5028 18.3477 20.8889C18.3477 20.275 18.9315 19.7778 19.6521 19.7778H22.6958V4.22222H16.6087V6.07407C16.6087 6.68795 16.025 7.18519 15.3044 7.18519C14.5837 7.18519 14 6.68795 14 6.07407V3.11111C14 2.49723 14.5837 2 15.3044 2H32.6959C33.4166 2 34 2.49723 34 3.11111Z" fill="#8A8A8A"/>
</svg>

Before

Width:  |  Height:  |  Size: 652 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 9">
<path d="M1.82148 8.68376C1.61587 8.68376 1.44996 8.60733 1.34177 8.46284C1.23057 8.31438 1.20157 8.10711 1.26219 7.89434L1.50561 7.03961C1.52502 6.97155 1.51151 6.89831 1.46918 6.8417C1.42684 6.7852 1.3606 6.75194 1.29025 6.75194H0.590376C0.384656 6.75194 0.21875 6.67562 0.110614 6.53113C-0.000591531 6.38256 -0.0295831 6.17529 0.0310774 5.96252L0.867308 3.03952L0.959638 2.71838C1.08375 2.28258 1.53638 1.9284 1.96878 1.9284H2.80622C2.90615 1.9284 2.99406 1.86177 3.02157 1.76508L3.29852 0.79284C3.4225 0.357484 3.87514 0.0033043 4.30753 0.0033043L6.09854 0.000112775L7.40967 0C7.61533 0 7.78124 0.0763259 7.88937 0.220813C8.00058 0.369269 8.02957 0.576538 7.96895 0.78931L7.59405 2.10572C7.4701 2.54096 7.01746 2.89503 6.58507 2.89503L4.79008 2.89844H3.95292C3.8531 2.89844 3.7653 2.96496 3.73762 3.06155L3.03961 5.49964C3.02008 5.56781 3.03359 5.64127 3.07604 5.69787C3.11837 5.75437 3.18461 5.78763 3.2549 5.78763C3.25507 5.78763 4.44105 5.78532 4.44105 5.78532H5.7483C5.95396 5.78532 6.11986 5.86164 6.228 6.00613C6.33921 6.1547 6.3682 6.36197 6.30754 6.57474L5.93263 7.89092C5.80869 8.32628 5.35605 8.68034 4.92366 8.68034L3.12872 8.68376H1.82148Z" fill="#8A8A8A"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,5 +0,0 @@
<svg width="48" height="24" viewBox="0 0 48 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M29.9316 2.13269H25.2057V3.57269H29.9316C31.1241 3.57269 32.0916 4.54018 32.0916 5.73269V18.0646C32.0916 19.2571 31.1241 20.2246 29.9316 20.2246H25.2057V21.6646H29.9316C30.8859 21.6646 31.8019 21.2849 32.4768 20.6099C33.1516 19.9349 33.5314 19.019 33.5314 18.0647V5.73281C33.5314 4.77843 33.1518 3.86249 32.4768 3.18761C31.8018 2.51273 30.8858 2.13293 29.9316 2.13293V2.13269Z" fill="#8A8A8A"/>
<path d="M30.2025 15.7602C30.5531 15.7602 30.8738 15.5652 31.0341 15.2539C31.1953 14.9427 31.1691 14.5677 30.9656 14.2817L29.0269 11.5601L26.7994 8.44014C26.6231 8.19359 26.3391 8.04733 26.0363 8.04733C25.7335 8.04733 25.4494 8.19358 25.2731 8.44014L25.2056 8.53389V15.7601L30.2025 15.7602Z" fill="#8A8A8A"/>
<path d="M23.0457 1.00018C22.6482 1.00018 22.3257 1.32269 22.3257 1.72018V2.13269H17.5999C16.6455 2.13269 15.7296 2.51237 15.0547 3.18737C14.3798 3.86237 14 4.77831 14 5.73257V18.0645C14 19.0189 14.3797 19.9348 15.0547 20.6097C15.7297 21.2846 16.6456 21.6644 17.5999 21.6644H22.3257V21.88C22.3257 22.2775 22.6482 22.6 23.0457 22.6C23.4432 22.6 23.7657 22.2775 23.7657 21.88V1.72C23.7657 1.32251 23.4432 1.00018 23.0457 1.00018ZM22.3257 15.7602H17.3289C16.9783 15.7602 16.6577 15.5652 16.4974 15.2539C16.3361 14.9427 16.3624 14.5677 16.5658 14.2817L17.4471 13.0433L18.6208 11.397H18.6199C18.7961 11.1495 19.0802 11.0033 19.383 11.0033C19.6867 11.0033 19.9708 11.1495 20.1471 11.397L21.318 13.0433L21.6517 13.5233L22.3258 12.568L22.3257 15.7602Z" fill="#8A8A8A"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,5 +0,0 @@
<svg width="48" height="24" viewBox="0 0 48 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M29.9316 2.13269H25.2057V3.57269H29.9316C31.1241 3.57269 32.0916 4.54018 32.0916 5.73269V18.0646C32.0916 19.2571 31.1241 20.2246 29.9316 20.2246H25.2057V21.6646H29.9316C30.8859 21.6646 31.8019 21.2849 32.4768 20.6099C33.1516 19.9349 33.5314 19.019 33.5314 18.0647V5.73281C33.5314 4.77843 33.1518 3.86249 32.4768 3.18761C31.8018 2.51273 30.8858 2.13293 29.9316 2.13293V2.13269Z" fill="#8A8A8A"/>
<path d="M29.0586 10.918C29.5299 11.2086 29.703 11.7469 29.7031 12.1836C29.7031 12.6202 29.5288 13.1584 29.0576 13.4492L25 16.0273V12.4717L25.6396 12.0801L25 11.6865V8.33887L29.0586 10.918Z" fill="#8A8A8A"/>
<path d="M23.0459 1C23.4433 1.0001 23.7656 1.32234 23.7656 1.71973V21.8799C23.7656 22.2773 23.4433 22.5995 23.0459 22.5996C22.6484 22.5996 22.3262 22.2774 22.3262 21.8799V21.6641H17.5996C16.6454 21.664 15.7296 21.2842 15.0547 20.6094C14.3798 19.9345 14 19.0188 14 18.0645V5.73242C14 4.77822 14.3799 3.86249 15.0547 3.1875C15.7295 2.51256 16.6453 2.13288 17.5996 2.13281H22.3262V1.71973C22.3263 1.32236 22.6485 1 23.0459 1ZM19.3008 5.00195C18.5469 5.04101 17.991 5.7594 18.001 6.48438V17.8672C17.9877 18.3783 18.241 18.8887 18.667 19.1621L18.6709 19.165C19.1025 19.4368 19.6599 19.4326 20.0879 19.1494L22 17.9336V14.3105L20.7266 15.0918V9.06055L22 9.84277V6.43359L20.0869 5.21875C19.8834 5.08395 19.6474 5.00777 19.4072 5L19.3008 5.00195Z" fill="#8A8A8A"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,3 +0,0 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0zm-13.239 0a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0zm6.619 6.619a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0zm0-13.238a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 665 B

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