Compare commits
358 Commits
devtools/r
...
cloud/1.35
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3766b97102 | ||
|
|
783b8dcd8f | ||
|
|
5069a4a272 | ||
|
|
73cc7c6b04 | ||
|
|
e401bc2a17 | ||
|
|
d44621b206 | ||
|
|
af46a55a8b | ||
|
|
313332884d | ||
|
|
0c4df3d3f5 | ||
|
|
a4922db8ad | ||
|
|
e3cdcc784e | ||
|
|
f5bd2bdab6 | ||
|
|
10389e216e | ||
|
|
b81f5fee48 | ||
|
|
31ecb276f8 | ||
|
|
e6475c3b56 | ||
|
|
bf5bdb4156 | ||
|
|
53e22f03a8 | ||
|
|
c8d9e88e2b | ||
|
|
411b728300 | ||
|
|
50f4c7e222 | ||
|
|
d0aa475064 | ||
|
|
8b8c7a3141 | ||
|
|
cf663b9cc7 | ||
|
|
14549c4e96 | ||
|
|
cc4c6bed81 | ||
|
|
d3ca590c03 | ||
|
|
964f92ee31 | ||
|
|
cb87c57211 | ||
|
|
1ca60adfdb | ||
|
|
af1dc69582 | ||
|
|
b92b3f0c79 | ||
|
|
d273b8f233 | ||
|
|
59c06c7d79 | ||
|
|
334511f482 | ||
|
|
6eca4aae86 | ||
|
|
626a7123fe | ||
|
|
b455766d12 | ||
|
|
eb05a4f3ee | ||
|
|
b38cf5a00e | ||
|
|
a5d7a96cb9 | ||
|
|
8b64253824 | ||
|
|
eb04dc20f3 | ||
|
|
194fbdf520 | ||
|
|
5f20d554f3 | ||
|
|
fd9747375d | ||
|
|
5187a77234 | ||
|
|
b22ba97a13 | ||
|
|
aab9a30511 | ||
|
|
b0f5a9ffe2 | ||
|
|
3f777fb54f | ||
|
|
6a73288ce3 | ||
|
|
d21ea0f65b | ||
|
|
7613e70f63 | ||
|
|
56b67085d0 | ||
|
|
4a91330e30 | ||
|
|
a1a507ed09 | ||
|
|
a7c694f248 | ||
|
|
0646bb368a | ||
|
|
a8ef7a602f | ||
|
|
9c157296be | ||
|
|
3e97225ff6 | ||
|
|
1cdea3063d | ||
|
|
b5ab45673a | ||
|
|
7d326cbc14 | ||
|
|
6b1bd4be16 | ||
|
|
57eee5c218 | ||
|
|
caca6c4163 | ||
|
|
0385a7de9b | ||
|
|
e41c6934db | ||
|
|
2a68dbff5a | ||
|
|
2957d9897f | ||
|
|
f2a0e5102e | ||
|
|
88bdc605a7 | ||
|
|
c1808db7c4 | ||
|
|
514c437a38 | ||
|
|
18b133d22f | ||
|
|
3e8a83547d | ||
|
|
91adcaf276 | ||
|
|
03e9dd4789 | ||
|
|
ac8c3847d2 | ||
|
|
c88fc99a86 | ||
|
|
3dd805a30e | ||
|
|
7c830a2f0b | ||
|
|
e7756eb6dd | ||
|
|
c83f3ff1a7 | ||
|
|
bf8d9de1c1 | ||
|
|
b9f75b6cc8 | ||
|
|
3c8b7b015c | ||
|
|
97fa128999 | ||
|
|
1e22c9067d | ||
|
|
273e39bbd1 | ||
|
|
ca5f24fcd9 | ||
|
|
8ba8b21fa0 | ||
|
|
1522622427 | ||
|
|
d83c3122ab | ||
|
|
c99865ce7f | ||
|
|
29af56c154 | ||
|
|
a65e63a322 | ||
|
|
8e28dda85c | ||
|
|
a7de97470b | ||
|
|
5fad29ed37 | ||
|
|
ea59fb5fc3 | ||
|
|
5cba1e3f88 | ||
|
|
c8f88d5ba7 | ||
|
|
f5f0e20332 | ||
|
|
b6efc52bf8 | ||
|
|
1b2df19f1b | ||
|
|
0eba638a0f | ||
|
|
d60ecbb3c3 | ||
|
|
969466c0a0 | ||
|
|
87244a6954 | ||
|
|
0eb2b9171a | ||
|
|
24ee353465 | ||
|
|
73e09a7fff | ||
|
|
987dcb189d | ||
|
|
fceb0017ce | ||
|
|
6156e22bac | ||
|
|
62f9e91724 | ||
|
|
e83cf0f5f6 | ||
|
|
c24e2ab5ba | ||
|
|
72b5444d5a | ||
|
|
b52b2bbc30 | ||
|
|
424bd21559 | ||
|
|
04286c033a | ||
|
|
59429cbe56 | ||
|
|
eb04178e33 | ||
|
|
b88d96d6cc | ||
|
|
dedc77786f | ||
|
|
356ebe538f | ||
|
|
2c06c58621 | ||
|
|
c13343b8fb | ||
|
|
ce4837a57c | ||
|
|
2903560416 | ||
|
|
6850c45d63 | ||
|
|
2b9f7ecedf | ||
|
|
73b08acfe0 | ||
|
|
c524ce3a2f | ||
|
|
aef40834f3 | ||
|
|
8209f5a108 | ||
|
|
77e453db36 | ||
|
|
d3e9e15f07 | ||
|
|
4d3f918e8e | ||
|
|
82fc96155f | ||
|
|
79a6421329 | ||
|
|
42314d227f | ||
|
|
8f300c7163 | ||
|
|
5b91434ac4 | ||
|
|
51a336fd36 | ||
|
|
ad630cfbfe | ||
|
|
41eb45754b | ||
|
|
f0a99a0a75 | ||
|
|
5139e0564e | ||
|
|
c9d5d5ab3e | ||
|
|
f385ee8ca2 | ||
|
|
4a3098f1f2 | ||
|
|
7b11510f9f | ||
|
|
2636136f87 | ||
|
|
5a4fd9ec40 | ||
|
|
418f8fff4e | ||
|
|
5c01861f4e | ||
|
|
973d7678a1 | ||
|
|
259e9563c8 | ||
|
|
63592af314 | ||
|
|
dd7e7e7383 | ||
|
|
97c7b33713 | ||
|
|
248929c655 | ||
|
|
6081404abb | ||
|
|
6820633fea | ||
|
|
8c5584c997 | ||
|
|
f80654ae31 | ||
|
|
795733b333 | ||
|
|
1b95cd25d1 | ||
|
|
10288ee239 | ||
|
|
bca7a435ed | ||
|
|
23ab924405 | ||
|
|
a8f6bea371 | ||
|
|
1d18583e42 | ||
|
|
f74c176423 | ||
|
|
cde49d5b64 | ||
|
|
5db6d1af9a | ||
|
|
4bf766d451 | ||
|
|
10fddc9694 | ||
|
|
57523a0c57 | ||
|
|
ec1a7f1da4 | ||
|
|
5e606f274f | ||
|
|
3443c8fb65 | ||
|
|
3c8def778e | ||
|
|
dbda8b47e8 | ||
|
|
57191151ac | ||
|
|
fe2676e8cd | ||
|
|
ab777bc65c | ||
|
|
431fe33ea3 | ||
|
|
5233749fe3 | ||
|
|
45bcf4096c | ||
|
|
f800c4091c | ||
|
|
3feeecc740 | ||
|
|
c087f37fcf | ||
|
|
8d4a6df7f8 | ||
|
|
52e915baf0 | ||
|
|
643533c572 | ||
|
|
8b5e43d7f3 | ||
|
|
e9d5ce7f3f | ||
|
|
19d98a09ea | ||
|
|
39d305df86 | ||
|
|
68274134c8 | ||
|
|
fb54669dc3 | ||
|
|
662974b222 | ||
|
|
2bf45f23dc | ||
|
|
2f87acf9aa | ||
|
|
497bafcaeb | ||
|
|
c6988380c2 | ||
|
|
d50a2fabc0 | ||
|
|
9c5f8a619c | ||
|
|
c7eac496c1 | ||
|
|
1e066ee6c8 | ||
|
|
5c330fdd25 | ||
|
|
2b7b100e2e | ||
|
|
379af28678 | ||
|
|
c263111eeb | ||
|
|
e887d69cdc | ||
|
|
7ff8bcfea3 | ||
|
|
573cda853b | ||
|
|
49824824e6 | ||
|
|
8e006bb8a3 | ||
|
|
a5f1eb0b92 | ||
|
|
1b30880e6c | ||
|
|
6d22562d40 | ||
|
|
04158deb02 | ||
|
|
072f1f6ced | ||
|
|
202dc3bbb2 | ||
|
|
2c437acff6 | ||
|
|
b97b21add0 | ||
|
|
dfe098897a | ||
|
|
d76c59cb14 | ||
|
|
c298d8a870 | ||
|
|
b50b34ac95 | ||
|
|
7a6cc39c39 | ||
|
|
2b751200be | ||
|
|
5b03d3fcbc | ||
|
|
1caf3fdb0c | ||
|
|
f39faf6b8e | ||
|
|
3ee921119d | ||
|
|
f61bfe666e | ||
|
|
d8795ec997 | ||
|
|
d3aa8dfc88 | ||
|
|
2cce0fe611 | ||
|
|
b9cb335255 | ||
|
|
2f89eb070c | ||
|
|
d94e0720f3 | ||
|
|
fe47a487bb | ||
|
|
63f68543e4 | ||
|
|
9806ba807a | ||
|
|
7433f470fc | ||
|
|
490bb22bd3 | ||
|
|
895775c319 | ||
|
|
e248ecfa4e | ||
|
|
923695ffde | ||
|
|
df653d6ce1 | ||
|
|
28dc9314d2 | ||
|
|
3acf9aae0a | ||
|
|
a055241e2e | ||
|
|
9d131a4267 | ||
|
|
c57ceaf826 | ||
|
|
29dbfa3f60 | ||
|
|
83f04490ba | ||
|
|
c5fe617347 | ||
|
|
8b2c1fc45d | ||
|
|
df66a96976 | ||
|
|
96d12330bb | ||
|
|
4b87b1fdc5 | ||
|
|
08b256c29d | ||
|
|
e6332046b0 | ||
|
|
5fa76e23d9 | ||
|
|
fcfb5437a9 | ||
|
|
5ff3a0ed52 | ||
|
|
5fa0295ff5 | ||
|
|
1348a0934a | ||
|
|
01f8e77251 | ||
|
|
31c03b669e | ||
|
|
c9da19b5b5 | ||
|
|
10222860eb | ||
|
|
4597b7e600 | ||
|
|
6782d04f00 | ||
|
|
4bb5c12fac | ||
|
|
8b5cfe7e55 | ||
|
|
135169003f | ||
|
|
d58a464c9c | ||
|
|
e54b972550 | ||
|
|
9bd63dbe6a | ||
|
|
a21c813d11 | ||
|
|
df373af987 | ||
|
|
c06a7279e2 | ||
|
|
ffc17c054b | ||
|
|
ddb00d02d5 | ||
|
|
4815d6b14c | ||
|
|
a9653ba9c7 | ||
|
|
30bafcd019 | ||
|
|
b789791fd9 | ||
|
|
723f53751e | ||
|
|
2539a7d2ce | ||
|
|
c9556d7aff | ||
|
|
58b051a473 | ||
|
|
0b33470744 | ||
|
|
fb3ce74d2f | ||
|
|
86d3f0ebd5 | ||
|
|
09c888e338 | ||
|
|
6d41e8b6e4 | ||
|
|
274f77869b | ||
|
|
f5c9f69678 | ||
|
|
c1e237255a | ||
|
|
a91b9f288f | ||
|
|
4adcf09cca | ||
|
|
1dbb3fc1b9 | ||
|
|
d6c5b33fce | ||
|
|
f5608435b4 | ||
|
|
9da82f47ef | ||
|
|
a8d6f7baff | ||
|
|
a7daa5071c | ||
|
|
639975b804 | ||
|
|
ff0d385db8 | ||
|
|
6e2e591937 | ||
|
|
27fcc4554f | ||
|
|
e563c1be75 | ||
|
|
5a297e520c | ||
|
|
85bec5ee47 | ||
|
|
bdf6d4dea2 | ||
|
|
b8a796212c | ||
|
|
bc553f12be | ||
|
|
6bb35d46c1 | ||
|
|
68c38f0098 | ||
|
|
236247f05f | ||
|
|
87d6d18c57 | ||
|
|
87106ccb95 | ||
|
|
a20fb7d260 | ||
|
|
836cd7f9ba | ||
|
|
acd855601c | ||
|
|
423a2e76bc | ||
|
|
26578981d4 | ||
|
|
38fb53dca8 | ||
|
|
a832141a45 | ||
|
|
d1f0211b61 | ||
|
|
cc42c2967c | ||
|
|
bb51a5aa76 | ||
|
|
674d884e79 | ||
|
|
6f89d9a9f8 | ||
|
|
08b206f191 | ||
|
|
14d94da52b | ||
|
|
a521066b25 | ||
|
|
ada0993572 | ||
|
|
e42715086e | ||
|
|
92968f3f9b | ||
|
|
73692464ef | ||
|
|
61b3ca046a | ||
|
|
f8912ebaf4 | ||
|
|
80b87c1277 | ||
|
|
00fa9b691b | ||
|
|
0cff8eb357 |
61
.cursorrules
@@ -1,61 +0,0 @@
|
||||
# Vue 3 Composition API Project Rules
|
||||
|
||||
## Vue 3 Composition API Best Practices
|
||||
- Use setup() function for component logic
|
||||
- Utilize ref and reactive for reactive state
|
||||
- Implement computed properties with computed()
|
||||
- Use watch and watchEffect for side effects
|
||||
- Implement lifecycle hooks with onMounted, onUpdated, etc.
|
||||
- Utilize provide/inject for dependency injection
|
||||
- Use vue 3.5 style of default prop declaration. Example:
|
||||
|
||||
```typescript
|
||||
const { nodes, showTotal = true } = defineProps<{
|
||||
nodes: ApiNodeCost[]
|
||||
showTotal?: boolean
|
||||
}>()
|
||||
```
|
||||
|
||||
- Organize vue component in <template> <script> <style> order
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
src/
|
||||
components/
|
||||
constants/
|
||||
composables/
|
||||
views/
|
||||
stores/
|
||||
services/
|
||||
App.vue
|
||||
main.ts
|
||||
```
|
||||
|
||||
## Styling Guidelines
|
||||
- Use Tailwind CSS for styling
|
||||
- Implement responsive design with Tailwind CSS
|
||||
|
||||
## PrimeVue Component Guidelines
|
||||
DO NOT use deprecated PrimeVue components. Use these replacements instead:
|
||||
- Dropdown → Use Select (import from 'primevue/select')
|
||||
- OverlayPanel → Use Popover (import from 'primevue/popover')
|
||||
- Calendar → Use DatePicker (import from 'primevue/datepicker')
|
||||
- InputSwitch → Use ToggleSwitch (import from 'primevue/toggleswitch')
|
||||
- Sidebar → Use Drawer (import from 'primevue/drawer')
|
||||
- Chips → Use AutoComplete with multiple enabled and typeahead disabled
|
||||
- TabMenu → Use Tabs without panels
|
||||
- Steps → Use Stepper without panels
|
||||
- InlineMessage → Use Message component
|
||||
|
||||
## Development Guidelines
|
||||
1. Leverage VueUse functions for performance-enhancing styles
|
||||
2. Use es-toolkit for utility functions
|
||||
3. Use TypeScript for type safety
|
||||
4. Implement proper props and emits definitions
|
||||
5. Utilize Vue 3's Teleport component when needed
|
||||
6. Use Suspense for async components
|
||||
7. Implement proper error handling
|
||||
8. Follow Vue 3 style guide and naming conventions
|
||||
9. Use Vite for fast development and building
|
||||
10. Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json
|
||||
11. Never use deprecated PrimeVue components listed above
|
||||
@@ -42,3 +42,7 @@ ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579
|
||||
# SENTRY_AUTH_TOKEN=private-token # get from sentry
|
||||
# SENTRY_ORG=comfy-org
|
||||
# SENTRY_PROJECT=cloud-frontend-staging
|
||||
|
||||
# Stripe pricing table configuration (used by feature-flagged subscription tiers UI)
|
||||
# VITE_STRIPE_PUBLISHABLE_KEY=pk_test_123
|
||||
# VITE_STRIPE_PRICING_TABLE_ID=prctbl_123
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Description: When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo
|
||||
name: 'Api: Update Electron API Types'
|
||||
description: 'When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Description: When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo
|
||||
name: 'Api: Update Manager API Types'
|
||||
description: 'When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
|
||||
on:
|
||||
# Manual trigger
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Description: When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo
|
||||
name: 'Api: Update Registry API Types'
|
||||
description: 'When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
|
||||
on:
|
||||
# Manual trigger
|
||||
|
||||
2
.github/workflows/ci-json-validation.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq
|
||||
name: "CI: JSON Validation"
|
||||
description: "Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
2
.github/workflows/ci-lint-format.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Linting and code formatting validation for pull requests
|
||||
name: "CI: Lint Format"
|
||||
description: "Linting and code formatting validation for pull requests"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
2
.github/workflows/ci-python-validation.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Validates Python code in tools/devtools directory
|
||||
name: "CI: Python Validation"
|
||||
description: "Validates Python code in tools/devtools directory"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
26
.github/workflows/ci-shell-validation.yaml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Description: Runs shellcheck on tracked shell scripts when they change
|
||||
name: "CI: Shell Validation"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**/*.sh'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/*.sh'
|
||||
|
||||
jobs:
|
||||
shell-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install shellcheck
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y shellcheck
|
||||
|
||||
- name: Run shellcheck
|
||||
run: bash ./scripts/cicd/check-shell.sh
|
||||
2
.github/workflows/ci-tests-e2e-forks.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Deploys test results from forked PRs (forks can't access deployment secrets)
|
||||
name: "CI: Tests E2E (Deploy for Forks)"
|
||||
description: "Deploys test results from forked PRs (forks can't access deployment secrets)"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
|
||||
19
.github/workflows/ci-tests-e2e.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages
|
||||
name: "CI: Tests E2E"
|
||||
description: "End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages"
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -124,12 +124,19 @@ jobs:
|
||||
- name: Run Playwright tests (${{ matrix.browser }})
|
||||
id: playwright
|
||||
run: |
|
||||
# Run tests with both HTML and JSON reporters
|
||||
# Run tests with blob reporter first
|
||||
pnpm exec playwright test --project=${{ matrix.browser }} --reporter=blob
|
||||
env:
|
||||
PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report
|
||||
|
||||
- name: Generate HTML and JSON reports
|
||||
if: always()
|
||||
run: |
|
||||
# Generate HTML report from blob
|
||||
pnpm exec playwright merge-reports --reporter=html ./blob-report
|
||||
# Generate JSON report separately with explicit output path
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
|
||||
pnpm exec playwright test --project=${{ matrix.browser }} \
|
||||
--reporter=list \
|
||||
--reporter=html \
|
||||
--reporter=json
|
||||
pnpm exec playwright merge-reports --reporter=json ./blob-report
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Description: Deploys Storybook previews from forked PRs (forks can't access deployment secrets)
|
||||
name: "CI: Tests Storybook (Deploy for Forks)"
|
||||
description: "Deploys Storybook previews from forked PRs (forks can't access deployment secrets)"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
|
||||
3
.github/workflows/ci-tests-storybook.yaml
vendored
@@ -1,10 +1,9 @@
|
||||
# Description: Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages
|
||||
name: "CI: Tests Storybook"
|
||||
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
# Post starting comment for non-forked PRs
|
||||
|
||||
2
.github/workflows/ci-tests-unit.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Unit and component testing with Vitest
|
||||
name: "CI: Tests Unit"
|
||||
description: "Unit and component testing with Vitest"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
2
.github/workflows/ci-yaml-validation.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Validates YAML syntax and style using yamllint with relaxed rules
|
||||
name: "CI: YAML Validation"
|
||||
description: "Validates YAML syntax and style using yamllint with relaxed rules"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
69
.github/workflows/cloud-backport-tag.yaml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: Cloud Backport Tag
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: ['closed']
|
||||
branches: [cloud/*]
|
||||
|
||||
jobs:
|
||||
create-tag:
|
||||
if: >
|
||||
github.event.pull_request.merged == true &&
|
||||
contains(github.event.pull_request.labels.*.name, 'backport')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout merge commit
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Create tag for cloud backport
|
||||
id: tag
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BRANCH="${{ github.event.pull_request.base.ref }}"
|
||||
if [[ ! "$BRANCH" =~ ^cloud/([0-9]+)\.([0-9]+)$ ]]; then
|
||||
echo "::error::Base branch '$BRANCH' is not a cloud/x.y branch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MAJOR="${BASH_REMATCH[1]}"
|
||||
MINOR="${BASH_REMATCH[2]}"
|
||||
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
if [[ "$VERSION" =~ ^${MAJOR}\.${MINOR}\.([0-9]+)(-.+)?$ ]]; then
|
||||
PATCH="${BASH_REMATCH[1]}"
|
||||
SUFFIX="${BASH_REMATCH[2]:-}"
|
||||
else
|
||||
echo "::error::Version '${VERSION}' does not match cloud branch '${BRANCH}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TAG="cloud/v${VERSION}"
|
||||
|
||||
if git ls-remote --tags origin "${TAG}" | grep -Fq "refs/tags/${TAG}"; then
|
||||
echo "::notice::Tag ${TAG} already exists; skipping"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git tag "${TAG}" "${{ github.event.pull_request.merge_commit_sha }}"
|
||||
git push origin "${TAG}"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "Created tag: ${TAG}"
|
||||
echo "Branch: ${BRANCH}"
|
||||
echo "Version: ${VERSION}"
|
||||
echo "Commit: ${{ github.event.pull_request.merge_commit_sha }}"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
2
.github/workflows/i18n-update-core.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Generates and updates translations for core ComfyUI components using OpenAI
|
||||
name: "i18n: Update Core"
|
||||
description: "Generates and updates translations for core ComfyUI components using OpenAI"
|
||||
|
||||
on:
|
||||
# Manual dispatch for urgent translation updates
|
||||
|
||||
21
.github/workflows/pr-backport.yaml
vendored
@@ -16,6 +16,10 @@ on:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: backport-${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
if: >
|
||||
@@ -78,8 +82,7 @@ jobs:
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
|
||||
else
|
||||
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
|
||||
LABELS=$(echo "$LABELS" | jq -r '.[].name')
|
||||
LABELS=$(jq -r '.pull_request.labels[].name' "$GITHUB_EVENT_PATH")
|
||||
fi
|
||||
|
||||
add_target() {
|
||||
@@ -237,8 +240,8 @@ jobs:
|
||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
||||
else
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
PR_TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
|
||||
MERGE_COMMIT=$(jq -r '.pull_request.merge_commit_sha' "$GITHUB_EVENT_PATH")
|
||||
fi
|
||||
|
||||
for target in ${{ steps.filter-targets.outputs.pending-targets }}; do
|
||||
@@ -327,8 +330,8 @@ jobs:
|
||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||
else
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
PR_TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
|
||||
PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
|
||||
fi
|
||||
|
||||
for backport in ${{ steps.backport.outputs.success }}; do
|
||||
@@ -365,9 +368,9 @@ jobs:
|
||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
||||
else
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
PR_NUMBER=$(jq -r '.pull_request.number' "$GITHUB_EVENT_PATH")
|
||||
PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
|
||||
MERGE_COMMIT=$(jq -r '.pull_request.merge_commit_sha' "$GITHUB_EVENT_PATH")
|
||||
fi
|
||||
|
||||
for failure in ${{ steps.backport.outputs.failed }}; do
|
||||
|
||||
2
.github/workflows/pr-claude-review.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: AI-powered code review triggered by adding the 'claude-review' label to a PR
|
||||
name: "PR: Claude Review"
|
||||
description: "AI-powered code review triggered by adding the 'claude-review' label to a PR"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -124,12 +124,16 @@ jobs:
|
||||
- name: Stage changed snapshot files
|
||||
id: changed-snapshots
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "=========================================="
|
||||
echo "STAGING CHANGED SNAPSHOTS (Shard ${{ matrix.shardIndex }})"
|
||||
echo "=========================================="
|
||||
|
||||
# Get list of changed snapshot files
|
||||
changed_files=$(git diff --name-only browser_tests/ 2>/dev/null | grep -E '\-snapshots/' || echo "")
|
||||
# Get list of changed snapshot files (including untracked/new files)
|
||||
changed_files=$( (
|
||||
git diff --name-only browser_tests/ 2>/dev/null || true
|
||||
git ls-files --others --exclude-standard browser_tests/ 2>/dev/null || true
|
||||
) | sort -u | grep -E '\-snapshots/' || true )
|
||||
|
||||
if [ -z "$changed_files" ]; then
|
||||
echo "No snapshot changes in this shard"
|
||||
@@ -151,6 +155,11 @@ jobs:
|
||||
# Strip 'browser_tests/' prefix to avoid double nesting
|
||||
echo "Copying changed files to staging directory..."
|
||||
while IFS= read -r file; do
|
||||
# Skip paths that no longer exist (e.g. deletions)
|
||||
if [ ! -f "$file" ]; then
|
||||
echo " → (skipped; not a file) $file"
|
||||
continue
|
||||
fi
|
||||
# Remove 'browser_tests/' prefix
|
||||
file_without_prefix="${file#browser_tests/}"
|
||||
# Create parent directories
|
||||
@@ -261,11 +270,19 @@ jobs:
|
||||
echo "CHANGES SUMMARY"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Changed files in browser_tests:"
|
||||
git diff --name-only browser_tests/ | head -20 || echo "No changes"
|
||||
echo ""
|
||||
echo "Total changes:"
|
||||
git diff --name-only browser_tests/ | wc -l || echo "0"
|
||||
echo "Changed files in browser_tests (including untracked):"
|
||||
CHANGES=$(git status --porcelain=v1 --untracked-files=all -- browser_tests/)
|
||||
if [ -z "$CHANGES" ]; then
|
||||
echo "No changes"
|
||||
echo ""
|
||||
echo "Total changes:"
|
||||
echo "0"
|
||||
else
|
||||
echo "$CHANGES" | head -50
|
||||
echo ""
|
||||
echo "Total changes:"
|
||||
echo "$CHANGES" | wc -l
|
||||
fi
|
||||
|
||||
- name: Commit updated expectations
|
||||
id: commit
|
||||
@@ -273,7 +290,7 @@ jobs:
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
|
||||
if git diff --quiet browser_tests/; then
|
||||
if [ -z "$(git status --porcelain=v1 --untracked-files=all -- browser_tests/)" ]; then
|
||||
echo "No changes to commit"
|
||||
echo "has-changes=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
|
||||
4
.github/workflows/release-branch-create.yaml
vendored
@@ -148,10 +148,10 @@ jobs:
|
||||
done
|
||||
|
||||
{
|
||||
echo "results<<'EOF'"
|
||||
echo "results<<EOF"
|
||||
cat "$RESULTS_FILE"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Ensure release labels
|
||||
if: steps.check_version.outputs.is_minor_bump == 'true'
|
||||
|
||||
127
.github/workflows/release-version-bump.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Manual workflow to increment package version with semantic versioning support
|
||||
name: "Release: Version Bump"
|
||||
description: "Manual workflow to increment package version with semantic versioning support"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -20,6 +20,13 @@ on:
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
schedule:
|
||||
# 00:00 UTC ≈ 4:00 PM PST / 5:00 PM PDT on the previous calendar day
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
concurrency:
|
||||
group: release-version-bump
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
bump-version:
|
||||
@@ -29,15 +36,99 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Prepare inputs
|
||||
id: prepared-inputs
|
||||
shell: bash
|
||||
env:
|
||||
RAW_VERSION_TYPE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version_type || '' }}
|
||||
RAW_PRE_RELEASE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.pre_release || '' }}
|
||||
RAW_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION_TYPE="$RAW_VERSION_TYPE"
|
||||
PRE_RELEASE="$RAW_PRE_RELEASE"
|
||||
TARGET_BRANCH="$RAW_BRANCH"
|
||||
|
||||
if [[ -z "$VERSION_TYPE" ]]; then
|
||||
VERSION_TYPE='patch'
|
||||
fi
|
||||
|
||||
if [[ -z "$TARGET_BRANCH" ]]; then
|
||||
TARGET_BRANCH='main'
|
||||
fi
|
||||
|
||||
{
|
||||
echo "version_type=$VERSION_TYPE"
|
||||
echo "pre_release=$PRE_RELEASE"
|
||||
echo "branch=$TARGET_BRANCH"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Close stale nightly version bump PRs
|
||||
if: github.event_name == 'schedule'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
script: |
|
||||
const prefix = 'version-bump-'
|
||||
const closed = []
|
||||
const prs = await github.paginate(github.rest.pulls.list, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100
|
||||
})
|
||||
|
||||
for (const pr of prs) {
|
||||
if (!pr.head?.ref?.startsWith(prefix)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (pr.user?.login !== 'github-actions[bot]') {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only clean up stale nightly PRs targeting main.
|
||||
// Adjust here if other target branches should be cleaned.
|
||||
if (pr.base?.ref !== 'main') {
|
||||
continue
|
||||
}
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number,
|
||||
state: 'closed'
|
||||
})
|
||||
|
||||
try {
|
||||
await github.rest.git.deleteRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `heads/${pr.head.ref}`
|
||||
})
|
||||
} catch (error) {
|
||||
if (![404, 422].includes(error.status)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
closed.push(pr.number)
|
||||
}
|
||||
|
||||
core.info(`Closed ${closed.length} stale PR(s).`)
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
ref: ${{ steps.prepared-inputs.outputs.branch }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate branch exists
|
||||
env:
|
||||
TARGET_BRANCH: ${{ steps.prepared-inputs.outputs.branch }}
|
||||
run: |
|
||||
BRANCH="${{ github.event.inputs.branch }}"
|
||||
BRANCH="$TARGET_BRANCH"
|
||||
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
|
||||
echo "❌ Branch '$BRANCH' does not exist"
|
||||
echo ""
|
||||
@@ -51,7 +142,7 @@ jobs:
|
||||
echo "✅ Branch '$BRANCH' exists"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
with:
|
||||
version: 10
|
||||
|
||||
@@ -59,20 +150,34 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Bump version
|
||||
id: bump-version
|
||||
env:
|
||||
VERSION_TYPE: ${{ steps.prepared-inputs.outputs.version_type }}
|
||||
PRE_RELEASE: ${{ steps.prepared-inputs.outputs.pre_release }}
|
||||
run: |
|
||||
pnpm version ${{ github.event.inputs.version_type }} --preid ${{ github.event.inputs.pre_release }} --no-git-tag-version
|
||||
set -euo pipefail
|
||||
if [[ -n "$PRE_RELEASE" && ! "$VERSION_TYPE" =~ ^pre(major|minor|patch)$ && "$VERSION_TYPE" != "prerelease" ]]; then
|
||||
echo "❌ pre_release was provided but version_type='$VERSION_TYPE' does not support --preid"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$PRE_RELEASE" ]]; then
|
||||
pnpm version "$VERSION_TYPE" --preid "$PRE_RELEASE" --no-git-tag-version
|
||||
else
|
||||
pnpm version "$VERSION_TYPE" --no-git-tag-version
|
||||
fi
|
||||
|
||||
NEW_VERSION=$(node -p "require('./package.json').version")
|
||||
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "NEW_VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Format PR string
|
||||
id: capitalised
|
||||
env:
|
||||
VERSION_TYPE: ${{ steps.prepared-inputs.outputs.version_type }}
|
||||
run: |
|
||||
CAPITALISED_TYPE=${{ github.event.inputs.version_type }}
|
||||
echo "capitalised=${CAPITALISED_TYPE@u}" >> $GITHUB_OUTPUT
|
||||
CAPITALISED_TYPE="$VERSION_TYPE"
|
||||
echo "capitalised=${CAPITALISED_TYPE@u}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
|
||||
@@ -83,8 +188,8 @@ jobs:
|
||||
body: |
|
||||
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
|
||||
**Base branch:** `${{ github.event.inputs.branch }}`
|
||||
**Base branch:** `${{ steps.prepared-inputs.outputs.branch }}`
|
||||
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
base: ${{ steps.prepared-inputs.outputs.branch }}
|
||||
labels: |
|
||||
Release
|
||||
|
||||
292
.github/workflows/release-weekly-comfyui.yaml
vendored
Normal file
@@ -0,0 +1,292 @@
|
||||
# Automated weekly workflow to bump ComfyUI frontend RC releases
|
||||
name: "Release: Weekly ComfyUI"
|
||||
|
||||
on:
|
||||
# Schedule for Monday at 12:00 PM PST (20:00 UTC)
|
||||
schedule:
|
||||
- cron: '0 20 * * 1'
|
||||
|
||||
# Allow manual triggering
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
comfyui_fork:
|
||||
description: 'ComfyUI fork to use for PR (e.g., Comfy-Org/ComfyUI)'
|
||||
required: false
|
||||
default: 'Comfy-Org/ComfyUI'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
resolve-version:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
current_version: ${{ steps.resolve.outputs.current_version }}
|
||||
target_version: ${{ steps.resolve.outputs.target_version }}
|
||||
target_minor: ${{ steps.resolve.outputs.target_minor }}
|
||||
target_branch: ${{ steps.resolve.outputs.target_branch }}
|
||||
needs_release: ${{ steps.resolve.outputs.needs_release }}
|
||||
diff_url: ${{ steps.resolve.outputs.diff_url }}
|
||||
latest_patch_tag: ${{ steps.resolve.outputs.latest_patch_tag }}
|
||||
|
||||
steps:
|
||||
- name: Checkout ComfyUI_frontend
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
path: frontend
|
||||
|
||||
- name: Checkout ComfyUI (sparse)
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: comfyanonymous/ComfyUI
|
||||
sparse-checkout: |
|
||||
requirements.txt
|
||||
path: comfyui
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Resolve release information
|
||||
id: resolve
|
||||
working-directory: frontend
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Run the resolver script
|
||||
if ! RESULT=$(pnpm exec tsx scripts/cicd/resolve-comfyui-release.ts ../comfyui .); then
|
||||
echo "Failed to resolve release information"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Resolver output:"
|
||||
echo "$RESULT"
|
||||
|
||||
# Validate JSON output
|
||||
if ! echo "$RESULT" | jq empty 2>/dev/null; then
|
||||
echo "Invalid JSON output from resolver"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse JSON output and set outputs
|
||||
echo "current_version=$(echo "$RESULT" | jq -r '.current_version')" >> $GITHUB_OUTPUT
|
||||
echo "target_version=$(echo "$RESULT" | jq -r '.target_version')" >> $GITHUB_OUTPUT
|
||||
echo "target_minor=$(echo "$RESULT" | jq -r '.target_minor')" >> $GITHUB_OUTPUT
|
||||
echo "target_branch=$(echo "$RESULT" | jq -r '.target_branch')" >> $GITHUB_OUTPUT
|
||||
echo "needs_release=$(echo "$RESULT" | jq -r '.needs_release')" >> $GITHUB_OUTPUT
|
||||
echo "diff_url=$(echo "$RESULT" | jq -r '.diff_url')" >> $GITHUB_OUTPUT
|
||||
echo "latest_patch_tag=$(echo "$RESULT" | jq -r '.latest_patch_tag')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## Release Information" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Current version: ${{ steps.resolve.outputs.current_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Target version: ${{ steps.resolve.outputs.target_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Target branch: ${{ steps.resolve.outputs.target_branch }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Needs release: ${{ steps.resolve.outputs.needs_release }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Diff: [${{ steps.resolve.outputs.current_version }}...${{ steps.resolve.outputs.target_version }}](${{ steps.resolve.outputs.diff_url }})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
trigger-release-if-needed:
|
||||
needs: resolve-version
|
||||
if: needs.resolve-version.outputs.needs_release == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Trigger release workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "Triggering release workflow for branch ${{ needs.resolve-version.outputs.target_branch }}"
|
||||
|
||||
# Trigger the release-version-bump workflow
|
||||
if ! gh workflow run release-version-bump.yaml \
|
||||
--repo Comfy-Org/ComfyUI_frontend \
|
||||
--ref main \
|
||||
--field version_type=patch \
|
||||
--field branch=${{ needs.resolve-version.outputs.target_branch }}; then
|
||||
echo "Failed to trigger release workflow"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Release workflow triggered successfully for ${{ needs.resolve-version.outputs.target_branch }}"
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## Release Workflow Triggered" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Branch: ${{ needs.resolve-version.outputs.target_branch }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Target version: ${{ needs.resolve-version.outputs.target_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [View workflow runs](https://github.com/Comfy-Org/ComfyUI_frontend/actions/workflows/release-version-bump.yaml)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
create-comfyui-pr:
|
||||
needs: [resolve-version, trigger-release-if-needed]
|
||||
if: always() && needs.resolve-version.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout ComfyUI fork
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }}
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
path: comfyui
|
||||
|
||||
- name: Sync with upstream
|
||||
working-directory: comfyui
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Fetch latest upstream to base our branch on fresh code
|
||||
# Note: This only affects the local checkout, NOT the fork's master branch
|
||||
# We only push the automation branch, leaving the fork's master untouched
|
||||
echo "Fetching upstream master..."
|
||||
if ! git fetch https://github.com/comfyanonymous/ComfyUI.git master; then
|
||||
echo "Failed to fetch upstream master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Checking out upstream master..."
|
||||
if ! git checkout FETCH_HEAD; then
|
||||
echo "Failed to checkout FETCH_HEAD"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Successfully synced with upstream master"
|
||||
|
||||
- name: Update requirements.txt
|
||||
working-directory: comfyui
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_VERSION="${{ needs.resolve-version.outputs.target_version }}"
|
||||
echo "Updating comfyui-frontend-package to ${TARGET_VERSION}"
|
||||
|
||||
# Update the comfyui-frontend-package version (POSIX-compatible)
|
||||
sed -i.bak "s/comfyui-frontend-package==[0-9.][0-9.]*/comfyui-frontend-package==${TARGET_VERSION}/" requirements.txt
|
||||
rm requirements.txt.bak
|
||||
|
||||
# Verify the change was made
|
||||
if ! grep -q "comfyui-frontend-package==${TARGET_VERSION}" requirements.txt; then
|
||||
echo "Failed to update requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Updated requirements.txt:"
|
||||
grep comfyui-frontend-package requirements.txt
|
||||
|
||||
- name: Build PR description
|
||||
id: pr-body
|
||||
run: |
|
||||
BODY=$(cat <<'EOF'
|
||||
Bumps frontend to ${{ needs.resolve-version.outputs.target_version }}
|
||||
|
||||
Test quickly:
|
||||
|
||||
```bash
|
||||
python main.py --front-end-version Comfy-Org/ComfyUI_frontend@${{ needs.resolve-version.outputs.target_version }}
|
||||
```
|
||||
|
||||
- Diff: [v${{ needs.resolve-version.outputs.current_version }}...v${{ needs.resolve-version.outputs.target_version }}](${{ needs.resolve-version.outputs.diff_url }})
|
||||
- PyPI: https://pypi.org/project/comfyui-frontend-package/${{ needs.resolve-version.outputs.target_version }}/
|
||||
- npm: https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types/v/${{ needs.resolve-version.outputs.target_version }}
|
||||
EOF
|
||||
)
|
||||
|
||||
# Add release PR note if release was triggered
|
||||
if [ "${{ needs.resolve-version.outputs.needs_release }}" = "true" ]; then
|
||||
RELEASE_NOTE="⚠️ **Release PR must be merged first** - check [release workflow runs](https://github.com/Comfy-Org/ComfyUI_frontend/actions/workflows/release-version-bump.yaml)"
|
||||
BODY=$''"${RELEASE_NOTE}"$'\n\n'"${BODY}"
|
||||
fi
|
||||
|
||||
# Save to file for later use
|
||||
printf '%s\n' "$BODY" > pr-body.txt
|
||||
cat pr-body.txt
|
||||
|
||||
- name: Create PR to ComfyUI
|
||||
working-directory: comfyui
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
COMFYUI_FORK: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Extract fork owner from repository name
|
||||
FORK_OWNER=$(echo "$COMFYUI_FORK" | cut -d'/' -f1)
|
||||
|
||||
echo "Creating PR from ${COMFYUI_FORK} to comfyanonymous/ComfyUI"
|
||||
|
||||
# Configure git
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Create/update branch (reuse same branch name each week)
|
||||
BRANCH="automation/comfyui-frontend-bump"
|
||||
git checkout -B "$BRANCH"
|
||||
git add requirements.txt
|
||||
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "Bump comfyui-frontend-package to ${{ needs.resolve-version.outputs.target_version }}"
|
||||
else
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Force push to fork (overwrites previous week's branch)
|
||||
# Note: This intentionally destroys branch history to maintain a single PR
|
||||
# Any review comments or manual commits will need to be re-applied
|
||||
if ! git push -f origin "$BRANCH"; then
|
||||
echo "Failed to push branch to fork"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create draft PR from fork to upstream
|
||||
PR_BODY=$(cat ../pr-body.txt)
|
||||
|
||||
# Try to create PR, ignore error if it already exists
|
||||
if ! gh pr create \
|
||||
--repo comfyanonymous/ComfyUI \
|
||||
--head "${FORK_OWNER}:${BRANCH}" \
|
||||
--base master \
|
||||
--title "Bump comfyui-frontend-package to ${{ needs.resolve-version.outputs.target_version }}" \
|
||||
--body "$PR_BODY" \
|
||||
--draft 2>&1; then
|
||||
|
||||
# Check if PR already exists
|
||||
set +e
|
||||
EXISTING_PR=$(gh pr list --repo comfyanonymous/ComfyUI --head "${FORK_OWNER}:${BRANCH}" --json number --jq '.[0].number' 2>&1)
|
||||
PR_LIST_EXIT=$?
|
||||
set -e
|
||||
|
||||
if [ $PR_LIST_EXIT -ne 0 ]; then
|
||||
echo "Failed to check for existing PR: $EXISTING_PR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ]; then
|
||||
echo "PR already exists (#${EXISTING_PR}), updating branch will update the PR"
|
||||
else
|
||||
echo "Failed to create PR and no existing PR found"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## ComfyUI PR Created" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Draft PR created in comfyanonymous/ComfyUI" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### PR Body:" >> $GITHUB_STEP_SUMMARY
|
||||
cat pr-body.txt >> $GITHUB_STEP_SUMMARY
|
||||
2
.github/workflows/weekly-docs-check.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Automated weekly documentation accuracy check and update via Claude
|
||||
name: "Weekly Documentation Check"
|
||||
description: "Automated weekly documentation accuracy check and update via Claude"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -9,8 +9,8 @@ module.exports = defineConfig({
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream, Civitai, Hugging Face.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
|
||||
|
||||
1
.npmrc
@@ -1,2 +1,3 @@
|
||||
ignore-workspace-root-check=true
|
||||
catalog-mode=prefer
|
||||
public-hoist-pattern[]=@parcel/watcher
|
||||
|
||||
@@ -2,25 +2,98 @@
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"ignorePatterns": [
|
||||
".i18nrc.cjs",
|
||||
"components.d.ts",
|
||||
"lint-staged.config.js",
|
||||
"vitest.setup.ts",
|
||||
".nx/*",
|
||||
"**/vite.config.*.timestamp*",
|
||||
"**/vitest.config.*.timestamp*",
|
||||
"components.d.ts",
|
||||
"coverage/*",
|
||||
"dist/*",
|
||||
"packages/registry-types/src/comfyRegistryTypes.ts",
|
||||
"playwright-report/*",
|
||||
"src/extensions/core/*",
|
||||
"src/scripts/*",
|
||||
"src/types/generatedManagerTypes.ts",
|
||||
"src/types/vue-shim.d.ts"
|
||||
"src/types/vue-shim.d.ts",
|
||||
"test-results/*",
|
||||
"vitest.setup.ts"
|
||||
],
|
||||
"plugins": [
|
||||
"eslint",
|
||||
"import",
|
||||
"oxc",
|
||||
"typescript",
|
||||
"unicorn",
|
||||
"vitest",
|
||||
"vue"
|
||||
],
|
||||
"rules": {
|
||||
"no-async-promise-executor": "off",
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"warn",
|
||||
"error"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-control-regex": "off",
|
||||
"no-eval": "off",
|
||||
"no-redeclare": "error",
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "primevue/calendar",
|
||||
"message": "Calendar is deprecated in PrimeVue 4+. Use DatePicker instead: import DatePicker from 'primevue/datepicker'"
|
||||
},
|
||||
{
|
||||
"name": "primevue/dropdown",
|
||||
"message": "Dropdown is deprecated in PrimeVue 4+. Use Select instead: import Select from 'primevue/select'"
|
||||
},
|
||||
{
|
||||
"name": "primevue/inputswitch",
|
||||
"message": "InputSwitch is deprecated in PrimeVue 4+. Use ToggleSwitch instead: import ToggleSwitch from 'primevue/toggleswitch'"
|
||||
},
|
||||
{
|
||||
"name": "primevue/overlaypanel",
|
||||
"message": "OverlayPanel is deprecated in PrimeVue 4+. Use Popover instead: import Popover from 'primevue/popover'"
|
||||
},
|
||||
{
|
||||
"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()`"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-self-assign": "allow",
|
||||
"no-unused-expressions": "off",
|
||||
"no-unused-private-class-members": "off",
|
||||
"no-useless-rename": "off",
|
||||
"import/default": "error",
|
||||
"import/export": "error",
|
||||
"import/namespace": "error",
|
||||
"import/no-duplicates": "error",
|
||||
"import/consistent-type-specifier-style": [
|
||||
"error",
|
||||
"prefer-top-level"
|
||||
],
|
||||
"jest/expect-expect": "off",
|
||||
"jest/no-conditional-expect": "off",
|
||||
"jest/no-disabled-tests": "off",
|
||||
"jest/no-standalone-expect": "off",
|
||||
"jest/valid-title": "off",
|
||||
"typescript/no-this-alias": "off",
|
||||
"typescript/no-unnecessary-parameter-property-assignment": "off",
|
||||
"typescript/no-unsafe-declaration-merging": "off",
|
||||
@@ -38,6 +111,19 @@
|
||||
"typescript/no-redundant-type-constituents": "off",
|
||||
"typescript/restrict-template-expressions": "off",
|
||||
"typescript/unbound-method": "off",
|
||||
"typescript/no-floating-promises": "error"
|
||||
}
|
||||
"typescript/no-floating-promises": "error",
|
||||
"vue/no-import-compiler-macros": "error",
|
||||
"vue/no-dupe-keys": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/*.{stories,test,spec}.ts",
|
||||
"**/*.stories.vue"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": "allow"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -64,7 +64,6 @@ const config: StorybookConfig = {
|
||||
deep: true,
|
||||
extensions: ['vue']
|
||||
})
|
||||
// Note: Explicitly NOT including generateImportMapPlugin to avoid externalization
|
||||
],
|
||||
server: {
|
||||
allowedHosts: true
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
"declaration-property-value-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"typesSyntax": {
|
||||
"radial-gradient()": "| <any-value>"
|
||||
},
|
||||
"ignoreProperties": {
|
||||
"speak": ["none"],
|
||||
"app-region": ["drag", "no-drag"],
|
||||
@@ -56,10 +59,7 @@
|
||||
"function-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreFunctions": [
|
||||
"theme",
|
||||
"v-bind"
|
||||
]
|
||||
"ignoreFunctions": ["theme", "v-bind"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
254
AGENTS.md
@@ -1,38 +1,244 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- Source: `src/` (Vue 3 + TypeScript). Key areas: `components/`, `views/`, `stores/` (Pinia), `composables/`, `services/`, `utils/`, `assets/`, `locales/`.
|
||||
- Routing/i18n/entry: `src/router.ts`, `src/i18n.ts`, `src/main.ts`.
|
||||
- Tests: unit/component in `tests-ui/` and `src/components/**/*.{test,spec}.ts`; E2E in `browser_tests/`.
|
||||
- Public assets: `public/`. Build output: `dist/`.
|
||||
- Config: `vite.config.mts`, `vitest.config.ts`, `playwright.config.ts`, `eslint.config.ts`, `.prettierrc`.
|
||||
|
||||
- Source: `src/`
|
||||
- Vue 3.5+
|
||||
- TypeScript
|
||||
- Tailwind 4
|
||||
- Key areas:
|
||||
- `components/`
|
||||
- `views/`
|
||||
- `stores/` (Pinia)
|
||||
- `composables/`
|
||||
- `services/`
|
||||
- `utils/`
|
||||
- `assets/`
|
||||
- `locales/`
|
||||
- Routing: `src/router.ts`,
|
||||
- i18n: `src/i18n.ts`,
|
||||
- Entry Point: `src/main.ts`.
|
||||
- Tests:
|
||||
- unit/component in `tests-ui/` and `src/**/*.test.ts`
|
||||
- E2E (Playwright) in `browser_tests/**/*.spec.ts`
|
||||
- Public assets: `public/`
|
||||
- Build output: `dist/`
|
||||
- Configs
|
||||
- `vite.config.mts`
|
||||
- `vitest.config.ts`
|
||||
- `playwright.config.ts`
|
||||
- `eslint.config.ts`
|
||||
- `.prettierrc`
|
||||
- etc.
|
||||
|
||||
## Monorepo Architecture
|
||||
|
||||
The project uses **Nx** for build orchestration and task management
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
- `pnpm dev`: Start Vite dev server.
|
||||
- `pnpm dev:electron`: Dev server with Electron API mocks.
|
||||
- `pnpm build`: Type-check then production build to `dist/`.
|
||||
- `pnpm preview`: Preview the production build locally.
|
||||
- `pnpm test:unit`: Run Vitest unit tests.
|
||||
- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`).
|
||||
- `pnpm lint` / `pnpm lint:fix`: Lint (ESLint). `pnpm format` / `format:check`: Prettier.
|
||||
- `pnpm typecheck`: Vue TSC type checking.
|
||||
- `pnpm dev:electron`: Dev server with Electron API mocks
|
||||
- `pnpm build`: Type-check then production build to `dist/`
|
||||
- `pnpm preview`: Preview the production build locally
|
||||
- `pnpm test:unit`: Run Vitest unit tests
|
||||
- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`)
|
||||
- `pnpm lint` / `pnpm lint:fix`: Lint (ESLint)
|
||||
- `pnpm format` / `pnpm format:check`: Prettier
|
||||
- `pnpm typecheck`: Vue TSC type checking
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Language: TypeScript, Vue SFCs (`.vue`). Indent 2 spaces; single quotes; no semicolons; width 80 (see `.prettierrc`).
|
||||
- Imports: sorted/grouped by plugin; run `pnpm format` before committing.
|
||||
- ESLint: Vue + TS rules; no floating promises; unused imports disallowed; i18n raw text restrictions in templates.
|
||||
- Naming: Vue components in PascalCase (e.g., `MenuHamburger.vue`); composables `useXyz.ts`; Pinia stores `*Store.ts`.
|
||||
|
||||
## Testing Guidelines
|
||||
- Frameworks: Vitest (unit/component, happy-dom) and Playwright (E2E).
|
||||
- Test files: `**/*.{test,spec}.{ts,tsx,js}` under `tests-ui/`, `src/components/`, and `src/lib/litegraph/test/`.
|
||||
- Coverage: text/json/html reporters enabled; aim to cover critical logic and new features.
|
||||
- Playwright: place tests in `browser_tests/`; optional tags like `@mobile`, `@2x` are respected by config.
|
||||
- Language:
|
||||
- TypeScript (exclusive, no new JavaScript)
|
||||
- Vue 3 SFCs (`.vue`)
|
||||
- Composition API only
|
||||
- Tailwind 4 styling
|
||||
- Avoid `<style>` blocks
|
||||
- Style: (see `.prettierrc`)
|
||||
- Indent 2 spaces
|
||||
- single quotes
|
||||
- no trailing semicolons
|
||||
- width 80
|
||||
- Imports:
|
||||
- sorted/grouped by plugin
|
||||
- run `pnpm format` before committing
|
||||
- ESLint:
|
||||
- Vue + TS rules
|
||||
- no floating promises
|
||||
- unused imports disallowed
|
||||
- i18n raw text restrictions in templates
|
||||
- Naming:
|
||||
- Vue components in PascalCase (e.g., `MenuHamburger.vue`)
|
||||
- composables `useXyz.ts`
|
||||
- Pinia stores `*Store.ts`
|
||||
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Commits: Use `[skip ci]` for locale-only updates when appropriate.
|
||||
- PRs: Include clear description, linked issues (`- Fixes #123`), and screenshots/GIFs for UI changes.
|
||||
- Quality gates: `pnpm lint`, `pnpm typecheck`, and relevant tests must pass. Keep PRs focused and small.
|
||||
|
||||
- PRs:
|
||||
- Include clear description
|
||||
- Reference linked issues (e.g. `- Fixes #123`)
|
||||
- Keep it extremely concise and information-dense
|
||||
- Don't use emojis or add excessive headers/sections
|
||||
- Follow the PR description template in the `.github/` folder.
|
||||
- Quality gates:
|
||||
- `pnpm lint`
|
||||
- `pnpm typecheck`
|
||||
- `pnpm knip`
|
||||
- Relevant tests must pass
|
||||
- Never use `--no-verify` to bypass failing tests
|
||||
- Identify the issue and present root cause analysis and possible solutions if you are unable to solve quickly yourself
|
||||
- Keep PRs focused and small
|
||||
- If it looks like the current changes will have 300+ lines of non-test code, suggest ways it could be broken into multiple PRs
|
||||
|
||||
## Security & Configuration Tips
|
||||
|
||||
- Secrets: Use `.env` (see `.env_example`); do not commit secrets.
|
||||
|
||||
## Vue 3 Composition API Best Practices
|
||||
|
||||
- Use `<script setup lang="ts">` for component logic
|
||||
- Utilize `ref` for reactive state
|
||||
- Implement computed properties with computed()
|
||||
- Use watch and watchEffect for side effects
|
||||
- Avoid using a `ref` and a `watch` if a `computed` would work instead
|
||||
- Implement lifecycle hooks with onMounted, onUpdated, etc.
|
||||
- Utilize provide/inject for dependency injection
|
||||
- Do not use dependency injection if a Store or a shared composable would be simpler
|
||||
- Use Vue 3.5 TypeScript style of default prop declaration
|
||||
- Example:
|
||||
|
||||
```typescript
|
||||
const { nodes, showTotal = true } = defineProps<{
|
||||
nodes: ApiNodeCost[]
|
||||
showTotal?: boolean
|
||||
}>()
|
||||
```
|
||||
|
||||
- Prefer reactive props destructuring to `const props = defineProps<...>`
|
||||
- Do not use `withDefaults` or runtime props declaration
|
||||
- Do not import Vue macros unnecessarily
|
||||
- Prefer `useModel` to separately defining a prop and emit
|
||||
- Be judicious with addition of new refs or other state
|
||||
- If it's possible to accomplish the design goals with just a prop, don't add a `ref`
|
||||
- If it's possible to use the `ref` or prop directly, don't add a `computed`
|
||||
- If it's possible to use a `computed` to name and reuse a derived value, don't use a `watch`
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
1. Leverage VueUse functions for performance-enhancing styles
|
||||
2. Use es-toolkit for utility functions
|
||||
3. Use TypeScript for type safety
|
||||
4. If a complex type definition is inlined in multiple related places, extract and name it for reuse
|
||||
5. In Vue Components, implement proper props and emits definitions
|
||||
6. Utilize Vue 3's Teleport component when needed
|
||||
7. Use Suspense for async components
|
||||
8. Implement proper error handling
|
||||
9. Follow Vue 3 style guide and naming conventions
|
||||
10. Use Vite for fast development and building
|
||||
11. Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json
|
||||
12. Avoid new usage of PrimeVue components
|
||||
13. Write tests for all changes, especially bug fixes to catch future regressions
|
||||
14. Write code that is expressive and self-documenting to the furthest degree possible. This reduces the need for code comments which can get out of sync with the code itself. Try to avoid comments unless absolutely necessary
|
||||
15. Do not add or retain redundant comments, clean as you go
|
||||
16. Whenever a new piece of code is written, the author should ask themselves 'is there a simpler way to introduce the same functionality?'. If the answer is yes, the simpler course should be chosen
|
||||
17. [Refactoring](https://refactoring.com/catalog/) should be used to make complex code simpler
|
||||
18. Try to minimize the surface area (exported values) of each module and composable
|
||||
19. Don't use barrel files, e.g. `/some/package/index.ts` to re-export within `/src`
|
||||
20. Keep functions short and functional
|
||||
21. Minimize [nesting](https://wiki.c2.com/?ArrowAntiPattern), e.g. `if () { ... }` or `for () { ... }`
|
||||
22. Avoid mutable state, prefer immutability and assignment at point of declaration
|
||||
23. Favor pure functions (especially testable ones)
|
||||
24. Watch out for [Code Smells](https://wiki.c2.com/?CodeSmell) and refactor to avoid them
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
- Frameworks:
|
||||
- Vitest (unit/component, happy-dom)
|
||||
- Playwright (E2E)
|
||||
- Test files:
|
||||
- Unit/Component: `**/*.test.ts`
|
||||
- E2E: `browser_tests/**/*.spec.ts`
|
||||
- Litegraph Specific: `src/lib/litegraph/test/`
|
||||
|
||||
### General
|
||||
|
||||
1. Do not write change detector tests
|
||||
e.g. a test that just asserts that the defaults are certain values
|
||||
2. Do not write tests that are dependent on non-behavioral features like utility classes or styles
|
||||
3. Be parsimonious in testing, do not write redundant tests
|
||||
See <https://tidyfirst.substack.com/p/composable-tests>
|
||||
4. [Don’t Mock What You Don’t Own](https://hynek.me/articles/what-to-mock-in-5-mins/)
|
||||
|
||||
### Vitest / Unit Tests
|
||||
|
||||
1. Do not write tests that just test the mocks
|
||||
Ensure that the tests fail when the code itself would behave in a way that was not expected or desired
|
||||
2. For mocking, leverage [Vitest's utilities](https://vitest.dev/guide/mocking.html) where possible
|
||||
3. Keep your module mocks contained
|
||||
Do not use global mutable state within the test file
|
||||
Use `vi.hoisted()` if necessary to allow for per-test Arrange phase manipulation of deeper mock state
|
||||
4. For Component testing, use [Vue Test Utils](https://test-utils.vuejs.org/) and especially follow the advice [about making components easy to test](https://test-utils.vuejs.org/guide/essentials/easy-to-test.html)
|
||||
5. Aim for behavioral coverage of critical and new features
|
||||
|
||||
### Playwright / Browser / E2E Tests
|
||||
|
||||
1. Follow the Best Practices described [in the Playwright documentation](https://playwright.dev/docs/best-practices)
|
||||
2. Do not use waitForTimeout, use Locator actions and [retrying assertions](https://playwright.dev/docs/test-assertions#auto-retrying-assertions)
|
||||
3. Tags like `@mobile`, `@2x` are respected by config and should be used for relevant tests
|
||||
|
||||
## External Resources
|
||||
|
||||
- Vue: <https://vuejs.org/api/>
|
||||
- Tailwind: <https://tailwindcss.com/docs/styling-with-utility-classes>
|
||||
- VueUse: <https://vueuse.org/functions.html>
|
||||
- shadcn/vue: <https://www.shadcn-vue.com/>
|
||||
- Reka UI: <https://reka-ui.com/>
|
||||
- PrimeVue: <https://primevue.org>
|
||||
- ComfyUI: <https://docs.comfy.org>
|
||||
- Electron: <https://www.electronjs.org/docs/latest/>
|
||||
- Wiki: <https://deepwiki.com/Comfy-Org/ComfyUI_frontend/1-overview>
|
||||
- Nx: <https://nx.dev/docs/reference/nx-commands>
|
||||
- [Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html)
|
||||
|
||||
## Project Philosophy
|
||||
|
||||
- Follow good software engineering principles
|
||||
- YAGNI
|
||||
- AHA
|
||||
- DRY
|
||||
- SOLID
|
||||
- Clean, stable public APIs
|
||||
- Domain-driven design
|
||||
- Thousands of users and extensions
|
||||
- Prioritize clean interfaces that restrict extension access
|
||||
|
||||
## Repository Navigation
|
||||
|
||||
- Check README files in key folders (tests-ui, browser_tests, composables, etc.)
|
||||
- Prefer running single tests for performance
|
||||
- Use --help for unfamiliar CLI tools
|
||||
|
||||
## GitHub Integration
|
||||
|
||||
When referencing Comfy-Org repos:
|
||||
|
||||
1. Check for local copy
|
||||
2. Use GitHub API for branches/PRs/metadata
|
||||
3. Curl GitHub website if needed
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- NEVER use `any` type - use proper TypeScript types
|
||||
- NEVER use `as any` type assertions - fix the underlying type issue
|
||||
- NEVER use `--no-verify` flag when committing
|
||||
- NEVER delete or disable tests to make them pass
|
||||
- NEVER circumvent quality checks
|
||||
- NEVER use the `dark:` tailwind variant
|
||||
- Instead use a semantic value from the `style.css` theme
|
||||
- e.g. `bg-node-component-surface`
|
||||
- NEVER use `:class="[]"` to merge class names
|
||||
- Always use `import { cn } from '@/utils/tailwindUtil'`
|
||||
- e.g. `<div :class="cn('text-node-component-header-icon', hasError && 'text-danger')" />`
|
||||
- Use `cn()` inline in the template when feasible instead of creating a `computed` to hold the value
|
||||
|
||||
120
CLAUDE.md
@@ -1,44 +1,19 @@
|
||||
# ComfyUI Frontend Project Guidelines
|
||||
# Claude Code specific instructions
|
||||
|
||||
@Agents.md
|
||||
|
||||
## Repository Setup
|
||||
|
||||
For first-time setup, use the Claude command:
|
||||
```
|
||||
|
||||
```sh
|
||||
/setup_repo
|
||||
```
|
||||
|
||||
This bootstraps the monorepo with dependencies, builds, tests, and dev server verification.
|
||||
|
||||
**Prerequisites:** Node.js >= 24, Git repository, available ports (5173, 6006)
|
||||
|
||||
## Quick Commands
|
||||
|
||||
- `pnpm`: See all available commands
|
||||
- `pnpm dev`: Start development server (port 5173, via nx)
|
||||
- `pnpm typecheck`: Type checking
|
||||
- `pnpm build`: Build for production (via nx)
|
||||
- `pnpm lint`: Linting (via nx)
|
||||
- `pnpm oxlint`: Fast Rust-based linting with Oxc
|
||||
- `pnpm format`: Prettier formatting
|
||||
- `pnpm test:unit`: Run all unit tests
|
||||
- `pnpm test:browser`: Run E2E tests via Playwright
|
||||
- `pnpm test:unit -- tests-ui/tests/example.test.ts`: Run single test file
|
||||
- `pnpm storybook`: Start Storybook development server (port 6006)
|
||||
- `pnpm knip`: Detect unused code and dependencies
|
||||
|
||||
## Monorepo Architecture
|
||||
|
||||
The project now uses **Nx** for build orchestration and task management:
|
||||
|
||||
- **Task Orchestration**: Commands like `dev`, `build`, `lint`, and `test:browser` run via Nx
|
||||
- **Caching**: Nx provides intelligent caching for faster rebuilds
|
||||
- **Configuration**: Managed through `nx.json` with plugins for ESLint, Storybook, Vite, and Playwright
|
||||
- **Dependencies**: Nx handles dependency graph analysis and parallel execution
|
||||
|
||||
Key Nx features:
|
||||
- Build target caching and incremental builds
|
||||
- Parallel task execution across the monorepo
|
||||
- Plugin-based architecture for different tools
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **First-time setup**: Run `/setup_repo` Claude command
|
||||
@@ -50,87 +25,6 @@ Key Nx features:
|
||||
|
||||
## Git Conventions
|
||||
|
||||
- Use [prefix] format: [feat], [bugfix], [docs]
|
||||
- Use `prefix:` format: `feat:`, `fix:`, `test:`
|
||||
- Add "Fixes #n" to PR descriptions
|
||||
- Never mention Claude/AI in commits
|
||||
|
||||
## External Resources
|
||||
|
||||
- PrimeVue docs: <https://primevue.org>
|
||||
- ComfyUI docs: <https://docs.comfy.org>
|
||||
- Electron: <https://www.electronjs.org/docs/latest/>
|
||||
- Wiki: <https://deepwiki.com/Comfy-Org/ComfyUI_frontend/1-overview>
|
||||
|
||||
## Project Philosophy
|
||||
|
||||
- Follow good software engineering principles
|
||||
- YAGNI
|
||||
- AHA
|
||||
- DRY
|
||||
- SOLID
|
||||
- Clean, stable public APIs
|
||||
- Domain-driven design
|
||||
- Thousands of users and extensions
|
||||
- Prioritize clean interfaces that restrict extension access
|
||||
|
||||
## Repository Navigation
|
||||
|
||||
- Check README files in key folders (tests-ui, browser_tests, composables, etc.)
|
||||
- Prefer running single tests for performance
|
||||
- Use --help for unfamiliar CLI tools
|
||||
|
||||
## GitHub Integration
|
||||
|
||||
When referencing Comfy-Org repos:
|
||||
|
||||
1. Check for local copy
|
||||
2. Use GitHub API for branches/PRs/metadata
|
||||
3. Curl GitHub website if needed
|
||||
|
||||
## Settings and Feature Flags Quick Reference
|
||||
|
||||
### Settings Usage
|
||||
```typescript
|
||||
const settingStore = useSettingStore()
|
||||
const value = settingStore.get('Comfy.SomeSetting') // Get setting
|
||||
await settingStore.set('Comfy.SomeSetting', newValue) // Update setting
|
||||
```
|
||||
|
||||
### Dynamic Defaults
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Example.Setting',
|
||||
defaultValue: () => window.innerWidth < 1024 ? 'small' : 'large' // Runtime context
|
||||
}
|
||||
```
|
||||
|
||||
### Version-Based Defaults
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Example.Feature',
|
||||
defaultValue: 'legacy',
|
||||
defaultsByInstallVersion: { '1.25.0': 'enhanced' } // Gradual rollout
|
||||
}
|
||||
```
|
||||
|
||||
### Feature Flags
|
||||
```typescript
|
||||
if (api.serverSupportsFeature('feature_name')) { // Check capability
|
||||
// Use enhanced feature
|
||||
}
|
||||
const value = api.getServerFeature('config_name', defaultValue) // Get config
|
||||
```
|
||||
|
||||
**Documentation:**
|
||||
- Settings system: `docs/SETTINGS.md`
|
||||
- Feature flags system: `docs/FEATURE_FLAGS.md`
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- NEVER use `any` type - use proper TypeScript types
|
||||
- NEVER use `as any` type assertions - fix the underlying type issue
|
||||
- NEVER use `--no-verify` flag when committing
|
||||
- NEVER delete or disable tests to make them pass
|
||||
- NEVER circumvent quality checks
|
||||
- NEVER use `dark:` or `dark-theme:` tailwind variants. Instead use a semantic value from the `style.css` theme, e.g. `bg-node-component-surface`
|
||||
- NEVER use `:class="[]"` to merge class names - always use `import { cn } from '@/utils/tailwindUtil'`, for example: `<div :class="cn('text-node-component-header-icon', hasError && 'text-danger')" />`
|
||||
|
||||
24
CODEOWNERS
@@ -1,8 +1,11 @@
|
||||
# Global Ownership
|
||||
* @Comfy-org/comfy_frontend_devs
|
||||
|
||||
# Desktop/Electron
|
||||
/apps/desktop-ui/ @webfiltered
|
||||
/src/stores/electronDownloadStore.ts @webfiltered
|
||||
/src/extensions/core/electronAdapter.ts @webfiltered
|
||||
/vite.electron.config.mts @webfiltered
|
||||
/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
|
||||
@@ -22,6 +25,9 @@
|
||||
# Link rendering
|
||||
/src/renderer/core/canvas/links/ @benceruleanlu
|
||||
|
||||
# Partner Nodes
|
||||
/src/composables/node/useNodePricing.ts @jojodecayz @bigcat88
|
||||
|
||||
# Node help system
|
||||
/src/utils/nodeHelpUtil.ts @benceruleanlu
|
||||
/src/stores/workspace/nodeHelpStore.ts @benceruleanlu
|
||||
@@ -31,10 +37,7 @@
|
||||
/src/components/graph/selectionToolbox/ @Myestery
|
||||
|
||||
# Minimap
|
||||
/src/renderer/extensions/minimap/ @jtydhr88
|
||||
|
||||
# Assets
|
||||
/src/platform/assets/ @arjansingh
|
||||
/src/renderer/extensions/minimap/ @jtydhr88 @Myestery
|
||||
|
||||
# Workflow Templates
|
||||
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
|
||||
@@ -53,11 +56,12 @@
|
||||
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
|
||||
|
||||
# Translations
|
||||
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer
|
||||
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer @Comfy-org/comfy_frontend_devs
|
||||
/src/locales/pt-BR/ @JonatanAtila @Yorha4D @KarryCharon @shinshin86
|
||||
|
||||
# LLM Instructions (blank on purpose)
|
||||
.claude/
|
||||
.cursor/
|
||||
.cursorrules
|
||||
**/AGENTS.md
|
||||
**/CLAUDE.md
|
||||
**/CLAUDE.md
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/desktop-ui",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"type": "module",
|
||||
"nx": {
|
||||
"tags": [
|
||||
@@ -87,11 +87,13 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "nx run @comfyorg/desktop-ui:lint",
|
||||
"typecheck": "nx run @comfyorg/desktop-ui:typecheck",
|
||||
"storybook": "storybook dev -p 6007",
|
||||
"build-storybook": "storybook build -o dist/storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@comfyorg/comfyui-electron-types": "0.4.73-0",
|
||||
"@comfyorg/comfyui-electron-types": "catalog:",
|
||||
"@comfyorg/shared-frontend-utils": "workspace:*",
|
||||
"@primevue/core": "catalog:",
|
||||
"@primevue/themes": "catalog:",
|
||||
|
||||
@@ -59,7 +59,8 @@ const LOCALES = [
|
||||
['fr', 'Français'],
|
||||
['es', 'Español'],
|
||||
['ar', 'عربي'],
|
||||
['tr', 'Türkçe']
|
||||
['tr', 'Türkçe'],
|
||||
['pt-BR', 'Português (BR)']
|
||||
] as const satisfies ReadonlyArray<[string, string]>
|
||||
|
||||
type SupportedLocale = (typeof LOCALES)[number][0]
|
||||
|
||||
@@ -22,7 +22,11 @@
|
||||
<h1 v-if="title" class="font-inter font-bold text-3xl text-neutral-300">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p v-if="statusText" class="text-lg text-neutral-400">
|
||||
<p
|
||||
v-if="statusText"
|
||||
class="text-lg text-neutral-400"
|
||||
data-testid="startup-status-text"
|
||||
>
|
||||
{{ statusText }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { PassThrough } from '@primevue/core'
|
||||
import Button from 'primevue/button'
|
||||
import Step, { type StepPassThroughOptions } from 'primevue/step'
|
||||
import Step from 'primevue/step'
|
||||
import type { StepPassThroughOptions } from 'primevue/step'
|
||||
import StepList from 'primevue/steplist'
|
||||
|
||||
defineProps<{
|
||||
@@ -50,8 +51,6 @@ defineProps<{
|
||||
canProceed: boolean
|
||||
/** Whether the location step should be disabled */
|
||||
disableLocationStep: boolean
|
||||
/** Whether the migration step should be disabled */
|
||||
disableMigrationStep: boolean
|
||||
/** Whether the settings step should be disabled */
|
||||
disableSettingsStep: boolean
|
||||
}>()
|
||||
|
||||
@@ -115,19 +115,18 @@ import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import { type ModelRef, computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import type { ModelRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import MigrationPicker from '@/components/install/MigrationPicker.vue'
|
||||
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
|
||||
import {
|
||||
PYPI_MIRROR,
|
||||
PYTHON_MIRROR,
|
||||
type UVMirror
|
||||
} from '@/constants/uvMirrors'
|
||||
import { PYPI_MIRROR, PYTHON_MIRROR } from '@/constants/uvMirrors'
|
||||
import type { UVMirror } from '@/constants/uvMirrors'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { ValidationState } from '@/utils/validationUtil'
|
||||
|
||||
import MigrationPicker from './MigrationPicker.vue'
|
||||
import MirrorItem from './mirror/MirrorItem.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const installPath = defineModel<string>('installPath', { required: true })
|
||||
@@ -229,6 +228,10 @@ const validatePath = async (path: string | undefined) => {
|
||||
}
|
||||
if (validation.parentMissing) errors.push(t('install.parentMissing'))
|
||||
if (validation.isOneDrive) errors.push(t('install.isOneDrive'))
|
||||
if (validation.isInsideAppInstallDir)
|
||||
errors.push(t('install.insideAppInstallDir'))
|
||||
if (validation.isInsideUpdaterCache)
|
||||
errors.push(t('install.insideUpdaterCache'))
|
||||
|
||||
if (validation.error)
|
||||
errors.push(`${t('install.unhandledError')}: ${validation.error}`)
|
||||
|
||||
@@ -16,7 +16,8 @@ export const DESKTOP_MAINTENANCE_TASKS: Readonly<MaintenanceTask>[] = [
|
||||
execute: async () => await electron.setBasePath(),
|
||||
name: 'Base path',
|
||||
shortDescription: 'Change the application base path.',
|
||||
errorDescription: 'Unable to open the base path. Please select a new one.',
|
||||
errorDescription:
|
||||
'The current base path is invalid or unsafe. Please select a new location.',
|
||||
description:
|
||||
'The base path is the default location where ComfyUI stores data. It is the location for the python environment, and may also contain models, custom nodes, and other extensions.',
|
||||
isInstallationFix: true,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// Import only English locale eagerly as the default/fallback
|
||||
// ESLint cannot statically resolve dynamic imports with path aliases (@frontend-locales/*),
|
||||
// but these are properly configured in tsconfig.json and resolved by Vite at build time.
|
||||
// eslint-disable-next-line import-x/no-unresolved
|
||||
|
||||
import enCommands from '@frontend-locales/en/commands.json' with { type: 'json' }
|
||||
// eslint-disable-next-line import-x/no-unresolved
|
||||
|
||||
import en from '@frontend-locales/en/main.json' with { type: 'json' }
|
||||
// eslint-disable-next-line import-x/no-unresolved
|
||||
|
||||
import enNodes from '@frontend-locales/en/nodeDefs.json' with { type: 'json' }
|
||||
// eslint-disable-next-line import-x/no-unresolved
|
||||
|
||||
import enSettings from '@frontend-locales/en/settings.json' with { type: 'json' }
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
@@ -27,7 +27,7 @@ function buildLocale<
|
||||
|
||||
// Locale loader map - dynamically import locales only when needed
|
||||
// ESLint cannot statically resolve these dynamic imports, but they are valid at build time
|
||||
/* eslint-disable import-x/no-unresolved */
|
||||
|
||||
const localeLoaders: Record<
|
||||
string,
|
||||
() => Promise<{ default: Record<string, unknown> }>
|
||||
@@ -40,7 +40,8 @@ const localeLoaders: Record<
|
||||
ru: () => import('@frontend-locales/ru/main.json'),
|
||||
tr: () => import('@frontend-locales/tr/main.json'),
|
||||
zh: () => import('@frontend-locales/zh/main.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/main.json')
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/main.json'),
|
||||
'pt-BR': () => import('@frontend-locales/pt-BR/main.json')
|
||||
}
|
||||
|
||||
const nodeDefsLoaders: Record<
|
||||
@@ -55,7 +56,8 @@ const nodeDefsLoaders: Record<
|
||||
ru: () => import('@frontend-locales/ru/nodeDefs.json'),
|
||||
tr: () => import('@frontend-locales/tr/nodeDefs.json'),
|
||||
zh: () => import('@frontend-locales/zh/nodeDefs.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/nodeDefs.json')
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/nodeDefs.json'),
|
||||
'pt-BR': () => import('@frontend-locales/pt-BR/nodeDefs.json')
|
||||
}
|
||||
|
||||
const commandsLoaders: Record<
|
||||
@@ -70,7 +72,8 @@ const commandsLoaders: Record<
|
||||
ru: () => import('@frontend-locales/ru/commands.json'),
|
||||
tr: () => import('@frontend-locales/tr/commands.json'),
|
||||
zh: () => import('@frontend-locales/zh/commands.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/commands.json')
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/commands.json'),
|
||||
'pt-BR': () => import('@frontend-locales/pt-BR/commands.json')
|
||||
}
|
||||
|
||||
const settingsLoaders: Record<
|
||||
@@ -85,7 +88,8 @@ const settingsLoaders: Record<
|
||||
ru: () => import('@frontend-locales/ru/settings.json'),
|
||||
tr: () => import('@frontend-locales/tr/settings.json'),
|
||||
zh: () => import('@frontend-locales/zh/settings.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/settings.json')
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/settings.json'),
|
||||
'pt-BR': () => import('@frontend-locales/pt-BR/settings.json')
|
||||
}
|
||||
|
||||
// Track which locales have been loaded
|
||||
@@ -151,12 +155,14 @@ export async function loadLocale(locale: string): Promise<void> {
|
||||
}
|
||||
|
||||
// Only include English in the initial bundle
|
||||
const messages = {
|
||||
en: buildLocale(en, enNodes, enCommands, enSettings)
|
||||
}
|
||||
const enMessages = buildLocale(en, enNodes, enCommands, enSettings)
|
||||
|
||||
// Type for locale messages - inferred from the English locale structure
|
||||
type LocaleMessages = typeof messages.en
|
||||
type LocaleMessages = typeof enMessages
|
||||
|
||||
const messages: Record<string, LocaleMessages> = {
|
||||
en: enMessages
|
||||
}
|
||||
|
||||
export const i18n = createI18n({
|
||||
// Must set `false`, as Vue I18n Legacy API is for Vue 2
|
||||
|
||||
@@ -85,6 +85,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
const electron = electronAPI()
|
||||
|
||||
// Reactive state
|
||||
const lastUpdate = ref<InstallValidation | null>(null)
|
||||
const isRefreshing = ref(false)
|
||||
const isRunningTerminalCommand = computed(() =>
|
||||
tasks.value
|
||||
@@ -97,6 +98,13 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
.some((task) => getRunner(task)?.executing)
|
||||
)
|
||||
|
||||
const unsafeBasePath = computed(
|
||||
() => lastUpdate.value?.unsafeBasePath === true
|
||||
)
|
||||
const unsafeBasePathReason = computed(
|
||||
() => lastUpdate.value?.unsafeBasePathReason
|
||||
)
|
||||
|
||||
// Task list
|
||||
const tasks = ref(DESKTOP_MAINTENANCE_TASKS)
|
||||
|
||||
@@ -123,6 +131,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
* @param validationUpdate Update details passed in by electron
|
||||
*/
|
||||
const processUpdate = (validationUpdate: InstallValidation) => {
|
||||
lastUpdate.value = validationUpdate
|
||||
const update = validationUpdate as IndexedUpdate
|
||||
isRefreshing.value = true
|
||||
|
||||
@@ -155,7 +164,11 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
}
|
||||
|
||||
const execute = async (task: MaintenanceTask) => {
|
||||
return getRunner(task).execute(task)
|
||||
const success = await getRunner(task).execute(task)
|
||||
if (success && task.isInstallationFix) {
|
||||
await refreshDesktopTasks()
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -163,6 +176,8 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
isRefreshing,
|
||||
isRunningTerminalCommand,
|
||||
isRunningInstallationFix,
|
||||
unsafeBasePath,
|
||||
unsafeBasePathReason,
|
||||
execute,
|
||||
getRunner,
|
||||
processUpdate,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTimeout } from '@vueuse/core'
|
||||
import { type Ref, computed, ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
/**
|
||||
* Vue boolean ref (writable computed) with one difference: when set to `true` it stays that way for at least {@link minDuration}.
|
||||
|
||||
@@ -29,7 +29,8 @@ import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import Button from 'primevue/button'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { type DialogAction, getDialog } from '@/constants/desktopDialogs'
|
||||
import { getDialog } from '@/constants/desktopDialogs'
|
||||
import type { DialogAction } from '@/constants/desktopDialogs'
|
||||
import { t } from '@/i18n'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
|
||||
159
apps/desktop-ui/src/views/MaintenanceView.stories.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
// eslint-disable-next-line storybook/no-renderer-packages
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
type UnsafeReason = 'appInstallDir' | 'updaterCache' | 'oneDrive' | null
|
||||
type ValidationIssueState = 'OK' | 'warning' | 'error' | 'skipped'
|
||||
|
||||
type ValidationState = {
|
||||
inProgress: boolean
|
||||
installState: string
|
||||
basePath?: ValidationIssueState
|
||||
unsafeBasePath: boolean
|
||||
unsafeBasePathReason: UnsafeReason
|
||||
venvDirectory?: ValidationIssueState
|
||||
pythonInterpreter?: ValidationIssueState
|
||||
pythonPackages?: ValidationIssueState
|
||||
uv?: ValidationIssueState
|
||||
git?: ValidationIssueState
|
||||
vcRedist?: ValidationIssueState
|
||||
upgradePackages?: ValidationIssueState
|
||||
}
|
||||
|
||||
const validationState: ValidationState = {
|
||||
inProgress: false,
|
||||
installState: 'installed',
|
||||
basePath: 'OK',
|
||||
unsafeBasePath: false,
|
||||
unsafeBasePathReason: null,
|
||||
venvDirectory: 'OK',
|
||||
pythonInterpreter: 'OK',
|
||||
pythonPackages: 'OK',
|
||||
uv: 'OK',
|
||||
git: 'OK',
|
||||
vcRedist: 'OK',
|
||||
upgradePackages: 'OK'
|
||||
}
|
||||
|
||||
const createMockElectronAPI = () => {
|
||||
const logListeners: Array<(message: string) => void> = []
|
||||
|
||||
const getValidationUpdate = () => ({
|
||||
...validationState
|
||||
})
|
||||
|
||||
return {
|
||||
getPlatform: () => 'darwin',
|
||||
changeTheme: (_theme: unknown) => {},
|
||||
onLogMessage: (listener: (message: string) => void) => {
|
||||
logListeners.push(listener)
|
||||
},
|
||||
showContextMenu: (_options: unknown) => {},
|
||||
Events: {
|
||||
trackEvent: (_eventName: string, _data?: unknown) => {}
|
||||
},
|
||||
Validation: {
|
||||
onUpdate: (_callback: (update: unknown) => void) => {},
|
||||
async getStatus() {
|
||||
return getValidationUpdate()
|
||||
},
|
||||
async validateInstallation(callback: (update: unknown) => void) {
|
||||
callback(getValidationUpdate())
|
||||
},
|
||||
async complete() {
|
||||
// Only allow completion when the base path is safe
|
||||
return !validationState.unsafeBasePath
|
||||
},
|
||||
dispose: () => {}
|
||||
},
|
||||
setBasePath: () => Promise.resolve(true),
|
||||
reinstall: () => Promise.resolve(),
|
||||
uv: {
|
||||
installRequirements: () => Promise.resolve(),
|
||||
clearCache: () => Promise.resolve(),
|
||||
resetVenv: () => Promise.resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ensureElectronAPI = () => {
|
||||
const globalWindow = window as unknown as { electronAPI?: unknown }
|
||||
if (!globalWindow.electronAPI) {
|
||||
globalWindow.electronAPI = createMockElectronAPI()
|
||||
}
|
||||
|
||||
return globalWindow.electronAPI
|
||||
}
|
||||
|
||||
const MaintenanceView = defineAsyncComponent(async () => {
|
||||
ensureElectronAPI()
|
||||
const module = await import('./MaintenanceView.vue')
|
||||
return module.default
|
||||
})
|
||||
|
||||
const meta: Meta<typeof MaintenanceView> = {
|
||||
title: 'Desktop/Views/MaintenanceView',
|
||||
component: MaintenanceView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
values: [
|
||||
{ name: 'dark', value: '#0a0a0a' },
|
||||
{ name: 'neutral-900', value: '#171717' },
|
||||
{ name: 'neutral-950', value: '#0a0a0a' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
name: 'All tasks OK',
|
||||
render: () => ({
|
||||
components: { MaintenanceView },
|
||||
setup() {
|
||||
validationState.inProgress = false
|
||||
validationState.installState = 'installed'
|
||||
validationState.basePath = 'OK'
|
||||
validationState.unsafeBasePath = false
|
||||
validationState.unsafeBasePathReason = null
|
||||
validationState.venvDirectory = 'OK'
|
||||
validationState.pythonInterpreter = 'OK'
|
||||
validationState.pythonPackages = 'OK'
|
||||
validationState.uv = 'OK'
|
||||
validationState.git = 'OK'
|
||||
validationState.vcRedist = 'OK'
|
||||
validationState.upgradePackages = 'OK'
|
||||
ensureElectronAPI()
|
||||
return {}
|
||||
},
|
||||
template: '<MaintenanceView />'
|
||||
})
|
||||
}
|
||||
|
||||
export const UnsafeBasePathOneDrive: Story = {
|
||||
name: 'Unsafe base path (OneDrive)',
|
||||
render: () => ({
|
||||
components: { MaintenanceView },
|
||||
setup() {
|
||||
validationState.inProgress = false
|
||||
validationState.installState = 'installed'
|
||||
validationState.basePath = 'error'
|
||||
validationState.unsafeBasePath = true
|
||||
validationState.unsafeBasePathReason = 'oneDrive'
|
||||
validationState.venvDirectory = 'OK'
|
||||
validationState.pythonInterpreter = 'OK'
|
||||
validationState.pythonPackages = 'OK'
|
||||
validationState.uv = 'OK'
|
||||
validationState.git = 'OK'
|
||||
validationState.vcRedist = 'OK'
|
||||
validationState.upgradePackages = 'OK'
|
||||
ensureElectronAPI()
|
||||
return {}
|
||||
},
|
||||
template: '<MaintenanceView />'
|
||||
})
|
||||
}
|
||||
@@ -47,6 +47,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unsafe migration warning -->
|
||||
<div v-if="taskStore.unsafeBasePath" class="my-4">
|
||||
<p class="flex items-start gap-3 text-neutral-300">
|
||||
<Tag
|
||||
icon="pi pi-exclamation-triangle"
|
||||
severity="warn"
|
||||
:value="t('icon.exclamation-triangle')"
|
||||
/>
|
||||
<span>
|
||||
<strong class="block mb-1">
|
||||
{{ t('maintenance.unsafeMigration.title') }}
|
||||
</strong>
|
||||
<span class="block mb-1">
|
||||
{{ unsafeReasonText }}
|
||||
</span>
|
||||
<span class="block text-sm text-neutral-400">
|
||||
{{ t('maintenance.unsafeMigration.action') }}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Tasks -->
|
||||
<TaskListPanel
|
||||
class="border-neutral-700 border-solid border-x-0 border-y"
|
||||
@@ -89,10 +111,10 @@
|
||||
import { PrimeIcons } from '@primevue/core/api'
|
||||
import Button from 'primevue/button'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import Tag from 'primevue/tag'
|
||||
import Toast from 'primevue/toast'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import RefreshButton from '@/components/common/RefreshButton.vue'
|
||||
import StatusTag from '@/components/maintenance/StatusTag.vue'
|
||||
@@ -139,6 +161,27 @@ const filterOptions = ref([
|
||||
/** Filter binding; can be set to show all tasks, or only errors. */
|
||||
const filter = ref<MaintenanceFilter>(filterOptions.value[0])
|
||||
|
||||
const unsafeReasonText = computed(() => {
|
||||
const reason = taskStore.unsafeBasePathReason
|
||||
if (!reason) {
|
||||
return t('maintenance.unsafeMigration.generic')
|
||||
}
|
||||
|
||||
if (reason === 'appInstallDir') {
|
||||
return t('maintenance.unsafeMigration.appInstallDir')
|
||||
}
|
||||
|
||||
if (reason === 'updaterCache') {
|
||||
return t('maintenance.unsafeMigration.updaterCache')
|
||||
}
|
||||
|
||||
if (reason === 'oneDrive') {
|
||||
return t('maintenance.unsafeMigration.oneDrive')
|
||||
}
|
||||
|
||||
return t('maintenance.unsafeMigration.generic')
|
||||
})
|
||||
|
||||
/** If valid, leave the validation window. */
|
||||
const completeValidation = async () => {
|
||||
const isValid = await electron.Validation.complete()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<img
|
||||
class="sad-girl"
|
||||
src="/assets/images/sad_girl.png"
|
||||
alt="Sad girl illustration"
|
||||
:alt="$t('notSupported.illustrationAlt')"
|
||||
/>
|
||||
|
||||
<div class="no-drag sad-text flex items-center">
|
||||
|
||||
@@ -85,11 +85,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
InstallStage,
|
||||
type InstallStageInfo,
|
||||
type InstallStageName,
|
||||
ProgressStatus
|
||||
import { InstallStage, ProgressStatus } from '@comfyorg/comfyui-electron-types'
|
||||
import type {
|
||||
InstallStageInfo,
|
||||
InstallStageName
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import type { Terminal } from '@xterm/xterm'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"id": "e0cb1d7e-5437-4911-b574-c9603dfbeaee",
|
||||
"revision": 0,
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "8bfe4227-f272-49e1-a892-0a972a86867c",
|
||||
"pos": [
|
||||
-317,
|
||||
-336
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"proxyWidgets": [
|
||||
[
|
||||
"-1",
|
||||
"batch_size"
|
||||
]
|
||||
]
|
||||
},
|
||||
"widgets_values": [
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "8bfe4227-f272-49e1-a892-0a972a86867c",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 1,
|
||||
"lastLinkId": 1,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "New Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [
|
||||
-562,
|
||||
-358,
|
||||
120,
|
||||
60
|
||||
]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [
|
||||
-52,
|
||||
-358,
|
||||
120,
|
||||
40
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "b4a8bc2a-8e9f-41aa-938d-c567a11d2c00",
|
||||
"name": "batch_size",
|
||||
"type": "INT",
|
||||
"linkIds": [
|
||||
1
|
||||
],
|
||||
"pos": [
|
||||
-462,
|
||||
-338
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [
|
||||
-382,
|
||||
-376
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "batch_size",
|
||||
"name": "batch_size",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "batch_size"
|
||||
},
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "LATENT",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
512,
|
||||
512,
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 1,
|
||||
"target_slot": 0,
|
||||
"type": "INT"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.35.1"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 422 B |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 82 KiB |
@@ -16,7 +16,6 @@ import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
import {
|
||||
NodeLibrarySidebarTab,
|
||||
QueueSidebarTab,
|
||||
WorkflowsSidebarTab
|
||||
} from './components/SidebarTab'
|
||||
import { Topbar } from './components/Topbar'
|
||||
@@ -28,19 +27,32 @@ dotenv.config()
|
||||
|
||||
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
|
||||
|
||||
class ComfyPropertiesPanel {
|
||||
readonly root: Locator
|
||||
readonly panelTitle: Locator
|
||||
readonly searchBox: Locator
|
||||
|
||||
constructor(readonly page: Page) {
|
||||
this.root = page.getByTestId('properties-panel')
|
||||
this.panelTitle = this.root.locator('h3')
|
||||
this.searchBox = this.root.getByPlaceholder('Search...')
|
||||
}
|
||||
}
|
||||
|
||||
class ComfyMenu {
|
||||
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
|
||||
private _workflowsTab: WorkflowsSidebarTab | null = null
|
||||
private _queueTab: QueueSidebarTab | null = null
|
||||
private _topbar: Topbar | null = null
|
||||
|
||||
public readonly sideToolbar: Locator
|
||||
public readonly propertiesPanel: ComfyPropertiesPanel
|
||||
public readonly themeToggleButton: Locator
|
||||
public readonly saveButton: Locator
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.sideToolbar = page.locator('.side-tool-bar-container')
|
||||
this.themeToggleButton = page.locator('.comfy-vue-theme-toggle')
|
||||
this.propertiesPanel = new ComfyPropertiesPanel(page)
|
||||
this.saveButton = page
|
||||
.locator('button[title="Save the current workflow"]')
|
||||
.nth(0)
|
||||
@@ -60,11 +72,6 @@ class ComfyMenu {
|
||||
return this._workflowsTab
|
||||
}
|
||||
|
||||
get queueTab() {
|
||||
this._queueTab ??= new QueueSidebarTab(this.page)
|
||||
return this._queueTab
|
||||
}
|
||||
|
||||
get topbar() {
|
||||
this._topbar ??= new Topbar(this.page)
|
||||
return this._topbar
|
||||
@@ -119,6 +126,20 @@ class ConfirmDialog {
|
||||
const loc = this[locator]
|
||||
await expect(loc).toBeVisible()
|
||||
await loc.click()
|
||||
|
||||
// Wait for the dialog mask to disappear after confirming
|
||||
const mask = this.page.locator('.p-dialog-mask')
|
||||
const count = await mask.count()
|
||||
if (count > 0) {
|
||||
await mask.first().waitFor({ state: 'hidden', timeout: 3000 })
|
||||
}
|
||||
|
||||
// Wait for workflow service to finish if it's busy
|
||||
await this.page.waitForFunction(
|
||||
() => window['app']?.extensionManager?.workflow?.isBusy === false,
|
||||
undefined,
|
||||
{ timeout: 3000 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,6 +256,9 @@ export class ComfyPage {
|
||||
await this.page.evaluate(async () => {
|
||||
await window['app'].extensionManager.workflow.syncWorkflows()
|
||||
})
|
||||
|
||||
// Wait for Vue to re-render the workflow list
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async setupUser(username: string) {
|
||||
@@ -317,19 +341,6 @@ export class ComfyPage {
|
||||
}
|
||||
await this.goto()
|
||||
|
||||
// Unify font for consistent screenshots.
|
||||
await this.page.addStyleTag({
|
||||
url: 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'
|
||||
})
|
||||
await this.page.addStyleTag({
|
||||
url: 'https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'
|
||||
})
|
||||
await this.page.addStyleTag({
|
||||
content: `
|
||||
* {
|
||||
font-family: 'Roboto Mono', 'Noto Color Emoji';
|
||||
}`
|
||||
})
|
||||
await this.page.waitForFunction(() => document.fonts.ready)
|
||||
await this.page.waitForFunction(
|
||||
() =>
|
||||
@@ -564,7 +575,7 @@ export class ComfyPage {
|
||||
async dragAndDrop(source: Position, target: Position) {
|
||||
await this.page.mouse.move(source.x, source.y)
|
||||
await this.page.mouse.down()
|
||||
await this.page.mouse.move(target.x, target.y)
|
||||
await this.page.mouse.move(target.x, target.y, { steps: 100 })
|
||||
await this.page.mouse.up()
|
||||
await this.nextFrame()
|
||||
}
|
||||
@@ -574,9 +585,15 @@ export class ComfyPage {
|
||||
fileName?: string
|
||||
url?: string
|
||||
dropPosition?: Position
|
||||
waitForUpload?: boolean
|
||||
} = {}
|
||||
) {
|
||||
const { dropPosition = { x: 100, y: 100 }, fileName, url } = options
|
||||
const {
|
||||
dropPosition = { x: 100, y: 100 },
|
||||
fileName,
|
||||
url,
|
||||
waitForUpload = false
|
||||
} = options
|
||||
|
||||
if (!fileName && !url)
|
||||
throw new Error('Must provide either fileName or url')
|
||||
@@ -613,6 +630,14 @@ export class ComfyPage {
|
||||
// Dropping a URL (e.g., dropping image across browser tabs in Firefox)
|
||||
if (url) evaluateParams.url = url
|
||||
|
||||
// Set up response waiter for file uploads before triggering the drop
|
||||
const uploadResponsePromise = waitForUpload
|
||||
? this.page.waitForResponse(
|
||||
(resp) => resp.url().includes('/upload/') && resp.status() === 200,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
: null
|
||||
|
||||
// Execute the drag and drop in the browser
|
||||
await this.page.evaluate(async (params) => {
|
||||
const dataTransfer = new DataTransfer()
|
||||
@@ -679,12 +704,17 @@ export class ComfyPage {
|
||||
}
|
||||
}, evaluateParams)
|
||||
|
||||
// Wait for file upload to complete
|
||||
if (uploadResponsePromise) {
|
||||
await uploadResponsePromise
|
||||
}
|
||||
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragAndDropFile(
|
||||
fileName: string,
|
||||
options: { dropPosition?: Position } = {}
|
||||
options: { dropPosition?: Position; waitForUpload?: boolean } = {}
|
||||
) {
|
||||
return this.dragAndDropExternalResource({ fileName, ...options })
|
||||
}
|
||||
@@ -1265,9 +1295,6 @@ export class ComfyPage {
|
||||
}, 'image/png')
|
||||
})
|
||||
}, filename)
|
||||
|
||||
// Wait a bit for the download to process
|
||||
await this.page.waitForTimeout(500)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1658,7 +1685,10 @@ export const comfyPageFixture = base.extend<{
|
||||
// Set tutorial completed to true to avoid loading the tutorial workflow.
|
||||
'Comfy.TutorialCompleted': true,
|
||||
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
|
||||
'Comfy.VueNodes.AutoScaleLayout': false
|
||||
'Comfy.VueNodes.AutoScaleLayout': false,
|
||||
// Disable toast warning about version compatibility, as they may or
|
||||
// may not appear - depending on upstream ComfyUI dependencies
|
||||
'Comfy.VersionCompatibility.DisableWarnings': true
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -65,7 +65,9 @@ export class VueNodeHelpers {
|
||||
* Select a specific Vue node by ID
|
||||
*/
|
||||
async selectNode(nodeId: string): Promise<void> {
|
||||
await this.page.locator(`[data-node-id="${nodeId}"]`).click()
|
||||
await this.page
|
||||
.locator(`[data-node-id="${nodeId}"] .lg-node-header`)
|
||||
.click()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,11 +79,13 @@ export class VueNodeHelpers {
|
||||
// Select first node normally
|
||||
await this.selectNode(nodeIds[0])
|
||||
|
||||
// Add additional nodes with Ctrl+click
|
||||
// Add additional nodes with Ctrl+click on header
|
||||
for (let i = 1; i < nodeIds.length; i++) {
|
||||
await this.page.locator(`[data-node-id="${nodeIds[i]}"]`).click({
|
||||
modifiers: ['Control']
|
||||
})
|
||||
await this.page
|
||||
.locator(`[data-node-id="${nodeIds[i]}"] .lg-node-header`)
|
||||
.click({
|
||||
modifiers: ['Control']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +160,7 @@ export class VueNodeHelpers {
|
||||
return {
|
||||
input: widget.locator('input'),
|
||||
incrementButton: widget.locator('button').first(),
|
||||
decrementButton: widget.locator('button').last()
|
||||
decrementButton: widget.locator('button').nth(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +60,6 @@ export class ComfyNodeSearchBox {
|
||||
await this.input.waitFor({ state: 'visible' })
|
||||
await this.input.fill(nodeName)
|
||||
await this.dropdown.waitFor({ state: 'visible' })
|
||||
// Wait for some time for the auto complete list to update.
|
||||
// The auto complete list is debounced and may take some time to update.
|
||||
await this.page.waitForTimeout(500)
|
||||
await this.dropdown
|
||||
.locator('li')
|
||||
.nth(options?.suggestionIndex || 0)
|
||||
|
||||
@@ -116,7 +116,6 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
async switchToWorkflow(workflowName: string) {
|
||||
const workflowLocator = this.getOpenedItem(workflowName)
|
||||
await workflowLocator.click()
|
||||
await this.page.waitForTimeout(300)
|
||||
}
|
||||
|
||||
getOpenedItem(name: string) {
|
||||
@@ -138,7 +137,13 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
.click()
|
||||
await this.page.keyboard.type(newName)
|
||||
await this.page.keyboard.press('Enter')
|
||||
await this.page.waitForTimeout(300)
|
||||
|
||||
// Wait for workflow service to finish renaming
|
||||
await this.page.waitForFunction(
|
||||
() => !window['app']?.extensionManager?.workflow?.isBusy,
|
||||
undefined,
|
||||
{ timeout: 3000 }
|
||||
)
|
||||
}
|
||||
|
||||
async insertWorkflow(locator: Locator) {
|
||||
@@ -148,124 +153,3 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
.click()
|
||||
}
|
||||
}
|
||||
|
||||
export class QueueSidebarTab extends SidebarTab {
|
||||
constructor(public readonly page: Page) {
|
||||
super(page, 'queue')
|
||||
}
|
||||
|
||||
get root() {
|
||||
return this.page.locator('.sidebar-content-container', { hasText: 'Queue' })
|
||||
}
|
||||
|
||||
get tasks() {
|
||||
return this.root.locator('[data-virtual-grid-item]')
|
||||
}
|
||||
|
||||
get visibleTasks() {
|
||||
return this.tasks.locator('visible=true')
|
||||
}
|
||||
|
||||
get clearButton() {
|
||||
return this.root.locator('.clear-all-button')
|
||||
}
|
||||
|
||||
get collapseTasksButton() {
|
||||
return this.getToggleExpandButton(false)
|
||||
}
|
||||
|
||||
get expandTasksButton() {
|
||||
return this.getToggleExpandButton(true)
|
||||
}
|
||||
|
||||
get noResultsPlaceholder() {
|
||||
return this.root.locator('.no-results-placeholder')
|
||||
}
|
||||
|
||||
get galleryImage() {
|
||||
return this.page.locator('.galleria-image')
|
||||
}
|
||||
|
||||
private getToggleExpandButton(isExpanded: boolean) {
|
||||
const iconSelector = isExpanded ? '.pi-image' : '.pi-images'
|
||||
return this.root.locator(`.toggle-expanded-button ${iconSelector}`)
|
||||
}
|
||||
|
||||
async open() {
|
||||
await super.open()
|
||||
return this.root.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async close() {
|
||||
await super.close()
|
||||
await this.root.waitFor({ state: 'hidden' })
|
||||
}
|
||||
|
||||
async expandTasks() {
|
||||
await this.expandTasksButton.click()
|
||||
await this.collapseTasksButton.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async collapseTasks() {
|
||||
await this.collapseTasksButton.click()
|
||||
await this.expandTasksButton.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async waitForTasks() {
|
||||
return Promise.all([
|
||||
this.tasks.first().waitFor({ state: 'visible' }),
|
||||
this.tasks.last().waitFor({ state: 'visible' })
|
||||
])
|
||||
}
|
||||
|
||||
async scrollTasks(direction: 'up' | 'down') {
|
||||
const scrollToEl =
|
||||
direction === 'up' ? this.tasks.last() : this.tasks.first()
|
||||
await scrollToEl.scrollIntoViewIfNeeded()
|
||||
await this.waitForTasks()
|
||||
}
|
||||
|
||||
async clearTasks() {
|
||||
await this.clearButton.click()
|
||||
const confirmButton = this.page.getByLabel('Delete')
|
||||
await confirmButton.click()
|
||||
await this.noResultsPlaceholder.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
/** Set the width of the tab (out of 100). Must call before opening the tab */
|
||||
async setTabWidth(width: number) {
|
||||
if (width < 0 || width > 100) {
|
||||
throw new Error('Width must be between 0 and 100')
|
||||
}
|
||||
return this.page.evaluate((width) => {
|
||||
localStorage.setItem('queue', JSON.stringify([width, 100 - width]))
|
||||
}, width)
|
||||
}
|
||||
|
||||
getTaskPreviewButton(taskIndex: number) {
|
||||
return this.tasks.nth(taskIndex).getByRole('button')
|
||||
}
|
||||
|
||||
async openTaskPreview(taskIndex: number) {
|
||||
const previewButton = this.getTaskPreviewButton(taskIndex)
|
||||
await previewButton.click()
|
||||
return this.galleryImage.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
getGalleryImage(imageFilename: string) {
|
||||
return this.galleryImage.and(this.page.getByAltText(imageFilename))
|
||||
}
|
||||
|
||||
getTaskImage(imageFilename: string) {
|
||||
return this.tasks.getByAltText(imageFilename)
|
||||
}
|
||||
|
||||
/** Trigger the queue store and tasks to update */
|
||||
async triggerTasksUpdate() {
|
||||
await this.page.evaluate(() => {
|
||||
window['app']['api'].dispatchCustomEvent('status', {
|
||||
exec_info: { queue_remaining: 0 }
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,10 +92,26 @@ export class Topbar {
|
||||
)
|
||||
// Wait for the dialog to close.
|
||||
await this.getSaveDialog().waitFor({ state: 'hidden', timeout: 500 })
|
||||
|
||||
// Check if a confirmation dialog appeared (e.g., "Overwrite existing file?")
|
||||
// If so, return early to let the test handle the confirmation
|
||||
const confirmationDialog = this.page.locator(
|
||||
'.p-dialog:has-text("Overwrite")'
|
||||
)
|
||||
if (await confirmationDialog.isVisible()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
async openTopbarMenu() {
|
||||
await this.page.waitForTimeout(1000)
|
||||
// If menu is already open, close it first to reset state
|
||||
const isAlreadyOpen = await this.menuLocator.isVisible()
|
||||
if (isAlreadyOpen) {
|
||||
// Click outside the menu to close it properly
|
||||
await this.page.locator('body').click({ position: { x: 500, y: 300 } })
|
||||
await this.menuLocator.waitFor({ state: 'hidden', timeout: 1000 })
|
||||
}
|
||||
|
||||
await this.menuTrigger.click()
|
||||
await this.menuLocator.waitFor({ state: 'visible' })
|
||||
return this.menuLocator
|
||||
@@ -163,15 +179,36 @@ export class Topbar {
|
||||
|
||||
await topLevelMenu.hover()
|
||||
|
||||
// Hover over top-level menu with retry logic for flaky submenu appearance
|
||||
const submenu = this.getVisibleSubmenu()
|
||||
try {
|
||||
await submenu.waitFor({ state: 'visible', timeout: 1000 })
|
||||
} catch {
|
||||
// Click outside to reset, then reopen menu
|
||||
await this.page.locator('body').click({ position: { x: 500, y: 300 } })
|
||||
await this.menuLocator.waitFor({ state: 'hidden', timeout: 1000 })
|
||||
await this.menuTrigger.click()
|
||||
await this.menuLocator.waitFor({ state: 'visible' })
|
||||
// Re-hover on top-level menu to trigger submenu
|
||||
await topLevelMenu.hover()
|
||||
await submenu.waitFor({ state: 'visible', timeout: 1000 })
|
||||
}
|
||||
|
||||
let currentMenu = topLevelMenu
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const commandName = path[i]
|
||||
const menuItem = currentMenu
|
||||
.locator(
|
||||
`.p-tieredmenu-submenu .p-tieredmenu-item:has-text("${commandName}")`
|
||||
)
|
||||
const menuItem = submenu
|
||||
.locator(`.p-tieredmenu-item:has-text("${commandName}")`)
|
||||
.first()
|
||||
await menuItem.waitFor({ state: 'visible' })
|
||||
|
||||
// For the last item, click it
|
||||
if (i === path.length - 1) {
|
||||
await menuItem.click()
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, hover to open nested submenu
|
||||
await menuItem.hover()
|
||||
currentMenu = menuItem
|
||||
}
|
||||
|
||||
@@ -462,7 +462,6 @@ export class NodeReference {
|
||||
async convertToSubgraph() {
|
||||
await this.clickContextMenuOption('Convert to Subgraph')
|
||||
await this.comfyPage.nextFrame()
|
||||
await this.comfyPage.page.waitForTimeout(256)
|
||||
const nodes = await this.comfyPage.getNodeRefsByTitle('New Subgraph')
|
||||
if (nodes.length !== 1) {
|
||||
throw new Error(
|
||||
@@ -511,7 +510,6 @@ export class NodeReference {
|
||||
// Double-click to enter subgraph
|
||||
await this.comfyPage.canvas.dblclick({ position, force: true })
|
||||
await this.comfyPage.nextFrame()
|
||||
await this.comfyPage.page.waitForTimeout(500)
|
||||
|
||||
// Check if we successfully entered the subgraph
|
||||
isInSubgraph = await this.comfyPage.page.evaluate(() => {
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import type { FullConfig } from '@playwright/test'
|
||||
import { config as loadEnv } from 'dotenv'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import { backupPath } from './utils/backupUtils'
|
||||
import { syncDevtools } from './utils/devtoolsSync'
|
||||
|
||||
loadEnv()
|
||||
dotenv.config()
|
||||
|
||||
export default function globalSetup(_: FullConfig) {
|
||||
export default function globalSetup(config: FullConfig) {
|
||||
if (!process.env.CI) {
|
||||
if (process.env.TEST_COMFYUI_DIR) {
|
||||
backupPath([process.env.TEST_COMFYUI_DIR, 'user'])
|
||||
backupPath([process.env.TEST_COMFYUI_DIR, 'models'], {
|
||||
renameAndReplaceWithScaffolding: true
|
||||
})
|
||||
|
||||
syncDevtools(process.env.TEST_COMFYUI_DIR)
|
||||
} else {
|
||||
console.warn(
|
||||
'Set TEST_COMFYUI_DIR in .env to prevent user data (settings, workflows, etc.) from being overwritten'
|
||||
|
||||
@@ -5,14 +5,18 @@ import type { AutoQueueMode } from '../../src/stores/queueStore'
|
||||
export class ComfyActionbar {
|
||||
public readonly root: Locator
|
||||
public readonly queueButton: ComfyQueueButton
|
||||
public readonly propertiesButton: Locator
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.root = page.locator('.actionbar')
|
||||
this.root = page.locator('.actionbar-container')
|
||||
this.queueButton = new ComfyQueueButton(this)
|
||||
this.propertiesButton = this.root.getByLabel('Toggle properties panel')
|
||||
}
|
||||
|
||||
async isDocked() {
|
||||
const className = await this.root.getAttribute('class')
|
||||
const className = await this.root
|
||||
.locator('.actionbar')
|
||||
.getAttribute('class')
|
||||
return className?.includes('static') ?? false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import path from 'path'
|
||||
|
||||
import type {
|
||||
@@ -8,9 +9,20 @@ import type {
|
||||
|
||||
export class ComfyTemplates {
|
||||
readonly content: Locator
|
||||
readonly allTemplateCards: Locator
|
||||
|
||||
constructor(readonly page: Page) {
|
||||
this.content = page.getByTestId('template-workflows-content')
|
||||
this.allTemplateCards = page.locator('[data-testid^="template-workflow-"]')
|
||||
}
|
||||
|
||||
async waitForMinimumCardCount(count: number) {
|
||||
return await expect(async () => {
|
||||
const cardCount = await this.allTemplateCards.count()
|
||||
expect(cardCount).toBeGreaterThanOrEqual(count)
|
||||
}).toPass({
|
||||
timeout: 1_000
|
||||
})
|
||||
}
|
||||
|
||||
async loadTemplate(id: string) {
|
||||
|
||||
@@ -75,13 +75,9 @@ test.describe('Background Image Upload', () => {
|
||||
// Upload the test image
|
||||
await fileChooser.setFiles(comfyPage.assetPath('image32x32.webp'))
|
||||
|
||||
// Wait for upload to complete and verify the setting was updated
|
||||
await comfyPage.page.waitForTimeout(500) // Give time for file reading
|
||||
|
||||
// Verify the URL input now has an API URL
|
||||
const urlInput = backgroundImageSetting.locator('input[type="text"]')
|
||||
const inputValue = await urlInput.inputValue()
|
||||
expect(inputValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/)
|
||||
await expect(urlInput).toHaveValue(/^\/api\/view\?.*subfolder=backgrounds/)
|
||||
|
||||
// Verify clear button is now enabled
|
||||
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
|
||||
@@ -191,14 +187,11 @@ test.describe('Background Image Upload', () => {
|
||||
)
|
||||
await uploadButton.hover()
|
||||
|
||||
// Wait for tooltip to appear and verify it exists
|
||||
await comfyPage.page.waitForTimeout(700) // Tooltip delay
|
||||
const uploadTooltip = comfyPage.page.locator('.p-tooltip:visible')
|
||||
await expect(uploadTooltip).toBeVisible()
|
||||
|
||||
// Move away to hide tooltip
|
||||
await comfyPage.page.locator('body').hover()
|
||||
await comfyPage.page.waitForTimeout(100)
|
||||
|
||||
// Set a background to enable clear button
|
||||
const urlInput = backgroundImageSetting.locator('input[type="text"]')
|
||||
@@ -209,8 +202,6 @@ test.describe('Background Image Upload', () => {
|
||||
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
|
||||
await clearButton.hover()
|
||||
|
||||
// Wait for tooltip to appear and verify it exists
|
||||
await comfyPage.page.waitForTimeout(700) // Tooltip delay
|
||||
const clearTooltip = comfyPage.page.locator('.p-tooltip:visible')
|
||||
await expect(clearTooltip).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -203,7 +203,6 @@ test.describe('Node Color Adjustments', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
|
||||
await comfyPage.page.waitForTimeout(128)
|
||||
|
||||
// Drag mouse to force canvas to redraw
|
||||
await comfyPage.page.mouse.move(0, 0)
|
||||
@@ -211,7 +210,6 @@ test.describe('Node Color Adjustments', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
|
||||
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
|
||||
await comfyPage.page.waitForTimeout(128)
|
||||
|
||||
await comfyPage.page.mouse.move(8, 8)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
|
||||
@@ -235,7 +233,6 @@ test.describe('Node Color Adjustments', () => {
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
|
||||
const saveWorkflowInterval = 1000
|
||||
await comfyPage.page.waitForTimeout(saveWorkflowInterval)
|
||||
const workflow = await comfyPage.page.evaluate(() => {
|
||||
return localStorage.getItem('workflow')
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 103 KiB |
@@ -43,7 +43,6 @@ test('Does not report warning on undo/redo', async ({ comfyPage }) => {
|
||||
|
||||
// Wait for any async operations to complete after dialog closes
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.waitForTimeout(100)
|
||||
|
||||
// Make a change to the graph
|
||||
await comfyPage.doubleClickCanvas()
|
||||
|
||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 81 KiB |
@@ -36,13 +36,10 @@ test.describe('Execute to selected output nodes', () => {
|
||||
await output1.click('title')
|
||||
|
||||
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
|
||||
// @note: Wait for the execution to finish. We might want to move to a more
|
||||
// reliable way to wait for the execution to finish. Workflow in this test
|
||||
// is simple enough that this is fine for now.
|
||||
await comfyPage.page.waitForTimeout(200)
|
||||
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
await expect(async () => {
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
}).toPass({ timeout: 2_000 })
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 97 KiB |